Documentation
¶
Overview ¶
Package transaction commits a bounded set of managed file writes inside one selected root as a single recoverable unit.
The filesystem gives no multi-file atomicity, so this package adds the smallest mechanism that keeps the promise the sync and archive owners make: after any interruption every declared output holds either all of its old bytes or all of its planned bytes, and exactly one deterministic recovery action exists.
The mechanism is a write-ahead manifest. Staged bytes are written and synced first; the manifest write is the commit point; replacement is a replayable projection of the manifest. A manifest that exists is committed and rolls forward. Staging without a manifest was never committed and rolls back.
Everything here is local deterministic filesystem logic: no network, no LLM, no clock of its own — the caller injects the time recovery judges against.
Index ¶
Constants ¶
const ( CodeInvalid = "transaction_invalid" CodeConflict = "transaction_conflict" CodeDrift = "transaction_drift" CodeAmbiguous = "transaction_ambiguous" CodeIO = "transaction_io" )
Stable refusal codes. Each names one operator action that is never a manual edit of state, history, evidence, or a manifest.
const ( ActionRollForward = "roll_forward" ActionRollback = "rollback" )
Recovery actions. A pending transaction resolves to exactly one of them.
const SchemaVersion = 1
SchemaVersion versions the manifest contract. A manifest from any other version is refused rather than reinterpreted.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Manifest ¶
type Manifest struct {
SchemaVersion int `json:"schema_version"`
ID string `json:"id"`
Operation string `json:"operation"`
Change string `json:"change"`
Revision uint64 `json:"revision"`
Timestamp string `json:"timestamp"`
Outputs []Output `json:"outputs"`
Moves []Move `json:"moves,omitempty"`
Record json.RawMessage `json:"record,omitempty"`
}
Manifest is the write-ahead record. ID is derived from every other field, so a manifest cannot be edited without losing its identity.
Record is the one history record this transaction owes. Carrying it inside the manifest is what makes an interrupted history append recoverable: the transaction identity proves the exact committed outputs before the missing record is appended, and the record's own content-derived id makes the append idempotent.
type Move ¶
Move is one managed directory relocation. Both paths are managed-relative. A move is old-or-new by rename, so it needs no staged bytes.
type Output ¶
type Output struct {
Path string `json:"path"`
Before string `json:"before,omitempty"`
After string `json:"after"`
}
Output is the committed description of one planned write. Before is empty exactly when the target did not exist when the plan was built.
type Pending ¶
type Pending struct {
ID string `json:"id"`
Action string `json:"action"`
Manifest *Manifest `json:"manifest,omitempty"`
}
Pending is one interrupted transaction and its sole legal recovery action.
func Inspect ¶
Inspect reports every interrupted transaction and its one legal action. It reads only; a malformed or ambiguous transaction fails closed here too, so a caller cannot proceed past one by not recovering.
type Request ¶
type Request struct {
Operation string
Change string
Revision uint64
Now time.Time
Outputs []Write
Moves []Move
Record *record.Record
Hook persist.Hook
}
Request is one commit attempt. Hook is nil in production; tests use it to interrupt the sequence at a stable boundary.
type Result ¶
type Result struct {
ID string `json:"id"`
Manifest Manifest `json:"manifest"`
NoOp bool `json:"no_op"`
}
Result is one committed transaction. NoOp is true when every target already held the planned bytes, so nothing was written and no manifest existed.
func Commit ¶
Commit stages, commits, and applies every declared output as one unit. It first resolves any interrupted transaction, so a retry after a crash is the same call rather than a separate repair verb.
func CommitUnderRootLock ¶
CommitUnderRootLock is Commit for a caller that already holds the root lock. It exists so an operation can take the root lock before the change lock — the one global lock order — instead of inverting it here.