toeverything/AFFiNE · error · FailedToUpsertSnapshot

failed_to_upsert_snapshot

failed_to_upsert_snapshot

Error message

Failed to store doc snapshot.

What it means

Thrown by `setDocSnapshot` when the `models.doc.upsert(...)` throws — the snapshot row could not be written. The original error is logged, the metric `snapshot_upsert_failed` is incremented, and `FailedToUpsertSnapshot` (category `internal_server_error`) is thrown so the DB error is not exposed. Because snapshot writes happen under `lockDocForUpdate`, contention is not the cause — a DB-level failure is.

Source

Thrown at packages/backend/server/src/core/doc/adapters/workspace.ts:351

        docId: snapshot.docId,
        blob,
        timestamp: snapshot.timestamp,
        editorId: snapshot.editor,
      });

      if (updatedSnapshot) {
        this.event.emitDetached('doc.snapshot.updated', {
          workspaceId: snapshot.spaceId,
          docId: snapshot.docId,
          blob,
        });
      }

      return !!updatedSnapshot;
    } catch (e) {
      metrics.doc.counter('snapshot_upsert_failed').add(1);
      this.logger.error('Failed to upsert snapshot', e);
      throw new FailedToUpsertSnapshot();
    }
  }

  protected override async lockDocForUpdate(
    workspaceId: string,
    docId: string
  ) {
    const lock = await this.mutex.acquire(`doc:update:${workspaceId}:${docId}`);

    if (!lock) {
      throw new Error('Too many concurrent writings');
    }

    return lock;
  }

  protected async lastDocHistory(workspaceId: string, id: string) {
    return this.models.history.getLatest(workspaceId, id);

View on GitHub (pinned to 26c515e050)

Solutions

  1. Retry the snapshot write after backoff (snapshots are deterministic from updates and safe to rewrite).
  2. Inspect server logs for the original `e` under 'Failed to upsert snapshot' to identify the DB cause.
  3. Check DB disk space, connectivity, and connection-pool health.
  4. Reduce overlapping writers per doc to avoid deadlocks (the per-doc mutex should already serialize; confirm lock acquisition is not being bypassed).

Example fix

// before
await adapter.setDocSnapshot(snapshot);

// after
async function setSnapshotWithRetry(snapshot, attempt = 0) {
  try {
    return await adapter.setDocSnapshot(snapshot);
  } catch (e) {
    if (e instanceof FailedToUpsertSnapshot && attempt < 3) {
      await sleep(2 ** attempt * 200);
      return setSnapshotWithRetry(snapshot, attempt + 1);
    }
    throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// Snapshots are written under a per-doc lock; confirm the input is well-formed
function isWellFormedSnapshot(s: unknown): boolean {
  return typeof s === 'object' && s !== null &&
    typeof (s as any).spaceId === 'string' &&
    typeof (s as any).docId === 'string' &&
    (s as any).bin instanceof Uint8Array;
}

if (!isWellFormedSnapshot(snapshot)) {
  throw new Error('Malformed snapshot — refusing to upsert');
}

Type guard

function isFailedToUpsertSnapshot(e: unknown): boolean {
  return typeof e === 'object' && e !== null &&
    (e as { code?: string }).code === 'failed_to_upsert_snapshot';
}

Try / catch

async function setSnapshotWithRetry(snapshot: DocRecord, attempt = 0) {
  try {
    return await adapter.setDocSnapshot(snapshot);
  } catch (e) {
    if (isFailedToUpsertSnapshot(e) && attempt < 3) {
      await sleep(2 ** attempt * 200);
      return setSnapshotWithRetry(snapshot, attempt + 1);
    }
    throw e;
  }
}

Prevention

When it happens

Trigger: The Postgres `doc.upsert` fails: connection loss, deadlock, disk full, a constraint/type error on the blob, or a schema mismatch.

Common situations: DB connectivity blips; storage exhaustion; deadlocks from overlapping snapshot writers; schema drift after a partial migration; oversized blob payloads.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/d62b93b7922d2812. Report an issue: GitHub.