mvcc

package
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package mvcc holds the timestamp and visibility primitives the versioned stores share.

It exists because versioning spans two packages that cannot import each other. Node labels and node properties live in [lpg]; the adjacency — which edges exist — lives in [adjlist], which lpg imports. Both must answer the same question about the same transaction, so the answer cannot live in either. Everything here is deliberately small, dependency-free and concurrency-safe.

The timestamp space

One uint64 carries three states, split at TxIDBase, which is what makes the visibility test a single comparison rather than a registry lookup:

ts <  TxIDBase        committed, and ts is the commit timestamp
ts >= TxIDBase        in flight, and ts is the transaction id
ts == AbortedTS       aborted

Commit timestamps and transaction ids are both monotonic and neither is ever reused, so a reader never has to ask whether a writer is still alive.

The encoding is Memgraph's, read from `src/storage/v2/mvcc.hpp` at master on 2026-07-31, where uncommitted deltas carry the writer's transaction id and committed ones its commit timestamp, separated by kTransactionInitialId.

Index

Constants

View Source
const (
	StoreNodeLabels      = "node labels"
	StoreNodeProperties  = "node properties"
	StoreNodeExistence   = "node existence"
	StoreAdjacency       = "adjacency"
	StoreEdgeTypes       = "edge relationship types"
	StoreEdgeTypesHandle = "edge relationship types by handle"
	StoreEdgeTypesOrd    = "edge relationship types by ordinal"
	StoreEdgePropsHandle = "edge properties by handle"
	StoreEdgePropsOrd    = "edge properties by ordinal"
	// StoreNodeConstraint is the per-node constraint stamp (rmp #2353). It is the
	// one store here whose granularity is the NODE rather than one of the node's
	// substores, and it exists precisely because the others are narrower: a
	// declared invariant spanning two substores cannot be enforced by conflict
	// detection that never compares them. It is stamped only for nodes under an
	// active existence constraint, so a schema declaring none never reaches it.
	StoreNodeConstraint = "node constraint"
	// StoreOther is where a name that is not one of the above is counted. It is
	// not a store; it is the bucket that keeps the cardinality bounded without
	// losing the count.
	StoreOther = "other"
)

The versioned stores a write-write conflict can be detected in. The value is the store's human name, as it appears in a Conflict's message.

View Source
const AbortedTS = ^uint64(0)

AbortedTS marks a transaction whose changes must never become visible.

It sits above TxIDBase and equals no transaction id a Clock can mint, so the ordinary rule in Visible already classifies it as another transaction's uncommitted work — no dedicated branch on the read path. It is distinguishable only so garbage collection can recognise a chain it may reclaim eagerly.

View Source
const ConflictStoreCount = len(conflictStores)

ConflictStoreCount is how many per-store conflict buckets exist, including StoreOther.

View Source
const DepthBuckets = 8

DepthBuckets is how many buckets DepthHist has. Bucket i holds the chains whose retained depth is in [2^i, 2^(i+1)), with the last bucket unbounded above.

View Source
const HorizonCapacity = horizonSlots

HorizonCapacity is the number of readers that can be registered at once, exported so an operator can compare it against a live reader count without reading the source. Past it, reclamation SUSPENDS: see Horizon.Enter and the rationale on horizonSlots.

View Source
const TxIDBase uint64 = 1 << 63

TxIDBase separates commit timestamps from transaction ids.

Variables

View Source
var ErrSerializationConflict = errors.New("mvcc: serialization conflict: the object was modified by a concurrent transaction")

ErrSerializationConflict is returned when a transaction tries to modify an object whose newest version it cannot see: either another transaction is still writing it, or another transaction committed it after this one began.

It is RETRIABLE. The transaction that receives it has not lost any work that a retry cannot redo — its own writes are discarded, it takes a fresh snapshot, and it tries again against a state that now includes the change it collided with. Callers should match it with errors.Is rather than by string.

Functions

func ConflictStoreIndex

func ConflictStoreIndex(store string) int

ConflictStoreIndex returns the dense index of store, or the index of StoreOther when the name is not one of the constants above.

A linear scan over ten entries, deliberately: it runs once per conflicting transaction — a path that is by definition exceptional — and a switch or a map would either duplicate the table or allocate a hash lookup to save nanoseconds nobody is waiting on.

func ConflictStoreMetric

func ConflictStoreMetric(i int) string

ConflictStoreMetric returns the metric-name suffix of the bucket at index i: the human name with no character a Prometheus metric name may not carry.

func ConflictStoreName

func ConflictStoreName(i int) string

ConflictStoreName returns the human name of the bucket at index i.

func Conflicts

func Conflicts(headTS, startTS, txID uint64) bool

Conflicts reports whether a transaction holding startTS and txID may modify an object whose newest version carries headTS.

It is the exact negation of Visible: a version the transaction could not have READ is a version it must not OVERWRITE. See the file comment for why the two share one predicate rather than having one each.

A headTS of zero means the object has no recorded version — nothing has written it since the last reclamation — and never conflicts.

