Skip to content

Generation-versioned observation persistence (companion to relation deadlock fix) #1214

Description

@phernandez

Problem

Observation persistence has the same delete-and-replace transaction shape as relation persistence: markdown persist deletes an entity's observation rows and re-inserts the new set inside the same multi-phase transaction as entity upserts and relation writes. This is the same lock structure that produces the production relation-table deadlock cluster (#1213), with lower observed collision frequency so far — concurrent persists of notes touching overlapping entities can interleave lock acquisition across phases.

No dedicated Logfire fingerprint has been attributed to observation rows yet; some of the 16 relation-cluster fingerprints' retried attempts recorded adjacent integrity/DBAPI failures consistent with contention beyond the relation table. This issue is the proactive completion of the same fix rather than a response to a distinct incident.

Design

Adopt the identical generation-versioned model specified in #1213:

  1. observation rows carry an indexed generation — the note's existing generation token (db_version/content checksum).
  2. Persist upserts the desired observation set with ON CONFLICT DO UPDATE SET generation = G guarded by WHERE observation.generation < G; stale writers become no-ops.
  3. Cleanup of superseded rows (DELETE ... WHERE entity_id = ? AND generation < G) is a separate short statement after upserts — brief union window, never a gap; observations are derived, eventually consistent state.
  4. Observation writes commit in their own short transaction, decoupled from entity/relation phases.

Note: observations may lack a natural conflict key today (position/content-based identity) — if so, part of this issue is defining a stable upsert identity (e.g. content-hash within entity) so ON CONFLICT has an arbiter, or an equivalent generation-swap strategy that avoids delete-then-insert in one transaction.

Scope

Core repo: observation repository + markdown persist flow, tenant-DB migration (observation.generation + index + backfill, plus upsert identity if needed). Sequence after (or with) the relation issue so both use one migration window and one persistence-flow refactor.

Acceptance criteria

  • Observation persistence performs no delete-then-insert inside a shared multi-phase transaction; writes are generation-guarded with separate bounded cleanup.
  • A stale generation's observation writes are no-ops and cleanup cannot delete newer-generation rows (both race directions tested).
  • Concurrent persists of notes with overlapping entities complete without deadlock in the same real-Postgres concurrency test added for relations.
  • Migration backfills generation (and upsert identity if introduced) for existing rows.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions