benbjohnson/litestream · warning

context cancelled while waiting for transaction: %w

Error message

context cancelled while waiting for transaction: %w

What it means

SetWriteEnabledWithTimeout(false) was waiting for an active SQLite transaction to finish before disabling write support, but the file's context (f.ctx) was cancelled first. The disabling attempt is abandoned and the disabling flag is rolled back so pending Lock() callers are woken.

Source

Thrown at vfs.go:1853

				f.cond.Broadcast() // Wake cond.Wait()
			case <-timeoutCh:
				f.cond.Broadcast() // Wake cond.Wait() on timeout
			case <-waitDone:
				// Normal completion, nothing to do
			}
		}()

		// Wait for active transaction to complete
		deadline := time.Now().Add(timeout)
		for f.inTransaction {
			// Check context before waiting
			select {
			case <-f.ctx.Done():
				close(waitDone)
				f.disabling = false
				f.cond.Broadcast() // Wake any waiting Lock() calls
				f.mu.Unlock()
				return fmt.Errorf("context cancelled while waiting for transaction: %w", f.ctx.Err())
			default:
			}
			// Check timeout if specified
			if timeout > 0 && time.Now().After(deadline) {
				close(waitDone)
				f.disabling = false
				f.cond.Broadcast() // Wake any waiting Lock() calls
				f.mu.Unlock()
				return fmt.Errorf("timeout waiting for transaction to complete (waited %v)", timeout)
			}
			f.cond.Wait() // Unlocks mu, waits for signal, relocks mu
		}
		close(waitDone) // Stop the watcher goroutine

		// Sync dirty pages if any exist
		if len(f.dirty) > 0 {
			if err := f.syncToRemoteWithLock(); err != nil {
				f.disabling = false

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Wait for in-flight transactions to complete (or roll them back) before disabling writes.
  2. Increase the shutdown grace period so the transaction can finish before context cancellation.
  3. Retry SetWriteEnabled(false) with a live context after the transaction ends.
  4. Use SetWriteEnabledWithTimeout(false, timeout) with an explicit timeout and handle both timeout and cancellation paths.
  5. Drain writers before shutdown (checkpoint/close connections) instead of cancelling mid-transaction.

Example fix

// before
ctxCancel() // cancels f.ctx while tx in flight
file.SetWriteEnabled(false) // -> context cancelled error
// after
if err := file.SetWriteEnabledWithTimeout(false, 30*time.Second); err != nil {
    log.Warn("disable deferred", "err", err) // retry after tx completes
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Check no transaction is active before disabling:
// serialize with your writer layer so inTransaction == false at disable time.
select {
case <-ctx.Done():
    return fmt.Errorf("context already cancelled")
default:
}

Try / catch

if err := file.SetWriteEnabledWithTimeout(false, 30*time.Second); err != nil {
    if errors.Is(err, context.Canceled) {
        // retry with a fresh context after shutdown drain
    }
}

Prevention

When it happens

Trigger: Calling SetWriteEnabled(false)/SetWriteEnabledWithTimeout(false, t) while f.inTransaction is true and the VFS/file context is cancelled (process shutdown, parent context cancelled, Close) before the transaction completes.

Common situations: Graceful shutdown that cancels the context while a long SQLite write transaction is still open; test harness tearing down contexts; connection drop causing ctx cancellation during a large batch write; calling Close() concurrently with an in-flight transaction.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/41421e933ed06868. Report an issue: GitHub.