Help us shape the future of Go! Take the 2026 Go Developer Survey

rowgate

package
v0.56.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package rowgate keeps a storage's catalogue judgements apart from the changes filex makes to that storage in two steps.

The two steps

A rename, a move, a delete into the trash and a restore from it change a storage first and the catalogue after: the bytes move (Mover.Move, trash.Put), then the rows follow (protocolsync.MoveRows, SoftDeleteAndRetag, RestoreNodeAt). Between the two, the catalogue says one thing and the storage another, and nothing on either side can tell that apart from a change made outside filex.

The judgements

The storage sync reads exactly that difference. A walk lists a directory and turns what it sees into rows; the tombstone pass takes a row the walk did not see, asks the storage for its object, and drops the row when the answer is "not found". Run inside the gap, both are wrong about a row that is fine:

  • a folder listed in its parent just before the rename, then listed itself just after it, looked empty, so everything below it looked gone (issue #192: a queued folder rename opened empty in the explorer, e2e 159 and 172 under load);
  • a row whose object had already moved, but which still named the old path, was confirmed gone by a Stat of that path and dropped - with its shares, versions and comments - a moment before the rename re-homed it;
  • a listing taken after the bytes moved but before the rows did would find an object with no row and catalogue it as a NEW file, which would then hold the name the rename's own row is moving to.

The gate

So each storage has one gate. A two-step change holds it SHARED (Move, MoveCtx) from the first byte it moves to the last row it writes; any number of them run at once. A judgement holds it ALONE (JudgeCtx): the walk while it lists one directory and applies that listing, the tombstone pass while it confirms and drops one batch of rows, the lazy catalogue while it reconciles one folder. A judgement therefore sees a storage either before a change or after it, never half way.

Changes first, judgements step back

The gate favours the changes. A judgement never queues ahead of them: it takes the gate only while no change holds it, and while one does it steps back and waits for the gate to come free (on its context, and within the budget its caller gives it: JudgeWithin), without keeping any change out. A change waits only while a judgement actually HOLDS the gate - one directory, one batch of rows, one folder - and, through MoveCtx, on the context of whoever asked, so a client that gives up while it waits takes nothing. Up to 0.54 the gate was a sync.RWMutex, which favours the writer: one judgement waiting behind a long change (a folder moved or deleted object by object over WebDAV or SFTP) kept every later change on that storage out, and the operations queue's one worker with them, until the long change finished. Now a long change only postpones the storage's scan, and says so (Snapshot, the filex_rowgate_* metrics, a warning once it has held the gate for LongHold).

The other side of that choice: a storage under a steady stream of overlapping changes is not judged until the stream pauses. Its scan is deferred, never wrong.

Fences

A folder moved or trashed object by object (an object store) is the long change that matters, and since 0.55 it does not hold the gate at all: it fences the folder's prefixes (FenceCtx, HoldCtx; fence.go says why that is enough and how the two are ordered). Judgements read the fence set once they hold the gate (Fences) and leave those prefixes alone; the rest of the storage is scanned as usual.

⚠ Never hold Move and ask for Judge on the same goroutine: the judgement waits for every change to let go, its own caller's included. Take the gate at the outermost step of the change (the queue's job, the request handler, a protocol server's verb through protocolsync's Relocate, Discard and Purge), not inside the shared helpers it calls, and never twice for one change. Every surface that changes a storage in two steps holds it since 0.55 (issue #201; docs/ARCHITECTURE.md "The row gate" lists them). Take it before opening a database transaction, never inside one: on SQLite a transaction holds the only connection, and the other side may be waiting for that connection while it holds the gate.

The gate is per process. Two filex processes over one database (a replica beside the main instance) do not see each other's gate; what narrows the gap there is that the tombstone pass reads a row again before it judges it and again before it drops it (internal/sync stillAsListed).

Index

Constants

This section is empty.

Variables

View Source
var ErrBusy = errors.New("rowgate: the storage is being changed; its judgement is deferred")

ErrBusy is a judgement that stepped back for as long as its caller allowed (JudgeWithin) and found the gate held by a change all that time. The caller defers what it was going to judge; nothing was judged.

View Source
var LongHold = time.Minute

LongHold is how long a change may hold the gate before it is reported: a warning when it lets go, and filex_rowgate_long_changes_total. The gate is not taken from it - a change finishes what it started - but the storage's scan has waited that long.

Functions

func Await added in v0.55.0

func Await(ctx context.Context, storageID int64) error

Await waits on ctx until no judgement holds storageID's gate, and takes nothing. A change whose first step runs outside the gate - a copy to another storage before the source is deleted under it - asks it before that step, so a request that goes while a judgement holds the gate starts nothing.

func Change

func Change(storageID int64, move func() error, follow func()) error

Change runs one two-step change under storageID's gate (Move): move, the storage side, and then follow, the catalogue side, which runs only when move succeeded. It answers move's error.

func ChangeCtx added in v0.55.0

func ChangeCtx(ctx context.Context, storageID int64, move func() error, follow func()) error

ChangeCtx is Change waiting for the gate on ctx (MoveCtx): when ctx ends first, neither move nor follow runs and it answers ctx's error.

func Clean added in v0.55.0

func Clean(p string) string

Clean is the form fences are kept and compared in: rooted, no trailing slash, no "." or ".." ("a/b/" and "/a/b" are one prefix).

func FenceCtx added in v0.55.0

func FenceCtx(ctx context.Context, storageID int64, prefixes ...string) (release func(), err error)

FenceCtx fences prefixes of storageID for one long change: call it before the first object moves and call release once the last row has followed (release is safe to call more than once; only the first call counts, and it counts as a finished change, Moves). It waits, on ctx, only while a judgement holds the gate; a ctx that ends first takes nothing and answers its error and a release that does nothing. While the fence stands, judgements take the gate and leave the prefixes alone (Fences); no change waits for it.

Empty prefixes are ignored; with none left, or with the storage's root among them, it is MoveCtx.

func FencedChangeCtx added in v0.55.0

func FencedChangeCtx(ctx context.Context, storageID int64, fence []string, move func() error, follow func()) error

FencedChangeCtx is ChangeCtx for a change that holds HoldCtx(ctx, storageID, fence...): move, then follow when move succeeded, and the hold let go however they end - a panic included.

func HoldCtx added in v0.55.0

func HoldCtx(ctx context.Context, storageID int64, fence ...string) (release func(), err error)

HoldCtx holds what one two-step change on storageID needs, and is where the choice between the gate and a fence is made: with fence empty (a short change - one file, a storage with real folders) it is MoveCtx, the gate; with fence (a long change - a folder moved or trashed object by object: its path and its destination's) it is FenceCtx, those prefixes only. It never takes both.

func Judge

func Judge(storageID int64) (release func())

Judge is JudgeCtx with no end: it waits until the gate is free. Tests use it; the storage sync judges within a budget (JudgeWithin).

func JudgeCtx added in v0.55.0

func JudgeCtx(ctx context.Context, storageID int64) (release func(), err error)

JudgeCtx holds storageID's gate alone for one judgement of the catalogue against the storage. It tries the gate, and while a change holds it, steps back until the gate is free - without keeping any change out in the meantime - or ctx ends, which it answers (and a release that does nothing). release is safe to call more than once.

func JudgeWithin added in v0.55.0

func JudgeWithin(ctx context.Context, storageID int64, wait time.Duration) (release func(), err error)

JudgeWithin is JudgeCtx for at most wait: a judgement still stepped back when wait has passed gives up with ErrBusy (ctx's own end is answered as ctx's error). The storage sync judges this way, and defers what it could not judge to its next pass.

func Judged added in v0.55.0

func Judged(storageID int64) bool

Judged reports whether a judgement holds storageID's gate right now: a change asking for it now would wait. The operations queue asks it before it starts a job, and puts the job back rather than wait.

func Move

func Move(storageID int64) (release func())

Move is MoveCtx for a change nobody can call off while it waits (the operations queue's job, which has no client): it waits for as long as a judgement holds the gate.

func MoveCtx added in v0.55.0

func MoveCtx(ctx context.Context, storageID int64) (release func(), err error)

MoveCtx holds storageID's gate for one two-step change: call it before the first byte moves and call release once the last row has followed (release is safe to call more than once; only the first call counts). Any number of changes hold the gate at once, and a judgement waiting for it keeps none of them out; a change waits only while a judgement holds the gate.

It waits on ctx - the request's own context, before the change detaches from the client (storage.DetachMutation): a client that has gone, or goes while the change waits, takes nothing, and the change is not made. It then answers ctx's error and a release that does nothing.

func Moves

func Moves(storageID int64) uint64

Moves is how many two-step changes (Move) have finished on storageID since the process started. A walk that read the whole storage up front (an object store's one-pass listing) compares it with the count it read BEFORE that listing: once it differs, the listing in memory may predate a change whose rows have already moved, and the walk asks the storage again instead.

func TryJudge added in v0.55.0

func TryJudge(storageID int64) (release func(), ok bool)

TryJudge holds storageID's gate alone if nothing holds it now, and reports whether it did. It never waits.

func TryMove added in v0.55.0

func TryMove(storageID int64) (release func(), ok bool)

TryMove holds storageID's gate for one change if no judgement holds it now, and reports whether it did. It never waits.

Types

type FenceSet added in v0.55.0

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

FenceSet is the prefixes fenced on one storage at one moment. It is never changed once built: Fences hands out the current one. A nil set fences nothing.

func Fences added in v0.55.0

func Fences(storageID int64) *FenceSet

Fences answers storageID's fence set as it stands: a lock-free load. A judgement asks it once it holds the gate (JudgeCtx), so a fence it does not see was not set before the judgement began, and cannot be until it ends.

func (*FenceSet) Covers added in v0.55.0

func (f *FenceSet) Covers(p string) bool

Covers reports whether p is a fenced prefix or below one. With nothing fenced it costs nothing; else one comparison per fenced prefix (a long change fences two).

func (*FenceSet) Empty added in v0.55.0

func (f *FenceSet) Empty() bool

Empty reports whether nothing is fenced.

func (*FenceSet) Kids added in v0.55.0

func (f *FenceSet) Kids(dir string) []string

Kids answers dir's fenced children, as cleaned full paths (one map lookup): what a listing of dir must leave out.

type State added in v0.55.0

type State struct {
	StorageID int64
	// Changes is how many two-step changes hold the gate now; OldestChange
	// is how long the one that took it first has held it (0 with none).
	Changes      int
	OldestChange time.Duration
	// Judged: a judgement holds the gate now.
	Judged bool
	// Fences is how many prefixes long changes fence now (FenceCtx), and
	// OldestFence how long the fence that came first has stood (0 with
	// none).
	Fences      int
	OldestFence time.Duration
	// LongChanges counts the changes that held the gate for LongHold or
	// more; DeferredJudgements the judgements that stopped waiting for it.
	// Both since the process started.
	LongChanges        uint64
	DeferredJudgements uint64
}

State is one storage's gate as Snapshot reads it.

func Snapshot added in v0.55.0

func Snapshot() []State

Snapshot reads every storage's gate, ordered by storage id (the metrics publish it at scrape time: internal/metrics).

Jump to

Keyboard shortcuts

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