Two-Phase Commit Journaling & Idempotency Fingerprinting #5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implement the two-phase commit boundary around external mutations: commit a
preparedoperation intent and canonical request fingerprint in SQLite before remote dispatch, enforce idempotency replay versus conflict detection, and atomically commit the confirmed result or uncertainty state alongside timeline events upon remote completion.Background & Technical Rationale
Interacting with remote CalDAV servers (such as Radicale) and external services inherently lacks distributed transaction guarantees. Network timeouts, server crashes, and process interruptions can occur at any point. To ensure the Life timeline remains consistent without phantom writes or duplicated actions:
operationswithstate = 'prepared', recording the canonical request fingerprint SHA-256 hash, actor, target resource, baseline ETag, and intended mutation payload.PUTwithIf-Match) is dispatched over the network. Crucially, no SQLite transaction is open during this network I/O.operations.statetoapplied,failed, orunknown, records the response payload, and appends the corresponding confirmed fact tolife_events.Idempotency-Key(or CLI--operation-id).X-Request-IDand authorization tokens are strictly excluded.Idempotency-Keyand an identical fingerprint, the service returns the recorded outcome without repeating the remote dispatch (verifying Scenario R01).Idempotency-Keybut a divergent fingerprint, the service rejects the request immediately withidempotency_conflict(HTTP 409 / Exit code 4) without mutating state (verifying Scenario R02).Exact Technical References & Specifications
Mutation Sequence Diagram
From
docs/technical/architecture.md#mutation-sequence:Operation State Machine
From
docs/technical/timeline-consistency.md#operation-state-machine:Idempotency and Approval Specifications
From
docs/technical/cli-api.md#idempotency-and-approval:Error Identifiers & Exit Codes
From
docs/technical/cli-api.md#error-identifiers:From
docs/technical/cli-api.md#transport-and-common-contract:Verification Scenarios (R01, R02, R04)
From
docs/technical/verification.md#recovery-scenarios:Formal Section Citations
docs/technical/architecture.md#mutation-sequence: Illustrates the two-phase commit boundary and sequence of operations.docs/technical/timeline-consistency.md#local-durability: Establishes the rule that prepared intent must commit before remote mutation.docs/technical/timeline-consistency.md#operation-state-machine: Defines the lifecycle statesprepared,applied,failed,unknown,needs_reconciliation.docs/technical/cli-api.md#idempotency-and-approval: Specifies the canonical fingerprinting algorithm and replay behavior.docs/technical/cli-api.md#error-identifiers: Definesidempotency_conflictandjournal_unavailable.docs/technical/verification.md#recovery-scenarios: Formulates normative test scenarios R01, R02, and R04.What to Build (Step-by-Step)
Implement Canonical Request Fingerprinting (
src/history/fingerprint.rs):POST,PATCH)./v1/tasks).sha256:<hex>.Implement Journal Service (
src/history/journal.rs):prepare_operation(op_id, fingerprint, actor, intent_json, baseline_json) -> Result<PrepareResult, JournalError>:operationstable for existingoperation_id.request_hash == fingerprint: returnPrepareResult::Replay(existing_row).request_hash != fingerprint: returnErr(JournalError::IdempotencyConflict).state = 'prepared',prepared_at = NOW(),updated_at = NOW().commit_operation_result(op_id, state, result_json, events: Vec<NewLifeEvent>) -> Result<(), JournalError>:operationsrow:state = ?,result_json = ?,updated_at = NOW().life_events.Wire Request Execution Pipeline in API Handlers (
src/api.rs):Idempotency-Keyheader on all state-changing endpoints; reject requests missingIdempotency-Keyimmediately with HTTP 400invalid_input(exit code 2). The CLI client generates a high-entropy operation ID before transmission when--operation-idis omitted.prepare_operation:PrepareResult::Replay: immediately return recorded result without invoking downstream backend.Err(JournalError::IdempotencyConflict): return HTTP 409 with error codeidempotency_conflict.journal_unavailableand DO NOT proceed to remote write.commit_operation_resultto transitionoperationstoappliedorfailed.commit_operation_resultwith stateunknown.Integration Test Suite for R01, R02, R04 (
tests/journal_idempotency.rs):prepare_operation. Assert that zero HTTP requests reach downstream mock and client receivesjournal_unavailable(503).Idempotency-Keyand payload. Assert client receives identical response and mock call count remains 1.Idempotency-Keybut modified title or ETag. Assert client receives HTTP 409idempotency_conflict, exit code 4, and zero additional mutations occur.Acceptance Criteria
Idempotency-Keywith identical semantic payload returns the recorded result without calling downstream backend (verifying Scenario R01).Idempotency-Keywith a different payload or route returnsidempotency_conflict(HTTP 409 / Exit code 4) without mutating state (verifying Scenario R02).X-Request-IDdo not alter the canonical fingerprint calculation.life_eventsare appended atomically within the same local transaction that transitionsoperationstoapplied.Idempotency-Keyheader returns HTTP 400invalid_input(exit code 2); the CLI client automatically generates a high-entropy operation ID before transmission when--operation-idis omitted.docs/technical/verification.mdfor R01, R02, and R04.Blocked by
Issue #005: Two-Phase Commit Journaling & Idempotency Fingerprintingto Two-Phase Commit Journaling & Idempotency Fingerprinting