Documentation
¶
Overview ¶
Package pstore ("pelican store") uses the cache V2 block store as an origin's primary data store rather than as a cache.
The block store underneath local_cache — encrypted 4080-byte blocks, multi-directory spreading, chunking for large objects, and a BadgerDB catalog that is itself encrypted at rest — is a general-purpose object store that happens to be used by a cache. pstore drives it directly, adds the one thing a cache never needed (a directory index), and exposes the result as an afero.Fs so it drops into the origin's existing serving stack.
Objects are immutable: Pelican has no mid-file edits, so a write always creates a complete new version and atomically swaps it in. There is no eviction; when the store fills, writes fail with ErrNoSpace until something is deleted.
See docs/pstore-design.md for the full design.
Index ¶
- Constants
- Variables
- func DeriveBackupKey(issuerKey jwk.Key) ([]byte, error)
- func DeriveFileKey(r io.Reader, keys BackupKeys) ([]byte, error)
- func FormatBackupKey(key []byte) string
- func ParseBackupKey(encoded string) ([]byte, error)
- type BackupConfig
- type BackupKeys
- type Config
- type Dirent
- type EntryType
- type ErrNoMatchingKey
- type ExportOptions
- type ExportReport
- type FS
- func (fs *FS) Chmod(name string, _ os.FileMode) error
- func (fs *FS) Chown(name string, _, _ int) error
- func (fs *FS) Chtimes(name string, _, _ time.Time) error
- func (fs *FS) Create(name string) (afero.File, error)
- func (fs *FS) Mkdir(name string, _ os.FileMode) error
- func (fs *FS) MkdirAll(path string, _ os.FileMode) error
- func (fs *FS) Name() string
- func (fs *FS) Open(name string) (afero.File, error)
- func (fs *FS) OpenFile(name string, flag int, _ os.FileMode) (afero.File, error)
- func (fs *FS) OpenFileConditional(name string, flag int, perm os.FileMode, size int64, requireAbsent bool, ...) (afero.File, error)
- func (fs *FS) OpenFileSized(name string, flag int, perm os.FileMode, size int64) (afero.File, error)
- func (fs *FS) Remove(name string) error
- func (fs *FS) RemoveAll(path string) error
- func (fs *FS) Rename(oldname, newname string) error
- func (fs *FS) Stat(name string) (os.FileInfo, error)
- type FsckOptions
- type FsckReport
- type GCStats
- type IntegrityConfig
- type ListEntry
- type ReadHandle
- type Store
- func (s *Store) AccessTime(name string) (time.Time, error)
- func (s *Store) BackupEncrypted(w io.Writer, keys BackupKeys) (uint64, error)
- func (s *Store) Close() error
- func (s *Store) Create(name string) (*WriteHandle, error)
- func (s *Store) CreateSized(name string, size int64) (*WriteHandle, error)
- func (s *Store) DB() *local_cache.CacheDB
- func (s *Store) Digests(name string) ([]local_cache.Checksum, error)
- func (s *Store) Export(ctx context.Context, opts ExportOptions) (*ExportReport, error)
- func (s *Store) Fsck(ctx context.Context, repair bool) (*FsckReport, error)
- func (s *Store) FsckWith(ctx context.Context, opts FsckOptions) (*FsckReport, error)
- func (s *Store) HasCapacityFor(contentLen int64) error
- func (s *Store) List(name string, after string, limit int) ([]ListEntry, bool, error)
- func (s *Store) Mkdir(name string) error
- func (s *Store) MkdirAll(name string) error
- func (s *Store) OpenRead(name string) (*ReadHandle, error)
- func (s *Store) ReadAll(name string) ([]byte, error)
- func (s *Store) ReadOnly() bool
- func (s *Store) ReclaimAbandonedAppends(minAge time.Duration) (int, error)
- func (s *Store) Remove(name string) error
- func (s *Store) RemoveAll(name string) error
- func (s *Store) Rename(oldName, newName string) error
- func (s *Store) Restore(r io.Reader, keys BackupKeys) error
- func (s *Store) RunGC(ctx context.Context) (GCStats, error)
- func (s *Store) StartBackups(ctx context.Context, egrp *errgroup.Group, cfg BackupConfig)
- func (s *Store) StartIntegrityChecks(ctx context.Context, egrp *errgroup.Group, cfg IntegrityConfig)
- func (s *Store) StartJanitor(ctx context.Context, egrp *errgroup.Group)
- func (s *Store) Stat(name string) (*Dirent, error)
- func (s *Store) Storage() *local_cache.StorageManager
- func (s *Store) Usage() (used, max int64)
- func (s *Store) VerifyObject(name string) (bool, error)
- type WriteHandle
- func (w *WriteHandle) Abort() error
- func (w *WriteHandle) Close() error
- func (w *WriteHandle) Generation() string
- func (w *WriteHandle) Path() string
- func (w *WriteHandle) RequireAbsent()
- func (w *WriteHandle) RequireGeneration(generation string)
- func (w *WriteHandle) Size() int64
- func (w *WriteHandle) Write(p []byte) (int, error)
Constants ¶
const FsckNoGracePeriod = time.Duration(-1)
FsckNoGracePeriod disables the in-flight-write grace period.
Only safe when nothing can be writing to the store -- a test, or an offline pass an operator has taken responsibility for.
Variables ¶
var ( // ErrNotExist is returned when a path has no entry. ErrNotExist = fs.ErrNotExist // ErrExist is returned when a path already has an entry and the caller // required that it not. ErrExist = fs.ErrExist // ErrNotDir is returned when a path component is not a directory. ErrNotDir = syscall.ENOTDIR // ErrIsDir is returned when a directory is used where a file is required. ErrIsDir = syscall.EISDIR // ErrNotEmpty is returned when a non-recursive removal targets a // directory that still has entries. ErrNotEmpty = syscall.ENOTEMPTY // ErrNoSpace is returned when a write would exceed the store's capacity. // // golang.org/x/net/webdav reports any failed write as 405 Method Not // Allowed, so the origin rewrites the status itself: see // origin_serve.statusForBackendError, which turns this into 507 // Insufficient Storage. ErrNoSpace = syscall.ENOSPC // ErrInvalidPath is returned for paths the index cannot represent. ErrInvalidPath = fs.ErrInvalid // ErrNotSupported is returned for operations pstore deliberately does not // implement, such as writing at an arbitrary offset. ErrNotSupported = errors.New("operation not supported by pstore") // ErrDraining is returned when a path is inside a subtree that a // recursive delete has removed but the janitor has not finished // reclaiming. Retrying shortly succeeds; the janitor speeds up while a // backlog exists. origin_serve.statusForBackendError reports it as 409 // Conflict, which is what tells a client the retry is worth making. ErrDraining = errors.New("path is still being reclaimed") // ErrPreconditionFailed is returned when a conditional write's // precondition does not hold -- that is, when Close finds the object no // longer carries the generation the caller required. // origin_serve.statusForBackendError reports it as 412 Precondition // Failed. ErrPreconditionFailed = errors.New("precondition failed") // ErrConflict is returned when two writers raced to install the same path // and this one's index transaction lost. // // The store's index is serializable, so a commit that reads an entry // another commit is replacing is aborted rather than serialized behind it. // Nothing is lost -- the winner's version is installed and the loser's is // discarded -- and retrying succeeds. The WebDAV lock system serializes // same-path PUTs before they reach here, so a client normally sees 423 // Locked instead; this is what a caller using the store directly gets. // origin_serve.statusForBackendError reports it as 409 Conflict rather // than letting an opaque database error surface as a 500. ErrConflict = badger.ErrConflict // ErrClosed is returned when a handle is used after Close. ErrClosed = fs.ErrClosed )
Sentinel errors. These alias the syscall/fs values the afero and WebDAV layers already understand, so that errors.Is(err, fs.ErrNotExist), os.IsNotExist, and the handler's status mapping all work without translation.
Functions ¶
func DeriveBackupKey ¶
DeriveBackupKey returns the level-2 pstore backup private key for one issuer key, which is what `metadata-backup-key` prints for escrow.
The same issuer key always yields the same backup key, so printing it twice gives the same answer -- that is the property that makes escrowing it useful. It opens every snapshot this origin writes; for the key that opens exactly one of them, see DeriveFileKey.
func DeriveFileKey ¶
func DeriveFileKey(r io.Reader, keys BackupKeys) ([]byte, error)
DeriveFileKey returns the level-3 key that opens exactly the archive in r, and nothing else.
It reads only the header, so r may be a file opened purely for this. keys supplies whatever the caller has: the origin's issuer keys, or an escrowed backup key -- either reaches the same answer, because the archive carries its own backup key sealed to both.
func FormatBackupKey ¶
FormatBackupKey renders a raw backup or per-file key the way a key file holds it.
func ParseBackupKey ¶
ParseBackupKey decodes a base64 backup or per-file key and checks its length.
Types ¶
type BackupConfig ¶
type BackupConfig struct {
// Dir receives the snapshot files. Empty disables backups.
Dir string
// Interval between snapshots.
Interval time.Duration
// Keep bounds how many snapshots are retained; zero keeps them all.
Keep int
// Keys seal each snapshot. They are supplied by the caller rather than
// read from configuration here, so this package stays independent of
// config and a test can seal to keys of its own. Backups do not start
// without them: a snapshot is a logical dump and is never written in the
// clear.
Keys BackupKeys
}
BackupConfig describes the periodic metadata backup.
type BackupKeys ¶
type BackupKeys struct {
// IssuerKeys are the origin's current issuer keys, by key ID (level 1).
// The archive's backup key is sealed to the pstore-domain key pair derived
// from each, and any one of them opens it -- so a snapshot written before a
// rotation still opens afterwards, as long as one of the keys it was
// written under survives.
IssuerKeys map[string]jwk.Key
// BackupKey is a raw 32-byte Curve25519 private key from level 2: the
// escrowed key printed by `pelican-server origin pstore metadata-backup-key`, or
// an independent key for backups that must be restorable somewhere that
// will never hold this origin's issuer keys.
//
// When writing, it *replaces* the issuer-derived recipients rather than
// joining them. When reading, it opens the envelope and yields the
// archive's own backup key, from which the per-file key is derived.
BackupKey []byte
// FileKey is one archive's own key (level 3), as printed by
// `metadata-backup-key <file>`. It decrypts that archive's body directly:
// no derivation, no envelope, and no issuer key need be present. It opens
// nothing else, which is the point of having it.
//
// It is read-only. Sealing a new archive requires a backup key to embed,
// and a per-file key is downstream of one.
FileKey []byte
}
BackupKeys names the keys a snapshot is sealed to, or opened with.
The three fields are the three levels of the hierarchy, and on the reading side any one of them is enough. They are passed in rather than read from configuration so that pstore stays free of the config package: the caller resolves the origin's issuer keys and hands them over, and a test supplies whatever keys it likes.
func (BackupKeys) Empty ¶
func (k BackupKeys) Empty() bool
Empty reports whether there is nothing here to *seal* to.
FileKey is deliberately not consulted: it opens one existing archive and cannot write a new one, so a caller holding only that has nothing to back up with.
type Config ¶
type Config struct {
// BaseDir holds the BadgerDB catalog. It is also the first storage
// directory when StorageDirs is empty.
BaseDir string
// StorageDirs are the directories object data is spread across. When
// empty, BaseDir is used as the sole storage directory.
StorageDirs []local_cache.StorageDirConfig
// InlineMaxBytes is the largest object stored directly in the catalog
// rather than on disk. Zero selects local_cache.InlineThreshold.
InlineMaxBytes int
// NamespaceLabel names the store in the catalog's usage counters.
//
// A store is one accounting unit spanning however many exports are mapped
// onto subtrees of it, because capacity is enforced store-wide (§7).
// Per-export accounting arrives with path-based quotas, where the lot
// tree is the natural place for it. Empty selects the store root.
NamespaceLabel string
}
Config describes a store to open.
type Dirent ¶
type Dirent struct {
Type EntryType `msgpack:"t"`
Generation string `msgpack:"g,omitempty"` // files only; also the ETag
Size int64 `msgpack:"s,omitempty"`
MTimeNanos int64 `msgpack:"m"`
Mode uint32 `msgpack:"o"`
}
Dirent is one entry in a directory.
Size, MTimeNanos, and Generation are denormalized from the object's CacheMetadata so that a listing is a single scan: without them, a PROPFIND over a large directory would need one additional point lookup per child. They are rewritten only at commit, in a transaction the write path performs anyway.
type ErrNoMatchingKey ¶
type ErrNoMatchingKey struct {
SealedKeyIDs []string
}
ErrNoMatchingKey reports a snapshot that none of the supplied keys open.
SealedKeyIDs names the keys it *was* sealed to, which is the only thing that tells an operator holding the wrong keyring what to go and find.
func (*ErrNoMatchingKey) Error ¶
func (e *ErrNoMatchingKey) Error() string
type ExportOptions ¶
type ExportOptions struct {
// Root is the subtree to export; empty means the whole store.
Root string
// Dest is the directory to write into. It is created if missing.
Dest string
// Verify checks each object against its recorded checksums as it is
// written, so a recovery does not quietly reproduce corrupt data.
Verify bool
// DryRun reports what would be written without writing it.
DryRun bool
// Resume skips objects already present in the destination instead of
// reporting them.
//
// An export of a large store is interrupted sooner or later -- a full
// disk, a lost session, an operator's Ctrl-C -- and the natural response
// is to run it again. Without this the second run reports every file the
// first one wrote as a failure, which is indistinguishable from real
// corruption at exactly the moment an operator most needs to tell the
// difference.
Resume bool
}
ExportOptions controls a recovery export.
type ExportReport ¶
type ExportReport struct {
// FilesWritten is how many objects were exported.
FilesWritten int
// DirsCreated is how many directories were created.
DirsCreated int
// BytesWritten totals the object content exported.
BytesWritten int64
// Failed lists objects that could not be exported, with the reason.
// A recovery continues past a bad object rather than abandoning the rest.
Failed map[string]string
// AlreadyPresent counts objects skipped because the destination already
// held a file of that name.
//
// Kept apart from Failed on purpose. "The previous run already wrote
// this" and "this object cannot be read" call for opposite responses, and
// a report that conflates them turns a resumed export into a false
// corruption report.
AlreadyPresent int
}
ExportReport summarizes a recovery export.
type FS ¶
type FS struct {
// contains filtered or unexported fields
}
FS adapts a Store to afero.Fs.
func (*FS) Chtimes ¶
Chtimes is not supported: an object's mtime is set when its version is committed.
func (*FS) Create ¶
Create makes (or replaces) an object and returns it open for writing, creating any missing parent directories.
Auto-creating parents is what lets a client PUT into a prefix that does not exist yet, which is how uploads normally arrive -- no explicit MKCOL. The origin has a shared afero wrapper that does this, but it derives the parent with filepath, and a pstore path is a namespace path that is always slash-separated: on Windows the wrapper would ask for a directory literally named "\a\b\c". Doing it here keeps the separator right on every platform.
func (*FS) Mkdir ¶
Mkdir creates a directory. perm is ignored: pstore has no per-entry permissions, since access control is the origin's authorization layer.
func (*FS) OpenFile ¶
OpenFile opens a path with the given flags.
Objects are immutable, so the write flags all mean the same thing: build a new version and swap it in at Close. O_APPEND is refused rather than silently truncating, since there is no way to honor it.
func (*FS) OpenFileConditional ¶
func (fs *FS) OpenFileConditional(name string, flag int, perm os.FileMode, size int64, requireAbsent bool, requireETag string) (afero.File, error)
OpenFileConditional is OpenFileSized with an HTTP conditional-write precondition attached, implementing origin_serve's ConditionalWriteFs.
The precondition is enforced by Close, inside the single transaction that resolves the existing entry and installs the new one, which is the only place it can be enforced without a race. Checking it here, at open time, would be exactly the stat-then-write the caller is trying to avoid: two concurrent "If-None-Match: *" PUTs would both find the path absent and both commit.
requireETag carries the client's entity tag verbatim; an object's tag is its generation, so the tag is unwrapped rather than compared as a string.
func (*FS) OpenFileSized ¶
func (fs *FS) OpenFileSized(name string, flag int, perm os.FileMode, size int64) (afero.File, error)
OpenFileSized is OpenFile with the object's expected length, which lets the write path pick its storage tier immediately instead of buffering to discover it. It implements origin_serve's size-hint interface.
The size is advisory: the object is stored at whatever length actually arrives, so a wrong or absent hint costs nothing but the buffering it would have avoided.
type FsckOptions ¶
type FsckOptions struct {
// Repair acts on what the pass finds rather than only reporting it.
Repair bool
// Deep additionally reads every object back and verifies it against the
// checksums recorded at ingest.
//
// This is the only check that detects at-rest corruption -- a flipped bit
// in a block file, or a truncated chunk -- because everything else reads
// only the catalog. It costs a full pass over the data, so it is off by
// default and belongs on a schedule rather than in a routine check.
Deep bool
// SkipOrphanScan omits the search for object versions no entry refers to.
//
// That search is the only part of a pass whose cost is not streaming: it
// must hold the set of referenced versions while it scans the catalog.
// Everything else -- unreadable entries, interrupted writes, counter
// drift -- is a streaming check. A frequent background pass therefore
// skips it, or covers one slice at a time; an operator running fsck by
// hand wants the whole answer and gets it.
SkipOrphanScan bool
// OrphanShardOf, when greater than one, splits the orphan comparison into
// that many slices of the instance-hash space and examines only
// OrphanShard. Rotating the slice across successive passes gives full
// coverage over time at a fraction of the memory.
//
// It is rounded up to a power of sixteen, because the slices are hex
// prefixes of the instance hash. Rotate over FsckReport.OrphanScanShards
// rather than over this value: they differ whenever it is not already a
// power of sixteen, and rotating over the smaller one would leave most of
// the store permanently unexamined.
OrphanShardOf int
// OrphanShard selects the slice, modulo the effective slice count.
OrphanShard int
// MinAge is how long an unreferenced version must have been complete
// before it counts as an orphan rather than a write still in flight.
//
// Zero selects fsckDefaultMinAge. The zero value is the safe one on
// purpose: a caller that forgets this field gets the grace period, not
// the hazard it exists to prevent. Pass FsckNoGracePeriod to switch it
// off, which only a test with no concurrent writers should do.
MinAge time.Duration
}
FsckOptions selects what a consistency pass checks.
type FsckReport ¶
type FsckReport struct {
// EntriesScanned is how many index entries were examined.
EntriesScanned int
// DirectoriesScanned is how many of those were directories.
DirectoriesScanned int
// DanglingEntries are files whose object version has no metadata. The
// entry is unreadable, so it is reported and, when repairing, removed.
DanglingEntries []string
// MissingData are objects whose metadata exists but whose blocks are
// incomplete, meaning a write was interrupted before it committed.
MissingData []string
// OrphanedInstances are object versions no entry refers to, which have
// been complete for longer than the grace period. They hold space and
// are queued for reclamation when repairing.
OrphanedInstances []string
// PendingInstances are object versions no entry refers to *yet*: a write
// that has not completed, or one that completed within the grace period.
//
// These are reported for visibility and never repaired. A live upload
// looks exactly like a crash-window orphan from the catalog's point of
// view, and reclaiming one would delete an object out from under the
// client still writing it. Reclamation of writes that really were
// abandoned belongs to the append-intent sweep, which can tell the
// difference.
PendingInstances []string
// CorruptObjects are objects whose stored data no longer matches the
// checksums recorded when they were written. Only populated by a deep
// pass.
CorruptObjects []string
// ObjectsVerified counts the objects a deep pass read back.
ObjectsVerified int
// UsageDrift is the difference, per storage directory, between the
// catalog's recorded usage and what the objects actually occupy.
UsageDrift map[local_cache.StorageID]int64
// OrphanScanPartial reports that the orphan comparison covered only a
// slice of the instance-hash space, so its findings are not exhaustive.
OrphanScanPartial bool
// OrphanScanShards is how many slices the space was divided into, which
// is the number of passes a caller must rotate through for full coverage.
//
// It can exceed what the caller asked for: the slices are hex prefixes of
// the instance hash, so a request is rounded up to a power of sixteen. A
// caller that rotates over its own number instead of this one would leave
// most of the store permanently unexamined.
OrphanScanShards int
// Repaired reports whether a repair pass ran. It says nothing about
// whether every finding was resolved -- see Resolved and Unresolved.
Repaired bool
// UsageResynced reports that the drift above was written back to the
// catalog, not merely recomputed in memory.
UsageResynced bool
}
FsckReport summarizes a consistency pass.
func (*FsckReport) Healthy ¶
func (r *FsckReport) Healthy() bool
Healthy reports whether the pass found nothing wrong.
Pending instances are deliberately excluded: a write in flight is what a busy origin looks like, not a fault.
func (*FsckReport) Resolved ¶
func (r *FsckReport) Resolved() []string
Resolved lists what a repair pass actually acted on.
This exists because the two lists are not the same and reporting them as though they were is how an operator comes to believe a store was fixed. Repair can unlink an unreadable entry, queue an unreferenced version, and rewrite a drifted counter. It cannot manufacture data that is gone, and it cannot un-corrupt a block.
func (*FsckReport) Unresolved ¶
func (r *FsckReport) Unresolved() []string
Unresolved lists findings that survive a repair pass, so a caller can exit non-zero rather than reporting success.
type GCStats ¶
type GCStats struct {
// InstancesFreed is the number of object versions whose blocks were
// released.
InstancesFreed int
// InstancesPinned is the number skipped because a reader still holds
// them; they are retried on a later pass.
InstancesPinned int
// SubtreeEntriesRemoved is the number of directory entries removed from
// detached subtrees.
SubtreeEntriesRemoved int
// SubtreesCompleted is the number of detached subtrees fully drained.
SubtreesCompleted int
// AbandonedAppendsReclaimed is the number of streaming writes whose
// writer never finished and whose space has now been released.
AbandonedAppendsReclaimed int
// BytesFreed is how much space the released object versions occupied,
// summed from each version's metadata before it was deleted. It excludes
// abandoned streaming writes, whose footprint the block store refunds
// directly without reporting it.
BytesFreed int64
// HitBatchLimit reports that the queue had more work than one sweep
// handles, so another should follow sooner than the resting period.
HitBatchLimit bool
}
GCStats reports what one sweep accomplished.
type IntegrityConfig ¶
type IntegrityConfig struct {
// IndexInterval is how often the catalog-only check runs. Zero disables
// it.
IndexInterval time.Duration
// DataInterval is how often every object is read back and verified.
// Zero disables it.
DataInterval time.Duration
// DataScanBytesPerSec caps the verification read rate so the scan does
// not compete with serving. Zero selects the block store's default.
DataScanBytesPerSec int64
}
IntegrityConfig schedules the background checks.
type ReadHandle ¶
type ReadHandle struct {
*local_cache.ObjectReader
// contains filtered or unexported fields
}
ReadHandle streams an object version.
The underlying ObjectReader pins the version for its own lifetime, so the janitor cannot reclaim it mid-read; nothing extra is needed here.
func (*ReadHandle) Close ¶
func (r *ReadHandle) Close() error
Close releases the reader, and with it the pin it holds.
func (*ReadHandle) Dirent ¶
func (r *ReadHandle) Dirent() *Dirent
Dirent returns the entry the handle was opened from.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the object store backing a pstore origin.
It owns the catalog and the block store, and is safe for concurrent use. A store has exclusive ownership of its directories for its lifetime, exactly as the cache does.
func Open ¶
Open initializes a store, creating it if the directory is empty.
Requires issuer keys to be available (config.GetIssuerPublicJWKS or the test helper), because the catalog and every object's data key are encrypted under a master key derived from them.
func OpenMaintenance ¶
OpenMaintenance opens a stopped store for inspection or repair.
When allowWrites is false every mutating entry point refuses, so an inspection command cannot modify the store even by mistake.
Requires issuer keys, since the catalog is encrypted under a key derived from them. The caller must Close the result.
func (*Store) AccessTime ¶
AccessTime returns when an object was last read, as maintained by the LRU index (§8).
This is deliberately not surfaced through FileInfo: it lives on the object's metadata rather than its directory entry, so reporting it from Stat would add a metadata read to every lookup to answer a question almost no caller asks. Operators get it from `pelican-server origin pstore stat`.
A zero time means the object has not been read since it was written, or was read within the debounce window before any access was recorded.
func (*Store) BackupEncrypted ¶
BackupEncrypted writes a snapshot sealed to keys.
This is the only way to write a snapshot. Encryption is not optional and there is no key to forget to configure: the archive's backup key is derived from the origin's issuer keys unless one is supplied outright, and a caller that can produce neither gets an error rather than a cleartext dump of the namespace.
**What ends up in the file** is the compressed catalog encrypted under a key belonging to this snapshot alone, preceded by a header that carries the salt that key was derived from *and the archive's backup key sealed once per recipient* -- one recipient per issuer key in keys.IssuerKeys, or the single supplied keys.BackupKey when there is one. Embedding the sealed backup key is what makes the file self-opening: any of those issuer keys reaches it, and so does the escrowed backup key on a machine that has never held them. An operator who escrowed the backup key can restore this file after the issuer keys are gone. See backup_crypto.go for the derivation and the envelope.
Truncation and tampering are detected by the AEAD construction itself, so there is no second framing layer inside the sealed one.
func (*Store) Close ¶
Close releases the store's resources.
Safe to call more than once: the origin closes a store both explicitly and from the shutdown goroutine watching the server context, and the block store's TTL caches do not tolerate being stopped twice.
func (*Store) Create ¶
func (s *Store) Create(name string) (*WriteHandle, error)
Create begins a new version of the object at name. The parent directory must exist. Nothing changes on disk until Close.
func (*Store) CreateSized ¶
func (s *Store) CreateSized(name string, size int64) (*WriteHandle, error)
CreateSized is Create for a caller that already knows the object's length.
The size only selects the storage tier up front; it is a hint, not a promise, and the object is stored at whatever length actually arrives. A declared size at or above the spill threshold goes straight to the streaming writer, so a large upload never buffers a megabyte first.
A negative size means unknown and behaves exactly like Create.
Declaring the size also sizes the streaming chunk, which is what lets a bounded store accept an object a little over the spill threshold: an undeclared write has to assume defaultStreamChunkSize.
func (*Store) DB ¶
func (s *Store) DB() *local_cache.CacheDB
DB exposes the catalog, for introspection and tests.
func (*Store) Digests ¶
func (s *Store) Digests(name string) ([]local_cache.Checksum, error)
Digests returns the checksums recorded for an object when it was written.
An object written before checksums were recorded yields an empty slice rather than an error, so callers can fall back rather than fail.
func (*Store) Export ¶
func (s *Store) Export(ctx context.Context, opts ExportOptions) (*ExportReport, error)
Export writes a subtree out as ordinary files.
It continues past objects it cannot read, collecting them in the report: on a damaged store, recovering most of the data and being told exactly what was lost is far more useful than stopping at the first bad block.
func (*Store) Fsck ¶
Fsck checks the store's internal consistency.
When repair is false nothing is modified, which is the mode an operator should reach for first. When true, dangling entries are removed, orphaned versions are queued for reclamation, and usage counters are resynced from the catalog.
func (*Store) FsckWith ¶
func (s *Store) FsckWith(ctx context.Context, opts FsckOptions) (*FsckReport, error)
FsckWith checks the store's internal consistency with explicit options.
func (*Store) HasCapacityFor ¶
HasCapacityFor reports whether a write of contentLen content bytes could currently be accepted.
It models exactly what the write path will reserve -- the per-block overhead, the chunk rounding a streamed object pays, and the per-directory placement unit -- via plannedFootprint. Answering a different question here than the write path asks would be worse than not checking at all: the origin would accept a PUT it is about to fail with a 507 halfway through the body, or refuse one that would have fit.
It remains advisory in one respect: a concurrent write may consume the headroom before this caller reserves it, so the write path enforces the limit independently. Its purpose is to let the origin refuse a PUT before the client streams a body that cannot land.
func (*Store) List ¶
List returns the direct children of a directory in name order.
after resumes a previous listing at the first name strictly greater than it; limit bounds the result (0 means unbounded). The bool reports whether more entries remain.
func (*Store) MkdirAll ¶
MkdirAll creates a directory and any missing ancestors. It succeeds when the directory already exists.
func (*Store) OpenRead ¶
func (s *Store) OpenRead(name string) (*ReadHandle, error)
OpenRead opens the current version of an object for reading.
Resolving the path and opening the version are two steps, and an overwrite can land between them: the swap commits, the superseded version is queued, the janitor finds it unpinned, and the open then fails on a generation that was current a microsecond earlier. That surfaces as a 500 on an object that existed the whole time.
Two things close that. The pin is taken inside the resolving transaction, so a janitor that can see the version is superseded can also see that somebody holds it — queuedInstance.claim refuses pinned versions. And the open is retried against a freshly resolved generation, which covers the residual window between the read snapshot and the pin actually landing. Retrying is safe because it is idempotent and bounded, and it terminates: either the path resolves to a version that opens, or it stops changing.
func (*Store) ReclaimAbandonedAppends ¶
ReclaimAbandonedAppends releases capacity held by streaming writes whose writer never finished, and returns how many objects it freed.
This closes the one gap the pg: queue cannot cover. A write is queued for reclamation at the start of materialization (WriteHandle.beginMaterialize), which protects everything from there to the commit. Before that point the object exists only as a partly written stream, and the block store records it under its own append-in-flight marker; a process that dies mid-upload leaves chunk files allocated and charged with nothing in the pg: queue pointing at them. The cache reclaims those from its eviction manager, which a pstore deliberately never constructs -- so without this the store that makes the heaviest use of streaming writes was the one never cleaning up after them.
The two mechanisms compose rather than double-count. Deleting an object clears its append marker in the same transaction, so a version the pg: queue already reclaimed is found here with no metadata and only its stale marker is dropped; conversely an object reclaimed here is gone before any pg: entry could refer to it.
minAge is the grace period; see appendReclaimGrace. Passing zero reclaims every record with no live writer, which is for tooling and tests rather than the janitor.
func (*Store) Remove ¶
Remove deletes a file, or an empty directory.
The entry and its reclamation record are written together, so the object's bytes can never become unreachable without also being queued for the janitor.
func (*Store) RemoveAll ¶
RemoveAll deletes a path and everything beneath it.
A large subtree cannot be removed in one transaction, so the entry is unlinked from its parent — making the subtree unreachable at once — and its root handed to the janitor to drain. Deletion need not be atomic: a partially drained subtree is invisible either way.
func (*Store) Rename ¶
Rename moves an entry, replacing any existing file at the destination.
Renaming a file rewrites one key. Renaming a directory rewrites every descendant's key, which is why it is O(descendants) — but object identity does not depend on the path, so no data file is touched and no bytes move.
func (*Store) Restore ¶
func (s *Store) Restore(r io.Reader, keys BackupKeys) error
Restore loads a metadata backup into this store.
There is one container and it is sealed, so the only question the file itself answers is whether it is a pstore snapshot at all: a file without the magic is refused as "not a backup", and one whose format version this build does not read is refused by number rather than misparsed. The two are separate messages on purpose -- they send an operator to different places.
**Any one of three keys opens an archive**, and keys may carry whichever the operator still has:
- keys.IssuerKeys -- the origin's issuer keys, the ordinary case on the origin itself. Any single key the archive was sealed to is enough, so a rotation does not strand the archives that overlapped it.
- keys.BackupKey -- the escrowed backup key from DeriveBackupKey. The archive carries its own backup key sealed to this one, so this restores on a machine that has never held the origin's issuer keys. **Escrowing it is what makes the loss of the issuer keys survivable.**
- keys.FileKey -- that one archive's own key from DeriveFileKey, which opens it and nothing else.
The store must be empty: restoring over live records would interleave two namespaces rather than replace one.
**A failed restore is not a no-op.** BadgerDB's loader writes as it reads, so a snapshot that turns out to be truncated or damaged part-way through leaves records in the directory and this handle unusable. There is no way to unwind that from here; the directory has to be discarded and the restore started again into a fresh one. This is why the container authenticates its header before a byte of the body is opened and marks its terminating chunk: most damage is caught before a single record is written, and what is caught late says plainly what state it left behind.
**The store must be reopened afterwards.** A restore replaces the whole database, including everything a store reads once at open and then caches: the hash salt that every instance hash is derived from, and the mapping from storage IDs to directories. Continuing to use the handle would compute hashes that match nothing and look for block files under empty paths. The salt is refreshed here so that a caller inspecting the namespace immediately after a restore sees the right thing, but reading object data requires a fresh open.
func (*Store) RunGC ¶
RunGC performs one reclamation pass and reports what it did.
Safe to call directly; the janitor simply calls it on a timer.
func (*Store) StartBackups ¶
StartBackups snapshots the catalog to dir every interval, for as long as the context lives.
Each pass writes a new timestamped file and prunes the oldest beyond keep, so a corrupted catalog can be rolled back to rather than only forward from. A failed pass is logged and retried at the next interval: a backup problem must not take the origin down.
One snapshot is taken immediately. Waiting for the first interval to elapse means an unwritable backup directory -- the wrong owner, a full filesystem, a typo in the configured path -- goes unreported for a whole interval, and a store that dies inside its first hour has no metadata backup at all, which is the case backups exist for.
After that the interval is a gap, not a cadence: the next snapshot starts an interval after the previous one *finished*. A snapshot of a large catalog is not instant, and the six-hour default is close enough to the time one can take that a fixed cadence would have the origin snapshotting back to back. See runPeriodically in integrity.go.
func (*Store) StartIntegrityChecks ¶
func (s *Store) StartIntegrityChecks(ctx context.Context, egrp *errgroup.Group, cfg IntegrityConfig)
StartIntegrityChecks runs the scheduled checks until the context is cancelled.
func (*Store) StartJanitor ¶
StartJanitor runs the reclamation sweep until the context is cancelled.
The interval adapts to the backlog. Each sweep handles a bounded batch, so a fixed period puts a ceiling on how fast space can come back: deleting a tree of a million objects at one batch per resting period would take days, during which the store stays full and refuses writes. A sweep that fills its batch halves the wait, down to janitorMinInterval; a sweep that does not steps back toward the resting period. Reclamation therefore accelerates exactly when there is something to reclaim and idles otherwise.
func (*Store) Storage ¶
func (s *Store) Storage() *local_cache.StorageManager
Storage exposes the block store, for introspection and tests.
func (*Store) Usage ¶
Usage reports the store's committed-plus-reserved bytes and its ceiling. A ceiling of zero means the store is unbounded.
func (*Store) VerifyObject ¶
VerifyObject reads one object back and checks it against the checksums recorded when it was written.
Returns false when the data no longer matches, which means at-rest corruption: every other check reads only the catalog and cannot see it.
type WriteHandle ¶
type WriteHandle struct {
// contains filtered or unexported fields
}
WriteHandle accumulates a new version of an object and installs it at Close.
Not safe for concurrent use. Nothing the handle writes is visible to readers until Close returns successfully.
func (*WriteHandle) Abort ¶
func (w *WriteHandle) Abort() error
Abort discards the pending version without touching the namespace.
func (*WriteHandle) Close ¶
func (w *WriteHandle) Close() error
Close finalizes the object's data and installs it at its path.
The install is one transaction: it re-checks preconditions, writes the new dirent, and queues the superseded version for reclamation. Committing the swap and the queue entry together is what makes overwrite crash-safe. A concurrent writer to the same path can abort that transaction, in which case it is retried against fresh state; see install.
func (*WriteHandle) Generation ¶
func (w *WriteHandle) Generation() string
Generation returns the version token this write will install, which is also the object's ETag.
func (*WriteHandle) Path ¶
func (w *WriteHandle) Path() string
Path returns the object path this write will install to.
func (*WriteHandle) RequireAbsent ¶
func (w *WriteHandle) RequireAbsent()
RequireAbsent makes Close fail with ErrExist if the object already exists, implementing "If-None-Match: *".
func (*WriteHandle) RequireGeneration ¶
func (w *WriteHandle) RequireGeneration(generation string)
RequireGeneration makes Close fail with ErrPreconditionFailed unless the object still carries the given generation, implementing "If-Match".
func (*WriteHandle) Size ¶
func (w *WriteHandle) Size() int64
Size returns how many bytes have been accepted so far. The object is not visible at its path until Close, so this is the only way to observe the pending version's length.