Documentation
¶
Overview ¶
Package cache implements magus's content-addressed build cache. Layout: cas/ (blobs) + manifests/ + logs/. The store is local; an optional pluggable RemoteBackend (see remote.go) lets a miss restore from, and a build publish to, a shared store.
Index ¶
- Constants
- Variables
- func ClampConcurrency(requested int) (n int, clamped bool)
- func ContextWithLimiter(ctx context.Context, lim *Limiter) context.Context
- func ContextWithProgress(ctx context.Context, p *Progress) context.Context
- func ContextWithTracer(ctx context.Context, t Tracer) context.Context
- func DefaultConcurrency() int
- func DepKey(project, target string) string
- func DigestEnvValues(inputs []string) []string
- func ExpandSources(globs []string, root string, outputGlobs, ignoreDirs []string) ([]string, error)
- func FailureHint() []tty.Line
- func FailureHintPlain() string
- func FormatMB(mb int) string
- func IsMintedRef(s string) bool
- func LooksLikeInvocationID(s string) bool
- func LooksLikeRef(s string) bool
- func MachineCeiling() int
- func NewContext(ctx context.Context, c *Cache) context.Context
- func PoolGauge(running, capacity int) string
- func PortableRef(cacheKey string) string
- func RedactKeyInputs(ctx context.Context, lines []string) []string
- func RegisterRemoteBackendOpener(fn func(ctx context.Context, selector string) (RemoteBackend, error))
- func ResolveConcurrency(configured int) int
- func SlotHeld(ctx context.Context) bool
- func SlotsHeld(ctx context.Context) int
- func SlowExecutions(journalPath string, minMs int64) map[string]bool
- func WithRemoteStats(ctx context.Context) context.Context
- func WithSlotHeld(ctx context.Context) context.Context
- func WithSlotsHeld(ctx context.Context, n int) context.Context
- func WithoutSlotHeld(ctx context.Context) context.Context
- type AmbiguousRefError
- type ArtifactVersion
- type CQE
- type Cache
- func (c *Cache) Collapsing() bool
- func (c *Cache) Delete(ctx context.Context, projectPaths ...string) error
- func (c *Cache) Dir() string
- func (c *Cache) DiskBytes() int64
- func (c *Cache) Evict(ctx context.Context) error
- func (c *Cache) Export(ctx context.Context, w io.Writer) error
- func (c *Cache) GetArtifact(ctx context.Context, v ArtifactVersion, dst string) error
- func (c *Cache) Import(ctx context.Context, r io.Reader) error
- func (c *Cache) IsCached(ctx context.Context, s Step) (bool, error)
- func (c *Cache) LastEntry(projectPath string) (*Manifest, string, error)
- func (c *Cache) LastEntryForTarget(projectPath, target string) (*Manifest, string, error)
- func (c *Cache) LastRecordedRun(projectPath, target string) (RecordedRun, error)
- func (c *Cache) ListArtifacts(ctx context.Context, projectPath, wsPath string) ([]ArtifactVersion, error)
- func (c *Cache) LogBase(ctx context.Context, base, vcs string)
- func (c *Cache) LogCache(ctx context.Context)
- func (c *Cache) LogCharms(ctx context.Context, charms string)
- func (c *Cache) LogDry(ctx context.Context, project, label, target string)
- func (c *Cache) LogDryBanner(ctx context.Context)
- func (c *Cache) LogDrySummary(ctx context.Context, planned int, elapsed time.Duration)
- func (c *Cache) LogMemoryPressure(ctx context.Context, p MemoryPressure)
- func (c *Cache) LogRemoteSummary(ctx context.Context)
- func (c *Cache) LogScope(ctx context.Context, label, source string)
- func (c *Cache) LogStage(ctx context.Context, label, target string, elapsed time.Duration, runErr error)
- func (c *Cache) LogSummary(ctx context.Context, elapsed time.Duration)
- func (c *Cache) OutputByRef(ctx context.Context, ref string) ([]byte, OutputDescriptor, error)
- func (c *Cache) Prune(ctx context.Context, cutoff time.Time, dryRun bool) (n int, freed int64, err error)
- func (c *Cache) PruneRemote(ctx context.Context, policy RetentionPolicy) error
- func (c *Cache) PublishOutput(ctx context.Context, ref string) (string, error)
- func (c *Cache) Remote() RemoteBackend
- func (c *Cache) Run(ctx context.Context, s Step, fn func(context.Context) error, opts ...RunOption) (Result, error)
- func (c *Cache) RunAll(ctx context.Context, steps []Step, fn func(context.Context, Step) error, ...) ([]Result, error)
- func (c *Cache) RunAside(ctx context.Context, s Step, fn func(context.Context) error, opts ...RunOption) (Result, error)
- func (c *Cache) Stats() Stats
- func (c *Cache) StepKey(ctx context.Context, s *Step) (key string, lines []string, err error)
- func (c *Cache) StepKeyMemo(ctx context.Context, s *Step, memo *SourceMemo) (key string, lines []string, err error)
- type ClassDigest
- type DerivedEdge
- type DerivedOrder
- type DroppedEdge
- type FSRemoteBackend
- func (r *FSRemoteBackend) Active(context.Context) bool
- func (r *FSRemoteBackend) GetArtifact(_ context.Context, projectPath, hash string) (io.ReadCloser, error)
- func (r *FSRemoteBackend) Name() string
- func (r *FSRemoteBackend) PutArtifact(_ context.Context, projectPath, hash string, data io.Reader) error
- type Failure
- type KeyInfo
- type KeyInputChange
- type KeyInputDiff
- type KeyMaterial
- type Limiter
- func (l *Limiter) Acquire(ctx context.Context) error
- func (l *Limiter) AcquireN(ctx context.Context, n int) error
- func (l *Limiter) Capacity() int
- func (l *Limiter) Release()
- func (l *Limiter) ReleaseN(n int)
- func (l *Limiter) SetHooks(onAcquire func(waitNs int64, n int), onRelease func(n int), ...)
- func (l *Limiter) Snapshot() LimiterStats
- func (l *Limiter) Yield(ctx context.Context, fn func() error) error
- type LimiterStats
- type LocalAdmitter
- type MachineAdmitter
- type MachineBudget
- type Manifest
- type Mark
- type MemoryPressure
- type Option
- func WithCollapse(collapse bool) Option
- func WithInsecureRemote() Option
- func WithLog(format string, level slog.Level) Option
- func WithLogger(l *slog.Logger) Option
- func WithMachineAdmission(admitter MachineAdmitter, noWait bool) Option
- func WithMaxImportBytes(n int64) Option
- func WithMutable(mutable bool) Option
- func WithRemoteBackend(t RemoteBackend) Option
- func WithSigningKey(seed []byte) Option
- func WithSilent(silent bool) Option
- func WithSizeMB(n int) Option
- func WithTrustedKeys(pubkeys [][]byte) Option
- type OutputBundle
- type OutputDescriptor
- type OutputRecord
- type OutputStore
- func (s *OutputStore) AdoptImported(cacheKey string, output []byte) (string, bool)
- func (s *OutputStore) Attempts(ref string) ([]OutputDescriptor, error)
- func (s *OutputStore) ByRef(ref string) ([]byte, OutputDescriptor, error)
- func (s *OutputStore) DescriptorByRef(ref string) (OutputDescriptor, error)
- func (s *OutputStore) InvocationByID(inv string) (journal.Invocation, error)
- func (s *OutputStore) InvocationEventsByID(inv string) (journal.Invocation, []journal.Event, error)
- func (s *OutputStore) InvocationEventsFrom(inv string, from int64) ([]journal.Event, int64, error)
- func (s *OutputStore) KeyInputsByKey(cacheKey string) ([]string, error)
- func (s *OutputStore) KeyInputsByRef(ref string) ([]string, error)
- func (s *OutputStore) LatestRefsByTarget() []OutputDescriptor
- func (s *OutputStore) ListDescriptors() []OutputDescriptor
- func (s *OutputStore) ListRunLogs(limit int) []RunLog
- func (s *OutputStore) Persist(ctx context.Context, cacheKey string, output []byte, d OutputDescriptor) (OutputDescriptor, error)
- func (s *OutputStore) PersistKeyInputs(ctx context.Context, cacheKey string, inputs []string) error
- func (s *OutputStore) RotateRuns(keepLast int, keepBytes int64) (removed int, bytesFreed int64)
- func (s *OutputStore) RunsStat() (bytes int64, count int64)
- func (s *OutputStore) StepRef(cacheKey string) string
- type PaneFocus
- type PrettyHandler
- func (h *PrettyHandler) Close() error
- func (h *PrettyHandler) Enabled(_ context.Context, lvl slog.Level) bool
- func (h *PrettyHandler) Failures() []Failure
- func (h *PrettyHandler) Handle(ctx context.Context, r slog.Record) error
- func (h *PrettyHandler) HitFailure(row int) (Failure, bool)
- func (h *PrettyHandler) ReleaseBand() error
- func (h *PrettyHandler) RendersBand() bool
- func (h *PrettyHandler) SetFocus(f PaneFocus)
- func (h *PrettyHandler) SetPreview(lines []string)
- func (h *PrettyHandler) SetSelection(n int)
- func (h *PrettyHandler) ToggleFocus() PaneFocus
- func (h *PrettyHandler) WithAttrs(_ []slog.Attr) slog.Handler
- func (h *PrettyHandler) WithGroup(_ string) slog.Handler
- func (h *PrettyHandler) Zone() *tty.Zone
- type Progress
- type RecordedRun
- type RefNotFoundError
- type RemoteBackend
- type RemotePruner
- type Result
- type RetentionPolicy
- type Ring
- type RunLog
- type RunOption
- type SourceMemo
- type Stalled
- type Stats
- type Step
- type TargetNode
- type Tracer
Examples ¶
Constants ¶
const ( HintKeyRerun = "rerun" HintKeyFocus = "focus" HintKeyCopy = "copy" HintKeyOutput = "output" HintKeyDone = "done" )
The failure prompt's instruction row: the text, and the click keys that name each action.
It lives beside the band rather than in the command that dispatches it: the failure list and the row saying what you can do with one are a single presentation, drawn into adjacent leases of the same zone. The command owns the VERBS; this owns what a reader is told and what a click resolves to.
Keeping them together also keeps the docs honest - a renderer that cannot import an unexported const hand-copies the strings, and the drift gate then compares that copy against itself while the terminal says something else.
const ( // MinRunsForYield is how many executions a target needs before a zero hit rate is // evidence rather than noise. MinRunsForYield = 8 // MinAvgMsForYield is the average execution time below which a never-cached target // is not worth reporting. MinAvgMsForYield = 2_000 )
A target that never replays is the most expensive kind of misconfiguration, because nothing about it looks wrong: every run passes, the cache reports no error, and the only symptom is that the build is slower than it should be. It is invisible per-run and obvious only in aggregate - which is why it is read back out of the invocation journals rather than detected live.
The thresholds keep this quiet until acting on it is worthwhile. A handful of runs proves nothing (a target legitimately misses while you edit its inputs), and a target that misses in milliseconds costs nothing to rerun.
const DefaultMaxRunBytes int64 = 2 << 30
DefaultMaxRunBytes bounds the runs dir on disk, which the count above cannot: one journal holds every output line of its run, so keeping N of them keeps N unbounded files.
2 GiB because it has to be some number and this one is big enough that an ordinary week of runs never reaches it, while a runaway log-emitting build cannot quietly eat a disk. RunsStat reports the current footprint, so the honest way to retune it is to look rather than to guess again.
const DefaultMaxRuns = 500
DefaultMaxRuns bounds how many invocation journals the runs dir retains, so a long-lived daemon's run-log dir stays bounded. Coarser than the per-key output cap (defaultOutputKeepLast): one file per invocation, kept newest-first by modtime. The RotateLogs job trims to this.
const ExitCodeMachineBusy = 75
ExitCodeMachineBusy is the process status a machine-budget refusal asks for: 75, EX_TEMPFAIL. It lives here rather than beside the CLI's other exit codes because the error is built here and has to state its own code - the daemon runs an adopted step in its own process and reads the code off the error, having lost the Go type.
The workspace lock's own contention error picks the same number for the same reason (lockContendedExit). Two independent decisions that agree, not one shared setting: coupling them would make a change to either silently move the other.
const KeyVersion = 7
KeyVersion is bumped when the set of hashed fields changes, forcing a full rebuild. It is the ONLY version in this system: the output descriptor carries no schema of its own, because a descriptor is stored under a key that already contains this number, and magus reads no descriptor it did not write.
7 drops that descriptor schema and evicts every entry predating the change, so no descriptor missing the reproduce fields (spell, args, vcs) survives to be read as though those were empty rather than unrecorded.
6 added the spell: line for explicit spell::op runs - emitted only when the filter is set, but a pre-6 store could already hold entries an op-form run recorded under the plain target's key, and only a bump evicts that poison class.
const RefPrefix = "out"
RefPrefix begins every target-output reference id ("out1a2b3c"). It is the provenance tag in the shared ref namespace - "out" for a target OUTPUT, alongside "mcp" for an MCP call payload (internal/trail) - so a ref names where it came from at a glance. No delimiter, matching the other prefixes. LooksLikeRef validates the shape of the argument to `magus query output <ref>`; it is not a router (retrieval is an explicit subcommand), so a shape-collision with a search term is impossible.
const RunsDir = "runs"
RunsDir is the cache subdir holding one union event log per invocation (<cacheDir>/runs/<inv>.jsonl). Shared by the writer (magus.BeginInvocation) and the reader (InvocationByID) so the two never drift on the path.
Variables ¶
var ErrArtifactMissing = errors.New("cache: artifact content not in store")
ErrArtifactMissing reports that a version's bytes are not in the store.
Named for the observation, not the cause: eviction is the usual reason, but a hand-cleared store or an entry that never stored its blob reach here too. Callers must not treat it as "no differences" - an empty diff reads as "unchanged", which is the most misleading answer available.
Functions ¶
func ClampConcurrency ¶ added in v0.4.0
ClampConcurrency caps a configured concurrency at what the machine can actually run, reporting whether it had to.
A configured value was previously taken at face value, so `concurrency: 32` in a magus.yaml written on a big machine ran 32 parallel steps on a laptop with 10 cores. That does not fail - it thrashes, and every target simply takes longer, which is the failure mode nothing ever gets attributed to. The number also outlives the machine it was chosen on: it travels in the repo, and the person it hurts is whoever has the smallest box.
The caller announces the clamp rather than applying it silently. A run that is quietly narrower than requested is the same invisible-cause problem in the other direction.
func ContextWithLimiter ¶
ContextWithLimiter stores lim in ctx for nested callers (e.g. magus.needs) to yield their slot.
func ContextWithProgress ¶ added in v0.4.3
ContextWithProgress installs p so the accounting edges can beat it without the heartbeat being threaded through every signature.
func ContextWithTracer ¶
ContextWithTracer returns a copy of ctx carrying t. A nil t is stored as a no-op so callers can wire it through unconditionally.
func DefaultConcurrency ¶
func DefaultConcurrency() int
DefaultConcurrency returns the concurrency cap: MAGUS_CONCURRENCY env var, then 4 on GitHub-hosted runners (RUNNER_ENVIRONMENT != self-hosted), then min(NumCPU, 8). A hosted runner reports its host's CPU count while giving the job a small slice of it, so NumCPU over-subscribes badly there.
This is the one place magus names a CI provider outside a spell, and it is startup ordering that forces it: the limiter is built before the magusfile is evaluated (see cmd/magus/main.go), so the CI provider spell that would otherwise answer this is not loaded yet. Everything else provider-specific lives in a spell; see internal/ci/annotate.
func DepKey ¶
DepKey returns the scheduling identity of a (project, target) node. Empty target → bare project path (backward-compatible with DependsOn-as-path callers).
func DigestEnvValues ¶ added in v0.4.0
DigestEnvValues returns lines with every env value replaced by a short digest ("env:NAME=abc" -> "env:NAME=sha256:<12hex>"). Env values are the one key-input class that routinely carries material a user would not publish (tokens ride env vars whether or not a secret provider registered them), so the raw value never leaves hashStep: the store persists DIGESTED lines, and every comparison surface digests its live lines the same way - which also keeps the two sides byte-comparable (a registry-based redaction would fire on one machine and not the other, turning every secret-bearing env line into a false diff). The digest still changes when the value changes, so the diff names the exact variable without exposing it.
func ExpandSources ¶ added in v0.4.0
ExpandSources is expandSources exported for a caller needing the SAME source walk the cache key is built from - today a spell op's Sources placeholder. Reusing this walk rather than a second one is what makes that op inherit root's declared ignore dirs instead of drifting from the set the key was built from.
Returns root-relative paths only, sorted: a caller building argv for a subprocess that runs IN root wants paths relative to it.
func FailureHint ¶ added in v0.4.0
FailureHint composes the instruction row.
The way out is aligned RIGHT so it is the last thing clipped rather than the first: as one string this row was 86 columns, and an 80-column terminal cut it to exactly "[esc] do".
func FailureHintPlain ¶ added in v0.4.0
func FailureHintPlain() string
FailureHintPlain is the same instruction as one line, for a terminal with no room to pin it. Printed rather than dropped: every other thing the prompt draws is a view, but this is the only statement of how to leave.
func FormatMB ¶ added in v0.4.0
FormatMB renders a declared memory figure. Base-1024 with binary suffixes and a space, matching fmtBytesLog rather than inventing a second spelling of the same quantity in one binary. Exported so a refusal, a wait notice, and `magus status` all say the same figure the same way.
func IsMintedRef ¶ added in v0.2.0
IsMintedRef reports whether s is a fully-minted reference id: the "out" prefix followed by exactly refHexLen hex digits (a portable step ref) or attemptHexLen digits (an attempt id, and every pre-portable ref). Unlike LooksLikeRef, which accepts any-length hex prefix so `magus query output` can take a git-style short ref, this rejects prefixes. Use it when scanning free text for a chainable ref, so short English words whose tail is coincidentally hex ("outed", "outface") are not mistaken for a ref.
func LooksLikeInvocationID ¶ added in v0.4.0
LooksLikeInvocationID reports whether s is shaped like an invocation id. The counterpart to LooksLikeRef, and deliberately only a recognizer: retrieval stays an explicit subcommand.
func LooksLikeRef ¶ added in v0.2.0
LooksLikeRef reports whether s is shaped like a target-output reference id (or a hex prefix of one). It is the `magus query` router discriminator: a match routes to output retrieval, while a real free-text query like "refactor" (non-hex tail) falls through to the graph grammar.
func MachineCeiling ¶ added in v0.4.0
func MachineCeiling() int
MachineCeiling is the most concurrent build steps this machine should ever run: one per CPU. It is a CEILING, not a default - DefaultConcurrency picks a smaller, gentler number when nothing is configured, and this only ever caps a number someone asked for.
func NewContext ¶ added in v0.4.0
NewContext stores c in ctx for magusfile bindings (e.g. magus.bust_cache).
func PoolGauge ¶ added in v0.4.0
PoolGauge renders the pool as filled and empty slots, or "" when there are too many slots to draw honestly. PoolGauge is exported for the documentation renderer, which draws the same gauge a run draws rather than a copy of what it looks like.
func PortableRef ¶ added in v0.4.0
PortableRef derives the user-facing reference id from the cache key alone: RefPrefix + the key's first refHexLen hex digits. Same inputs -> same key -> same ref on every machine, so an inspect line from CI or a teammate's terminal resolves locally, and two machines printing DIFFERENT refs for one target proves their inputs differ. The ref names the STEP (the outputs/<cacheKey>/ directory); execution-level identity lives a level down in attempt ids. Exported so the CLI can predict the ref a computed-but-not-yet-run key would print.
func RedactKeyInputs ¶ added in v0.4.0
RedactKeyInputs replaces every value the run's secret resolver has registered with its mask, one line at a time. It is the second net behind DigestEnvValues: env values are digested by construction, but a registered credential can ride a non-env class too - an `arg:` line carrying `--token=<value>`, say - and nothing else strips it.
BOTH sides of a comparison must pass through it. The store redacts at write, so a live line that skipped this differs from its redacted stored twin on every run: a phantom diff no edit can settle.
Per line rather than over a joined blob, because a key input may itself contain a newline; splitting a redacted join back apart would resegment the set. Always returns a fresh slice of the same length, holding the lines verbatim when nothing is registered.
func RegisterRemoteBackendOpener ¶
func RegisterRemoteBackendOpener(fn func(ctx context.Context, selector string) (RemoteBackend, error))
RegisterRemoteBackendOpener installs the opener that OpenRemoteBackend delegates to. It is meant to be called once, from a backend package's init; a second call panics rather than silently shadowing the first.
func ResolveConcurrency ¶ added in v0.4.0
ResolveConcurrency returns the width a run actually gets from a configured value: the default when nothing is configured, clamped to what the machine can run.
It is the resolution the run path applies - cmd/magus/main.go when it builds the bootstrap limiter, Magus.limiter per workspace - so a reporter can answer "how many slots does this box give a build" without re-deriving it. Those two sites keep their own copy because they announce the clamp as it takes effect; this one only reports.
func SlowExecutions ¶ added in v0.4.0
SlowExecutions returns the "project\x00target" keys that actually EXECUTED in one invocation journal and took at least minMs. Cached replays are excluded: a replay proves the cache works for that target, which is the opposite of the finding.
This is the gate that makes a per-run cache-yield check affordable. Scanning the workspace's whole journal history on every run would be absurd; scanning it after a target just spent a minute executing is free by comparison, and a fast run reads only its own journal and stops.
func WithRemoteStats ¶ added in v0.4.0
WithRemoteStats installs run-scoped remote-cache counters on ctx.
func WithSlotHeld ¶
WithSlotHeld marks ctx as holding a single limiter slot.
func WithSlotsHeld ¶
WithSlotsHeld marks ctx as holding n limiter slots. A hand-back site (Yield, os.with_slots, archive.*) must release exactly n so it gives back its whole hold, not one slot: a weighted step holds more than one, and releasing only one would leave it pinning slots it then blocks trying to re-reserve.
Types ¶
type AmbiguousRefError ¶ added in v0.2.0
AmbiguousRefError is returned by output lookup when a ref (or prefix) matches more than one stored identity. Candidates are pasteable-back refs, sorted: a step candidate renders as its portable ref (lengthened past refHexLen if two keys collide), an attempt candidate as its full file stem - so the CLI can list them for the user to disambiguate (git-style).
func (*AmbiguousRefError) Error ¶ added in v0.2.0
func (e *AmbiguousRefError) Error() string
type ArtifactVersion ¶ added in v0.4.0
type ArtifactVersion struct {
Output OutputRecord
Target string // the target whose run produced it
CreatedAt time.Time // when that entry was written
EntryHash string // the cache key; OutputStore.StepRef maps it to an output ref
}
ArtifactVersion is one cached version of a declared output: the record a run snapshotted, plus which run it was.
Output is EMBEDDED rather than restated. An earlier version copied four of its five fields and dropped Symlink, which was a live bug: a symlink record carries no Blob, so every symlink version hashed alike, collapsed to one row, and resolved to the cas/ directory itself.
func (ArtifactVersion) ShortBlob ¶ added in v0.4.0
func (v ArtifactVersion) ShortBlob() string
ShortBlob abbreviates the content hash for display. Empty for a symlink record, which has no content of its own.
type CQE ¶
type CQE struct {
UserData uint64 // tag attached at submission
Result int32 // bytes read on success, negated errno on failure
}
CQE is a copy-by-value io_uring completion event.
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is an on-disk content-addressed build cache handle.
func FromContext ¶ added in v0.4.0
FromContext retrieves the Cache stored by NewContext, or nil.
func Open ¶
Open returns a Cache rooted at dir (created on demand). MAGUS_CACHE_WRITE_ENABLED=false opens read-only (replays hits, never writes). Logger respects MAGUS_LOG_FORMAT/LEVEL.
func (*Cache) Collapsing ¶
Collapsing reports whether the cache is withholding per-project subprocess output until failure (collapse-on-success). Callers use it to decide whether to attach a stage observer that prints progress lines for the otherwise-hidden work.
func (*Cache) Delete ¶ added in v0.4.0
Delete removes cached manifests for the given project paths (all if none given). Orphaned blobs are collected after manifests are deleted.
func (*Cache) Dir ¶ added in v0.2.0
Dir returns the cache's root directory. Subsystems that write sibling artifacts next to the cache (the symbol index a `scip` op produces) resolve their paths under it, so those artifacts stay out of the working tree.
func (*Cache) DiskBytes ¶ added in v0.2.0
DiskBytes returns the approximate on-disk size of the cache: the summed file sizes of cas/ (the blob store) and manifests/ (the index), stat-only (no file reads, unlike scanManifests). The result is memoized for diskSizeTTL so it is cheap to poll for a live dashboard. Returns 0 for a dirless cache.
func (*Cache) Evict ¶ added in v0.4.0
Evict removes LRU entries down to the size cap, then collects unreferenced CAS blobs.
func (*Cache) Export ¶
Export writes the cache as a gzip-compressed tar archive (paths relative to the cache root, so Import can extract into any target directory).
func (*Cache) GetArtifact ¶ added in v0.4.0
GetArtifact writes v to dst, creating parent directories.
Content is cloned from the store rather than copied where the filesystem supports reflink (APFS, btrfs, XFS), so comparing a large artifact costs almost nothing.
func (*Cache) Import ¶
Import extracts a gzip-compressed tar archive produced by Export into the cache directory. Existing files are overwritten; entries older than what is on disk are skipped.
func (*Cache) IsCached ¶ added in v0.4.0
IsCached reports whether step s would replay from cache rather than run: its inputs hash to a manifest already present locally. It is Run's hash-and-lookup without the execution or the remote fetch - a read-only "is this up to date?" probe (e.g. status reporting whether a project's symbol index reflects current sources). A missing manifest is "not fresh", not an error; only a hashing failure returns one.
func (*Cache) LastEntry ¶
LastEntry returns the manifest and log-file path of the most recently created cache entry for projectPath. Returns an error wrapping fs.ErrNotExist when no entries exist for the project.
func (*Cache) LastEntryForTarget ¶
LastEntryForTarget returns the manifest and log-file path of the most recently created cache entry for projectPath with the given target. Returns an error wrapping fs.ErrNotExist when no matching entries exist.
func (*Cache) LastRecordedRun ¶ added in v0.4.0
func (c *Cache) LastRecordedRun(projectPath, target string) (RecordedRun, error)
LastRecordedRun returns the most recent entry recorded for target in projectPath, the comparison peer for "why would a run here MISS": its key inputs are what FirstKeyInputChange pairs the live ones against. Charms are deliberately not part of the lookup - an entry recorded under different charms is still the last thing this target stored here, and the charm lines then show up as the difference that explains the miss.
Wraps fs.ErrNotExist when nothing is recorded for that target.
func (*Cache) ListArtifacts ¶ added in v0.4.0
func (c *Cache) ListArtifacts(ctx context.Context, projectPath, wsPath string) ([]ArtifactVersion, error)
ListArtifacts returns every cached version of wsPath, newest first.
wsPath is WORKSPACE-relative, matching how snapshot recorded it and what TargetArtifact.Path carries; it is not relative to projectPath.
Consecutive versions with identical content collapse to one, keeping the earliest of each run: a target that ran twenty times producing the same bytes changed the artifact once, and the question is when content first appeared.
func (*Cache) LogBase ¶ added in v0.4.0
LogBase emits what a run's affected set was compared against, beside the projects and charms headers, because it is the third input that decides what runs: the same command against a different base is a different build.
base already reads "git diff vs origin/main" - the VCS name in front of the ref, rather than a git:// URI, because no such scheme is standard across git, Mercurial and jj and inventing one would put a magus-only string where a reader expects a ref they can paste straight back into their own VCS. vcs is accepted for a caller that has the two apart.
func (*Cache) LogCache ¶ added in v0.4.0
LogCache emits the cache header: which tiers this run can reach and whether it may write to them.
Always printed, including the boring case, because the value is in never having to wonder. "Why was nothing cached" and "am I even talking to the remote" were previously answerable only by reading config and env, and the answer differs per event - a pull request reads the shared cache but must not publish to it, which is invisible unless something says so.
It names the backend rather than probing it. Active() is a spell op on the real implementation - arbitrary Buzz, with the whole host surface - and calling it here put that on the path before the first line of output, where a slow probe stalls the run with nothing on screen to explain the pause. Presence and name are known without asking; whether the backend engages is the run's business, not the header's.
func (*Cache) LogCharms ¶ added in v0.2.0
LogCharms emits the active-charm header (the charms mixed into this run, e.g. the magus.yaml default_charms like `rw`) so the reader sees up front what state the run executes under - and can tell at a glance whether a default charm actually took effect. Routed through the cache logger like LogScope so every format receives it.
func (*Cache) LogDry ¶
LogDry emits a per-target dry-run line through the cache logger, in place of the executed pass/fail line. project is the workspace-relative path, carried so the line can print the same repro command an executed one does.
func (*Cache) LogDryBanner ¶
LogDryBanner emits the one-time dry-run banner through the cache logger.
func (*Cache) LogDrySummary ¶ added in v0.4.0
LogDrySummary emits the end-of-run footer for a dry run: the same cache.summary event a real run ends with, marked dry and carrying what WOULD have run.
It is the same event on purpose. A dry run previously just stopped after its last [dry] line, so the one shape a reader had learned to look for at the bottom of a run - the summary - was missing exactly when they were checking a plan. Reusing cache.summary also means every output format (json, jsonl, template) keeps reporting a footer rather than only the text renderer growing one.
The cache's own counters are not consulted: nothing executed, so they are all zero and would report "0 ran" for a plan that intends to run plenty.
func (*Cache) LogMemoryPressure ¶ added in v0.4.0
func (c *Cache) LogMemoryPressure(ctx context.Context, p MemoryPressure)
LogMemoryPressure warns that the host is running out of memory, routed through the cache logger like every other header.
Warn rather than Info because a killed runner never lets magus reach its summary. Only what magus already streamed survives, and this is the line that explains an otherwise unattributable shutdown signal.
func (*Cache) LogRemoteSummary ¶ added in v0.4.0
LogRemoteSummary accounts for what the remote cache did this run, reading the run-scoped counters off ctx. It is the only place the ZERO case gets stated: a run that never touched a configured remote says so rather than saying nothing, and silence is what made that indistinguishable from working.
func (*Cache) LogScope ¶
LogScope emits the projects header through the cache logger so all output formats (pretty/text/JSON) receive the same event.
func (*Cache) LogStage ¶
func (c *Cache) LogStage(ctx context.Context, label, target string, elapsed time.Duration, runErr error)
LogStage emits a per-stage progress event for one magus.needs sub-target that ran under a project, routed through the cache logger like the other events. In collapse mode (where a project's subprocess output is withheld) these lines give the reader a checklist of what ran and whether it passed; runErr is nil on success.
func (*Cache) LogSummary ¶
LogSummary emits an end-of-run [summary] footer through the cache logger, drawn from the cache's own hit/miss/error counters. Like LogScope it routes through the logger so every output format receives the same event.
func (*Cache) OutputByRef ¶ added in v0.4.0
OutputByRef resolves a ref to its captured bytes and descriptor, consulting the LOCAL store first and then, only if the ref is unknown here, the remote bundle namespace. That order matters: local is authoritative and free, and a remote lookup is a network call no one asked for until the answer is not here. A miss reports which stores were consulted, so "not found" never hides where magus looked.
func (*Cache) Prune ¶
func (c *Cache) Prune(ctx context.Context, cutoff time.Time, dryRun bool) (n int, freed int64, err error)
Prune removes cache entries whose CreatedAt is before cutoff and then GCs orphaned blobs. Returns the count of entries removed and total bytes freed. When dryRun is true no files are deleted; counts are still returned.
func (*Cache) PruneRemote ¶
func (c *Cache) PruneRemote(ctx context.Context, policy RetentionPolicy) error
PruneRemote evicts remote cache artifacts per policy. It errors when no remote backend is configured, when the backend does not implement RemotePruner, or when the backend is inactive in this environment (the same gate fetch/push use, so a misconfigured prune fails loudly instead of silently no-opping).
func (*Cache) PublishOutput ¶ added in v0.4.0
PublishOutput uploads the run behind ref as a signed output bundle, so another machine can resolve that exact ref. Passing runs already travel with the cache artifact; this is what makes a FAILING run shareable, and it is always explicit. It errors when no remote backend is configured, when the backend is inactive, or when the cache holds no signing key (an unsigned bundle no consumer would accept).
func (*Cache) Remote ¶ added in v0.2.0
func (c *Cache) Remote() RemoteBackend
Remote returns the configured remote backend, or nil when local-only. Exposed so subsystems with their own artifacts (the knowledge-graph shard store) can ride the same backend as build artifacts, under the same signing/verification.
func (*Cache) Run ¶
func (c *Cache) Run(ctx context.Context, s Step, fn func(context.Context) error, opts ...RunOption) (Result, error)
Run executes fn under the cache. On a hash match it replays recorded outputs; otherwise fn runs and its outputs are snapshotted. Per-hash locking prevents manifest races when multiple RunAll goroutines share the same key.
Example ¶
ExampleCache_Run shows the minimal round-trip: a miss on the first call runs the work; a second call for the same step replays it as a hit without running.
The cache's own progress logging is raised to Error level so only the example's own prints reach stdout, keeping the output deterministic (the real CLI logs "cache miss/hit (<duration>)" lines, whose durations vary run to run).
// A fresh, private store so the first run is always a cold miss regardless of
// any earlier run or example. t.TempDir is unavailable in an Example, so this
// manages its own temp dir.
dir, err := os.MkdirTemp("", "magus-cache-example-run")
if err != nil {
fmt.Println("setup:", err)
return
}
defer os.RemoveAll(dir)
c, err := Open(context.Background(), dir, WithLog("text", slog.LevelError))
if err != nil {
fmt.Println("open:", err)
return
}
step := Step{
ProjectPath: "api",
WorkspaceRoot: ".",
Target: "build",
}
ran := 0
fn := func(_ context.Context) error {
ran++
return nil
}
// First run: cold miss → fn is called.
r1, err := c.Run(context.Background(), step, fn)
if err != nil {
fmt.Println("run1:", err)
return
}
fmt.Println("run1 hit:", r1.Hit)
// Second run on the same store: hit → fn is skipped.
r2, err := c.Run(context.Background(), step, fn)
if err != nil {
fmt.Println("run2:", err)
return
}
fmt.Println("run2 hit:", r2.Hit)
fmt.Println("fn ran:", ran, "time(s)")
Output: run1 hit: false run2 hit: true fn ran: 1 time(s)
func (*Cache) RunAll ¶
func (c *Cache) RunAll(ctx context.Context, steps []Step, fn func(context.Context, Step) error, opts ...RunOption) ([]Result, error)
RunAll schedules steps concurrently (bounded by WithLimiter, or DefaultConcurrency). Step.DependsOn (same-target) and Step.RunAfter (exact node keys) impose scheduling order for in-scope steps only; out-of-scope deps are ignored. A cyclic graph over either edge kind is rejected before any goroutine launches. Upstream cache keys fold into dependent Step.Deps transitively (happens-before: markDone writes the key before waitForDeps returns in dependents). Every goroutine launches immediately and blocks on deps without holding a slot, so the pool never deadlocks and g.Wait() always drains cleanly.
Example ¶
ExampleCache_RunAll shows fan-out across multiple steps with bounded concurrency and per-result callbacks. RunAll schedules its steps concurrently, so the order callbacks fire in is not deterministic; the example collects the outcomes and sorts them before printing.
dir, err := os.MkdirTemp("", "magus-cache-example-runall")
if err != nil {
fmt.Println("setup:", err)
return
}
defer os.RemoveAll(dir)
c, err := Open(context.Background(), dir, WithLog("text", slog.LevelError))
if err != nil {
fmt.Println("open:", err)
return
}
steps := []Step{
{ProjectPath: "api", WorkspaceRoot: ".", Target: "test"},
{ProjectPath: "web", WorkspaceRoot: ".", Target: "test"},
}
var mu sync.Mutex
var missed []string
results, err := c.RunAll(
context.Background(), steps,
func(_ context.Context, _ Step) error { return nil },
WithLimiter(NewLimiter(4)),
OnMiss(func(r *Result) {
mu.Lock()
missed = append(missed, r.ProjectPath)
mu.Unlock()
}),
)
if err != nil {
fmt.Println("runall:", err)
return
}
sort.Strings(missed)
fmt.Println("results:", len(results))
fmt.Println("missed:", missed)
Output: results: 2 missed: [api web]
func (*Cache) RunAside ¶ added in v0.4.3
func (c *Cache) RunAside(ctx context.Context, s Step, fn func(context.Context) error, opts ...RunOption) (Result, error)
RunAside runs one step OUTSIDE a batch under the same accounting a batch step gets: [Cache.admit]'s limiter slots, machine claim, inflight record and heartbeat, wrapped around Cache.Run's captured log, output ref and journal result event.
It exists because post-batch work is still work. A re-run dispatched straight at the interpreter claims nothing, appears in no inflight set and emits no journal event, so every observer reports an idle machine while the invocation still holds every project lock - which is exactly how a settle pass wedged a gate for over an hour on 2026-09-04 with `magus status` reporting 0 slots in use and nothing running.
Concurrency is the caller's to arrange: unlike RunAll there is no dependency barrier and no exclusive-step isolation here, because a single step has nothing to order against.
func (*Cache) StepKey ¶ added in v0.4.0
StepKey computes s's cache key plus the pre-hash key inputs behind it, without executing or storing anything. It is the SDK seam for the live half of the works-on-my-machine diff: `describe target --cache` keys the step exactly as a run would, then compares these lines against the set stored behind a ref. The returned key is what portable refs truncate, so callers can also predict the ref a run of this step would print.
func (*Cache) StepKeyMemo ¶ added in v0.4.0
func (c *Cache) StepKeyMemo(ctx context.Context, s *Step, memo *SourceMemo) (key string, lines []string, err error)
StepKeyMemo is StepKey with an optional SourceMemo. It exists for a caller that keys MANY steps sharing one WorkspaceRoot in one pass - IdentifyRef's sweep, via ComputeTargetKey - so distinct targets whose buildStep gives them the SAME Sources baseline expand and hash that source set once instead of once per target/charm combination. Pass nil for ordinary StepKey behavior; see SourceMemo's doc for the safety condition on passing a real one.
computeTargetKey (run.go), the implementation behind the `describe target --cache` seam StepKey's own doc describes, calls StepKeyMemo directly rather than through StepKey; only tests exercise StepKey itself.
type ClassDigest ¶ added in v0.4.0
type ClassDigest struct {
Class string `json:"class"`
Digest string `json:"digest"` // sha256 over the class's lines, truncated to 12 hex
Count int `json:"count"` // how many key inputs the class contributes
}
ClassDigest summarizes one component class of a cache key: every key input shares a label prefix ("src", "env", "tool", ...), and the class digest hashes the class's lines in key order. Two machines comparing digests learn WHICH CLASS disagrees without shipping the full lines - small enough for a URL fragment - while the full lines (CLI side) name the exact file or variable.
func ClassDigests ¶ added in v0.4.0
func ClassDigests(inputs []string) []ClassDigest
ClassDigests folds key inputs into one digest per component class, preserving first- appearance order (the hash order of the key itself, so output is stable).
type DerivedEdge ¶ added in v0.4.3
type DerivedEdge struct {
Writer, Reader int
// Ordered reports the batch schedule honors this edge: the writer's step(s) all
// run strictly before the reader's, via a coarse DependsOn edge or a derived
// RunAfter edge. An unordered edge is a settling candidate for the caller.
Ordered bool
// contains filtered or unexported fields
}
DerivedEdge records that Writer's declared writes intersect Reader's declared reads, so Reader's result depends on running after Writer. Indices into DerivedOrder.Nodes.
type DerivedOrder ¶ added in v0.4.3
type DerivedOrder struct {
Nodes []TargetNode
Edges []DerivedEdge
// Dropped holds the edges cycle resolution removed from Edges. Kept out of the
// schedule and out of TopoNodes, but the caller folds them back in as
// permanently unordered edges so their readers can still settle.
Dropped []DroppedEdge
// RunAfter maps a step's node key to the step node keys it must wait for,
// beyond its coarse DependsOn. RunAll's barrier waits these exactly.
RunAfter map[string][]string
}
DerivedOrder is the result of DeriveTargetOrder: the target-granular writer-before-reader edges a batch implies, and the step-level ordering that enforces the enforceable subset.
func DeriveTargetOrder ¶ added in v0.4.3
func DeriveTargetOrder(steps []Step, nodes []TargetNode) *DerivedOrder
DeriveTargetOrder derives cross-step, target-granular ordering from declared footprints. steps are the batch RunAll will schedule; nodes are the targets it runs (each step's own target plus chain members), with resolved globs.
An edge is derived when one node's writes can touch a path another node's reads can match, the two never run inside the same step, and they are not the same target (a target reading what it writes is not an edge). Glob intersection is conservative: uncertainty derives the edge, which can only over-order.
No cycle is fatal: a cycle is legitimate authoring (every sibling routing index declares it writes its own MAGUS.md and reads the others'), and the answer is the same one the entangled shape already gets. Cycle resolution drops edges into Dropped until the rest is acyclic; the drops are still settled after the batch. Edges whose direction the step schedule can honor become RunAfter entries; the rest are marked unordered for the caller to settle. Coarse DependsOn edges always win over derived ones: project-level ordering (and the affected set) is never widened or narrowed here.
func (*DerivedOrder) TopoNodes ¶ added in v0.4.3
func (d *DerivedOrder) TopoNodes() []int
TopoNodes returns every node index in dependency order: each edge's writer before its reader. Dropped edges are excluded, which is what makes the walk well defined. Deterministic.
type DroppedEdge ¶ added in v0.4.3
type DroppedEdge struct {
DerivedEdge
// Reason is "weak" for a baseline-fallback side that yielded, "cycle" for the
// tie-break that breaks a cycle of explicit declarations.
Reason string
}
DroppedEdge is a derived edge removed to break a cycle. The dependency is real, so it stays a settling candidate; only the schedule cannot express it.
type FSRemoteBackend ¶
type FSRemoteBackend struct {
// contains filtered or unexported fields
}
FSRemoteBackend is a local-filesystem RemoteBackend: artifacts are stored as gzip-tarballs under <dir>/<flat-project>/<hash>.tar.gz. Useful for testing and for sharing a cache between local workspaces on the same machine.
func NewFSRemoteBackend ¶
func NewFSRemoteBackend(dir string) (*FSRemoteBackend, error)
NewFSRemoteBackend returns an FSRemoteBackend rooted at dir (created on demand).
func (*FSRemoteBackend) Active ¶
func (r *FSRemoteBackend) Active(context.Context) bool
Active reports true: a filesystem backend is usable wherever its dir is.
func (*FSRemoteBackend) GetArtifact ¶
func (r *FSRemoteBackend) GetArtifact(_ context.Context, projectPath, hash string) (io.ReadCloser, error)
GetArtifact opens the artifact file. Returns (nil, nil) when not found.
func (*FSRemoteBackend) Name ¶ added in v0.4.0
func (r *FSRemoteBackend) Name() string
Name identifies this backend in the run header. "fs" rather than the directory: the header names the KIND of tier, and the path is already config the reader can look up.
func (*FSRemoteBackend) PutArtifact ¶
func (r *FSRemoteBackend) PutArtifact(_ context.Context, projectPath, hash string, data io.Reader) error
PutArtifact writes the artifact to the filesystem atomically.
type Failure ¶ added in v0.4.0
type Failure struct {
Project string
Target string
// OutputRef is the reference for the captured log, the same one the
// scrolling `inspect:` line names. Deterministic, so it stays valid for as
// long as the entry is in the cache.
OutputRef string
// LogPath is where the captured output was written. It is what makes the
// ref linkable: a file:// URL resolves with nothing running.
LogPath string
// Dur is how long the target ran before it failed, kept as a FIELD rather
// than only baked into Heading.
//
// A single pre-formatted string cannot be laid out. Durations are the one
// column a reader scans down - "which of these was slow" - and that only
// works if they share a right edge, which needs them separable from the
// text they follow. Heading stays for the plain-output path, where there
// is no column to align to.
Dur time.Duration
}
Failure is one failed target, as the pinned band holds it.
It carries what an ACTION needs - which project, which target, which captured output - alongside the line the reader sees. The band used to hold only the rendered heading, which meant the failures were on screen and unreachable: everything needed to rerun one was known at the moment it was formatted and thrown away immediately afterwards.
type KeyInfo ¶
KeyInfo is the public identity of a key: its base64 public key and derived keyid.
func SigningKeyInfo ¶
SigningKeyInfo derives the public key + keyid of a base64 seed without echoing the seed — for `magus config cache key id` reading MAGUS_CACHE_SIGNING_KEY.
func TrustedKeyInfo ¶
TrustedKeyInfo validates a base64 Ed25519 public key and returns it normalized with its derived keyid — for `magus config cache key id <pubkey>`.
type KeyInputChange ¶ added in v0.4.0
type KeyInputChange struct {
Class string `json:"class"` // the component class: "src", "env", "tool", ...
Input string `json:"input"` // the input's identity: the file, variable, or tool
// Recorded and Live are the values the two keys hashed under Input. The *Absent
// flags separate "that key had no such input" from "it hashed an empty value":
// classes like dep and charm carry no value slot, so appearing and disappearing is
// the only difference they can express.
Recorded string `json:"recorded,omitempty"`
RecordedAbsent bool `json:"recorded_absent,omitempty"`
Live string `json:"live,omitempty"`
LiveAbsent bool `json:"live_absent,omitempty"`
}
KeyInputChange is one key input that does not agree between a recorded run's key and the live one.
func FirstKeyInputChange ¶ added in v0.4.0
func FirstKeyInputChange(recorded, live []string) []KeyInputChange
FirstKeyInputChange reports which inputs disagree between a recorded run's key inputs and the live ones, in LIVE key order (inputs only the recorded key had trail behind, in recorded order). Live order is the order hashStepInputs writes, so the slice LEADS with the earliest component class - the target's own definition before its sources, sources before env, env before tools - which is the order a reader wants to be told about: a changed target definition explains a moved source hash, never the reverse. Empty exactly when the two sides agree.
This is the one pairing rule for both comparison surfaces; DiffKeyInputs projects the same result onto whole lines for `--against`.
Both sides must already carry digested env values (DigestEnvValues); the store persists digested lines and a raw live line would read as a difference on every env var. Identity, not the whole line, is what pairs the two sides, so a source file whose hash moved counts once. Multiplicity collapses: a line repeated within one side is compared once.
type KeyInputDiff ¶ added in v0.4.0
type KeyInputDiff struct {
Class string `json:"class"`
StoredOnly []string `json:"stored_only,omitempty"`
LiveOnly []string `json:"live_only,omitempty"`
}
KeyInputDiff is one component class's stored-vs-live disagreement: lines only the stored key has and lines only the live key has. A class absent from the slice matched exactly.
func DiffKeyInputs ¶ added in v0.4.0
func DiffKeyInputs(stored, live []string) []KeyInputDiff
DiffKeyInputs projects FirstKeyInputChange onto whole lines: the same identity pairing decides what moved, and each moved input contributes its stored line to StoredOnly and its live line to LiveOnly, grouped by component class. A source file whose hash changed appears once on each side, which is exactly the shape a reader needs to see what drifted.
Classes come back in stored-key order, then any class only the live key has, in live order - the order `--against` has always rendered, and a shape scripts read. The pairing's own live-first ordering is the right lead for a SINGLE first difference and the wrong one for a full listing, where the reader is scanning classes rather than being handed a culprit.
Lines are emitted verbatim rather than rebuilt from a change's Input and value, because that join is not invertible: env keys its value behind "=" but its unset marker behind ":", and the classes with no value slot at all would gain a trailing separator.
type KeyMaterial ¶
KeyMaterial is a freshly minted signing keypair, base64-encoded. SeedB64 is the secret (MAGUS_CACHE_SIGNING_KEY); PubB64 goes in trusted_keys.
func GenerateSigningKey ¶
func GenerateSigningKey() (KeyMaterial, error)
GenerateSigningKey mints a fresh Ed25519 keypair. Lives here, beside the verifier, so the keyid derivation never drifts.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter is a FIFO-fair weighted semaphore that bounds concurrent work and exposes live occupancy metrics for the pool inspector.
func LimiterFromContext ¶
LimiterFromContext retrieves the Limiter stored by ContextWithLimiter, or nil.
func NewLimiter ¶
NewLimiter returns a Limiter with capacity n. n <= 0 means unlimited (Acquire and AcquireN always succeed immediately).
func (*Limiter) Acquire ¶
Acquire blocks until 1 slot is available or ctx is cancelled. Returns ctx.Err() on cancellation.
func (*Limiter) AcquireN ¶
AcquireN acquires n slots under FIFO fairness. n < 1 is floored to 1 (a defensive guard; every real caller already passes >= 1). A request above capacity can never be satisfied by any wait, so it fails immediately rather than blocking forever; callers decide whether to clamp first (RunAll, where a slot count is a coarse throttle) or surface the error (os.with_slots/archive, where reserving more slots than exist would desync the reservation from the tool's own worker count). Returns ctx.Err() on cancellation.
func (*Limiter) SetHooks ¶
func (l *Limiter) SetHooks(onAcquire func(waitNs int64, n int), onRelease func(n int), onWait func(delta int))
SetHooks installs optional callbacks fired on every Acquire/Release. Must not block. Stored atomically so a SetHooks racing with concurrent Acquire/Release is safe.
onWait mirrors the internal queued counter exactly: it fires with +n the instant a caller begins waiting for n slots (before the blocking Acquire) and with -n once that Acquire returns, whether it acquired or the context was cancelled. Net inflight of onWait tracks Limiter.Snapshot's Queued.
func (*Limiter) Snapshot ¶
func (l *Limiter) Snapshot() LimiterStats
Snapshot returns a point-in-time view of the limiter.
func (*Limiter) Yield ¶
Yield releases the caller's slots for the duration of fn, then re-acquires them before returning. It releases every slot the caller holds (SlotsHeld(ctx), at least 1): a weighted step holds more than one, and releasing only one would leave it pinning slots that fn's own AcquireN then blocks on forever. The caller MUST hold a slot; a slotless caller would over-release the semaphore. Re-acquire uses a non-cancellable context so the caller always returns with its slots held (RunAll releases unconditionally; a slotless return would panic). The re-acquire re-enters the FIFO queue, so a yielding goroutine goes to the back.
Trade-off: the non-cancellable re-acquire can block a returning yield on a saturated limiter even after ctx is cancelled, slowing shutdown until peers free the slots.
type LimiterStats ¶
type LimiterStats struct {
Capacity int // total slots; 0 = unlimited
Running int // currently acquired slots
Queued int // slots currently blocked in Acquire/AcquireN
}
LimiterStats is a point-in-time view of the concurrency pool.
type LocalAdmitter ¶ added in v0.4.0
type LocalAdmitter struct{ Budget *MachineBudget }
LocalAdmitter reaches a budget held in THIS process. It is what the daemon's own workspaces use: dialing its own socket from inside a request it is serving would have it wait on itself.
func (LocalAdmitter) Drop ¶ added in v0.4.0
func (l LocalAdmitter) Drop(_ context.Context, waiter string)
func (LocalAdmitter) Release ¶ added in v0.4.0
func (l LocalAdmitter) Release(_ context.Context, id string)
func (LocalAdmitter) Request ¶ added in v0.4.0
func (l LocalAdmitter) Request(_ context.Context, waiter string, c types.MachineClaim) (types.MachineVerdict, error)
type MachineAdmitter ¶ added in v0.4.0
type MachineAdmitter interface {
Request(ctx context.Context, waiter string, c types.MachineClaim) (types.MachineVerdict, error)
Release(ctx context.Context, id string)
Drop(ctx context.Context, waiter string)
}
MachineAdmitter is the budget as a client reaches it: the daemon over the proc socket, or a MachineBudget directly when this process IS the daemon.
type MachineBudget ¶ added in v0.4.0
type MachineBudget struct {
// contains filtered or unexported fields
}
MachineBudget is admission across every magus on the host: one budget of concurrency slots and declared memory, arbitrated in one place.
The Limiter cannot answer this. It is per-process, so N worktrees each admit up to their own capacity and nothing sums them. Both read the same folded figure: the limiter throttles peers within a process, this decides whether the machine can seat the step at all.
It lives in the user's daemon, which is what makes a queue possible. An earlier attempt kept it in a flocked lease directory and had to refuse rather than wait, because a passive directory cannot tell a waiter when its turn came. A process can.
DECLARED, not observed, so the same command on the same host reaches the same verdict whatever a browser is doing. Observed pressure stays advisory in sys/mem.
func NewMachineBudget ¶ added in v0.4.0
func NewMachineBudget(budgetMB, budgetSlots int) *MachineBudget
NewMachineBudget returns a budget of budgetMB megabytes and budgetSlots concurrency slots. A non-positive figure leaves that half unlimited, which is what a host magus cannot measure falls back to.
func (*MachineBudget) Drop ¶ added in v0.4.0
func (b *MachineBudget) Drop(waiter string)
Drop retires a waiter that gave up, so the queue behind it moves now rather than after machineWaiterStaleAfter.
func (*MachineBudget) Release ¶ added in v0.4.0
func (b *MachineBudget) Release(id string)
Release returns a granted claim. An unknown id is ignored: a client that lost its daemon mid-run releases into a budget that never recorded it.
func (*MachineBudget) Request ¶ added in v0.4.0
func (b *MachineBudget) Request(waiter string, c types.MachineClaim) types.MachineVerdict
Request is one poll for admission on behalf of waiter, an id stable across this step's polls and unique machine-wide. It grants, refuses, or queues, and never blocks: the wait belongs to the client, which is the process that can report it.
func (*MachineBudget) Snapshot ¶ added in v0.4.0
func (b *MachineBudget) Snapshot() types.MachineSnapshot
Snapshot reports the whole budget. Read-only: it retires nothing, so a status command can ask what the machine is doing without moving a queue. Entries whose process is gone are FILTERED rather than deleted, so the report never shows a corpse the next Request would retire anyway.
type Manifest ¶
type Manifest struct {
ProjectPath string `json:"projectPath"`
Hash string `json:"hash"`
Target string `json:"target,omitempty"`
Outputs []OutputRecord `json:"outputs"`
CreatedAt time.Time `json:"createdAt"`
// Platform is runtime.GOOS+"/"+runtime.GOARCH at the time this entry was
// produced (e.g. "darwin/arm64"). It is NOT part of the cache key - the key
// must stay platform-free so an output ref (a truncated key) is identical on
// every machine - so it lives here instead, as a replay-time gate. src: lines
// are content hashes, so darwin and linux compute the SAME digest for the
// same commit; without this field a Linux CI pass could replay on a darwin
// laptop as a pass for code darwin never compiled (or vice versa), and worse
// for a platform-conditional file like hash_iouring_linux.go, which darwin
// never even builds. Empty means "written before this field existed"; see the
// mismatch check in readManifest for how that is treated.
Platform string `json:"platform,omitempty"`
// DurationMs is how long the run that produced this entry took. A cache HIT replays that run's
// result, so this is exactly the work the hit avoided - a measured figure for this target on
// this machine, not an average over targets that never ran. Cache.Stats sums it across hits.
//
// Absent (zero) on every manifest written before this field, and on an entry whose run was not
// timed. Those hits count toward Hit and contribute nothing to Saved, so the total understates
// rather than invents - which is why the console labels it as saved THIS SESSION rather than
// implying it covers the cache's whole history.
DurationMs int64 `json:"durationMs,omitempty"`
// Return is what the target returned (str or [str]), stored so a cache HIT can
// replay it. A hit never invokes the target, so without this a target would
// print its result on the first run and nothing on the second. Absent for the
// `> void` targets that are the overwhelming majority, and absent from every
// manifest written before returns existed - which read back as no value, the
// same as a void target, so old entries stay valid.
Return any `json:"return,omitempty"`
}
Manifest is the on-disk record of a single cache entry.
type Mark ¶ added in v0.4.3
type Mark struct {
At time.Time
Project string
Target string
What string // "running", "executing", "finished"
Log string
}
Mark names one transition Progress saw. Log is the step's captured log path, empty until [Cache.captureRun] has opened one.
type MemoryPressure ¶ added in v0.4.0
type MemoryPressure struct {
AvailableBytes int64
TotalBytes int64
// SwapUsedBytes and SwapGrowthBytes are the machine's swap and how much of it
// this run added. Growth is the attributable half: a machine up for weeks
// carries swap that predates the run, and reporting the level alone would
// blame this run for it. Both 0 where the platform cannot report swap.
SwapUsedBytes int64
SwapGrowthBytes int64
// SwapTriggered reports that swap growth is why the watchdog spoke, as opposed to
// falling headroom. See mem.Reading.
SwapTriggered bool
// BuzzObjects and BuzzPeak are the script VM's heap counts. Its heap never
// frees, so a magusfile can consume the machine with no subprocess looking
// guilty; 0 means the caller did not measure.
BuzzObjects int
BuzzPeak int
// BuzzHotSite is the source position responsible for the most heap growth,
// as "source:line", or "" when nothing was sampled.
BuzzHotSite string
}
MemoryPressure is what the run-time watchdog observed. Data, not a sentence: every other Log* method on Cache takes the facts and builds the record here, and a warning assembled across two packages has no single owner for its wording.
type Option ¶
type Option func(*Cache)
Option configures a Cache at open time.
func WithCollapse ¶
WithCollapse enables collapse-on-success output: a project's subprocess output is captured. Failures show an excerpt; the output ref keeps the full log.
func WithInsecureRemote ¶
func WithInsecureRemote() Option
WithInsecureRemote allows a remote backend to run with no trust set, importing unsigned artifacts without authentication. Open otherwise refuses that combination. Only for a fully trusted store (e.g. a local cross-workspace cache); never for a shared cache that an untrusted party could write.
func WithMachineAdmission ¶ added in v0.4.0
func WithMachineAdmission(admitter MachineAdmitter, noWait bool) Option
WithMachineAdmission routes every step through machine-wide admission: it takes its concurrency slots and declared memory_mb from a budget shared by every magus on the host, queueing behind the peers already holding it. noWait fails fast (MGS3009) instead of queueing.
admitter must reach the ONE arbiter for this machine, which is the user's daemon. Omitting the option leaves admission per-process, which is what a library caller with no host to arbitrate wants.
Applied after WithLogger, so a lost arbiter is reported through the caller's logger rather than the default one.
func WithMaxImportBytes ¶
WithMaxImportBytes sets the per-entry byte cap used by Import (default 10 GiB).
func WithMutable ¶
WithMutable controls whether the cache writes new entries on a miss (default true).
func WithRemoteBackend ¶
func WithRemoteBackend(t RemoteBackend) Option
WithRemoteBackend configures a remote backend that is consulted on local miss.
func WithSigningKey ¶
WithSigningKey sets the Ed25519 seed (32 bytes) used to sign artifacts on push. Set only in trusted CI; without it the cache cannot publish trusted artifacts.
func WithSilent ¶
WithSilent enables silent output mode: on top of quiet's suppression, a failing project's dump is bounded to its tail (with a pointer to the retained full log) and only target-marked important lines are bubbled up. See captureRun.
func WithSizeMB ¶
WithSizeMB caps cache disk usage to n MiB. 0 means unlimited.
func WithTrustedKeys ¶
WithTrustedKeys sets the raw Ed25519 public keys (32 bytes each) that remote artifacts must be signed by. A non-empty set makes verification mandatory.
type OutputBundle ¶ added in v0.4.0
type OutputBundle struct {
Schema int `json:"schema"`
Descriptor OutputDescriptor `json:"descriptor"`
KeyInputs []string `json:"key_inputs,omitempty"`
}
OutputBundle is the metadata half of a published output: the run's descriptor plus the key inputs behind it. The captured bytes travel beside it as a separate member.
type OutputDescriptor ¶ added in v0.2.0
type OutputDescriptor struct {
Ref string `json:"ref"`
Project string `json:"project"`
Target string `json:"target,omitempty"`
Inv string `json:"inv,omitempty"` // invocation id of the run that produced this output
Failed bool `json:"failed"`
ErrMsg string `json:"error,omitempty"` // failure message; empty on success
TimestampMs int64 `json:"timestamp_ms"` // unix milliseconds, matching DurationMs' unit
DurationMs int64 `json:"duration_ms"`
// Ref is the key-derived portable id shared by every attempt of the step; the
// fields below carry the identity it derives from.
Key string `json:"key,omitempty"` // full cache key hash (64 hex)
KeyVersion int `json:"key_version,omitempty"` // hashStep KeyVersion that produced Key
Attempt string `json:"attempt,omitempty"` // execution-unique id; the file stem
MagusVersion string `json:"magus_version,omitempty"`
// The cache key pins a TREE STATE (via its source content hashes)
// without naming it - it deliberately contains no commit, branch, or base. Revision
// and Dirty close that gap: they record the VCS state the run's inputs were read
// at, so a ref fetched from a foreign machine (CI, a teammate) can say not just
// WHICH target produced it but which commit reproduces it. A v2 descriptor (or
// earlier) carries neither field, which reads as "unknown, no VCS, or predates
// this field" - never an error, since resolving it is best-effort by construction
// (a workspace with no VCS is a supported, silent no-op).
Revision string `json:"revision,omitempty"` // full VCS revision hash inputs were read at; "" when unknown
Dirty bool `json:"dirty,omitempty"` // working tree had uncommitted changes; Revision alone then is necessary but not sufficient to reproduce
// The last key inputs a reader cannot recover from the fields above. Charms are
// folded into Target by reproTarget and sources are pinned by Revision, but these
// key the cache while appearing nowhere else, so `magus x <ref>` would otherwise
// rebuild a DIFFERENT invocation and report success. Spell is the sharpest:
// `go::go-build` and `go-build` are the same Target with different bodies.
Spell string `json:"spell,omitempty"` // spell::op filter that selected the definition
ExtraArgs []string `json:"extra_args,omitempty"` // trailing args forwarded after --
VCSName string `json:"vcs,omitempty"` // provider Revision came from: git, hg, sl, jj
// Platform is GOOS/GOARCH, the same string readManifest and importArtifact
// refuse a mismatch on. Recorded unconditionally, NOT gated on the
// cache.include.os/arch settings: those govern what keys a build, while this
// explains a refusal the guards perform regardless of them.
Platform string `json:"platform,omitempty"`
}
OutputDescriptor is a stored execution's identity and outcome, written beside its verbatim output blob. It gives `magus query output <ref> -o json`, the MCP tool, and the viewer header the run's project/target/status/timing without anyone parsing the output bytes.
type OutputRecord ¶
type OutputRecord struct {
Path string `json:"path"` // repo-relative
Blob string `json:"blob"` // sha256 hex of contents
Mode uint32 `json:"mode"` // file mode bits & 0o777
Symlink string `json:"symlink,omitempty"` // if non-empty, restore as symlink to this target
Size int64 `json:"size"` // bytes (for sanity-check on replay)
}
OutputRecord captures one declared output file.
type OutputStore ¶ added in v0.2.0
type OutputStore struct {
// contains filtered or unexported fields
}
OutputStore is the cache's output-retrieval repository: it persists each execution's captured output VERBATIM under <cacheDir>/outputs and resolves refs back to bytes/metadata. The user-facing ref is PORTABLE: a truncation of the step's cache key, so equal inputs mint the same ref on every machine. One execution is two sibling files named by an execution-unique attempt id:
outputs/<cacheKey>/<attempt>.out the exact bytes the process wrote outputs/<cacheKey>/<attempt>.json its OutputDescriptor (identity + outcome)
The .out blob is the source of truth: ByRef reads it straight through, byte-for-byte what ran. Per-line structured events live once in the invocation journal, so output is never stored twice. Grouping by cache key keeps a nondeterministic target's runs together, making keep-last-K a per-directory prune and a ref lookup a shallow scan. Safe for concurrent Persist calls (each mints a distinct attempt).
func NewOutputStore ¶ added in v0.2.0
func NewOutputStore(cacheDir string) *OutputStore
NewOutputStore builds a store rooted at the cache ROOT dir (not the outputs subdir). It joins "outputs" and RunsDir itself, so ByRef works on an Inspect workspace with no live cache.
func (*OutputStore) AdoptImported ¶ added in v0.4.0
func (s *OutputStore) AdoptImported(cacheKey string, output []byte) (string, bool)
AdoptImported completes an execution record that arrived from a remote artifact. An import ships the producer's DESCRIPTOR but not its output blob (the bytes travel once, as the build log), so the attempt is a descriptor with no sibling .out and stays unresolvable. Given the log bytes a cache hit just replayed, this writes that missing blob and returns the step's portable ref - so a fresh machine answers under the SAME ref the producer printed instead of minting a local one. Reports false when the key has no orphan descriptor to complete (nothing imported, or already completed), leaving the caller to persist its own attempt.
func (*OutputStore) Attempts ¶ added in v0.4.0
func (s *OutputStore) Attempts(ref string) ([]OutputDescriptor, error)
Attempts lists every stored execution of the step ref names, newest first - the keep-last-K history behind one portable ref (`magus query output <ref> --attempts`). ref may be the step ref, a unique prefix, or any attempt id within the step; the whole directory answers either way. Pre-portable descriptors carry no Attempt field; their file stem (which was the v1 ref) fills it so every row is addressable. A blob whose descriptor is missing or unreadable (a Persist that died between its two writes) still rows up - minimally, from the blob itself - because a listing that silently omits a retrievable execution reads as "it does not exist".
func (*OutputStore) ByRef ¶ added in v0.2.0
func (s *OutputStore) ByRef(ref string) ([]byte, OutputDescriptor, error)
ByRef resolves a ref (or unique prefix) to the target's VERBATIM captured output bytes plus its metadata. The bytes are read straight from the <ref>.out blob - exactly what the process wrote, no reconstruction. This is the retrieval entry point for `magus query output <ref>` (print path).
func (*OutputStore) DescriptorByRef ¶ added in v0.4.0
func (s *OutputStore) DescriptorByRef(ref string) (OutputDescriptor, error)
DescriptorByRef resolves a ref (or unique prefix) to its stored descriptor alone, without reading the output blob - the identity views (`--identity`, `--against`) want the metadata, and a captured log can be large. A resolvable ref whose descriptor is missing or unreadable yields the error, unlike ByRef, which still has bytes to return and so degrades to a zero descriptor.
func (*OutputStore) InvocationByID ¶ added in v0.2.0
func (s *OutputStore) InvocationByID(inv string) (journal.Invocation, error)
InvocationByID reads the union run log (<cacheDir>/runs/<inv>.jsonl) for one invocation id and rebuilds its header: the command lineage (subcommand/args/trigger), timing, and outcome. It is how a stored output (OutputDescriptor.Inv) is traced back to the run that produced it - `magus query output <ref> --identity` and the viewer surface this lineage. Reads off the cache ROOT (RunsDir), not outputsDir. Returns fs.ErrNotExist when the run log has aged out.
func (*OutputStore) InvocationEventsByID ¶ added in v0.4.0
func (s *OutputStore) InvocationEventsByID(inv string) (journal.Invocation, []journal.Event, error)
InvocationEventsByID reads one invocation's run log and returns its header alongside the EVENTS it was reconstructed from. OutputStore.InvocationByID keeps only the header, which is all a `--identity` line needs; this is for a caller that wants the stream itself.
It exists because the events were being read and discarded: journal.KindSecret records every credential a run reached for, and the docs offer that as the answer to "what did this run touch", but nothing could read it back. Anything answering a question FROM the journal starts here.
inv must be a FULL invocation id - unlike an output ref there is no prefix resolution, because a run log is addressed by exact filename. An id failing LooksLikeInvocationID is refused before it is joined onto the runs dir: this is reachable from the daemon's Connect API, where an unvalidated id reads any .jsonl on the machine. Returns fs.ErrNotExist when the log has aged out under the RotateLogs cap.
func (*OutputStore) InvocationEventsFrom ¶ added in v0.4.0
InvocationEventsFrom reads one invocation's journal starting at byte offset from and returns the events on COMPLETE lines plus the offset just past the last one. It is the tailing counterpart to OutputStore.InvocationEventsByID, which reads the file whole.
A journal is appended while its run is still going (journal.FileHandler flushes every kind but output), so a follower resumes from where it stopped instead of re-reading megabytes it has already delivered - which is what keeps watching a long build proportional to what arrived. Stopping at the last newline is what makes a concurrent writer safe to read: a half-written line is left for the next call rather than parsed as corruption.
A file shorter than from means the id was reused after a cache clean, so the read restarts at zero rather than seeking past the end and reporting nothing forever. Returns fs.ErrNotExist when the log never existed, has aged out, or inv is not shaped like an invocation id.
func (*OutputStore) KeyInputsByKey ¶ added in v0.4.0
func (s *OutputStore) KeyInputsByKey(cacheKey string) ([]string, error)
KeyInputsByKey returns the stored pre-hash key inputs for an EXACT cache key, the shape a manifest records. It does not scan the store the way KeyInputsByRef must: a prefix search would be a slower route to the same directory, and it fails outright once the key's attempt blobs have been pruned away while this sidecar remains.
func (*OutputStore) KeyInputsByRef ¶ added in v0.4.0
func (s *OutputStore) KeyInputsByRef(ref string) ([]string, error)
KeyInputsByRef returns the stored pre-hash key inputs behind ref (step ref, unique prefix, or any attempt id within the step). fs.ErrNotExist when the step resolves but predates key input persistence.
func (*OutputStore) LatestRefsByTarget ¶ added in v0.2.0
func (s *OutputStore) LatestRefsByTarget() []OutputDescriptor
LatestRefsByTarget returns the newest stored execution per (project, target): one OutputDescriptor each, the most recent by TimestampMs (ties broken by attempt id, then ref, so the choice is stable regardless of directory iteration order). It scans every cache-key directory's descriptor sidecars. This is what folds each target's last output ref onto its knowledge-graph node without the graph builder parsing the store's on-disk layout.
Descriptors store the REPRO target (bare name plus charm suffix, see reproTarget); this collapses that back to the bare declared target so the newest run is picked across charm variants and Target matches a knowledge-graph node. Descriptors without a target are skipped, as is anything unreadable - fewer entries, never an error. Sorted by project then bare target for deterministic assembly.
func (*OutputStore) ListDescriptors ¶ added in v0.2.0
func (s *OutputStore) ListDescriptors() []OutputDescriptor
ListDescriptors returns every stored execution's descriptor, newest run first, across all cache keys - the feed for the console's run browser (the log-viewer tree groups them project -> target -> run so a reader can browse recent runs and open any one's captured output). Unlike LatestRefsByTarget, which collapses to the single newest run per target, this keeps every retained execution (the store holds keep-last-K per cache key), so a target's recent history is browsable. The REPRO target is preserved verbatim (with any charm suffix) so a run's exact invocation stays visible; the caller collapses to the bare name for grouping if it wants. Best-effort: an absent or unreadable store, or an undecodable descriptor, yields fewer entries, never an error.
func (*OutputStore) ListRunLogs ¶ added in v0.4.0
func (s *OutputStore) ListRunLogs(limit int) []RunLog
ListRunLogs returns the newest retained invocation journals, newest first by modtime, capped at limit (limit <= 0 returns every retained one). It is the run browser's feed and the invocation-addressed counterpart to OutputStore.ListDescriptors, which lists stored OUTPUTS.
Cost is bounded per journal, not per event: a head read for the started event and a tail read for the finished one. A run killed before it finished has no finished event, so its Status is empty and FinishedMs falls back to the last event the tail window holds - the same honest degradation journal.InvocationFromEvents makes.
Best-effort throughout: an unreadable dir, an unparsable line, or a journal whose head is not a started event yields fewer rows, never an error.
func (*OutputStore) Persist ¶ added in v0.2.0
func (s *OutputStore) Persist(ctx context.Context, cacheKey string, output []byte, d OutputDescriptor) (OutputDescriptor, error)
Persist writes the execution's captured output VERBATIM as outputs/<cacheKey>/<attempt>.out (byte-for-byte what the process wrote, so `magus query output <ref>` is a straight read - never a reconstruction) plus an <attempt>.json descriptor, then prunes the cache key's directory to keep-last-K. Per-line structured events are NOT stored here - they live in the invocation journal, so no output is stored twice. Returns the descriptor as stamped and stored: Ref is the step's portable ref (shared by every attempt of this key), Attempt the execution-unique id that names the files just written. Best-effort at the call site: on error the caller keeps the run's own outcome.
func (*OutputStore) PersistKeyInputs ¶ added in v0.4.0
PersistKeyInputs writes the step's pre-hash key inputs beside its attempts, env values digested (DigestEnvValues) and the result secret-redacted line-by-line as a second net for non-env classes. One file per cache key: the lines are a property of the KEY, so later attempts of the same step overwrite with identical content. Best-effort at the call site: an error just means a later --identity/--against has no lines to explain with.
func (*OutputStore) RotateRuns ¶ added in v0.2.0
func (s *OutputStore) RotateRuns(keepLast int, keepBytes int64) (removed int, bytesFreed int64)
RotateRuns keeps the newest invocation journals (runs/<inv>.jsonl, by modtime) and removes the rest, returning how many it deleted and the bytes that freed. The runs dir is flat (one file per invocation, not keyed like outputs/), so this is a single keep-last over the whole directory - the run-log analogue of pruneKey, and the worker behind the rotate-logs job.
TWO caps, and the tighter one wins. keepLast bounds the COUNT; keepBytes bounds the total on disk. Count alone does not bound anything: a journal holds every output line of its run, so its size is whatever the subprocess printed, and 500 of them is 500 times an unbounded number. A run that emits a gigabyte earns a gigabyte.
Best-effort: an unreadable dir or a failed remove is skipped, never fatal. keepLast <= 0 is a no-op (never wipe the whole dir by accident), and keepBytes <= 0 means no size cap.
func (*OutputStore) RunsStat ¶ added in v0.2.0
func (s *OutputStore) RunsStat() (bytes int64, count int64)
RunsStat reports the invocation-journal directory's current footprint: total bytes across every runs/<inv>.jsonl and the number of such files. Best-effort and read-only; a missing dir is (0, 0). It is what the RotateLogs job reports as its target size.
func (*OutputStore) StepRef ¶ added in v0.4.0
func (s *OutputStore) StepRef(cacheKey string) string
StepRef returns the step's portable ref when at least one execution is stored for cacheKey, or "" if none. A cache HIT reuses it instead of re-persisting identical output under a fresh attempt - so hits point at the existing events, not bloat the store. Pre-portable directories qualify too: the ref derives from the key, not from what any stored descriptor says.
type PrettyHandler ¶ added in v0.4.0
type PrettyHandler struct {
// contains filtered or unexported fields
}
PrettyHandler renders both cache events (known cache.* messages) and general diagnostics in a compact, scannable style: colored ASCII status glyphs on a TTY, bracketed prefixes on plain streams. It carries no timestamps or level=/key= boilerplate; that noise is what makes raw slog output hard to read interactively.
On a TTY, errors are written into a sticky region at the bottom of the terminal so they never scroll off, under a live pool status line (scroll margins are set on the first cache.pool or cache.error, and reset on cache.summary or Close). Non-TTY writers fall through to plain output; the region is harmless when disabled.
func NewPrettyHandler ¶
func NewPrettyHandler(w io.Writer, level slog.Level) *PrettyHandler
NewPrettyHandler builds the unified pretty handler. Color is driven by the terminal-ness of w's file descriptor (any writer exposing Fd(), not just an *os.File); a writer without one -- a bytes.Buffer in tests, a pipe -- renders plain. TTY detection runs per-Handle so late redirects are noticed.
On a TTY writer, a sticky region is reserved at the bottom of the terminal: a live pool status line on top, then [fail] lines that do not scroll off. The reservation is lazy (the region opens on the first cache.pool or cache.error) and reset on cache.summary or Close; a pipe / bytes.Buffer / non-TTY writer disables it.
func NewPrettyHandlerFor ¶ added in v0.4.0
func NewPrettyHandlerFor(w io.Writer, level slog.Level, p tty.Probe, now func() time.Time) *PrettyHandler
NewPrettyHandlerFor returns a handler that draws into w, measured through p.
Exported for a caller that is deliberately NOT the process terminal: the documentation renderer, which drives a real handler against an in-memory screen so the pictures it publishes are drawn by the code a reader will actually meet. NewPrettyHandler is the production entry point and keys on standard error to keep one handler per terminal; this one makes no such claim, so two callers get two independent handlers and neither takes the process band. A nil now defaults to time.Now; pass a fixed one to render a picture whose bytes do not depend on when it was rendered.
func StderrHandler ¶ added in v0.4.0
func StderrHandler() *PrettyHandler
StderrHandler returns the process's display handler if one was built, or nil. It is how a caller that did not construct the handler - the CLI's exit path, offering an interactive prompt over the run's pinned failures - reaches the one holding them.
func (*PrettyHandler) Close ¶ added in v0.4.0
func (h *PrettyHandler) Close() error
Close releases the sticky error region if one was reserved. Idempotent and safe to defer; required so a panic or interrupted run does not leave the terminal with the scroll margins still set. The next non-magus command the user runs inherits a clean terminal state.
func (*PrettyHandler) Enabled ¶ added in v0.4.0
Enabled reports whether a level is worth handling. slog calls it from every logging goroutine, and setLevel writes h.level under the mutex, so this reads it under the mutex too.
func (*PrettyHandler) Failures ¶ added in v0.4.0
func (h *PrettyHandler) Failures() []Failure
Failures returns the failures currently pinned in the band, in the order they are DRAWN, skipping empty slots. It is the keyboard counterpart to HitFailure: a caller offering "select with the arrow keys" needs the list, not a row.
Drawn order, not arrival order. The ring keeps a failure in the row it was painted into, so once it has wrapped the oldest entry sits in the middle - and the list has to match the screen, because a reader picks by position.
func (*PrettyHandler) Handle ¶ added in v0.4.0
Handle renders one record. It deliberately does NOT skip on ctx.Err(): a handler must not treat cancellation as permission to drop output. The check that used to live here was inert for as long as it existed, because every call site reached slog through Logger.Info/Warn/..., which passes context.Background() - Err() was never non-nil. Once the run path started passing its REAL context (so records could reach the secret resolver), it woke up and began eating exactly the lines that matter most: in a concurrent run, the first failure cancels the errgroup, and every [pass]/[fail] that finished afterwards, plus the [summary] footer and the Ctrl-C service-release warning, vanished from the default output while -o json still showed them.
func (*PrettyHandler) HitFailure ¶ added in v0.4.0
func (h *PrettyHandler) HitFailure(row int) (Failure, bool)
HitFailure maps an absolute terminal row to the failure drawn on it.
The handler answers this rather than exposing its band layout, because the layout is its own business: the status line owns the first row and the ring owns the rest, and a caller that had to know that would be a second copy of the arrangement waiting to drift. Callers pass the Row from a mouse event straight through.
Reports false for the status row, for an empty ring slot, and for any row outside this handler's band - including a click on another consumer's rows.
func (*PrettyHandler) ReleaseBand ¶ added in v0.4.0
func (h *PrettyHandler) ReleaseBand() error
ReleaseBand hands this handler's rows back, for a caller that held them past the end of a run to offer an interactive prompt over the pinned failures.
func (*PrettyHandler) RendersBand ¶ added in v0.4.0
func (h *PrettyHandler) RendersBand() bool
RendersBand reports whether this handler has a live band to paint into. The cache asks before emitting pool samples, so a piped, JSON, or CI run pays nothing for a feature it cannot show, and the CLI asks before offering an interactive prompt over failures it may not have drawn.
Not named Enabled: that one belongs to slog.Handler and answers an entirely different question (whether a level is worth handling).
func (*PrettyHandler) SetFocus ¶ added in v0.4.0
func (h *PrettyHandler) SetFocus(f PaneFocus)
SetFocus moves focus between the two views, which resizes them: the focused one takes the golden ratio's major share.
Resizing on focus rather than offering a drag handle is deliberate - there is no pointer contract to invent, and the pane you are working in is the one that should be big.
func (*PrettyHandler) SetPreview ¶ added in v0.4.0
func (h *PrettyHandler) SetPreview(lines []string)
SetPreview gives the band a right-hand column: the captured output of whatever is selected. Nil or empty returns it to a single column.
This is the "two views, one run" surface. It is deliberately not two PANES - nothing here manages a terminal, and a caller cannot put arbitrary content in it. Both columns are things this handler already owns, which is the line between showing a reader their run and becoming a multiplexer.
func (*PrettyHandler) SetSelection ¶ added in v0.4.0
func (h *PrettyHandler) SetSelection(n int)
SetSelection highlights the nth pinned failure, counting only the occupied slots that PrettyHandler.Failures returns. A negative n clears it.
Selection lives here rather than in the prompt for the reason the band does: this repaints on a timer, so a highlight painted from outside would be erased by the next status tick.
func (*PrettyHandler) ToggleFocus ¶ added in v0.4.0
func (h *PrettyHandler) ToggleFocus() PaneFocus
ToggleFocus swaps which view has the major share, and reports the new focus.
func (*PrettyHandler) WithAttrs ¶ added in v0.4.0
func (h *PrettyHandler) WithAttrs(_ []slog.Attr) slog.Handler
func (*PrettyHandler) WithGroup ¶ added in v0.4.0
func (h *PrettyHandler) WithGroup(_ string) slog.Handler
func (*PrettyHandler) Zone ¶ added in v0.4.0
func (h *PrettyHandler) Zone() *tty.Zone
Zone returns the terminal owner this handler paints its band into.
Exposed so a second consumer can lease rows from the SAME owner: the failure prompt puts its instruction row directly beneath the band, and two zones over one terminal each compute their margins from their own row arithmetic and overwrite each other - the exact failure tty.Zone exists to prevent. In production both sides reach the same singleton through standard error and the sharing is invisible; for any other writer it has to be asked for.
type Progress ¶ added in v0.4.3
type Progress struct {
// contains filtered or unexported fields
}
Progress is one invocation's liveness heartbeat: when something last moved, and what it was. Every accounting edge beats it - a step taking or handing back its seat ([Cache.admit]) and every line of subprocess output - so "is anything happening?" is one comparison rather than a poll across the limiter, the inflight set and the journal.
It answers a different question from a deadline on a target. A deadline bounds work that runs LONG; this records whether ANY work is happening, which is what an invocation stops doing when it wedges with its project locks still held.
A nil *Progress is usable and inert, so a caller with no heartbeat installed (a bare Cache.Run in a test) needs no guard of its own.
func NewProgress ¶ added in v0.4.3
func NewProgress() *Progress
NewProgress returns a heartbeat beating as of now.
func ProgressFromContext ¶ added in v0.4.3
ProgressFromContext retrieves the heartbeat stored by ContextWithProgress, or nil. Every Progress method is nil-safe, so a caller need not check.
func (*Progress) Beat ¶ added in v0.4.3
func (p *Progress) Beat()
Beat records that something moved without naming it, in one atomic store. The named transition on record is deliberately left alone: under a stall nothing beats at all, so the last transition IS the last thing that ran.
func (*Progress) Idle ¶ added in v0.4.3
Idle reports how long it has been since anything beat. A nil heartbeat is never idle, so a watchdog reading one cannot fire on work it was never wired to see.
type RecordedRun ¶ added in v0.4.0
type RecordedRun struct {
Key string
CreatedAt time.Time
KeyInputs []string
// contains filtered or unexported fields
}
RecordedRun identifies the most recent cache entry recorded for one target in one project: the key it was stored under, when it was stored, and the pre-hash key inputs behind that key. KeyInputs is nil when the entry predates key-input persistence, which leaves the key comparable but nothing line-level to name.
RecordedRun.WouldReplay answers the separate question of whether some entry - not necessarily this one - would replay for a given key.
func (RecordedRun) WouldReplay ¶ added in v0.4.0
func (r RecordedRun) WouldReplay(key string) bool
WouldReplay reports whether a run whose cache key is key would replay a stored entry here instead of executing: a manifest sits at that exact key and passes the gates a hit applies (key, project, platform).
The entry it finds need not be this one. RecordedRun.Key names only the NEWEST entry, so an edit followed by a revert leaves Key pointing at the edited run while the key a run now mints belongs to an older entry that still hits - and that is the case a verdict read off the newest entry alone gets wrong.
False on a zero RecordedRun and on an empty key.
type RefNotFoundError ¶ added in v0.4.0
RefNotFoundError reports a ref that resolved in none of the stores consulted, and names them. "Not found" is only actionable if the reader knows where magus looked - a foreign ref that was never published looks identical to a mistyped one otherwise.
func (*RefNotFoundError) Error ¶ added in v0.4.0
func (e *RefNotFoundError) Error() string
func (*RefNotFoundError) Is ¶ added in v0.4.0
func (e *RefNotFoundError) Is(target error) bool
Is reports RefNotFoundError as fs.ErrNotExist, so existing not-found handling (the CLI's MGS8001 path) keeps working while the message gains the store list.
type RemoteBackend ¶
type RemoteBackend interface {
// Name identifies the backend to a human: the spell that provides it ("s3", "gha").
// It must not probe or dial - the run header calls it before any work, precisely so
// the header cannot be what makes a run hang.
Name() string
// Active reports whether the backend is usable in the current environment.
// The cache skips both fetch and push when it returns false, so a backend
// gated on its environment (e.g. one that only runs under a specific CI
// provider) costs nothing per build elsewhere. Implementations should make it
// cheap — the cache may call it once per build — and cache any probe.
Active(ctx context.Context) bool
// GetArtifact streams the stored artifact for (projectPath, hash). Returns (nil, nil)
// when no artifact is present.
GetArtifact(ctx context.Context, projectPath, hash string) (io.ReadCloser, error)
// PutArtifact stores the artifact bytes for (projectPath, hash) from r.
PutArtifact(ctx context.Context, projectPath, hash string, r io.Reader) error
}
RemoteBackend is a pluggable remote backend for cache artifacts, keyed by (projectPath, hash). The local cache consults it on a local miss (before building) and populates it after a successful build. The artifact payload is an opaque byte stream — its format is the cache's concern, not the store's — so an implementation is effectively a content-addressed blob store. Implementations must be safe for concurrent use.
func OpenRemoteBackend ¶
func OpenRemoteBackend(ctx context.Context, selector string) (RemoteBackend, error)
OpenRemoteBackend opens the registered remote backend for selector, or errors when no opener has been registered (no backend was linked into this binary).
type RemotePruner ¶
type RemotePruner interface {
PruneArtifacts(ctx context.Context, policy RetentionPolicy) error
}
RemotePruner is an optional capability a RemoteBackend may implement to support retention-based eviction (`magus config cache prune --remote`). A backend that does not implement it cannot be pruned — the cache's built-in eviction governs only the local store. PruneArtifacts enumerates the remote store and evicts artifacts matching policy; it runs out of band (a maintenance command), never on the build hot path.
type Result ¶
type Result struct {
ProjectPath string
Hash string
Hit bool
Duration time.Duration
Outputs []string // absolute paths written or replayed
Ref string // per-execution output reference id (see recordOutput); "" when the output store is absent or persistence failed
// Saved is the per-hit half of [Stats.SavedMs]: the duration the entry recorded when it
// was written, which this hit replayed instead of running. Zero on a miss, and zero for
// an entry written before the manifest carried a duration - the same understatement
// SavedMs carries, for the same reason.
Saved time.Duration
}
Result is the outcome of a Cache.Run call.
type RetentionPolicy ¶
type RetentionPolicy struct {
OlderThan time.Duration // evict artifacts older than this; 0 disables the age bound
KeepLast int // keep only the newest N artifacts; 0 disables the count bound
DryRun bool // report intended deletions without performing them
}
RetentionPolicy describes which remote cache artifacts a prune should evict. The two bounds are independent and additive: an artifact is evicted if it is older than OlderThan OR falls outside the newest KeepLast. A zero field disables that bound.
type Ring ¶
type Ring struct {
// contains filtered or unexported fields
}
Ring is an io_uring instance with its three mmaps.
type RunLog ¶ added in v0.4.0
type RunLog struct {
Inv string `json:"inv"`
Arguments []string `json:"arguments,omitempty"` // full argv, subcommand included
Trigger string `json:"trigger,omitempty"` // one of journal's Trigger* constants
StartedMs int64 `json:"started_ms"`
FinishedMs int64 `json:"finished_ms,omitempty"`
Status string `json:"status,omitempty"` // pass|fail; empty when the run was interrupted
MagusVersion string `json:"magus_version,omitempty"`
SizeBytes int64 `json:"size_bytes,omitempty"`
}
RunLog summarizes one retained invocation journal - the row the console's run browser lists so a reader can find a past run by the COMMAND that produced it rather than by a ref id somebody printed on a terminal.
It is deliberately not journal.Invocation: rebuilding one of those means reading a whole stream, and a journal holds every output line the run captured, so listing 500 of them would read hundreds of megabytes to show a list. Every field here comes off the two lifecycle events that bracket the file - the first line and the last - which OutputStore.ListRunLogs reads with bounded seeks.
type RunOption ¶
type RunOption func(*runCtx)
RunOption configures a single Cache.Run (or RunAll) invocation.
func OnResult ¶
OnResult fires after every Cache.Run regardless of outcome (after OnHit/OnMiss/OnError). Multiple OnResult options accumulate; all fire in registration order.
func WithLimiter ¶
WithLimiter shares an external Limiter with RunAll instead of creating a private one, so in-process tasks and nested calls compete for the same concurrency budget.
func WithMaxFailures ¶ added in v0.4.0
WithMaxFailures bounds how many steps may fail before RunAll stops admitting more, as a budget rather than a boolean: 1 is fail-fast, 3 tolerates three, and 0 (the default) is unlimited. A step that fails only because a dependency failed does not count - it is a consequence, not an independent finding - so a budget of 1 stops at the first REAL failure rather than at whichever cascade victim reports first.
type SourceMemo ¶ added in v0.4.0
type SourceMemo struct {
// contains filtered or unexported fields
}
SourceMemo memoizes the expandSources+hashFiles result for one sweep of steps sharing a WorkspaceRoot, keyed by the (Sources, Outputs, IgnoreDirs) tuple that determines it.
ONLY for prediction: profiling IdentifyRef's sweep showed expandSources' WalkDir re-walking the workspace root as the dominant cost - 57% of a 355ms sweep - with dozens of walks over an IDENTICAL Sources set, because buildStep gives most targets in a project the same baseline.
It must NEVER reach a path that executes a target: the memo assumes nothing on disk changes between calls, which holds for a read-only sweep and nothing else. A real Run/RunAll handed one could replay a source hash observed before that step's build wrote to its own inputs. Construct one per sweep with NewSourceMemo, thread it explicitly, and let it fall out of scope.
Threaded as an explicit parameter rather than through ctx like gopherbuzz's TargetMemo, deliberately: an explicit parameter is what makes "prediction only" provable by grep.
func NewSourceMemo ¶ added in v0.4.0
func NewSourceMemo() *SourceMemo
NewSourceMemo returns an empty memo. See SourceMemo's doc for its one intended caller shape: created and discarded around a single prediction sweep.
type Stalled ¶ added in v0.4.0
type Stalled struct {
// Project is the human display form ("(root)" for the workspace root).
Project string
// ProjectPath is the raw workspace-relative key the journal recorded ("." for the
// root). Carried alongside the display form so a caller can look the project up in
// the workspace - doctor matches these against per-target policy to tell a target
// that CANNOT replay by design from one that merely does not.
ProjectPath string
Target string
Runs int
TotalMs int64
}
Stalled is one (project, target) pair that executed repeatedly and never replayed from cache. TotalMs is the wall clock spent doing so.
func StalledTargets ¶ added in v0.4.0
StalledTargets reports targets that execute repeatedly and never replay from cache, worst wall-clock first.
It reads the invocation journals magus already writes (<cacheDir>/runs/*.jsonl), so it costs no new bookkeeping: every target result is recorded there with a status of "cached" (replayed) or "pass"/"fail" (executed). A pair with many executions and no replays at all is not a cold cache, it is a cache that cannot work - almost always because the target's footprint includes inputs it does not read, so ordinary edits keep busting a key that had no reason to change.
When only is non-empty, only those "project\x00target" keys are considered. The run path passes the targets that just executed, so a normal run never pays to assess the whole workspace.
type Stats ¶
type Stats struct {
Hit int
Miss int
Error int
// SavedMs is the summed recorded duration of the runs those hits replayed. Measured, not
// modeled - each figure is how long that exact target took on this machine when it last ran.
// Understates when entries predate Manifest.DurationMs, and never overstates.
SavedMs int64
}
Stats holds per-Cache counters reported in the end-of-run summary.
type Step ¶
type Step struct {
ProjectPath string // repo-relative project directory
Sources []string // doublestar globs (relative to WorkspaceRoot) for the cache key
// IgnoreDirs are the non-source dir names this project's resolved spells generate
// (vendor, node_modules, ...); pruned from the source walk so they are never hashed.
// The field itself is not written into the key - only the resulting file set is, so
// two ignore sets that yield the same files hash identically.
IgnoreDirs []string
EnvAllow []string // env var names whose values contribute to the key
// ExecOverrides are per-op ctx.withEnv / ctx.withCwd execution overrides ("env:K=V", "cwd:V"), extracted
// statically. Unlike EnvAllow, which names env vars whose PROCESS value is read, a
// derived override's value lives in the magusfile, so it is hashed directly.
ExecOverrides []string
// Observations are per-target ctx.observes declarations ("key=value"): facts OUTSIDE
// the tree that the target's answer depends on and no other key input can see - a
// vulnerability feed's id, a remote schema's revision. Hashed directly like
// ExecOverrides, since both halves are written in the magusfile; unlike
// ExecOverrides they change nothing about how the target runs, so they get their own
// line class rather than reusing exec:. The value is opaque here - magus compares it
// and never interprets it.
Observations []string
Outputs []string // globs snapshotted into cache and replayed on hit
// RequiredOutputs is the subset of Outputs that must each match at least one file,
// rather than the whole set merely matching something. It carries the globs another
// project's build order depends on (a cross-project output), where producing nothing
// is a build failure rather than an empty result: the manifest would omit the file
// and later cache hits would replay a partial output set into a tree this target does
// not own. Ordinary outputs stay lenient - a glob that legitimately matches nothing
// is common, and only a total miss is suspicious.
RequiredOutputs []string
// OutputsDeclared reports that Outputs came from the TARGET (ctx.writesFiles) rather than
// being inherited from the project or a bound spell. Only then does producing nothing mean
// the target broke its promise.
//
// Inherited globs routinely match nothing: binding the typescript spell contributes
// `dist/**` to every target on the project, so a check-only target like a test - which
// produces no files at all - would otherwise fail its snapshot for a glob it never claimed.
// That failure hid for a long time because snapshot only runs on a cache MISS, and those
// targets always replayed.
OutputsDeclared bool
// Updates and OwnedOutputs are unhashed: both are already covered by Sources and
// Outputs, and hashing either would change every existing key. They exist so
// checkSourceMutation can tell a declared write from an undeclared one (MGS4007).
Updates []string // ctx.modifiesExistingFiles globs
// OwnedOutputs spans EVERY target in EVERY project, not the running one, because
// ctx.needs puts a chained target's writes inside this step's window and a workspace
// target does the same across projects.
OwnedOutputs []string
Deps []string // upstream project hashes folded into the key
DependsOn []string // upstream project paths for scheduling (not hashed)
// RunAfter holds node keys (DepKey) of batch steps this one must wait for,
// derived from declared writer-before-reader footprints (DeriveTargetOrder).
// Unlike DependsOn it crosses target names and is exact. Scheduling only:
// never hashed, never folded into Deps, never part of the affected set.
RunAfter []string
WorkspaceRoot string
Target string // mixed into key to distinguish targets on the same sources
Charms []string // active charm names (sorted), mixed into key so charm-variant runs differ
// ExtraArgs are the args after `--`, forwarded to the target. They change what
// the target does, so like Charms they MUST key the cache: without them a run
// with different args replays the previous run's result. Order is significant
// (`-run X` is not `X -run`), so unlike Charms they are never sorted.
ExtraArgs []string
// Spell is the explicit `spell::op` filter of the invocation, empty on a plain
// target run. It keys the cache because the filter selects WHICH DEFINITION
// runs: an explicit op bypasses the magusfile export shadowing the same name,
// so a compile-only go::go-build must not satisfy (or be satisfied by) the
// go-build target's entry.
Spell string
SpellDefVersion string // binary fingerprint; forces miss on magus upgrade
ToolVersions []string // "spell:tool:token" strings (run.go builds them); forces miss on toolchain upgrade
// PlatformIndependent drops the host-platform line from the key, so one entry
// serves every platform. Resolved from the target's declaration or its spells';
// false (the default) keys the platform. See types.PlatformSensitivity.
// IncludeOS and IncludeArch select which host facts key this step. Separate
// because they move independently; see config.CacheInclude.
IncludeOS bool
IncludeArch bool
NoCache bool // when true, always run fn; never replay or snapshot (long-running targets)
SkipReplay bool // when true, never replay a hit (always run fn), but still snapshot on success - a forced rebuild that refreshes the entry, unlike NoCache which never snapshots either (magus run --no-cache)
Exclusive bool // RunAll only: when true, runs alone; no other batch step runs concurrently (ignored by Run, which has no batch)
Slots int // RunAll only: concurrency slots held while running (0 or 1 = one slot); clamped to the limiter's capacity. Never hashed.
// MemoryMB is RunAll only: the declared peak memory this step will reach,
// including every target it composes, carried alongside the slot count Slots
// derives from the same figure. Slots throttle peers inside THIS process; the
// figure is what machine-wide admission arbitrates, and a slot count cannot be
// converted back into it (the conversion divides by a per-process budget). 0 means
// undeclared: no claim, no refusal. Never hashed.
MemoryMB int
// MemoryDeclaredBy is RunAll only: the target whose policy MemoryMB came from,
// which is not this step when the figure was inherited from a target it composes
// with ctx.needs. A refusal names it so the reader is sent to the magusfile line
// to change rather than to the target they typed. Never hashed.
MemoryDeclaredBy string
Label string // display-only project name for logs (root reads as e.g. "magus", not "."); never hashed
// Revision and Dirty are the VCS state the run's inputs were read at, resolved ONCE
// per invocation by the caller (a per-target probe would spawn a VCS subprocess per
// step) and copied onto every step. Display-only provenance for the output
// descriptor (recordOutput) - never hashed, so a run before vs. after a commit still
// shares a cache entry when the tree content is unchanged.
// VCSName is the provider the two above came from ("git", "hg", "sl", "jj"). Recorded
// because a bare hash does not identify its own kind: a git SHA and an hg node id
// are both 40 hex, and a colocated jj repo can yield either a git commit or a jj
// commit_id. Comparing two revisions without it is a confident answer to the
// wrong question.
VCSName string
Revision string
Dirty bool
}
Step is the hashable description of a cached build step.
type TargetNode ¶ added in v0.4.3
type TargetNode struct {
Project string
Target string
// Steps are the node keys (DepKey) of the batch steps whose execution runs this
// target. Usually one; a target composed by several steps' chains carries all of
// them, and derivation treats an edge as ordered only when every owner is.
Steps []string
// Reads is the target's source globs; Writes its output plus in-place-update
// globs. Updates count as writes here even though describe.go derives no
// DependsOn edge from them: within one batch, ordering a reader after an
// in-place editor can only over-order, and the motivating chain
// (changelog-generate -> content-generate) is declared exactly that way.
Reads []string
Writes []string
// DeclaredReads is false when Reads fell back to the project baseline because the
// target names no ctx.readsFiles. Edges into such a reader are weak: the baseline
// is a whole-project over-approximation, so a cycle through a weak edge is an
// artifact to drop, not an authoring error to report.
DeclaredReads bool
DeclaredWrites bool
// IgnoreDirs are dir names the reader's source walk prunes (vendor, gen, ...).
// A fallback reader provably never sees files under them, so writes landing
// wholly inside one derive no edge.
IgnoreDirs []string
}
TargetNode is one target's declared file footprint, the unit order derivation works on: a batch step's own target, or a chain member that step composes. Reads and Writes are workspace-rooted doublestar globs, resolved the same way the cache key resolves them (buildStep), so what orders and what hashes agree.
func (TargetNode) Key ¶ added in v0.4.3
func (n TargetNode) Key() string
Key returns the node's scheduling identity, shared with the step barrier.
type Tracer ¶
type Tracer interface {
StartSpan(ctx context.Context, name string) (context.Context, func(err error))
}
Tracer opens a child span for an internal cache phase — hashing inputs, replaying a hit, or snapshotting outputs. It is the cache's only window onto a tracing backend: the observability layer implements it and installs it on the run context with ContextWithTracer, so this package keeps no OpenTelemetry dependency of its own. StartSpan returns a context carrying the new span and a func that ends it, recording err as the span's status.
Source Files
¶
- artifact.go
- barrier.go
- cache.go
- capture.go
- context.go
- defaults.go
- eviction.go
- glob.go
- hash.go
- hash_iouring_linux.go
- hash_iouring_linux_ring.go
- inflight.go
- keyinputs.go
- last_entry.go
- limiter.go
- lock.go
- log.go
- machine.go
- machinegate.go
- manifest.go
- mtime.go
- mutation.go
- notice.go
- options.go
- order.go
- output.go
- progress.go
- prune.go
- publish.go
- remote.go
- remote_fs.go
- scope.go
- signing.go
- snapshot.go
- stat_linux.go
- topology.go
- topology_linux.go
- trace.go
- yield.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package reflink copies regular files using the most efficient mechanism the host platform and filesystem provide, transparently falling back to a plain userspace copy where no acceleration is available.
|
Package reflink copies regular files using the most efficient mechanism the host platform and filesystem provide, transparently falling back to a plain userspace copy where no acceleration is available. |