An ABORTED head CONFLICTS (rmp #2318 — this reverses rmp #2300)

rmp #2300 exempted an aborted head, on the argument that displacing a version no reader can see cannot lose an update. The argument is true and the conclusion was wrong, because it is not the VERSION that the next writer displaces — it is the STORED VALUE, which still holds the aborted transaction's writes with the aborted version as the only thing masking them. A writer allowed through builds its new value on top of that dirty base, and then:

T_abort adds label L to n, aborts. Stored = {…, L}; the head delta is aborted.
T2 adds M and commits, building from the dirty stored bag: {…, L, M}.
A reader after T2 walks the chain, finds T2's delta VISIBLE, and BREAKS —
  never reaching the aborted delta behind it.
The reader sees L.

Measured exactly that (`reader sees L=true M=true`): a committed read observing work from a transaction that was told it failed, which is an ATOMICITY violation. For the ADJACENCY it is worse and unrecoverable, because that chain holds entry SNAPSHOTS rather than undo actions — T2's entry itself contains the aborted edge, so no walk can reconstruct a value that was never recorded.

So an aborted head conflicts, and Conflicts is once again the plain negation of Visible. What makes that safe is the OTHER half of rmp #2318: the background vacuum now WITHDRAWS an aborted version — restoring the stored value through the reader's own walk and then releasing the record — and an abort wakes it unconditionally rather than through the churn threshold. Without a cleaner this branch was a liveness bug and was measured as one: "the FIRST transaction to abort on an object made that object permanently unwritable", and examples/27_concurrent_txn's writers exhausted a nine-attempt retry chain on their first aborted account. With one, the cost is a transient retriable serialization failure, which is already this module's contract.

Memgraph never needed the exemption because its abort path restores each object and UNLINKS the transaction's deltas before returning (`InMemoryStorage::InMemoryAccessor::Abort`, src/storage/v2/inmemory/storage.cpp:1482-1790, read 2026-08-04 at commit 0e8aa326), so no aborted delta is ever at a head to be tested. GoGraph withdraws on the vacuum instead of at abort, because doing it at abort needs the transaction's own write set — Memgraph's `transaction_.deltas` — and keeping one taxes every write to serve the rare path.

func DepthBucketLabel

func DepthBucketLabel(i int) string

DepthBucketLabel returns the metric-name suffix of bucket i.

func DepthBucketLow

func DepthBucketLow(i int) int

DepthBucketLow returns the smallest depth that falls in bucket i.

func Visible

func Visible(ts, startTS, txID uint64) bool

Visible reports whether a change stamped ts is visible to a reader that started at startTS running as transaction txID.

The three cases, in Memgraph's order:

  • the change is the reader's OWN uncommitted work, so it is visible;
  • the change is committed, so it is visible when it committed at or before the reader started;
  • the change belongs to another transaction that has not committed (or has aborted), so it is never visible.

Callers hold versions as UNDO records, so most of them want the negation: "must I undo this to see my version?" is `!Visible(...)`.

Types

type Clock

type Clock struct {
	// contains filtered or unexported fields
}

Clock mints commit timestamps and transaction ids from the two disjoint ranges either side of TxIDBase.

Safe for concurrent use.

func (*Clock) AbandonCommitTS

func (c *Clock) AbandonCommitTS(ts uint64)

AbandonCommitTS records that ts was allocated but will never be published — the transaction failed after taking a timestamp and nothing it wrote will ever be visible under it.

It exists because the frontier is CONTIGUOUS: a timestamp that is neither published nor abandoned stalls it forever, and every later commit becomes permanently invisible to new readers while the commit log grows without bound. An allocate-then-fail path is therefore obliged to call this, and the obligation is why it is a named operation rather than an internal detail of Clock.PublishCommitTS.

Every allocation in the module still publishes, and rmp #2300 did NOT change that: a transaction refused by write-write conflict detection aborts WITHOUT allocating a commit timestamp at all ([lpg.Graph.endWrite] marks the record AbortedTS and returns), so there is nothing to abandon. This remains the operation an allocate-THEN-fail path would owe the frontier, and it has no caller — which is the honest state to record rather than deleting it and leaving the obligation undocumented for whoever next writes such a path.

func (*Clock) AwaitQuiescent

func (c *Clock) AwaitQuiescent(ctx context.Context) error

AwaitQuiescent blocks until no allocated commit timestamp remains unpublished — until Clock.InFlightCommits would report zero — or until ctx finishes.

What it is for: the durability/visibility boundary (rmp #2349)

Committing is two steps, and this module deliberately does NOT hold one lock across both: a timestamp is allocated, the WAL record carrying it is fsynced, and only afterwards is the timestamp published. Between the fsync and the publish a transaction is DURABLE BUT INVISIBLE, and any observer that reads a durability position and a visibility position at that moment gets two numbers describing DIFFERENT sets of transactions.

A checkpoint is exactly such an observer, and for it the disagreement is unrecoverable: it takes the durable WAL offset as the prefix its image folds, and the image itself at a visible instant. A transaction inside the window is below the offset and absent from the image, so truncating that prefix discards the only record of an acknowledged commit.

This is the wait that closes the window, and it is PostgreSQL's answer to the same problem. A backend there raises DELAY_CHKPT_START before inserting its commit record and clears it after updating pg_xact (src/backend/access/transam/xact.c, commit b5978350, lines 1469-1471 and 1577-1582), and CreateCheckPoint spins until no backend is inside that window (src/backend/access/transam/xlog.c:7695-7712) before it moves on. Its own comment names the cause — "xact.c does commit record XLOG insertion and clog update as two separate steps protected by different locks, but again that seems best on grounds of minimizing lock contention" (xlog.c:7684-7687) — and states the trade-off it chose: "it seems better to make checkpoint take a bit longer than to hold off insertions longer than necessary". That is the trade-off taken here too, which is why the wait is on the OBSERVER and the commit path pays nothing.

Memgraph reaches the same property by the other route, and the contrast is why it was not copied: InMemoryStorage::CreateTransaction loads the start timestamp and the last durable timestamp under ONE acquisition of engine_lock_ (src/storage/v2/inmemory/storage.cpp:2833-2844, commit b3ac3cdc), so its snapshot reads a mutually consistent pair by construction. That works because its commit publishes durability and visibility under the same engine lock — which is the convoy rmp #2302 and rmp #2193 removed here to make writes scale.

Termination

It terminates once allocation stops, since every allocated timestamp is discharged by Clock.PublishCommitTS or Clock.AbandonCommitTS. A caller that observes it while allocation continues may loop indefinitely by design — that is the caller's to bound, and it is why this takes a context. The intended caller holds an admission gate closed, so no new timestamp can be allocated while it waits.

Safe for concurrent use.

func (*Clock) AwaitVisible

func (c *Clock) AwaitVisible(ctx context.Context, floor uint64) error

AwaitVisible blocks until every commit at or below floor is visible — that is, until Clock.ReadTS would return floor or more — or until ctx finishes.

It returns nil once the frontier has reached floor, and ctx's error otherwise. A floor of zero, or one the frontier has already passed, returns immediately without registering anything.

It is bounded by the transactions it waits on, not by this call

The frontier advances when the in-flight commits below floor finish, and each of those is a transaction that is either committing or aborting — both of which discharge their timestamp (Clock.PublishCommitTS, Clock.AbandonCommitTS). So the wait terminates unless a transaction never discharges its timestamp at all, which is the permanent-frontier-stall condition that MVCCStats.InFlightCommits exists to report and that no path in the module is allowed to create. A caller that cannot tolerate an unbounded wait passes a ctx with a deadline, which is why this takes one.

Safe for concurrent use.

func (*Clock) AwaitingVisible

func (c *Clock) AwaitingVisible() int64

AwaitingVisible reports how many callers are currently blocked in Clock.AwaitVisible.

It is the observable form of the cost this mechanism moves onto the read side: a value that is persistently non-zero says sessions are waiting on the frontier rather than reading, which is the condition an operator would otherwise have to infer from latency alone.

Safe for concurrent use.

func (*Clock) InFlightCommits

func (c *Clock) InFlightCommits() uint64

InFlightCommits reports how many allocated commit timestamps have not yet finished: the distance between the frontier a reader starts at and the newest timestamp handed out.

It is the quantity to watch when readers look stale — a commit stuck between allocation and publication holds the frontier for every reader — and it is what makes the commit log's memory bound observable, since the log retains exactly this window.

Safe for concurrent use.

func (*Clock) NextCommitTS

func (c *Clock) NextCommitTS() uint64

NextCommitTS allocates the next commit timestamp. Monotonic, never reused.

Allocating is NOT publishing: the caller must call Clock.PublishCommitTS once the timestamp is stored in the transaction's commit record and its changes are therefore visible.

func (*Clock) NextTxID

func (c *Clock) NextTxID() uint64

NextTxID allocates a transaction id, drawn from above TxIDBase so it can never be mistaken for a commit timestamp.

func (*Clock) PublishCommitTS

func (c *Clock) PublishCommitTS(ts uint64)

PublishCommitTS announces that every change committed at ts is now visible.

It does NOT simply raise the visible instant to ts. It records ts as finished and moves the instant to the newest timestamp below which nothing is still in flight, which is the same number only while commits are serialised. Publishing out of allocation order is the case this exists for: a reader must never be handed an instant that includes a commit but excludes an earlier one that has not finished yet. See [commitLog] for the shape and the prior art.

Monotonic: the frontier only ever advances, and a late publisher whose timestamp is already behind it changes nothing.

func (*Clock) RatchetTo

func (c *Clock) RatchetTo(floor uint64) uint64

RatchetTo raises the clock so that every timestamp it subsequently allocates, and the instant every new reader starts at, is at least floor. It NEVER lowers either, and it is a no-op when the clock has already passed floor.

It returns the resulting allocation counter.

What it is for: recovery, and why the clock is DERIVED (rmp #2309)

A process-local clock constructed at zero on every open would re-mint instants that a previous process already made visible and made durable. The fix is not a persisted counter — two of the three reference engines deliberately removed theirs. InnoDB keeps TRX_SYS_TRX_ID_STORE "only for the purpose of upgrading" and instead folds a max over every rollback segment at startup, then calls init_max_trx_id(max + 1). Memgraph derives max(delta_ts)+1 from the WAL and info.start_timestamp+1 from a snapshot, then restores timestamp_ = max(timestamp_, next_timestamp). PostgreSQL does persist nextXid in pg_control but STILL ratchets it per record during replay (AdvanceNextFullTransactionIdPastXid).

So the clock is derived from what the durable record actually says, and RAISED rather than trusted — which is what this method is. A second source of truth would be one that can disagree with the log after a torn tail.

Why it raises the VISIBLE frontier too, and why that is not a shortcut

Both counters move. Raising only the allocation counter would leave the frontier at zero, so every recovered commit would be invisible to a new reader until some later commit's publication happened to sweep the frontier past it — a graph that reads as empty immediately after recovery.

It is sound here and ONLY here because recovery has no in-flight commits by construction: every transaction in the file either reached its durable marker or is discarded with the torn tail, so there is no allocated-but-unfinished instant for the frontier to be holding back. That is exactly the precondition the contiguous frontier normally enforces, satisfied by the situation rather than by the commit log — which is why this must not be called on a live clock.

Not safe for concurrent use, and not safe on a clock with commits in flight: call it during open, before the graph is published to any reader or writer. # THREE things move, not two, and the third is not optional

The allocation counter and the visible frontier are atomics, but the CONTIGUITY that produces the frontier lives in [commitLog], and it must be rebased with them. A log that still believes timestamp 1 is unfinished computes a frontier of 0 for ever, so [Clock.finishCommitTS] — which only ever RAISES visible — can never move it again, and every commit after the ratchet is invisible for the life of the process. Writes keep succeeding and readers simply never see them.

The first version of this method moved only the two counters. internal/sim's full-stack crash-recovery scenario caught it as node LOSS against its oracle (21 expected, 15 present), which looks nothing like a clock defect — see [commitLog.rebase] and TestClock_RatchetKeepsTheFrontierMovable.

func (*Clock) ReadTS

func (c *Clock) ReadTS() uint64

ReadTS returns the timestamp a reader starting now must use.

Why this is the PUBLISHED instant and not the allocated one

Committing is two steps: allocate a timestamp, then store it into the shared record. Between them the transaction's changes are still invisible — every reader sees the in-flight transaction id — but the allocation counter has already moved.

A reader that started at the allocated-but-unpublished value straddles that commit. It reads one object before the store and undoes the transaction there, reads another after the store and finds the transaction visible (its timestamp now equals the reader's own start timestamp), and reports a state that never existed. Example 27's bank-transfer invariant caught it exactly that way: "readers observed a torn total 40 time(s)". The barrier had been hiding it — a reader could not run while a writer held it — and it surfaced the moment reads stopped taking it (rmp #2290).

Returning the published instant closes it: a transaction is either wholly before a reader's start or wholly after it, with no window in between.

And why the published instant is a CONTIGUOUS frontier, not a maximum

This comment used to end by saying that publication happens in allocation order because commits are serialised by the write barrier, so one counter sufficed and no in-progress list was needed — "which is what PostgreSQL's snapshot xip_list and Memgraph's commit_log_->OldestActive() exist to supply when commits are NOT serialised". Sprint 334 is where commits stop being serialised, so that is exactly what rmp #2298 supplied.

The counter is no longer a maximum over published timestamps. It is the newest timestamp below which NOTHING is still in flight, maintained by [commitLog] on the publish path. Without that, writer B allocating 5 and finishing before writer A's 4 would hand a reader an instant containing 5 but not 4 — the same straddled commit described above, arrived at from the other direction.

The cost of the read is unchanged, and that is the point of the shape chosen: one atomic load here, one comparison in Visible. See [commitLog] for the prior art and the trade it accepts.

type CommitInfo

type CommitInfo struct {
	// contains filtered or unexported fields
}

CommitInfo is the commit record SHARED by every version one transaction writes, in every store.

Publishing a transaction is a single atomic store into it, so all of its changes — labels, properties and topology alike — become visible at one instant however many there are and however many stores they span. That is the whole reason it is a pointer rather than a timestamp copied into each record, and it is why the same type has to be reachable from both packages.

Memgraph heap-allocates the equivalent for the same reason, stated in `src/storage/v2/transaction.hpp`: "`Delta`s have a pointer to it, and that pointer must stay valid after the `Transaction` is moved".

Safe for concurrent use.

func NewCommitInfo

func NewCommitInfo(txID uint64) *CommitInfo

NewCommitInfo returns a record stamped with an in-flight transaction id.

func NewCommittedInfo

func NewCommittedInfo(ts uint64) *CommitInfo

NewCommittedInfo returns a record already committed at ts. It is the autocommit form: a single-statement write is committed the instant it is made and its record is never mutated again.

func (*CommitInfo) Abort

func (c *CommitInfo) Abort()

Abort makes every change stamped with this record permanently invisible.

func (*CommitInfo) Commit

func (c *CommitInfo) Commit(commitTS uint64)

Commit publishes every change stamped with this record, atomically.

func (*CommitInfo) TS

func (c *CommitInfo) TS() uint64

TS returns the record's current timestamp.

type Conflict

type Conflict struct {
	// Store names the versioned store the conflict was detected in — "node
	// labels", "node properties", "adjacency", and so on. It is the first thing
	// a reader of a bug report needs and the last thing a stack trace gives.
	Store string
	// HeadTS is the effective timestamp of the version that blocked the write:
	// another transaction's id while it is in flight, or its commit timestamp
	// once it has committed.
	HeadTS uint64
	// StartTS and TxID are the losing transaction's snapshot and identity.
	StartTS uint64
	TxID    uint64
}

Conflict describes a detected write-write conflict, so an error message can say WHICH transaction lost to WHAT rather than only that something collided.

It wraps ErrSerializationConflict, so `errors.Is(err, ErrSerializationConflict)` identifies it and `errors.As` recovers the detail.

func NewConflict

func NewConflict(store string, headTS, startTS, txID uint64) *Conflict

NewConflict builds the typed error for a detected conflict in store.

func (*Conflict) ConcurrentWriter

func (c *Conflict) ConcurrentWriter() bool

ConcurrentWriter reports whether the blocking version belongs to a transaction that has NOT finished — first-updater-wins — as opposed to one that committed after this transaction's snapshot, which is first-committer-wins.

Both are serialization failures and both are retriable; they are distinguished only so an operator reading metrics can tell overlapping writers from a snapshot that went stale.

func (*Conflict) Error

func (c *Conflict) Error() string

func (*Conflict) Unwrap

func (c *Conflict) Unwrap() error

Unwrap makes errors.Is(err, ErrSerializationConflict) true.

type DepthHist

type DepthHist struct {
	// contains filtered or unexported fields
}

DepthHist is a log2-bucketed histogram of retained version-chain depth.

The zero value is ready to use, and means "nothing measured yet" — which is distinguishable from "every chain is short", because every bucket is zero rather than the first one being large.

Safe for concurrent use: one writer (the single sweeper) and any number of readers.

func (*DepthHist) Load

func (h *DepthHist) Load() Depths

Load returns a readable copy.

Safe for concurrent use; see the file comment for what a mid-fill read means.

func (*DepthHist) Observe

func (h *DepthHist) Observe(depth int)

Observe records one chain of the given retained depth. A depth of zero — a chain the reclaimer removed entirely — is not a retained chain and is ignored.

func (*DepthHist) Reset

func (h *DepthHist) Reset()

Reset clears every bucket. Called by the reclaimer as it starts a store, so the histogram describes that store's latest sweep rather than accumulating over the life of the process.

type Depths

type Depths struct {
	// Buckets[i] is how many chains had a retained depth in [2^i, 2^(i+1)).
	Buckets [DepthBuckets]uint64
	// Deepest is the largest retained depth observed, exactly.
	Deepest uint64
}

Depths is a readable copy of a DepthHist.

func (*Depths) Add

func (d *Depths) Add(o Depths)

Add accumulates o into d, so several stores' histograms can be reported as one distribution.

func (*Depths) Chains

func (d *Depths) Chains() uint64

Chains returns how many chains the histogram counted.

type Gate

type Gate struct {
	// contains filtered or unexported fields
}

Gate is a weak/strong exclusion gate: any number of WEAK holders may proceed together, a STRONG holder excludes every weak holder and every other strong holder, and an uncontended weak acquisition touches only one striped cache line plus a read-mostly flag.

The zero value is ready to use. Safe for concurrent use.

It is NOT re-entrant in either mode.

func (*Gate) StrongLock

func (g *Gate) StrongLock()

StrongLock acquires the gate exclusively, excluding every weak holder and every other strong holder. It returns once no weak holder remains.

func (*Gate) StrongLockCtx

func (g *Gate) StrongLockCtx(ctx context.Context) error

StrongLockCtx is Gate.StrongLock with the wait bounded by ctx. It returns ctx's error, holding nothing, when ctx finishes before the acquisition completes.

A strong acquirer waits for two things — other strong acquirers, and the drain of every weak holder — and both are unbounded in principle, so a caller with a deadline needs this for the same reason Gate.WeakLockCtx exists.

As there, the acquisition itself cannot be abandoned once started: the underlying mutexes have no cancellable acquire, so a hold that lands after the caller gave up must still be released, which the helper below does.

func (*Gate) StrongUnlock

func (g *Gate) StrongUnlock()

StrongUnlock releases an acquisition made with Gate.StrongLock.

func (*Gate) TryWeakLock

func (g *Gate) TryWeakLock(hint uint64) (int, bool)

TryWeakLock attempts a weak acquisition without ever blocking. It reports false when a strong holder is present or arriving, in which case nothing is held.

It exists so a caller can bound its wait with a context; see Gate.WeakLockCtx.

func (*Gate) WeakHolders

func (g *Gate) WeakHolders() int

WeakHolders reports how many fast-path slot claims are outstanding.

It exists so the gate's occupancy is observable rather than merely suffered, matching the observability mandate every other bounded structure here follows.

IT IS A GAUGE, NOT AN EXACT COUNT OF CRITICAL-SECTION OCCUPANCY, and must not be used as an exclusion oracle. Gate.WeakLock claims its slot BEFORE it learns whether a strong holder is present, so a claim counted here may belong to an acquirer that is about to back out and block — one that never enters its critical section at all. The count is therefore an upper bound. It also excludes holders parked on the blocking path, which are not on the fast path by definition.

func (*Gate) WeakLock

func (g *Gate) WeakLock(hint uint64) int

WeakLock acquires the gate in weak mode and returns the token that must be passed to Gate.WeakUnlock.

It blocks only when a strong holder is present or arriving.

hint selects the striped slot. It must be cheap for the caller to produce WITHOUT touching shared state — that requirement is the whole design, for the measured reason recorded on the Gate struct — and it should be well spread across concurrent callers. A transaction id is the intended source: Clock.NextTxID mints them sequentially, so concurrent transactions land on distinct slots, and the caller already has one in hand. Correctness does not depend on hint at all: two callers sharing a slot merely share a cache line, and any value is safe.

func (*Gate) WeakLockAuto

func (g *Gate) WeakLockAuto() int

WeakLockAuto is Gate.WeakLock for a caller that has no natural hint in hand.

It draws the stripe from math/rand/v2.Uint64, whose generator is per-P inside the runtime and therefore touches NO shared cache line — which is the entire requirement. An ordinary shared counter would reintroduce the bottleneck this type exists to remove, as the struct comment records having measured.

Prefer Gate.WeakLock with a real per-transaction value where one exists: it is marginally cheaper and gives a caller's repeated acquisitions stripe affinity.

func (*Gate) WeakLockCtx

func (g *Gate) WeakLockCtx(ctx context.Context, hint uint64) (int, error)

WeakLockCtx is Gate.WeakLock with the wait bounded by ctx. It returns ctx's error, holding nothing, when ctx finishes before the acquisition succeeds.

Why a weak acquirer needs a deadline at all

Weak acquirers do not wait for each other, so the only thing that can block one is a DDL. That wait is legitimate but unbounded — a DDL runs a full backfill scan — and a caller carrying a deadline is entitled to hear about it rather than be held past it. Losing that bound is not hypothetical: before rmp #2306 an autocommit write carrying a 200 ms deadline blocked for TEN MINUTES behind an open transaction and returned only when the harness killed it.

The fast path is unchanged and costs nothing extra: ctx is consulted only once the try has already failed, so an uncontended acquisition never touches it.

func (*Gate) WeakLockCtxAuto

func (g *Gate) WeakLockCtxAuto(ctx context.Context) (int, error)

WeakLockCtxAuto is Gate.WeakLockCtx for a caller with no natural hint, drawing the stripe the same way Gate.WeakLockAuto does.

Use this rather than passing a constant. A constant hint sends every caller to the SAME stripe, which reinstates the single shared cache line this type exists to remove — and on the autocommit write path that is the hottest line in the engine.

func (*Gate) WeakUnlock

func (g *Gate) WeakUnlock(slot int)

WeakUnlock releases a weak acquisition made with Gate.WeakLock.

type Horizon

type Horizon struct {
	// contains filtered or unexported fields
}

Horizon tracks the start timestamps of active readers so a reclaimer can compute the oldest version any of them can still reach.

The zero value is ready to use. Safe for concurrent use.

func (*Horizon) Active

func (h *Horizon) Active() int

Active reports how many slots currently hold a reader.

It exists so the cost of a stalled reader is observable rather than merely suffered: versions accumulate behind the oldest active reader, and a deployment needs to be able to see that happening.

Readers that found no slot are NOT counted here; Horizon.Unregistered reports those separately, because they have a different consequence — they suspend reclamation altogether rather than merely holding it back. It counts OCCUPANCY BITS, not non-zero timestamps: a released slot keeps its previous occupant's timestamp (see Horizon.Enter), so counting timestamps would report every slot the graph has ever used as active.

func (*Horizon) Enter

func (h *Horizon) Enter(startTS uint64) int

Enter announces that a reader with the given start timestamp is active and returns the slot it occupies, which must be passed to Horizon.Leave.

It never blocks and never fails.

Why a slot is never shared

The first version of this let two readers share a slot, keeping the older timestamp. It is UNSOUND, and the failure is silent data loss rather than a crash: readers A (start 10) and B (start 20) share a slot holding 10; A finishes and clears it; the watermark jumps to the clock; a reclaimer frees versions superseded at 15 that B can still reach. The slot is exclusive precisely so that "occupied" and "some specific reader is still here" are the same statement.

When every slot is taken — more concurrent readers than [horizonSlots] — the reader is UNREGISTERED, and the watermark collapses to zero until it leaves, so nothing is reclaimed. That is the sound direction: reclamation stops, and correctness does not. It is also observable, via Horizon.Unregistered.

A slot between its claim and its stamp carries NO timestamp

Horizon.Leave invalidates the timestamp before it clears the occupancy bit, so a slot being re-claimed reads as zero — "claimed, instant not yet known" — until this method stores its new occupant's instant. Horizon.Oldest answers zero by holding everything back, so the window is conservative.

It used to hold the PREVIOUS occupant's instant instead, which is also conservative — an older instant holds back more, given the clock is monotonic — but which makes the watermark UNDER-REPORT rather than suspend, and an under-reporting watermark cannot be told apart from one that has passed a live reader. See Horizon.Leave for the measurement that decided it and for what the invariant buys.

func (*Horizon) EnterHolding

func (h *Horizon) EnterHolding() int

EnterHolding claims a slot for a reader that has NOT yet read the clock, and returns it for Horizon.Publish.

The race it closes

The obvious sequence is: read the clock, then register. A reclaimer landing between the two computes its watermark from a clock that has already moved past the reader's start timestamp, and frees versions the reader is about to need. The window is nanoseconds and the failure is a wrong answer.

Registering FIRST removes it. Between EnterHolding and Publish the slot holds back EVERYTHING, so a reclaimer in that window frees nothing at all; once the timestamp is published the ordinary rule applies. The cost is that a reclaimer racing a starting reader does no work — the right direction, and the window is a single clock read wide.

The barrier used to make this unnecessary: a reader under visMu.RLock excluded every writer, and only writers reclaimed. Once reads stop taking the barrier, neither half of that is true.

func (*Horizon) Leave

func (h *Horizon) Leave(slot int)

Leave releases the slot a reader took from Horizon.Enter.

Because a slot is exclusive, clearing it cannot release the watermark on another reader's behalf.

It INVALIDATES THE TIMESTAMP and then clears the OCCUPANCY BIT, in that order.

It also DETECTS the release of a slot that was not held: the atomic And returns the word as it stood, so testing the bit costs nothing beyond a branch. See Horizon.StaleLeaves for why that particular corruption is the one worth a permanent guard.

Why the timestamp is invalidated rather than left behind

This method used to leave the timestamp behind, so a slot between its claim and its Horizon.Publish read as its PREVIOUS occupant's instant. That is safe — an older instant holds back more than the arriving reader needs — but it makes the watermark UNDER-REPORT for that window, and an under-reporting watermark is indistinguishable from the one corruption that matters. Measured: with the residue in place the watermark was seen to move BACKWARDS 1 734 to 4 165 times per 30-second run of TestIsolation_ApplyAtomically_View_NoPartialReads, all of it benign, which left no way to assert the invariant that a live reader is never passed (rmp #2420).

Zeroing it instead makes an occupied slot's timestamp exactly one of two things: zero, meaning "claimed, instant not yet known", which Horizon.Oldest answers by holding EVERYTHING back; or this occupant's own published instant. Never a third reader's stale one. The watermark is then monotone, and Horizon.StaleLeaves and its lpg counterpart become assertable rather than merely informative.

The order is load-bearing: the timestamp is invalidated BEFORE the occupancy bit is cleared, because until the bit is cleared this slot is still ours. Zeroing after would race a re-claimer's Publish and clobber a LIVE reader's instant, which is the unsafe direction.

The cost is one store to a cache line this goroutine already owns — it published its own instant into that same line when it entered — against the previous version's zero stores. What it buys back is that a pass landing in the claim window reclaims nothing instead of reclaiming conservatively, which is a window one store wide.

func (*Horizon) Oldest

func (h *Horizon) Oldest(fallback uint64) uint64

Oldest returns the reclamation watermark: the oldest start timestamp any active reader announced, or fallback when none is active.

It returns ZERO — reclaim nothing — while any reader failed to get a slot, because such a reader's start timestamp is unknown and assuming anything about it would be assuming in the unsafe direction.

The caller supplies fallback, normally the clock's current value, because only the caller knows a timestamp that is newer than every version yet not newer than any reader that has begun. Nothing superseded AFTER the returned value may be reclaimed.

No locks and no writes, so a reclaimer may call it as often as it likes without disturbing readers.

Cost: O(active readers), not O(capacity)

It reads the [horizonWords] occupancy words and then only the slots whose bit is set, so an idle graph costs 16 loads and a graph with k readers costs 16+k. It used to touch all 1024 slot cache lines unconditionally, which cost 448 ns and — because [Graph.EndRead] calls this on every read once versions exist — put 200 ns on every single read under a writer (rmp #2292).

A slot claimed but not yet stamped

A reader that has taken its bit but not yet stored its timestamp leaves the slot reading zero, which is not a timestamp. The only sound response is to hold everything back, exactly as Horizon.EnterHolding does for the same reason: the arriving reader's start timestamp is not yet known, and assuming anything about it would be assuming in the unsafe direction. The window is one store wide, and since Horizon.Leave invalidates the timestamp it is the ONLY state in which an occupied slot reads zero — so this branch is what makes the returned watermark either exact or "reclaim nothing", never a stale value from a previous occupant. That is the property [MVCCStats.WatermarkRegressions] rests on.

func (*Horizon) Publish

func (h *Horizon) Publish(slot int, startTS uint64)

Publish announces the start timestamp of a reader that claimed its slot with Horizon.EnterHolding. It is a no-op for a reader that got no slot, which is already holding reclamation back through Horizon.Unregistered.

func (*Horizon) SlotState

func (h *Horizon) SlotState(slot int) (startTS uint64, occupied bool)

SlotState reports the start instant a slot currently announces and whether it is still occupied, for a caller that must verify a reader is still represented in the watermark.

The instant is decoded: it is what Horizon.Oldest would contribute for this slot, so a reader can compare it against its OWN start timestamp. A slot claimed but not yet published reads as (0, true), which is the hold-everything state. An unregistered slot reads as (0, false).

Two atomic loads, no locks and no writes. It exists because the invariant "my slot still holds MY instant for as long as I am reading" is the one the whole reclamation design rests on, and until this accessor existed it could only be checked from inside this package.

Safe for concurrent use.

func (*Horizon) StaleLeaves

func (h *Horizon) StaleLeaves() int64

StaleLeaves reports how many times a slot was released whose occupancy bit was already clear.

It must be ZERO. A non-zero value means a horizon slot was returned twice, or a slot number was released by something that never claimed it, and the consequence is an Isolation violation rather than a leak: the next release lands on ANOTHER reader's bit, that reader stops being counted by Horizon.Oldest, and the reclamation watermark advances past an instant it can still reach.

Exported so the invariant is observable from outside the package — a test or an operator can assert it directly instead of inferring it from a torn read.

Safe for concurrent use.

func (*Horizon) Unregistered

func (h *Horizon) Unregistered() int64

Unregistered reports how many active readers failed to get a slot.

Non-zero means reclamation is suspended. It is exported so that state is diagnosable rather than presenting as an unexplained memory growth.

type Tx

type Tx struct {
	// contains filtered or unexported fields
}

Tx names the write transaction one write belongs to, as carried by the write itself.

The ZERO VALUE carries no transaction, which every store must read as "this write is not transactional": a direct Go-API mutation, committed the instant it is made. That reading is the correct one rather than a concession — such a call is per-operation atomic by contract, has no snapshot to be stale against and shares no commit instant with anything else.

It is ONE word and is passed by value, so threading it through a write path costs no allocation and no indirection beyond the one the shared record already requires.

Safe for concurrent use; it is an immutable handle onto state that is itself safe for concurrent use.

func NewTx

func NewTx(st *TxState) Tx

NewTx returns the handle for the transaction whose stamping state is st, or the zero handle when st is nil.

func (Tx) ID

func (tx Tx) ID() uint64

ID returns the identity of the transaction tx names, or zero.

It is stable from the transaction's FIRST write, because TxState.Arm stores it when the window opens — which is what a per-(shard, transaction) decision such as the adjacency's builder-reuse test needs, and what the commit RECORD cannot offer, since that is allocated lazily by the first version.

func (Tx) Record

func (tx Tx) Record() *CommitInfo

Record returns the shared commit record the version being created right now must point at, allocating the transaction's record if this is its first version and counting that version.

It returns nil in two cases, which a caller must treat identically — as "this write is not in a transaction", stamping it with a fresh commit timestamp of its own:

  • tx is the zero value, so there is no transaction;
  • tx names a transaction whose window has already been retracted, which is a caller retaining the handle past its bracket. Stamping with the retracted record would give the version a commit timestamp in the PAST and make it visible to snapshots that predate the write; a fresh timestamp is later than the write actually happened, which is the safe direction. See the WriteStamp file comment.

Called once per version created, never on a read.

func (Tx) Valid

func (tx Tx) Valid() bool

Valid reports whether tx names a transaction.

It is false for the zero value, which is what an untransacted write presents.

type TxState

type TxState struct {
	// contains filtered or unexported fields
}

TxState is ONE write transaction's stamping state: the commit record its versions share, and how many of them there are.

It belongs to the transaction, not to the graph. Two concurrent writers hold two distinct values and neither can observe the other's — which is the whole point of rmp #2301 and what a per-graph field could not give.

The zero value is not armed. Arm it with TxState.Arm, hand it to WriteStamp.Publish so untransacted writes can find it, and close it with WriteStamp.End or TxState.Retract.

Safe for concurrent use.

func (*TxState) Arm

func (st *TxState) Arm(txID uint64) bool

Arm opens st's stamping window for the transaction identified by txID, and reports whether it could be opened.

It fails, returning false, when st still holds a record — which means it was retracted by nobody, or a late writer allocated one into it after its owner finished. Such a state must NOT be recycled: a version pointing at that record would publish with the wrong transaction. A caller reusing pooled state takes a fresh one instead.

Arming allocates nothing; the record appears when the first version asks for it.

func (*TxState) Ensure

func (st *TxState) Ensure() *CommitInfo

Ensure returns the record the version being created right now must point at, allocating it if this is the first version of the transaction, and counts that version.

It returns nil when st has no open window, which a caller must treat as "this write is not in a transaction" and stamp with a fresh commit timestamp of its own. See the file comment for why a retracted window may not answer with its own past record.

Called once per version created, never on a read.

func (*TxState) OpenRecord

func (st *TxState) OpenRecord() *CommitInfo

OpenRecord returns st's record WITHOUT allocating one and without counting a version, or nil when st has no open window or has not needed a record yet.

It is TxState.Ensure for a caller that wants the record only as an IDENTITY — to ask "which transaction is writing?" rather than "give me something to stamp a version with". See WriteStamp.OpenInfo.

func (*TxState) Retract

func (st *TxState) Retract() (*CommitInfo, int64)

Retract closes st's window and returns the record its versions share, together with how many of them there are.

A nil record means the transaction created no version, so there is nothing to publish and nothing to reclaim. The caller publishes the record — Retract deliberately does not, because only the caller knows whether the commit timestamp must be allocated before or after some other step.

func (*TxState) Reusable

func (st *TxState) Reusable() bool

Reusable reports whether st can be armed for a new transaction — the test TxState.Arm makes, without arming.

A recycling caller needs it separately because it must decide whether to keep a pooled value BEFORE it has a transaction id to arm with.

func (*TxState) TxID

func (st *TxState) TxID() uint64

TxID returns the identity of the transaction currently armed on st, or zero.

type WriteCounters

type WriteCounters struct {
	// contains filtered or unexported fields
}

WriteCounters counts write-transaction outcomes without a shared cache line.

The zero value is ready to use. Safe for concurrent use.

func (*WriteCounters) Abort

func (c *WriteCounters) Abort(txID uint64)

Abort records that the transaction identified by txID was refused publication.

It must be called for EVERY refused transaction, including one that versioned nothing before it was doomed: Commits and Aborts partition the outcomes, and a failure the substrate does not count is a failure an operator cannot see.

func (*WriteCounters) BeginWriter

func (c *WriteCounters) BeginWriter(txID uint64)

BeginWriter records that the transaction identified by txID has opened.

func (*WriteCounters) Commit

func (c *WriteCounters) Commit(txID uint64)

Commit records that the transaction identified by txID published its versions.

func (*WriteCounters) Conflict

func (c *WriteCounters) Conflict(storeIdx int)

Conflict records one doomed transaction, attributed to store.

It takes the store's INDEX rather than its name so the caller pays the table scan once and this function pays nothing; see ConflictStoreIndex.

func (*WriteCounters) EndWriter

func (c *WriteCounters) EndWriter(txID uint64)

EndWriter records that the transaction identified by txID has closed, whatever its outcome. It must be called exactly once for each WriteCounters.BeginWriter and with the same id, or the writer gauge drifts.

func (*WriteCounters) Load

func (c *WriteCounters) Load() WriteCounts

Load sums every stripe and returns the current reading.

It touches all [counterStripes] cache lines, so it belongs off every request path — the vacuum's metrics publication and an explicit stats call are its only callers. See the file comment for what a striped sum guarantees.

Safe for concurrent use.

func (*WriteCounters) Writers

func (c *WriteCounters) Writers() int64

Writers returns just the in-flight write-transaction count.

Separate from WriteCounters.Load because it is the one field a caller may legitimately want on its own — a shutdown path checking that nothing is still writing — and it reads a third of the lines.

Safe for concurrent use.

type WriteCounts

type WriteCounts struct {
	// Writers is how many write transactions are in flight.
	Writers int64
	// Commits and Aborts are the two OUTCOMES, and they partition the transactions
	// the substrate reached a decision about: Commits published an instant, Aborts
	// were refused publication. Cumulative and never reset, so two observers cannot
	// take them from each other.
	//
	// A transaction that versioned nothing and hit no conflict is in NEITHER, and
	// that is not an omission: it published no instant, so counting it as a commit
	// would put commits above the number of instants the clock ever allocated, and
	// it refused nothing, so counting it as an abort would invent a failure. There
	// was no decision to record.
	Commits uint64
	Aborts  uint64
	// Conflicts is how many write transactions were refused for a write-write
	// conflict, and ByStore attributes them to the store the first refusal came
	// from. Indexed by [ConflictStoreIndex].
	//
	// It is a CAUSE, not an outcome, so it is a SUBSET of Aborts rather than a third
	// bucket beside them — the same relationship PostgreSQL's pg_stat_database has
	// between xact_rollback and its conflict counters, where a transaction killed by
	// a conflict appears in both. Aborts says how many transactions failed;
	// Conflicts says how many of them failed for this reason.
	//
	// One per DOOMED TRANSACTION, not one per refused write: a doomed transaction
	// meets its conflict again on every write it still attempts, and a count that
	// scaled with transaction size could not be divided by Commits.
	Conflicts uint64
	ByStore   [ConflictStoreCount]uint64
}

WriteCounts is a point-in-time reading of WriteCounters.

func (*WriteCounts) ConflictRate

func (c *WriteCounts) ConflictRate() float64

ConflictRate returns conflicts as a fraction of the transactions that reached an outcome, or zero when none has.

The denominator is Commits+Aborts and NOT Commits alone: a workload in which every transaction conflicts would otherwise divide by zero and report no contention at all. Conflicts is not added to it, because a conflicting transaction is already counted in Aborts and adding it would deflate its own rate.

type WriteStamp

type WriteStamp struct {
	// contains filtered or unexported fields
}

WriteStamp resolves how the version records a write creates are timestamped when the write does not carry its own transaction.

Between [WriteStamp.Begin] and WriteStamp.End it names the open transaction's TxState, and every version resolved through it takes that transaction's shared record. Outside that window each version takes a fresh commit timestamp of its own, which is the correct reading of a direct mutation made outside any transaction: it is committed the instant it is made.

The zero value has no clock and stamps everything zero — visible to every reader — which is what an unversioned store wants. Attach a clock with WriteStamp.SetClock.

Safe for concurrent use.

func (*WriteStamp) AmbientResolutions

func (w *WriteStamp) AmbientResolutions() int64

AmbientResolutions returns the running count without resetting it, for a caller that must observe the counter from two places — a gate that samples before and after a region rather than owning the counter outright.

func (*WriteStamp) Armed

func (w *WriteStamp) Armed() bool

Armed reports whether a transaction is currently open on this stamp.

The higher layer uses it to tell "I am inside the barrier" from "I am a bare mutator", because the two need different reclamation treatment: the first is swept when the transaction closes, the second has to arrange its own.

func (*WriteStamp) Clock

func (w *WriteStamp) Clock() *Clock

Clock returns the attached clock, or nil.

func (*WriteStamp) End

func (w *WriteStamp) End() (*CommitInfo, int64)

End closes the window this stamp names and returns the record the transaction's versions share, together with how many of them there are.

See TxState.Retract, which does the work; End additionally clears the slot, so an untransacted write arriving afterwards is stamped as what it is.

It is correct ONLY while at most one transaction is open at a time, because it closes whichever transaction the slot happens to name rather than the caller's own. A caller that can overlap another writer must use WriteStamp.EndFor; see it for what the difference costs.

func (*WriteStamp) EndFor

func (w *WriteStamp) EndFor(st *TxState) (*CommitInfo, int64)

EndFor closes the window of the transaction the CALLER owns and returns the record its versions share, together with how many of them there are. It clears the slot only if it still names st, so a writer that published later keeps its own window open.

Why this exists and [WriteStamp.End] is not enough

End takes whichever transaction the slot names. While the visibility barrier admitted one writer at a time that was always the caller's own, and rmp #2301 left it that way on purpose: the slot's contract was only ever "whichever arrived last". rmp #2304 lets two write brackets overlap, and then End is audit finding E3 in its last remaining form — the same silent loss the state moved onto TxState to prevent, one level up:

writer A  Publish(&A.tx)
writer B  Publish(&B.tx)          the slot now names B
writer A  End                     retracts B: takes B's record and count
writer B  End                     the slot is nil — retracts nothing

A publishes B's record at A's commit timestamp, so B's writes become visible at the wrong instant and B's own versions keep an in-flight transaction id for ever: invisible to every reader, unreclaimable by every reclaimer. And, as with E3, every field involved is atomic, so -race is silent on it.

Clearing the slot conditionally is the second half. An unconditional clear would leave an overlapping writer's untransacted writes — every write the Cypher engine makes resolves the transaction through this slot — stamped as though no transaction were open, so one statement's mutations would take a fresh timestamp each and stop being atomically visible.

Pinned by TestWriteStamp_EndForClosesOnlyItsOwnTransaction.

func (*WriteStamp) Info

func (w *WriteStamp) Info() *CommitInfo

Info returns the record of the open transaction, allocating it if this is the first caller to need one, or nil when no transaction is open.

It is WriteStamp.Stamp for a caller that only ever wants the record form — lpg's delta chains, which have no use for an inline timestamp when a transaction is open.

func (*WriteStamp) OpenInfo

func (w *WriteStamp) OpenInfo() *CommitInfo

OpenInfo returns the record of the open transaction WITHOUT allocating one and without counting a version, or nil when no transaction is open or its record has not been allocated yet.

It is WriteStamp.Info for a caller that wants the record only as an IDENTITY — to ask "which transaction is writing?" rather than "give me something to stamp a version with". The adjacency uses it to decide whether a shard's private slot-array builder belongs to the transaction now writing (rmp #2301); getting nil there is not a problem, it just means clone rather than mutate in place, which is what a transaction's first version would do anyway.

The distinction matters because WriteStamp.Stamp and WriteStamp.Info both have side effects — they allocate the shared record on first use and add to the version count — and an identity check must have neither, or a write that records nothing would be charged a version and an untransacted write would be handed a commit timestamp it never uses.

func (*WriteStamp) OpenTxID

func (w *WriteStamp) OpenTxID() uint64

OpenTxID returns the identity of the transaction currently open on this stamp, or zero when none is.

It is WriteStamp.OpenInfo for a caller that wants only the transaction's IDENTITY and needs it to be stable from the transaction's FIRST write. OpenInfo cannot offer that: the commit record is allocated lazily by the first version that needs one, so a caller asking before then gets nil and a caller asking after gets a record — two different answers within one transaction.

The id has no such gap. TxState.Arm stores it when the window opens, before any write can happen, which is what rmp #2299 minted it eagerly for. So a per-(shard, transaction) decision keyed on this is stable for the whole transaction, where the same decision keyed on the record is not.

It allocates nothing and counts no version.

func (*WriteStamp) Publish

func (w *WriteStamp) Publish(st *TxState)

Publish names st as the transaction that untransacted writes resolve to.

st must already be armed (TxState.Arm); arming and publishing are separate so neither has a failure mode a caller can ignore — Arm can refuse a recycled state, and a Publish that armed internally would either swallow that or leave the caller to unpick a half-opened window.

It allocates nothing. The caller owns st and must close the window with exactly one WriteStamp.End.

Concurrent Publish calls are ordinary since rmp #2320. Each writer arms its OWN state, so no record and no count is lost; the slot names whichever arrived last, and that is the only thing the slot has ever promised — a write that needs its own transaction must CARRY it rather than look it up, which is what Tx is for.

func (*WriteStamp) SetClock

func (w *WriteStamp) SetClock(c *Clock)

SetClock attaches the clock that mints commit timestamps.

Must be called before any write and never concurrently with another operation.

Not safe for concurrent use.

func (*WriteStamp) Stamp

func (w *WriteStamp) Stamp() (*CommitInfo, uint64)

Stamp returns how the version being created right now records its visibility: the open transaction's shared record, or nil with a fresh commit timestamp when no transaction is open.

It is the AMBIENT resolution — it answers with whichever transaction the slot names, which is the caller's own only while at most one write bracket is open at a time. A write that can overlap another writer must carry its transaction and call Tx.Record instead; see Tx for what adopting a concurrent transaction's record measured. Every resolution through here is counted, so that separation is testable rather than asserted (WriteStamp.TakeAmbient).

Called once per version created, never on a read.

func (*WriteStamp) TakeAmbient

func (w *WriteStamp) TakeAmbient() int64

TakeAmbient returns how many versions have resolved their transaction through this stamp's SLOT — rather than carrying it — since the last call, and resets the counter.

A write path that carries its transaction must leave it at zero. That is the assertion rmp #2320's acceptance rests on, and it is a direct observation rather than an inference from chain shapes: a single ambient resolution inside a statement is enough to split that statement across two commit records once a second bracket is open.

Reads and resets, so exactly one consumer may use it at a time.

func (*WriteStamp) TakeUntracked

func (w *WriteStamp) TakeUntracked() int64

TakeUntracked returns how many versions have been stamped outside any transaction since the last call, and resets the counter.

func (*WriteStamp) UntransactedStamp

func (w *WriteStamp) UntransactedStamp() (*CommitInfo, uint64)

UntransactedStamp returns a fresh commit timestamp for a version that belongs to no transaction, BYPASSING the slot entirely.

Two callers need it. A write that carries no transaction at all and never had one — WriteStamp.Stamp's fallback. And a write that DOES carry a transaction whose window has since been retracted: it must not fall back to the ambient slot, because the slot may name a live concurrent transaction and adopting that record is precisely the defect rmp #2320 removed.

The timestamp is published immediately, because an untransacted write is committed the instant it is made: there is no record for a reader to find in flight.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL