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

vaultlock

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: 17 Imported by: 0

Documentation

Overview

Package vaultlock is the vault's write lock (encryption level 3, docs/E2E-VAULT-FORMAT.md → The write lock): at most one session writes a vault, and the server is what says which.

The rules, all decided here and nowhere else:

  • a lock belongs to the key file's vault.id within its tenant (Key), not to a path: renaming the vault folder keeps it, and a copy of a vault made on the server shares it;
  • it is HELD while the lease has not run out (60 seconds, renewed by the heartbeat and by every vault write) AND the holder is not idle (a vault write or an active renewal within the person's idle time, 1 to 10 minutes, 3 by default, read when the lock is taken);
  • it is free again the moment it ends - released, run out, idle or broken - UNLESS an index write of its holder is still running: then it is free when that write ends, or 60 seconds after it began;
  • the token is 32 random bytes, handed out once, kept only as its SHA-256.

The state lives in the database (vault_locks, migration 00096), not in this process: filex can run more than one process on one database, and every decision below reads the row and writes it back by compare-and-set on its revision. Nothing is cached. The clock and the random source can be replaced, so a test can walk a lock through a lease without waiting.

A lock that ends because time ran out has nobody to say so: its row is closed - and OnEnded told, for the audit row and the realtime event - by the next call that finds it ended (a renewal, a take, a state read).

Index

Constants

View Source
const (
	Lease              = 60 * time.Second
	Heartbeat          = 15 * time.Second
	IndexWriteMax      = 60 * time.Second
	DefaultIdleMinutes = 3
	MinIdleMinutes     = 1
	MaxIdleMinutes     = 10
	// TokenBytes is the size of a lock token before encoding.
	TokenBytes = 32
)

The format's numbers (docs/E2E-VAULT-FORMAT.md → Lock semantics).

View Source
const (
	ReasonReleased = "released"
	ReasonExpired  = "expired"
	ReasonIdle     = "idle"
	ReasonBroken   = "broken"
	ReasonTaken    = "taken"
	// ReasonLockedIdle is what a client adds when it locked the vault itself
	// after 15 minutes of nothing (docs/E2E-VAULT-FORMAT.md → The idle lock)
	// while it held the write lock: a release, recorded with this reason.
	ReasonLockedIdle = "locked_idle"
)

Why a lock ended - the `reason` of VAULT_LOCK_LOST and of the vault.unlock audit row. Taken is said only to the holder of a lost lock that somebody else holds now.

Variables

View Source
var Clients = map[string]bool{"web": true, "desktop": true, "cli": true, "mount": true}

Clients a lock may name.

View Source
var ErrContention = errors.New("the vault's lock row kept changing; try again")

ErrContention is a row that kept changing under every attempt to write it.

View Source
var ErrIdleRange = fmt.Errorf("the idle time is %d to %d minutes", MinIdleMinutes, MaxIdleMinutes)

ErrIdleRange refuses an idle time outside 1 to 10 minutes.

View Source
var ErrIndexRunning = errors.New("an index write of this lock is still running")

ErrIndexRunning refuses a second index write while one of the holder's is still running.

View Source
var ErrLocked = errors.New("the vault is locked by another session")

ErrLocked: somebody else holds the lock (*LockedError says who).

View Source
var ErrLost = errors.New("the vault's write lock was lost")

ErrLost: the token does not hold the lock (*LostError says why).

Functions

func HashToken

func HashToken(token string) (string, bool)

HashToken is the stored form of a token: the hex SHA-256 of its 32 bytes. ok is false for a string that is not a token.

Types

type Ended

type Ended struct {
	Key
	Place
	Holder Holder
	// Reason: released, expired, idle, broken or locked_idle.
	Reason string
	// By is who broke the lock (Reason broken); zero otherwise.
	By       Holder
	FirstGen int64
	LastGen  int64
	Since    time.Time
	At       time.Time
}

Ended is a lock that ended, told once to OnEnded by the call that closed its row.

type Finder

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

Finder answers which vault folder holds a path - the question every write door asks through writegate (acl.Resolver.AttachVaults): inside a vault, only the vault API writes.

A path is in a vault when the encrypted folder it sits in (e2e.FindRoot: the nearest folder whose key file is in the catalogue) has a key file that names the vault feature (e2e.ParseVaultKeyFile). The key file is read from the storage once per version of it: the answer is kept, keyed by the key file's row as the catalogue has it NOW (size, etag, modification time), so a rewritten key file is read again. What it remembers is a fact about bytes that cannot change - a vault's `v`, `req` and `vault` never do (writegate refuses a rewrite that would) - not a decision another process could have changed.

⚠ It sees what the catalogue sees, as e2e.FindRoot does: a vault whose key file has no row yet (a lazily catalogued folder nobody opened) is not recognised until it has one.

func NewFinder

func NewFinder(nodes e2e.NodeByPathLookup, drivers func(int64) (storage.Driver, error)) *Finder

NewFinder builds a Finder over the catalogue and the storage drivers.

func (*Finder) Vault

func (f *Finder) Vault(ctx context.Context, storageID int64, rel string) (root string, info e2e.VaultInfo, ok bool)

Vault is VaultRoot with the vault's key-file block.

func (*Finder) VaultRoot

func (f *Finder) VaultRoot(ctx context.Context, storageID int64, rel string) (string, bool)

VaultRoot is the vault folder rel is inside of, or is ("" for a vault at the storage's root); ok is false when rel is in no vault.

type Holder

type Holder struct {
	UserID int64
	Name   string
	Client string
	Label  string
}

Holder is who holds a lock: the person, the kind of client and its label.

type Key

type Key struct {
	Tenant int64
	Vault  string
}

Key names one vault's lock: its tenant and its id (32 lower-case hex).

type Lock

type Lock struct {
	Holder Holder
	Since  time.Time
	// ExpiresAt is when it ends if nothing more happens: the lease or the
	// idle time, whichever comes first (or the end of a running index write).
	ExpiresAt time.Time
	// contains filtered or unexported fields
}

Lock is a lock as State shows it.

func (*Lock) Mine

func (l *Lock) Mine(token string) bool

Mine reports whether token is this lock's.

type LockedError

type LockedError struct {
	Holder     Holder
	Since      time.Time
	RetryAfter time.Duration
}

LockedError names who holds the lock, since when, and how long until it could be free.

func (*LockedError) Error

func (e *LockedError) Error() string

func (*LockedError) Unwrap

func (e *LockedError) Unwrap() error

type LostError

type LostError struct {
	Reason string
	Holder *Holder
}

LostError says why a token no longer holds its lock: expired, idle, broken, released or taken - and, for taken and broken, who: the session that holds it now, or the person who broke it (only a name: a break names no client).

func (*LostError) Error

func (e *LostError) Error() string

func (*LostError) Unwrap

func (e *LostError) Unwrap() error

type Place

type Place struct {
	StorageID int64
	Path      string
}

Place is where a lock was taken: the vault folder its holder named.

type Renewed

type Renewed struct {
	ExpiresAt time.Time
	IdleUntil time.Time
}

Renewed is a renewal's answer.

type Service

type Service struct {

	// OnEnded is told of every lock this service sees end, once, after the
	// row that records the ending was written. Nil = nobody.
	OnEnded func(ctx context.Context, e Ended)
	// contains filtered or unexported fields
}

Service takes, renews, releases and breaks vault locks.

func New

func New(store Store) *Service

New builds a Service on store with the real clock and crypto/rand.

func (*Service) BeginIndex

func (s *Service) BeginIndex(ctx context.Context, k Key, token string) error

BeginIndex records that token's holder starts an index write: it must hold the lock, no other index write of it may be running (ErrIndexRunning), and the write renews the lease and counts as activity. From here until EndIndex - or 60 seconds - the lock is not free, even if it ends.

func (*Service) Break

func (s *Service) Break(ctx context.Context, k Key, by Holder) (*Ended, error)

Break ends whatever lock k has now, for by (the handler decides who may). broken is the lock that was broken - nil when none was held (a lock that had already run out is closed with its own reason, not broken). Its holder's next call is told `broken`, and by whom.

func (*Service) EndIndex

func (s *Service) EndIndex(ctx context.Context, k Key, token string, gen int64, committed bool) error

EndIndex records the end of token's index write: committed says whether generation gen is now in place (the first and last generation of the lock follow it). The lock it was written under may have ended meanwhile; the record is kept all the same, and a lock still held is renewed by it.

func (*Service) IdleMinutes

func (s *Service) IdleMinutes(ctx context.Context, userID int64) (int, error)

IdleMinutes is the idle time of a person: what they set, else 3.

func (*Service) Now

func (s *Service) Now() time.Time

Now is the service's clock.

func (*Service) Release

func (s *Service) Release(ctx context.Context, k Key, token, reason string) error

Release ends token's lock, if its row still records it: reason released, or locked_idle when the client says it locked the vault itself after 15 minutes of nothing (docs/E2E-VAULT-FORMAT.md → The idle lock) - recorded as said, since that client's write lock has usually ended for idle minutes before and nobody closed the row yet. Any other reason reads as released; a plain release of a lock that had already run out is recorded with the reason it ended for. Releasing a lock the token does not hold does nothing.

func (*Service) Renew

func (s *Service) Renew(ctx context.Context, k Key, token string, active bool) (*Renewed, error)

Renew renews token's lock: the lease starts again, and active also counts as activity (the holder is about to write). A *LostError when the token does not hold the lock any more, with the reason.

func (*Service) SetClock

func (s *Service) SetClock(now func() time.Time)

SetClock replaces the clock (tests).

func (*Service) SetIdleMinutes

func (s *Service) SetIdleMinutes(ctx context.Context, userID int64, minutes int) error

SetIdleMinutes stores a person's idle time: ErrIdleRange outside 1 to 10. A lock already held keeps the idle time it was taken with.

func (*Service) SetRand

func (s *Service) SetRand(r io.Reader)

SetRand replaces the token source (tests).

func (*Service) State

func (s *Service) State(ctx context.Context, k Key) (*Lock, error)

State is k's lock as everybody may see it: nil when the vault is free. A lock found ended is closed here (OnEnded is told). A lock that ended while its holder's index write still runs is shown until that write ends.

func (*Service) Take

func (s *Service) Take(ctx context.Context, k Key, p Place, h Holder) (*Taken, error)

Take gives the lock of k to a new session of h, at p: a *LockedError when it is held, or not free yet because its holder's index write still runs. A lock that ended without anybody closing its row is closed here first.

func (*Service) Touch

func (s *Service) Touch(ctx context.Context, k Key, token string) error

Touch is a vault write by token's holder (a pack stored, files deleted): it must hold the lock, and the write renews the lease and counts as activity.

type Store

type Store interface {
	GetVaultLock(ctx context.Context, tenantID int64, vaultID string) (*model.VaultLock, error)
	InsertVaultLock(ctx context.Context, l *model.VaultLock) (bool, error)
	UpdateVaultLock(ctx context.Context, l *model.VaultLock, rev int64) (bool, error)
	GetVaultIdleMinutes(ctx context.Context, userID int64) (int, error)
	SetVaultIdleMinutes(ctx context.Context, userID int64, minutes int) error
}

Store is what the service needs of the database (db.Store has it, VaultLockSQL).

type Taken

type Taken struct {
	Token        string
	LeaseSeconds int
	IdleSeconds  int
	ExpiresAt    time.Time
	IdleUntil    time.Time
}

Taken is a lock just taken: Token is shown this once.

Jump to

Keyboard shortcuts

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