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 ¶
- Variables
- func Await(ctx context.Context, storageID int64) error
- func Change(storageID int64, move func() error, follow func()) error
- func ChangeCtx(ctx context.Context, storageID int64, move func() error, follow func()) error
- func Clean(p string) string
- func FenceCtx(ctx context.Context, storageID int64, prefixes ...string) (release func(), err error)
- func FencedChangeCtx(ctx context.Context, storageID int64, fence []string, move func() error, ...) error
- func HoldCtx(ctx context.Context, storageID int64, fence ...string) (release func(), err error)
- func Judge(storageID int64) (release func())
- func JudgeCtx(ctx context.Context, storageID int64) (release func(), err error)
- func JudgeWithin(ctx context.Context, storageID int64, wait time.Duration) (release func(), err error)
- func Judged(storageID int64) bool
- func Move(storageID int64) (release func())
- func MoveCtx(ctx context.Context, storageID int64) (release func(), err error)
- func Moves(storageID int64) uint64
- func TryJudge(storageID int64) (release func(), ok bool)
- func TryMove(storageID int64) (release func(), ok bool)
- type FenceSet
- type State
Constants ¶
This section is empty.
Variables ¶
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.
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
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 ¶
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
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
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
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
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
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
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
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 ¶
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.
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
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
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).
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.