Skip to content

Store::create is not transactional when reusing an existing id #23

Open

Description

@fabracht

Summary

Store::create is not fully transactional when called with an id that already exists in the local store. On a remote rejection (409 Conflict / 403 Ownership), the rollback deletes the row at that id — losing the pre-existing row instead of restoring it.

Scenario

  1. A legitimate local row X exists at id = K (e.g. holding unique key S1).
  2. A caller invokes create(id = K, …) with a different non-id unique field value (e.g. S2) that is already held remotely by another row.
  3. create optimistically upserts memory/persistence at K (overwriting X), then sync_create returns 409.
  4. rollback_local_create deletes id = K → both the new data and the original X are gone.

This only bites when create() is used with an already-existing id to change a non-id unique field — effectively using create() where update() is meant. Claims that use a fresh id (the intended exclusive-key pattern) are already transactional.

Verification

Modeled in TLA+ (CreateRollback): the current delete-by-id rollback satisfies a transactional invariant (phase=done ⇒ local = prior) when prior = none (fresh id) and violates it when prior = legit (id reuse). A snapshot-restore variant satisfies it in both cases.

Notes / options

  • The loss originates in create()'s pre-existing upsert-over-existing semantics (step 3), not the rollback itself.
  • A partial snapshot-restore only covers the synchronous online-create path; the transient-then-flush rollback (flush_loop / on_connected) cannot restore prior state because it isn't captured there.
  • Cleanest fix: make create reject an Origin::Local insert when the id already exists locally (create ≠ upsert), which is correct-by-construction and cheap (one local read for Origin::Local). This is a behavior change (create-over-existing-id currently upserts), so it warrants its own PR.

Follow-up from the review of #22. Documented as a known limitation on Store::create in that PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions