Documentation
¶
Overview ¶
Package store owns the SQLite database: connection pools, PRAGMAs, migrations and every SQL query in the project.
The write model ¶
One file, two pools. The writer pool has exactly one connection, so database/sql is the write queue: it serialises writers, applies backpressure and honours context deadlines, and no SQLITE_BUSY can come from paceq's own writers because there are never two. Every writer transaction begins IMMEDIATE, which avoids the lock upgrade that busy_timeout cannot retry. The reader pool opens read-only with query_only, so the engine rather than convention enforces that reads cannot write.
The payoff is that read, compute in Go, then write is safe inside one transaction without optimistic locking. Nobody else can have written in between.
Five rules ¶
Breaking one of these is a review stopper.
- No process execution, file I/O or network I/O inside a write transaction, ever. The single write connection is held for the whole callback, and lock hold time is the real scarcity in this system.
- All mutation goes through methods on *Store. The writer handle is private, and an architecture test fails the build if a database handle reaches the exported API.
- Admission control is read, compute, write inside one IMMEDIATE transaction, not a clever atomic statement.
- RETURNING rows are consumed in full before the transaction runs anything else. The writer pool has one connection to lose.
- The database never runs on a network or FUSE filesystem. SQLite file locking is undefined there and the corruption shows up weeks later.
Schema conventions ¶
These hold for every table in the schema, and a new table that breaks one is a review stopper. Retrofitting any of them means rebuilding every table.
- Every table is STRICT. A column declared INTEGER holds integers, and a string that looks like a number is an error rather than a silent conversion. This needs SQLite 3.37, which the driver ships.
- Every time column is INTEGER unix milliseconds UTC. Sortable, indexable, no parsing, no timezone ambiguity. A timezone is stored separately as an IANA name, and only where a time has to be interpreted rather than ordered.
- Every status is TEXT with a CHECK listing the values it may take. The premise of this product is that reading the tables with the sqlite3 shell explains what happened, which integer codes do not, and the CHECK mirrors the state machine in internal/model so both ends enforce it.
- Structured values are canonical JSON in a TEXT column: object keys sorted, no insignificant whitespace. Canonical form is what makes a hash of the text stable and two rows comparable.
The schema a migrated database ends up with is checked in as schema.golden.sql, and TestGoldenSchema fails on any change to it. Reviewing a schema change means reading that diff.
auto_vacuum is INCREMENTAL, set by Open while the database still has no schema. It is the one setting here that cannot be changed afterwards without a full VACUUM holding an exclusive lock, so it is decided at creation. Databases created before paceq set it keep NONE, and the doctor check reports them.
The state directory ¶
A state directory holds one lock file and one database, and one process owns both. OpenState takes an exclusive flock on the lock file before the database is opened for writing, so a second paceq is refused before it touches a page of a file somebody else owns, and it is told which process to stop. The lock lives in the kernel: it is released when the process dies, however it dies, so there is no stale lock to clean up and the lock file is kept between runs.
The lock covers a state directory, not a database file. Two processes pointed at the same database through different state directories both start, which the role leases in M2-02 are what make safe.
StartSession records who is running, from when, and on which boot. A session row still open at the next start belongs to a run that never got to say goodbye, and is marked crashed. The boot id, read from /proc/sys/kernel/random/boot_id, is the strongest evidence in the system: a changed one means the machine restarted, so no process paceq started can have survived. Platforms without it degrade to lease expiry, which is slower and still correct.
Migrations ¶
Migrate applies the SQL files embedded from the migrations directory, in version order, on the write connection. Forward only, one migration per transaction, sha256 pinned per applied file, and PRAGMA user_version as the fence that stops an old binary from writing to a newer database. The rules a migration file has to follow are in migrations/README.md.
PRAGMA values are read back from both pools at startup. A mismatch fails Open, naming the setting. Misconfigured durability is a startup error, never a warning and never a quiet degradation.
Rules 1 and 4 cannot be checked mechanically and are enforced in review. Rules 2, 3 and 5 are covered by tests in this package and in internal/arch.
Index ¶
- Constants
- Variables
- func CheckMode(path string, want fs.FileMode) error
- func KnownSchemaVersion() (int, error)
- func ProcessStartTicksReadable() bool
- func ReadProcessStartTicks(pid int) (int64, bool)
- type AttemptProcess
- type AutoVacuumMode
- type BeginSensorTickInput
- type BeginSensorTickResult
- type CancelRequest
- type ClaimSpec
- type ClaimedRun
- type CursorInput
- type FinishReason
- type GapSchedule
- type IntegrityReport
- type JobApplyResult
- type JobRunSummary
- type JobVersion
- type JobVersionInput
- type JobView
- type LeaseEvent
- type LeaseGrant
- type LeaseInput
- type LeaseRef
- type LeaseRenewal
- type LockedError
- type LogMeta
- type ManualTriggerInput
- type ManualTriggerResult
- type MissedTick
- type NewRun
- type NewStep
- type Options
- type Outage
- type OutageInput
- type PermissionError
- type ReapOptions
- type ReapedRun
- type ResetResult
- type ResetSensorInput
- type RetryPlan
- type Run
- type RunDetail
- type RunEvent
- type RunFilter
- type RunSummary
- type ScheduleInput
- type ScheduleRow
- type SensorRow
- type SensorSummary
- type SensorTickCommitInput
- type SensorTickCommitResult
- type SensorTrigger
- type Session
- type StateLock
- type Step
- type StepDep
- type StepOutcome
- type Store
- func (s *Store) AbandonedChains(ctx context.Context) ([]string, error)
- func (s *Store) AcquireOrRenew(ctx context.Context, name, holder string, ttl time.Duration) (LeaseGrant, bool, error)
- func (s *Store) ActiveAttempts(ctx context.Context) ([]AttemptProcess, error)
- func (s *Store) ActiveRunViolations(ctx context.Context) ([]Violation, error)
- func (s *Store) ActiveRunsForJob(ctx context.Context, job string) (int, string, error)
- func (s *Store) AppendLeaseEvent(ctx context.Context, e LeaseEvent) error
- func (s *Store) AppendRunEvent(ctx context.Context, e RunEvent) error
- func (s *Store) ApplyJobs(ctx context.Context, inputs []JobVersionInput) ([]JobApplyResult, error)
- func (s *Store) AutoVacuum(ctx context.Context) (AutoVacuumMode, error)
- func (s *Store) BeginSensorTick(ctx context.Context, in BeginSensorTickInput) (BeginSensorTickResult, error)
- func (s *Store) BootChanged() bool
- func (s *Store) CancelRequested(ctx context.Context, runID string) (bool, string, error)
- func (s *Store) CheckpointTruncate(ctx context.Context) error
- func (s *Store) ClaimRun(ctx context.Context, runID string, in LeaseInput) (string, int64, error)
- func (s *Store) ClaimRuns(ctx context.Context, spec ClaimSpec) ([]ClaimedRun, error)
- func (s *Store) ClaimableRunIDs(ctx context.Context) ([]string, error)
- func (s *Store) Close() error
- func (s *Store) CommitSensorTick(ctx context.Context, in SensorTickCommitInput) (SensorTickCommitResult, error)
- func (s *Store) CreateRunWithSteps(ctx context.Context, in NewRun) (Run, error)
- func (s *Store) CurrentJobVersion(ctx context.Context, jobName string) (JobVersion, error)
- func (s *Store) DrainRun(ctx context.Context, runID string, ref LeaseRef, code reason.Code) (bool, error)
- func (s *Store) DueSchedules(ctx context.Context, nowMilli int64, max int) ([]ScheduleRow, error)
- func (s *Store) DumpForIdempotence(ctx context.Context) ([]string, error)
- func (s *Store) FailHangingTicks(ctx context.Context, startedBefore time.Time) ([]string, error)
- func (s *Store) FinishRun(ctx context.Context, runID string, ref LeaseRef, fr FinishReason) (string, error)
- func (s *Store) Fsck(ctx context.Context) ([]Violation, error)
- func (s *Store) GapSchedules(ctx context.Context) ([]GapSchedule, error)
- func (s *Store) GetRun(ctx context.Context, idOrPrefix string) (RunDetail, error)
- func (s *Store) GetSchedule(ctx context.Context, jobName, name string) (ScheduleRow, error)
- func (s *Store) InjectActiveRunOverflow(ctx context.Context) (string, error)
- func (s *Store) InjectBackwardsTimestamp(ctx context.Context) (string, error)
- func (s *Store) InjectBrokenEventChain(ctx context.Context) (string, error)
- func (s *Store) InjectDependencyCycle(ctx context.Context) (string, error)
- func (s *Store) InjectDuplicateRunKey(ctx context.Context) (string, error)
- func (s *Store) InjectFailedStepUnderSucceededRun(ctx context.Context) (string, error)
- func (s *Store) InjectOrphanTick(ctx context.Context) (string, error)
- func (s *Store) InjectTerminalStepPending(ctx context.Context) (string, error)
- func (s *Store) InjectUnexplainedDeferral(ctx context.Context) (string, error)
- func (s *Store) InjectUnexplainedTerminal(ctx context.Context) (string, error)
- func (s *Store) IntegrityCheck(ctx context.Context) (IntegrityReport, error)
- func (s *Store) Job(ctx context.Context, name string) (JobView, error)
- func (s *Store) JobLastRuns(ctx context.Context, jobName string) ([]JobRunSummary, error)
- func (s *Store) JobNames(ctx context.Context) ([]string, error)
- func (s *Store) JobPaused(ctx context.Context, jobName string) (bool, error)
- func (s *Store) JobVersionByID(ctx context.Context, versionID string) (JobVersion, error)
- func (s *Store) JournalMode(ctx context.Context) (string, error)
- func (s *Store) KnownAttempts(ctx context.Context) ([]AttemptProcess, error)
- func (s *Store) LatestSession(ctx context.Context) (Session, bool, error)
- func (s *Store) LeaseEvents(ctx context.Context, name string, limit int) ([]LeaseEvent, error)
- func (s *Store) LeaseHolder(ctx context.Context, name string) (LeaseGrant, bool, error)
- func (s *Store) ListAllSchedules(ctx context.Context) ([]ScheduleRow, error)
- func (s *Store) ListJobVersions(ctx context.Context, jobName string) ([]JobVersion, error)
- func (s *Store) ListRuns(ctx context.Context, f RunFilter) ([]RunSummary, error)
- func (s *Store) MaterializeManualTrigger(ctx context.Context, in ManualTriggerInput) (ManualTriggerResult, error)
- func (s *Store) MaterializeTick(ctx context.Context, in TickInput) (TickResult, error)
- func (s *Store) Migrate(ctx context.Context) error
- func (s *Store) MissedTickEvidence(ctx context.Context) ([]TickEvidence, error)
- func (s *Store) NextRetryWait(ctx context.Context, runID string) (time.Duration, bool, error)
- func (s *Store) NextRunnableStep(ctx context.Context, runID string) (string, bool, error)
- func (s *Store) ObserveRunCancel(ctx context.Context, runID string, ref LeaseRef, actor string, ...) error
- func (s *Store) OpenSession(ctx context.Context) (Session, bool, error)
- func (s *Store) Outages(ctx context.Context) ([]Outage, error)
- func (s *Store) OutagesWithoutTicks(ctx context.Context) ([]Outage, error)
- func (s *Store) OverrideBootIDForTest(boot string)
- func (s *Store) Path() string
- func (s *Store) PauseSchedule(ctx context.Context, jobName, name string) (ScheduleRow, error)
- func (s *Store) PendingSteps(ctx context.Context, runID string) ([]Step, error)
- func (s *Store) QuickFsck(ctx context.Context) ([]Violation, error)
- func (s *Store) ReapExpiredRuns(ctx context.Context, opt ReapOptions) ([]ReapedRun, error)
- func (s *Store) RecordAttemptProcess(ctx context.Context, runID, step string, ref LeaseRef, pid int, ...) error
- func (s *Store) RecordMissedTicks(ctx context.Context, sessionID string, outageID int64, ticks []MissedTick) (int, error)
- func (s *Store) RecordOrphanKill(ctx context.Context, runID string, pid int) error
- func (s *Store) RecordOutage(ctx context.Context, in OutageInput) (int64, error)
- func (s *Store) RecordStepOutcome(ctx context.Context, runID, name string, out StepOutcome, ref LeaseRef) error
- func (s *Store) ReleaseLease(ctx context.Context, name, holder string) (bool, error)
- func (s *Store) RenewRunLeases(ctx context.Context, owner string, ttl time.Duration) ([]LeaseRenewal, error)
- func (s *Store) RequestCancel(ctx context.Context, runID, by, why string) (CancelRequest, error)
- func (s *Store) RequeueCrashedRun(ctx context.Context, runID string) error
- func (s *Store) ResetSensor(ctx context.Context, in ResetSensorInput) (ResetResult, error)
- func (s *Store) ResumeSchedule(ctx context.Context, jobName, name string, nextTickAt time.Time) (ScheduleRow, error)
- func (s *Store) RunEvents(ctx context.Context, idOrPrefix string) ([]RunEvent, error)
- func (s *Store) RunExists(ctx context.Context, runID string) (bool, error)
- func (s *Store) RunIDByRunKey(ctx context.Context, sourceID, runKey string) (string, error)
- func (s *Store) ScheduleCursor(ctx context.Context, jobName, name string) (*time.Time, time.Time, error)
- func (s *Store) ScheduleTicks(ctx context.Context, jobName, name string) ([]TickView, error)
- func (s *Store) SchemaVersion(ctx context.Context) (int, error)
- func (s *Store) SetJobPaused(ctx context.Context, jobName string, paused bool) error
- func (s *Store) SetSensorCursor(ctx context.Context, in CursorInput) error
- func (s *Store) StartSession(ctx context.Context, version string) (Session, error)
- func (s *Store) StartStep(ctx context.Context, runID, name string, ref LeaseRef) error
- func (s *Store) StepDeps(ctx context.Context, runID string) ([]StepDep, error)
- func (s *Store) StopSession(ctx context.Context, sessionID string) error
- func (s *Store) SyncSensors(ctx context.Context, job string, sensors []spec.Sensor) (SyncResult, error)
- func (s *Store) TouchSession(ctx context.Context, sessionID string) error
- func (s *Store) UnexplainedReasons(ctx context.Context) ([]UnexplainedReason, error)
- func (s *Store) UpsertJobVersion(ctx context.Context, in JobVersionInput) (JobVersion, bool, error)
- func (s *Store) UpsertSchedule(ctx context.Context, in ScheduleInput) (ScheduleRow, error)
- type SyncResult
- type TickEvidence
- type TickInput
- type TickResult
- type TickView
- type UnexplainedReason
- type Violation
Constants ¶
const ( // DefaultRunLeaseTTL is how long a claim lasts. Sixty seconds, renewed // every twenty. DefaultRunLeaseTTL = 60 * time.Second // DefaultClockSkewAllowance is how long past the expiry the reaper waits // before it takes a run. Ten seconds. DefaultClockSkewAllowance = 10 * time.Second // DefaultRequeueBackoff is how long a reaped run waits before it is due // again. The crash already cost a ttl; the backoff keeps a job that kills // its executor from burning the queue the moment it is requeued. DefaultRequeueBackoff = 30 * time.Second // DefaultMaxCrashCount is the poison quarantine line (02 section 5.7): // once a run has outlived this many executors, the next reap fails it for // good instead of requeueing it. DefaultMaxCrashCount = 5 )
The run lease. Ownership of a run is explicit and time bounded: one statement claims, one transaction renews every run the owner holds at once, the reaper takes what expired and hands it to a new holder, and every write from a holder carries the fencing token so a frozen worker can never overwrite its successor's verdict (issue #60).
The timing constants are the ones the issue fixes. The ttl is renewed every tick a third its length, so two lost renewals are tolerated before leadership of a run is even in question. The skew allowance is what keeps the reaper late on purpose: the owner measures its budget on a monotonic clock and gives up early; the reaper waits out the wall clock plus skew and takes over sent.
const ( OutcomeTriggered = "triggered" OutcomeSkipped = "skipped" OutcomeError = "error" )
Tick outcomes as stored in ticks.outcome.
const ( // DirMode and DatabaseMode are the only modes paceq accepts on its own // state. Anything wider means the state was readable by another user, which // is a refusal rather than something to correct quietly. DirMode fs.FileMode = 0o700 DatabaseMode fs.FileMode = 0o600 )
const DatabaseFileName = "state.db"
DatabaseFileName is the database inside a state directory. The name is fixed so a state directory is self describing: one lock, one database, one owner.
const DefaultDeferBackoff = 500 * time.Millisecond
DefaultDeferBackoff is how far a queued overlap defers its run. It is a policy number with one constraint: when the blocking run ends, the deferred run must already be due, so a release costs a wake rather than a wait. Half a second keeps every release inside the one second the acceptance criterion allows, with the whole backoff left as slack.
const UnexplainedReasonSQL = `` /* 680-byte string literal not displayed */
UnexplainedReasonSQL is the audit query behind the reason code rule (06 section 2.1): it lists every terminal run, step, tick and trigger that sits in the database without a usable reason code. "Without a usable code" means NULL, empty, or the literal UNKNOWN, which is the value a rotting catalogue reaches for; the catalogue itself holds no UNKNOWN code, so a stored one is always a bug.
The schema CHECKs refuse the NULL and empty cases at write time. This query exists for the cases a CHECK cannot see: rows written before a constraint existed, and the UNKNOWN case, which no CHECK names. It returns zero rows on a healthy database, and testutil.AssertNoUnknownReasons runs it as the teardown assertion of every integration test from M1 on. It later becomes part of `paceq fsck` (M1-12).
Variables ¶
var ( // ErrRunNotFound is returned when no run has the id, or the id prefix. ErrRunNotFound = errors.New("no run has that id") // ErrAmbiguousRunID is returned when an id prefix matches more than one // run. The caller types more characters; nobody guesses on their behalf. ErrAmbiguousRunID = errors.New("the id prefix matches more than one run") // ErrConcurrencyKeyHeld is returned when an active run already holds the // concurrency key. It is an ordinary outcome, not a fault: the caller // records a skip with a reason code and moves on. ErrConcurrencyKeyHeld = errors.New("an active run already holds the concurrency key") )
Errors callers act on rather than report. Everything else that comes out of this file is a failure and reads as one.
var ( ErrNotClaimable = errors.New("the run cannot be claimed") ErrStepNotPending = errors.New("the step is not pending") )
ErrNotClaimable is returned when a run cannot be claimed: it is not queued, it is not available yet, or a cancellation is waiting to be observed. It is an ordinary outcome for a worker polling for work, not a fault.
var ErrLeaseLost = errors.New("the run lease was lost")
ErrLeaseLost is what every result write answers when the writer's lease has gone: another holder claimed after a reap, or the row moved on. A caller that sees it must discard its result and stop working on the run; the engine turns that into a run.result_discarded event and kills its process group.
var ErrNotFound = errors.New("no such row")
ErrNotFound wraps every refusal of this file that means "no such row". A caller that only cares whether something existed matches this; one that cares which table it asked about reads the message.
var ErrReadOnly = errors.New("the store is open read only")
ErrReadOnly is returned when a store opened through OpenReadOnly is asked to write. It lives beside the transaction machinery rather than beside any one writer, because refusing is the transaction machinery's own act.
Functions ¶
func CheckMode ¶
CheckMode refuses a path that is readable by anyone but its owner. Widening permissions back is deliberately not offered: correcting them quietly would hide that the state has been exposed, which is the fact an operator needs.
func KnownSchemaVersion ¶
KnownSchemaVersion is the highest schema version this build carries. It answers without a database, because version has to answer on a machine that has none.
func ProcessStartTicksReadable ¶
func ProcessStartTicksReadable() bool
ProcessStartTicksReadable reports whether this platform can read start ticks at all. It is how the sweep decides between verifying and failing closed without pretending.
func ReadProcessStartTicks ¶
ReadProcessStartTicks returns field 22 of /proc/<pid>/stat: the kernel's record of when the process started, in clock ticks since boot. Together with the pid it forms the process identity the orphan sweep verifies before any signal (issue #62): a pid alone is recyclable, pid plus start ticks is not.
ok is false when the process does not exist any more, has already been reaped, or cannot be read by this user. Every caller fails closed on false: an unreadable process is nobody's to kill.
Types ¶
type AttemptProcess ¶
AttemptProcess is one attempt's recorded process identity: which pid led its process group, and what /proc said its start time was when it was spawned. The pair is what lets the orphan sweep tell a surviving child of a dead executor from a recycled pid that happens to carry the same number.
Its write lives beside the other step transitions; this file only reads baselines back for the sweep.
type AutoVacuumMode ¶
type AutoVacuumMode int
AutoVacuumMode is the file level setting that decides whether SQLite ever returns freed pages to the filesystem. It is stored in the database header, so it survives restarts and belongs to the file rather than the connection.
const ( // AutoVacuumNone never shrinks the file. Deleted pages are reused, and the // file only ever grows. AutoVacuumNone AutoVacuumMode = 0 // AutoVacuumFull moves pages on every commit, which puts the cost on the // hot path. AutoVacuumFull AutoVacuumMode = 1 // AutoVacuumIncremental keeps a free page list that PRAGMA // incremental_vacuum releases in bounded batches, off the hot path. AutoVacuumIncremental AutoVacuumMode = 2 )
func (AutoVacuumMode) String ¶
func (m AutoVacuumMode) String() string
type BeginSensorTickInput ¶
type BeginSensorTickInput struct {
SensorName string
CursorBefore string
DaemonSessionID string
Now time.Time
}
BeginSensorTickInput names the sensor evaluation that is about to start.
type BeginSensorTickResult ¶
type BeginSensorTickResult struct {
// TickID is the intention row CommitSensorTick will close.
TickID string
// CursorVersion is the CAS guard read at the start. CommitSensorTick
// only advances the cursor when this value still holds, so a commit from
// an evaluation that was already overtaken is refused instead of writing
// an old result over a new one.
CursorVersion int64
}
BeginSensorTickResult is everything the commit half needs to guard the evaluation it is about to record.
type CancelRequest ¶
type CancelRequest struct {
CancelRequestedAt time.Time
CancelRequestedBy string
CancelReason string
}
CancelRequest is the durable cancellation record read back after it was written. The request is not a transition and carries no event: the event belongs to whoever observes the request and stops the run (02 section 5.8).
type ClaimSpec ¶
type ClaimSpec struct {
// Owner is the executor name every later write must come from.
Owner string
// TTL is how long each claim lasts. Zero means DefaultRunLeaseTTL.
TTL time.Duration
// Limit caps the batch. Zero means one.
Limit int
// Only restricts the claim to these ids. An executor claiming the exact
// run it was handed uses this; the empty slice claims whatever is due.
Only []string
}
ClaimSpec says who claims and how much.
type ClaimedRun ¶
type ClaimedRun struct {
ID string
JobName string
JobVersionID string
RunKey string
Attempt int
LeaseEpoch int64
ParamsJSON string
}
ClaimedRun is one run this process just took: everything ExecuteRun needs to drive it, with the fencing token first among equals.
type CursorInput ¶
CursorInput names a sensor and the value to move its cursor to. Moving the cursor is deliberately distinct from a reset: it touches cursor and the cursor guard, and nothing about the dedup table. This is the store-level half of `cursor set` (the M3-06 CLI arrives later).
type FinishReason ¶
FinishReason is how a run ends: the reason code the machine validated and the canonical detail object beside it, such as which step failed.
type GapSchedule ¶
GapSchedule is one enabled schedule as the gap walk needs it: enough to recompute where its fire times fell inside an outage.
type IntegrityReport ¶
type IntegrityReport struct {
// Integrity is pragma integrity_check's verdict, "ok" when the file
// is sound.
Integrity string
// ForeignKeyIssues counts foreign_key_check rows: references that
// point at nothing. Zero is the only healthy number.
ForeignKeyIssues int
}
IntegrityReport is what SQLite itself says about the database file after a crash. WAL recovery on open fixes torn transactions; these two pragmas are the proof that it did.
type JobApplyResult ¶
type JobApplyResult struct {
// JobName is the job the input named.
JobName string
// VersionID is the id of the version this spec maps to after the apply:
// the new row when one was written, the existing row otherwise.
VersionID string
// Version is that row's number.
Version int
// Created reports whether this apply wrote a new job_versions row. A false
// here is the idempotent case: the file was already loaded as it stands,
// and the database came out of the transaction byte for byte the same.
Created bool
// Sensors is the sensor sync that ran in the same transaction, so a job's
// new definition and its sensor rows land and are reported together.
Sensors SyncResult
}
JobApplyResult says what apply did with one job spec.
type JobRunSummary ¶
type JobRunSummary struct {
JobName string
RunID string
State string
ReasonCode string
CreatedAt time.Time
StartedAt time.Time
FinishedAt time.Time
StepsTotal int
StepsDone int
// The deferral facts (#68), read straight off the newest run so status
// can say "utsatt" with its reason without a second query.
AvailableAt time.Time
DeferReason string
ReasonData string
}
JobRunSummary is what paceq status shows for one job: the job's newest run and how far its steps got. A job that has never run carries an empty RunID and state.
type JobVersion ¶
type JobVersion struct {
ID string
JobName string
Version int
SpecHash string
SpecJSON string
SourcePath string
CreatedAt time.Time
}
JobVersion is one immutable snapshot of a job spec.
type JobVersionInput ¶
type JobVersionInput struct {
// JobName is the stable human identity of the job, and its primary key.
JobName string
Description string
// SourcePath is where the spec was read from, for messages. Empty when the
// spec came from somewhere that has no path.
SourcePath string
// MaxConcurrent is how many runs of this job may be active at once. Zero
// means one, which is the default a job that says nothing gets.
MaxConcurrent int
// SpecHash is the digest of the canonical spec, "sha256:<hex>". It is the
// whole of idempotent reload: the same digest is the same version.
SpecHash string
// SpecJSON is the canonical spec itself, the intermediate representation a
// run is materialised from.
SpecJSON string
// Sensors are the sensor definitions of the job, materialised as rows in
// the same transaction that writes the version. Nil or empty means the
// job declares no sensors, and any sensor rows it owned are removed.
Sensors []spec.Sensor
}
JobVersionInput is one job spec as the loader read it. It carries no id and no version number: which version this spec is, is a fact about the database and not about the file.
type JobView ¶
type JobView struct {
Name string
Description string
MaxConcurrent int
// Paused is the operator pause flag. A paused job still admits what a
// person starts; it only stands its schedules down.
Paused bool
SourcePath string
// CurrentVersion is the version number the job currently points at,
// zero when no version has ever been applied to it.
CurrentVersion int
CreatedAt time.Time
UpdatedAt time.Time
}
JobView is one job as `paceq jobs show` reads it back: the identity, the ceiling that admission control enforces, and where the job came from.
type LeaseEvent ¶
LeaseEvent is one recorded moment in a lease's life: taken, lost or taken over from a dead holder. It is the stored explanation beside the structured log line, written through AppendLeaseEvent.
type LeaseGrant ¶
type LeaseGrant struct {
Name string
Holder string
Epoch int64
AcquiredAt time.Time
ExpiresAt time.Time
}
LeaseGrant is one live lease row as the database computed it.
A lease is the right for one holder to act as the singleton of one role: "scheduler" decides when jobs fire, "reaper" reaps what died, and so on. Epoch is the fencing token: it grows by exactly one every time the lease changes hands through an expiry, and never moves on a renewal, so a stale holder can always be told apart from the current one by comparing numbers.
type LeaseInput ¶
type LeaseInput struct {
// Owner is the executor's name. Every later write to this run has to
// come from the same name, which is what makes the lease a fence and
// not a decoration.
Owner string
// TTL is how long the claim lasts. Zero means DefaultRunLeaseTTL.
TTL time.Duration
}
LeaseInput says who claims a run and for how long.
type LeaseRef ¶
LeaseRef is who claims to hold a run's lease, and at which fencing token. Every result write proves its standing by carrying one.
The zero LeaseRef means the system itself, writing against an executor that is already gone: recovery closing a dead attempt's steps. That path is real, and it is gated the other way round: the store refuses it while the lease is still live, because a live lease means someone else is driving.
type LeaseRenewal ¶
type LeaseRenewal struct {
ID string
LeaseEpoch int64
CancelRequestedAt time.Time
CancelRequestedBy string
}
LeaseRenewal is one held run's answer to the heartbeat: the token the owner still holds, and any cancellation request that arrived since. The answer is the whole two way channel (11 section 4.3): cancellation needs no push, no extra polling and no second mechanism, because every renewal carries it.
type LockedError ¶
type LockedError struct {
Path string
Err error
// Owner is the session row of the process holding the lock, when the
// database could be read and named one. Its message says so when it is nil.
Owner *Session
}
LockedError is returned when another process already owns the state directory. It is a distinct type so a caller can tell "somebody else is running" from "this directory is broken".
func (*LockedError) Error ¶
func (e *LockedError) Error() string
Error explains the refusal the way every paceq error does: what went wrong, where, and what to do next. The owner is named from the session row when there is one, because "already running" without a pid is not actionable.
func (*LockedError) Unwrap ¶
func (e *LockedError) Unwrap() error
type LogMeta ¶
LogMeta is what the log sink reported for one attempt: where the file is relative to the log root, how big it grew, whether the quota cut it, and the last lines of output. It lands beside the verdict, in the same transaction, because a verdict whose evidence went missing is exactly the drift the write model refuses.
type ManualTriggerInput ¶
type ManualTriggerInput struct {
// JobName is the job whose current version runs.
JobName string
// Actor is who typed the command, recorded on the queued event and on
// the tick's session trail. Empty becomes "system".
Actor string
// ParamsJSON is the parameter object for the run, empty for none.
ParamsJSON string
}
ManualTriggerInput is one person's decision to run a job now.
type ManualTriggerResult ¶
ManualTriggerResult names everything one manual decision created. The three rows are born in one transaction, so any later reader sees all of them or none of them.
type MissedTick ¶
MissedTick is one schedule slot inside a gap that nobody evaluated.
type NewRun ¶
type NewRun struct {
JobName string
JobVersionID string
// TriggerID is the decision this run came out of, empty for a run nobody
// triggered, such as a manual start.
TriggerID string
// Origin says which mechanism produced the run: schedule, sensor, manual,
// retry, replay or backfill. The schema refuses anything else.
Origin string
// RunKey is the dedup key the trigger carried, empty when there is none.
RunKey string
// ConcurrencyKey caps how many runs sharing it may be active. Empty means
// unlimited. A second active run on one key fails with
// ErrConcurrencyKeyHeld, decided by the database and not by a read here.
ConcurrencyKey string
// ScheduledFor is the logical slot a schedule or backfill run belongs to,
// zero for everything else. It is not when the run will start.
ScheduledFor time.Time
// AvailableAt is the earliest the run may be claimed. Zero means now. A
// time in the future needs DeferReason: a run held back always says why.
AvailableAt time.Time
DeferReason string
// ParamsJSON is the canonical parameter object, empty for none.
ParamsJSON string
// MaxAttempts is how many times the run may be attempted. Zero means one.
MaxAttempts int
// ReplayOf is the run this one replays, empty when it replays nothing.
ReplayOf string
// Actor is who caused this, recorded on the queued event. Empty is system.
Actor string
// Steps are the run's steps in spec order, which is the order M1 runs them
// in. Their dependency edges are frozen here and are never read from the
// spec again.
Steps []NewStep
}
NewRun is a run to materialise, with the steps it will run.
type NewStep ¶
type NewStep struct {
Name string
// DependsOn are the steps that have to finish first. The names have to be
// steps of the same run; nothing checks that here, because M1 runs steps in
// order and M4-02 is what reads the edges.
DependsOn []string
// MaxAttempts is how many times the step may be attempted. Zero means one.
MaxAttempts int
}
NewStep is one step of a run to materialise.
type Options ¶
type Options struct {
// Synchronous is "normal" (default) or "full". Nothing else is accepted.
Synchronous string
// AllowNetworkFS skips the refusal to run on a network or FUSE filesystem.
AllowNetworkFS bool
// Clock is the clock the retry backoff waits on. A nil Clock means
// clock.System(). Tests that want the backoff to be instant pass their own.
Clock clock.Clock
}
Options configures Open.
type Outage ¶
type Outage struct {
ID int64
From time.Time
To time.Time
DetectedAt time.Time
Kind string
PrevSession string
MissedTicks int
}
Outage is one stored downtime record.
type OutageInput ¶
type OutageInput struct {
// From and To bound the unaccounted period. From is the last heartbeat of
// the session that died; To is when the replacement started.
From time.Time
To time.Time
// Kind is the schema's word for what happened: 'crash' or 'boot' from
// startup reconciliation, 'clock_jump' from jump detection.
Kind string
// PrevSession is the daemon_sessions row whose death opened the gap, or
// empty when no session row survived to name (a first start after a wipe).
PrevSession string
}
OutageInput names the downtime an outage row explains.
type PermissionError ¶
PermissionError is a path another user can read. It is a distinct type so a caller can tell a refusal it can explain and act on from a failure it cannot, and so the CLI can map it to its own exit code rather than report it as an internal error.
func (*PermissionError) Error ¶
func (e *PermissionError) Error() string
type ReapOptions ¶
type ReapOptions struct {
// Skew is the wall clock allowance past the expiry before a lease counts
// as dead. Zero means DefaultClockSkewAllowance.
Skew time.Duration
// Backoff is how long a requeued run waits before it is due again.
Backoff time.Duration
// MaxCrashCount is the poison line. Zero means DefaultMaxCrashCount.
MaxCrashCount int
// Limit caps the sweep. Zero means reapLimit.
Limit int
// IgnoreLease widens the sweep from "leases that expired" to "every
// running run". It exists for startup reconciliation after a machine
// restart (#62): a changed boot id proves every child process dead, so
// lease_expires_at has stopped being evidence about anything and waiting
// it out would be pure delay. Nothing else may set it. The decision arms
// are the same either way; only candidate selection changes, and the
// fencing token still rises in every arm.
IgnoreLease bool
}
ReapOptions tunes one sweep. Zero fields mean the defaults above.
type ReapedRun ¶
type ReapedRun struct {
ID string
State string
ReasonCode string
CrashCount int
Attempt int
LeaseEpoch int64
}
ReapedRun is one run the reaper took, with where it went.
type ResetResult ¶
type ResetResult struct {
Sensor string
OldEpoch int64
NewEpoch int64
Cursor *string
ForgotKeys bool
}
ResetResult reports what one reset did: the epoch it started from and where the bump took it, so the caller (the CLI, later) can tell the operator.
type ResetSensorInput ¶
ResetSensorInput names the sensor being reset and the scope of the reset. A nil Cursor sets the cursor to NULL (the full "start over" form); a value replays from a chosen point. ForgetRunKeys erases the sensor's run_key rows; it is the only path that deletes them, and is never implicit.
type RetryPlan ¶
type RetryPlan struct {
// NextAttemptAt is when the step becomes runnable again. Zero falls
// back to now, the M1 behaviour before backoff existed.
NextAttemptAt time.Time
// ReasonCode names the scheduled retry on the row and its event.
// Empty keeps the outcome's own code.
ReasonCode reason.Code
// DetailJSON replaces the event's detail object on the pending path,
// where the facts that matter are the attempt number and the
// backoff, not the exit verdict.
DetailJSON string
}
RetryPlan is the schedule a caller computed for a further attempt of a step whose machine transition went back to pending. The machine still decides whether the step goes back to pending; the plan only fills in the when and the words. Backoff arithmetic lives with the caller, which holds both the policy and the clock.
type Run ¶
type Run struct {
ID string
JobName string
JobVersionID string
TriggerID string
Origin string
RunKey string
State string
ConcurrencyKey string
AvailableAt time.Time
DeferReason string
ScheduledFor time.Time
ParamsJSON string
Attempt int
MaxAttempts int
// LeaseOwner and LeaseEpoch say who is executing the run now. The epoch
// is the fencing token: it rises on every claim and on every reap, so a
// writer whose lease was taken can always be told apart from the current
// holder by comparing numbers. Every result write proves its standing by
// carrying both.
LeaseOwner string
LeaseEpoch int64
LeaseExpiresAt time.Time
// HeartbeatAt is the last renewal of the lease. It moves in one batched
// transaction for every run an owner holds.
HeartbeatAt time.Time
// CrashCount is how many executors have died holding this run's lease.
// Crossing the reaper's crash budget ends the run in the poison
// quarantine instead of offering it to yet another executor.
CrashCount int
// The cancellation request, durable before anything is killed. Empty
// time means nobody asked.
CancelRequestedAt time.Time
CancelRequestedBy string
CancelReason string
ReasonCode string
ReasonText string
// ReasonData is the canonical detail object beside the reason code: for
// a failed run, which step failed and why the run ended as it did.
ReasonData string
Error string
CreatedAt time.Time
StartedAt time.Time
FinishedAt time.Time
UpdatedAt time.Time
}
Run is one attempt at one job version.
type RunDetail ¶
RunDetail is a run with its steps, which is what showing one run needs. Events are not here: they are the explain query, and they are read on their own because most callers do not want them.
type RunEvent ¶
type RunEvent struct {
RunID string
// StepName is set on an event about a step, empty on one about the run.
StepName string
// At is when it happened. Zero means now.
At time.Time
// Kind names the transition: run.queued, step.started, step.retry_scheduled
// and so on.
Kind string
FromState string
ToState string
ReasonCode string
// Actor is who caused it: system, reaper, operator, cli:<uid>. Empty is
// system.
Actor string
// DetailJSON is the canonical detail object. Empty is an empty object.
DetailJSON string
}
RunEvent is one state transition, as explain will read it back.
type RunFilter ¶
type RunFilter struct {
JobName string
// States restricts the listing to these run states. Empty means all.
States []string
// Before is the keyset cursor: only runs whose id sorts below it. Ids are
// ULIDs, so this walks the history backwards in time. Empty starts at the
// newest run.
Before string
// Limit caps the page. Zero is 50 and anything above 500 is 500.
Limit int
}
RunFilter narrows a listing. The zero filter lists the newest runs of every job.
type RunSummary ¶
type RunSummary struct {
ID string
JobName string
Origin string
State string
ReasonCode string
CreatedAt time.Time
StartedAt time.Time
FinishedAt time.Time
// The deferral facts (#68): available_at decides whether a queued row
// reads as deferred, defer_reason names why it waits, and reason_data
// carries the blocking run id the display points at.
AvailableAt time.Time
DeferReason string
ReasonData string
}
RunSummary is one line of a listing.
type ScheduleInput ¶
type ScheduleInput struct {
JobName string
Name string
Kind string
Expr string
Timezone string
SpringForward string
FallBack string
Catchup string
CatchupLimit int
CatchupWindowMS int64
// Overlap is the policy for ticks that fire while the job's concurrency
// limit is held: "skip" (default) or "queue".
Overlap string
Paused bool
NextTickAt time.Time
}
ScheduleInput names a schedule to create or replace. Fields left empty take the schema defaults.
type ScheduleRow ¶
type ScheduleRow struct {
ID string
JobName string
Name string
Kind string
Expr string
Timezone string
SpringForward string
FallBack string
Catchup string
CatchupLimit int
CatchupWindowMS int64
// Overlap is what a tick does when the job's max_concurrent is already
// held: "skip" stands down and records why, "queue" materialises the run
// deferred into the future with a defer_reason. Empty means skip.
Overlap string
Paused bool
LastTickAt *time.Time
NextTickAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
// contains filtered or unexported fields
}
ScheduleRow is one schedule as the due query returns it. Every column the loop needs to decide what fired comes back at once, so processing one schedule costs one read.
type SensorRow ¶
type SensorRow struct {
Name string
JobName string
Kind string
IntervalMS int64
MinIntervalMS int64
TimeoutMS int64
Paused bool
Cursor string
CursorVersion int64
DedupEpoch int64
NextEvalAt int64
}
SensorRow is one sensor as the commit path reads it: the cursor the evaluation started from, the version that guards it, and the dedup epoch that makes a cursor reset safe.
type SensorSummary ¶
type SensorSummary struct {
Name string
JobName string
Kind string
ExecJSON string
SpecJSON string
IntervalMS int64
MinIntervalMS int64
TimeoutMS int64
MaxTriggersPerTick int
Paused bool
NextEvalAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
SensorSummary is the read-side view of one sensor row as the readers here return it.
type SensorTickCommitInput ¶
type SensorTickCommitInput struct {
// TickID is the intention row opened by BeginSensorTick.
TickID string
SensorName string
JobName string
// CursorVersion is the guard BeginSensorTick read. It is compared inside
// the transaction; a stale value refuses the commit.
CursorVersion int64
// CursorAfter moves the sensor cursor when Outcome is triggered. It is
// the value the whole evaluation built towards.
CursorAfter string
// DedupEpoch is the sensor's dedup epoch, the same value BeginSensorTick
// saw on the row. It prevents a cursor reset from replaying an old key
// into an already-ringed table.
DedupEpoch int64
// Triggers are the run keys this evaluation decided to fire. Each is
// registered against the run_keys gate first, so a replayed evaluation
// folds into the run that already exists instead of creating a twin.
Triggers []SensorTrigger
// Outcome is OutcomeTriggered, OutcomeSkipped or OutcomeError. Only a
// triggered evaluation moves the cursor and creates runs.
Outcome string
// ReasonCode and ReasonText belong on a skipped or error tick. A skipped
// sensor's own reason travels verbatim (plan: the sensor says why it
// skipped and paceq does not paraphrase).
ReasonCode reason.Code
ReasonText string
// NextEvalAt is when the sensor becomes due again. Set on every commit.
NextEvalAt int64
// DurationMs is how long the evaluation took, for the tick row.
DurationMs int64
Now time.Time
}
SensorTickCommitInput is everything a completed sensor evaluation decided, ready to be written or refused as one unit.
type SensorTickCommitResult ¶
type SensorTickCommitResult struct {
// Accepted is how many new runs were created.
Accepted int
// Deduped is how many triggers folded into an existing run.
Deduped int
// RunIDs are the ids of the accepted runs, in trigger order.
RunIDs []string
// Fenced is true when the commit was refused because the sensor already
// advanced past the cursor_version this evaluation started from. Nothing
// was written except the tick being marked error with TICK_MISSED_LEASE_LOST.
Fenced bool
}
SensorTickCommitResult reports what one atomic commit did.
type SensorTrigger ¶
SensorTrigger is one trigger a sensor evaluation produced: the run key that deduplicates it and the parameters that vary per trigger. Each becomes a triggers row and, on a new run key, a runs row.
type Session ¶
type Session struct {
ID string
Version string
BootID string
PID int
StartedAt time.Time
LastSeenAt time.Time
StoppedAt time.Time
StopReason string
}
Session is one run of paceq against this database. Its id is the instance identity: a ULID minted at startup, not a process id, because process ids are recycled and a recycled one would make a dead run look like a live one both in the history and in a lease.
type StateLock ¶
type StateLock struct {
// contains filtered or unexported fields
}
StateLock is the exclusive claim on one state directory. It is held by an open file description for as long as the process lives, so no second paceq can write to the same state, and the kernel releases it even when the process is killed.
func AcquireStateLock ¶
AcquireStateLock takes the exclusive lock on dir, creating the directory when it does not exist. It never waits: a daemon that blocks at startup looks hung, so a held lock is an immediate error naming the process that holds it.
The lock is taken before the database is opened for writing, which is what makes two writers impossible rather than merely unlikely.
type Step ¶
type Step struct {
Name string
Index int
State string
Attempt int
MaxAttempts int
// ExitCode is meaningful only when HasExitCode is set. A step killed by a
// signal, or one that never started, has no exit code at all, and zero is a
// perfectly ordinary success.
ExitCode int
HasExitCode bool
Signal string
StartedAt time.Time
FinishedAt time.Time
DurationMS int64
ReasonCode string
ReasonText string
ReasonData string
Error string
LogPath string
// LogBytes, LogTruncated and ErrorTail are the log facts that live in
// the database: how big the log grew, whether the quota cut it, and the
// last few KiB of output. The tail is what explain shows after the log
// file itself is gone.
LogBytes int64
LogTruncated bool
ErrorTail string
// NextAttemptAt is when a pending step that failed with attempts left
// becomes runnable again. M1 computes no retries, so nothing sets it
// yet; the claim gate reads it all the same, because M1-09 will fill
// it without touching a single reader.
NextAttemptAt time.Time
}
Step is one step of a run.
type StepDep ¶
StepDep is one edge as it was frozen when the run was materialised. M1 runs steps in index order and never reads these; they exist so the record of why a run waited survives, and so M4 inherits its graph from rows rather than from a spec that may since have changed.
type StepOutcome ¶
type StepOutcome struct {
// Event is one of the step machine's input names: step_succeeded,
// step_failed, cancel_observed or upstream_failed.
Event string
// ReasonCode explains a terminal outcome. The machine refuses a
// terminal transition without one.
ReasonCode reason.Code
// ExitCode is what the process exited with. Nil means there is none:
// a signalled step and a step that never ran have no exit code, and
// zero is a perfectly ordinary success.
ExitCode *int
Signal string
FinishedAt time.Time
// LogMeta carries the attempt's log facts. Empty means the attempt
// produced no log, which is what a skip is.
LogMeta LogMeta
// DetailJSON is the canonical detail object on the event and on the
// row's reason_data, empty for none.
DetailJSON string
// Retry schedules a further attempt when this event sends the step
// back to pending. Nil means runnable again at once. It never changes
// the transition itself; the machine decides from the row.
Retry *RetryPlan
}
StepOutcome is one event the engine hands to a step: the runner came back, or an upstream ended in a way that closes this step, or a cancellation was observed. Event names the model event, and the machine decides whether the step may take it.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store owns every connection to the database file. Both handles are private: no package outside internal/store may hold a writable database handle.
func Open ¶
Open opens the database at path with a single-connection writer pool and a read-only reader pool, then verifies that every PRAGMA is actually in effect. It creates the file when it does not exist and works against an empty, unmigrated database.
func OpenReadOnly ¶
OpenReadOnly opens only the read pool of a database: no writer connection, no state lock, no migration and none of the startup writes. It exists for the commands that must work while the daemon holds the state lock, such as paceq logs, which reads files and read only database rows and nothing else.
A write through the returned store fails with ErrReadOnly instead of reaching the database.
func OpenState ¶
OpenState claims a state directory and opens the database in it.
The order is the point. The lock is taken before the database is opened for writing, so a second paceq is refused before it can touch a single page of a file another process owns. The returned store holds the lock until Close.
func (*Store) AbandonedChains ¶
AbandonedChains names every half written trigger chain: a tick that claims it triggered but has no trigger row, an accepted trigger with no run, a run without its steps, a run without its queued event. Each pattern is a chain a SIGKILL could have cut in the middle if materialisation were not one transaction. An empty result says no aborted chain left anything behind.
Skipped ticks and deduped triggers are deliberately not findings: standing down is a decision those rows record, not a broken promise.
func (*Store) AcquireOrRenew ¶
func (s *Store) AcquireOrRenew(ctx context.Context, name, holder string, ttl time.Duration) (LeaseGrant, bool, error)
AcquireOrRenew takes the named lease for holder, or renews the holding one holder already has. It is idempotent: calling it again with the same inputs moves nothing but expires_at.
ok is false when another holder owns a lease that is still alive; the grant returned then is empty and the caller is the follower. ttl counts from the moment the database applies the statement, which this store reads from its own injected clock. The method deliberately takes no now parameter: lease time is always computed here, never compared across processes.
func (*Store) ActiveAttempts ¶
func (s *Store) ActiveAttempts(ctx context.Context) ([]AttemptProcess, error)
ActiveAttempts lists the baselines whose step and whose run are still running right now. A pid found on this list with matching start ticks is a legitimate live worker, and the orphan sweep leaves it alone.
func (*Store) ActiveRunViolations ¶
ActiveRunViolations returns every job whose running rows outnumber its max_concurrent: the I12 invariant of 02 section 4.3. It is part of the full fsck sweep, and it is the check the startup quick subset calls as well once #62's reconciliation lands there - the call site is one line, because this method already reads exactly what that subset needs.
func (*Store) ActiveRunsForJob ¶
ActiveRunsForJob reads the occupancy of one job. It exists for tests and for the fsck invariant; production decisions never call it, because a read outside the transaction that decides is exactly the shape this feature exists to avoid.
func (*Store) AppendLeaseEvent ¶
func (s *Store) AppendLeaseEvent(ctx context.Context, e LeaseEvent) error
AppendLeaseEvent records one moment of a lease's life: taken, lost or taken over. The code comes from the closed reason catalogue, so the row explains itself the way run_events rows do.
func (*Store) AppendRunEvent ¶
AppendRunEvent records one transition on its own. Transitions that happen inside a write of their own carry their event in that same transaction instead, which is what keeps the history from disagreeing with the state.
func (*Store) ApplyJobs ¶
func (s *Store) ApplyJobs(ctx context.Context, inputs []JobVersionInput) ([]JobApplyResult, error)
ApplyJobs records a batch of job specs and leaves every job pointing at the version its spec hashes to. The whole batch is one transaction on purpose: apply either lands complete or not at all, because a half applied catalog is the one state an operator cannot reason about. Any failure rolls every job in the batch back, including the sensor rows each job's spec declares.
Idempotency lives in the schema, not in a check: job_versions carries UNIQUE (job_name, spec_hash), the insert comes first, and a conflict means the version was already there. Two applies racing on the same file therefore end in one row no matter which commit lands first.
The inputs are finished facts: parsing happened before this was called, and nothing inside the transaction reads a file or a clock. Ids are minted before the transaction opens, for the same reason UpsertJobVersion mints its own there.
func (*Store) AutoVacuum ¶
func (s *Store) AutoVacuum(ctx context.Context) (AutoVacuumMode, error)
AutoVacuum is the mode the database file was created with. doctor reports it, because a database left at NONE never gives disk back after retention and the way out costs a full VACUUM.
func (*Store) BeginSensorTick ¶
func (s *Store) BeginSensorTick(ctx context.Context, in BeginSensorTickInput) (BeginSensorTickResult, error)
BeginSensorTick writes the intention row for one sensor evaluation: a tick in 'running' carrying the cursor it started from. A crash after this point leaves a visible interrupted tick that reconciliation closes; there is no such thing as a sensor evaluation the database silently forgets started.
The write is deliberately separate from CommitSensorTick. The sensor runs between them, outside any transaction (plan 00 section 3.11: no process execution or network I/O inside a write transaction).
func (*Store) BootChanged ¶
BootChanged reports whether the machine has restarted since the last session ran. It answers for the session StartSession opened, and is false until then.
The evidence lasts for one start. StartSession reads the recorded boot id and overwrites it in the same transaction, so the first start after a reboot is the only one that sees the change: every later start on that boot reports false. Whoever reconciles has to act on it then, or the fact is gone.
False is not the same as "no restart": a platform without a boot id reports false, and then a surviving process can only be ruled out by waiting for its lease to expire.
func (*Store) CancelRequested ¶
CancelRequested reports whether a cancellation is waiting, and who asked. The executor reads this between steps and on its poll while a step runs.
func (*Store) CheckpointTruncate ¶
CheckpointTruncate runs PRAGMA wal_checkpoint(TRUNCATE) through the writer pool: every committed frame moves into the database and the wal file shrinks to zero bytes. The daemon calls it after its last write, before Close, so the next start opens a small file instead of replaying a long wal.
An error here is reported, not fatal: a checkpoint that could not finish costs startup speed, never correctness.
func (*Store) ClaimRun ¶
ClaimRun claims exactly this run for the caller. It is the single run form of ClaimRuns and shares its one statement.
The returned state is "running" with the new fencing token, or "cancelled" when a cancellation was waiting before the run ever started: the sweep in the same transaction closed it, and there was never a lease to hand out. Anything else is ErrNotClaimable.
func (*Store) ClaimRuns ¶
ClaimRuns claims up to Limit due runs for Owner in one transaction: the cancel sweep for queued runs asked to stop, then the claim statement, then one run.started event per claimed row. Everything lands together or not at all, which is why a crash mid-claim leaves every run exactly as it was.
Runs that are not due yet, already claimed, or asked to stop come back some other cycle; the result only names what this call took.
func (*Store) ClaimableRunIDs ¶
ClaimableRunIDs names the queued runs that are due now, in claim order: by available_at first, then id, which is the order the claim index keeps. A parked run whose time has not arrived is invisible here, exactly as the claim gate would have it.
func (*Store) Close ¶
Close releases every connection in both pools, and the state lock when this store took one. The lock goes last: it may not be handed on while a connection to the database it protects is still open.
func (*Store) CommitSensorTick ¶
func (s *Store) CommitSensorTick(ctx context.Context, in SensorTickCommitInput) (SensorTickCommitResult, error)
CommitSensorTick records one finished sensor evaluation, all or nothing: the tick outcome, every trigger, every run and its event, the run_keys dedup gate, and the cursor advance share ONE BEGIN IMMEDIATE transaction. A crash at any point between BEGIN IMMEDIATE and COMMIT leaves the database as if the evaluation had never committed, so a restart re-evaluates from the old cursor and the dedup gate makes the replay a no-op. There is no window in which runs exist without their cursor, or a cursor exists without its runs.
The cursor advance is guarded. CursorVersion was read at evaluation start; if the sensor has since been advanced by another evaluation (a takeover, a reset, a concurrent pass), the CAS matches zero rows, the commit is refused and the tick records TICK_MISSED_LEASE_LOST. An old result never overwrites a newer one.
func (*Store) CreateRunWithSteps ¶
CreateRunWithSteps materialises a run: the run row, every step, the frozen dependency edges and the queued event, in one transaction. A crash anywhere in it leaves no rows at all, so there is no such thing as a run without its steps or a transition without its event.
A concurrency key held by another active run returns ErrConcurrencyKeyHeld. The database decides that, through the partial unique index, and this method never reads the key first: a read followed by a write is a decision made on information that can already be out of date.
func (*Store) CurrentJobVersion ¶
CurrentJobVersion returns the version a job currently points at. This is a read outside any transaction: a caller that decides based on it races an apply by design, and the callers that must not race choose versions inside MaterializeManualTrigger instead.
func (*Store) DrainRun ¶
func (s *Store) DrainRun(ctx context.Context, runID string, ref LeaseRef, code reason.Code) (bool, error)
DrainRun hands a claimed run back to the queue at a clean stop. It is the whole handback in one transaction: the running steps go back to pending with their attempts restored (05 section 3.2, point 4), because the attempt was cut short by the daemon's own stop and produced no verdict, and then the run itself is requeued without counting a crash, because the executor left on purpose.
The epoch still rises, so any writer from the drained attempt stays fenced out, and available_at moves to now so the next executor can claim at once. The CAS decides whether anything is owed: a caller whose lease has moved on gets handed=false and writes nothing.
func (*Store) DueSchedules ¶
DueSchedules returns up to max unpaused schedules whose next tick is due at or before nowMilli, oldest next tick first.
func (*Store) DumpForIdempotence ¶
DumpForIdempotence renders every user table as ordered, comparable text, leaving out only the daemon session rows whose timestamps move on every start by design. Two calls bracketing a pass of OnStartup that wrote nothing come back equal, which is exactly the assertion AC7 and AC12 of issue #62 rest on: bit-identical after the first pass, untouched when healthy.
The SQL lives here because all SQL lives here; callers get back plain strings they compare themselves.
func (*Store) FailHangingTicks ¶
FailHangingTicks closes every tick still marked running whose evaluation started before the given instant. A running tick older than this process's session belongs to a daemon that died mid evaluation; nothing else could ever have written its verdict. Each closed tick records why: the code says the daemon crashed, not that the sensor failed.
The returned slice names what closed, in no particular order, so a caller logs exactly as much as happened. An empty result means there was nothing to close, which is the common case for the periodic safety net.
func (*Store) FinishRun ¶
func (s *Store) FinishRun(ctx context.Context, runID string, ref LeaseRef, fr FinishReason) (string, error)
FinishRun closes a run whose steps are all terminal. The aggregate comes from the machine's guards, which read the step rows inside the same transaction, so the verdict can never disagree with the steps it describes: any failed step fails the run, and a run with a step still open is refused outright. The lease is released here.
The verdict is a CAS on the fence: the UPDATE carries owner and epoch, and zero rows affected means the lease was taken over while this writer worked. The refusal comes back as ErrLeaseLost and nothing at all is written: worst case is duplicate work, never duplicate state.
func (*Store) Fsck ¶
Fsck sweeps the whole database and returns every violation it finds, empty when the state is sound. It reads only: a checker that writes would be part of whatever it is checking.
func (*Store) GapSchedules ¶
func (s *Store) GapSchedules(ctx context.Context) ([]GapSchedule, error)
GapSchedules lists the schedules that were live to fire during a gap. Paused schedules are left out on purpose: their slots were not owed, and writing missed ticks beside them would turn an operator's pause into apparent downtime.
func (*Store) GetRun ¶
GetRun reads one run and its steps. The argument is a whole id or any prefix of one, the way a git object is named: ids are ULIDs, so a prefix is a range scan on the primary key and not a scan of the table.
A prefix matching more than one run is ErrAmbiguousRunID rather than the first match. Picking one would mean cancelling or replaying whichever run the database happened to order first.
The run and its steps are two queries against the read pool, and the pool has many connections, so they are two snapshots and not one. A step that changes between them shows up as a step further along than the run says. That is the deliberate price of never opening an explicit read transaction: a read snapshot held open across a listing is what stops WAL checkpointing and grows the file without bound. Anything that has to see one instant reads it inside a write transaction instead.
func (*Store) GetSchedule ¶
GetSchedule reads one schedule by job name and schedule name.
func (*Store) InjectActiveRunOverflow ¶
InjectActiveRunOverflow plants I12: two running runs on one job whose max_concurrent is 1. The seeded night job has one queued run; the injection sets it to running and creates a second running run, so the active count exceeds the ceiling.
func (*Store) InjectBackwardsTimestamp ¶
InjectBackwardsTimestamp moves a step's finish before its start: I13.
func (*Store) InjectBrokenEventChain ¶
InjectBrokenEventChain rewrites the second event's from_state, so the chain no longer picks up where the first event ended: I15.
func (*Store) InjectDependencyCycle ¶
InjectDependencyCycle plants I9: two steps of one run pointing at each other, so no order of execution can ever be legal. Like the duplicate run key, nothing in the schema forbids it; only the health check can see it.
func (*Store) InjectDuplicateRunKey ¶
InjectDuplicateRunKey plants I3: one job whose run_key names two runs. There is no UNIQUE index to dodge on purpose, because this invariant lives in application law, not in the schema; that is exactly why the health check has to re-read it at startup.
func (*Store) InjectFailedStepUnderSucceededRun ¶
InjectFailedStepUnderSucceededRun marks one step failed while the run still says succeeded, so the stored state and the steps' aggregate disagree: I10.
func (*Store) InjectOrphanTick ¶
InjectOrphanTick writes a manual tick that claims it triggered, with no trigger row behind it. AbandonedChains names it; no invariant check does, because the chain sweep is where this broken promise lives.
func (*Store) InjectTerminalStepPending ¶
InjectTerminalStepPending flips one step of a terminal run back to pending: the exact row I2 exists for. The run's aggregate moves with it, so the sweep reports I2 and I10 for the same planted row.
func (*Store) InjectUnexplainedDeferral ¶
InjectUnexplainedDeferral pushes a queued run's availability into the future and clears its defer_reason: a run held back that no longer says why. The CHECK constraint refuses this shape, so the injection lifts the checks for the one statement and puts them straight back: I14.
func (*Store) InjectUnexplainedTerminal ¶
InjectUnexplainedTerminal clears a terminal run's reason code: the catalogue rule swept along with the invariants.
func (*Store) IntegrityCheck ¶
func (s *Store) IntegrityCheck(ctx context.Context) (IntegrityReport, error)
IntegrityCheck runs both pragmas and reports what they said. It reads only; SQLite answers integrity_check from the same connection pool every other reader uses, so nothing here disturbs a writer.
func (*Store) Job ¶
Job reads one job back. An unknown name is ErrNotFound, the same answer every other single row read gives.
func (*Store) JobLastRuns ¶
JobLastRuns reads one row per job, with that job's newest run joined beside it. An empty jobName reads every job; a name narrows the answer to one. Jobs come back in name order, and a job without runs is still listed, because status is a per job report rather than a listing of runs.
The newest run is decided by id alone. Ids are ULIDs, so id order is time order and the subquery stays a single index lookup per job.
func (*Store) JobNames ¶
JobNames lists every job in name order. It is what a did you mean suggestion is drawn from when a command is given a job that does not exist.
func (*Store) JobVersionByID ¶
JobVersionByID returns exactly the version named, however old. This is how execution reads the bytes a run was frozen with.
func (*Store) JournalMode ¶
JournalMode is the journal mode the database file is in. Open refuses anything but WAL, so a report built on this states what the file holds rather than what the code assumes.
func (*Store) KnownAttempts ¶
func (s *Store) KnownAttempts(ctx context.Context) ([]AttemptProcess, error)
KnownAttempts lists every baseline ever recorded, whatever happened to the step afterwards. This is the sweep's history half: a process still carrying a dead attempt's PACEQ_RUN_ID is identified by a baseline that outlived the attempt.
func (*Store) LatestSession ¶
LatestSession is the most recent session row, open or closed. It is how an operator or the health surface asks "who ran here last, and how did it end": a closed row carries its stop_reason, an open one means the process died without saying goodbye.
func (*Store) LeaseEvents ¶
LeaseEvents returns the recorded moments of one lease, newest first. limit caps the read; pass a small number unless something really needs the history.
func (*Store) LeaseHolder ¶
LeaseHolder returns the current row for the named lease, whether anyone holds it or not. It is the read side for status views and for whoever has to decide how long ago a holder was last seen. It reads only: nothing in this method writes, so it is safe next to a running daemon.
func (*Store) ListAllSchedules ¶
func (s *Store) ListAllSchedules(ctx context.Context) ([]ScheduleRow, error)
ListAllSchedules returns every schedule row: unpaused first, then paused, alphabetically within each group.
func (*Store) ListJobVersions ¶
ListJobVersions reads every version of one job, newest first. A job's history is short, so the whole list comes back: a report that says "you are on version 4 of 7" needs the same rows a rollback check needs.
func (*Store) ListRuns ¶
ListRuns is one page of run history, newest first. Ids are ULIDs, so id order is time order and the cursor is the last id of the previous page.
There is no OFFSET anywhere in this package. An offset re-reads everything it skips, and a listing that holds a read snapshot open while it does so is what starves WAL checkpointing and grows the file without bound.
func (*Store) MaterializeManualTrigger ¶
func (s *Store) MaterializeManualTrigger(ctx context.Context, in ManualTriggerInput) (ManualTriggerResult, error)
MaterializeManualTrigger records a manual decision end to end: the tick, the trigger and the run with its steps and edges, in one transaction. It is the same chain a schedule or a sensor produces, which is what keeps explain complete for hand started runs from day one and lets M2 and M3 add new source kinds without a second code path.
The steps come out of job_versions.spec_json, the immutable version the job currently points at. Nothing re-reads the YAML file, so an apply that lands after the decision cannot change what this run does.
A paused job still runs here. A pause governs automatic firing; a person typing the command has already decided.
func (*Store) MaterializeTick ¶
materializeTick claims one fire-time end to end. The order inside the transaction is the write model's: claim first, then the consequences, then progress, and every consequence aborts together when any statement refuses.
func (*Store) Migrate ¶
Migrate applies every migration the database is missing. It is idempotent and safe to call on every startup.
func (*Store) MissedTickEvidence ¶
func (s *Store) MissedTickEvidence(ctx context.Context) ([]TickEvidence, error)
MissedTickEvidence lists every synthetic missed tick. It is how tests and the explain surface read the gap's evidence back without touching SQL outside this package.
func (*Store) NextRetryWait ¶
NextRetryWait reports how long the executor must wait before some pending step of the run passes its retry gate: the gap to the soonest next_attempt_at, clamped to zero once it is due. Waiting is false when no pending step carries a time at all, which is the executor's signal that nothing here will ever become runnable again. This read is what keeps retry free of state of its own: the engine sleeps on the answer instead of owning a scheduler.
func (*Store) NextRunnableStep ¶
NextRunnableStep names the next step the engine may start: the lowest index that is pending, past its retry gate, and whose every frozen upstream has succeeded. A step waiting on upstream that has not succeeded is skipped over, which is the degenerated claim predicate M4-02 replaces with the whole graph.
func (*Store) ObserveRunCancel ¶
func (s *Store) ObserveRunCancel(ctx context.Context, runID string, ref LeaseRef, actor string, code reason.Code) error
ObserveRunCancel effectuates a cancellation somebody requested earlier: the caller has already killed the process group outside any transaction (the machine lists that effect first, and the engine performs it before calling here), the steps that were running are cancelled by their own events, and this closes whatever pending steps remain and then the run itself.
The caller must still hold the lease at its token; the write is a CAS like every other result write, so a holder that lost the run while killing the process group cannot cancel over whatever the new owner is doing.
It refuses when nobody asked: a run nobody asked to cancel is never cancelled, whatever the caller claims. The event names the person who made the request, not the executor that observed it.
func (*Store) OpenSession ¶
OpenSession is the session that has not ended, if there is one. It is what names the process holding the state lock, and what tells a restart that the last run never finished.
func (*Store) Outages ¶
Outages lists every stored outage, oldest first. It is how tests and the explain surface read history back.
func (*Store) OutagesWithoutTicks ¶
OutagesWithoutTicks lists outage rows whose synthetic tick evidence has not been written yet (or legitimately holds zero slots). It is how reconciliation finishes what a crash mid-write left half explained: the row itself is the marker, and a later pass completes it.
func (*Store) OverrideBootIDForTest ¶
OverrideBootIDForTest pins what the platform's boot id read reports from now on. Tests cannot edit /proc/sys/kernel/random/boot_id, and a changed boot id is the strongest evidence reconciliation has (issue #62), so the value itself is what gets replaced: call it before a start to stage one boot, and again with a different value to stage a machine restart. An empty value lifts the override.
It exists for the startup reconciliation tests and their crash harness rows; production code has no reason to call it. It takes a value rather than a callback on purpose: the store's exported surface accepts no function values.
func (*Store) PauseSchedule ¶
PauseSchedule sets paused=1 on the schedule and returns the new row. Idempotent: calling on an already-paused schedule succeeds without error and does not change updated_at.
func (*Store) PendingSteps ¶
PendingSteps lists a run's steps that have not started, in index order.
func (*Store) QuickFsck ¶
QuickFsck returns every critical violation it finds, empty when the state is sound enough to serve.
func (*Store) ReapExpiredRuns ¶
ReapExpiredRuns takes every run whose lease expired past the skew allowance and decides its fate in the one transaction: a cancellation request on a dead owner completes as cancelled, a run past its crash budget fails into the poison quarantine, a run with no attempt budget left fails reconciled, and everything else is requeued with a backoff for a new holder. The fencing token rises in every arm, including the failures: a late zombie must not overwrite a verdict someone else owns, whatever the verdict was.
Each decision writes its own run_events row, by the reaper, in the same transaction as the state change (G10).
func (*Store) RecordAttemptProcess ¶
func (s *Store) RecordAttemptProcess(ctx context.Context, runID, step string, ref LeaseRef, pid int, startTicks int64) error
RecordAttemptProcess stamps which live process is carrying one running step: its pid and the kernel's start ticks for that pid, read at spawn time. The engine writes it through OnStart the instant a spawn succeeds, so the evidence is on file before any verdict could exist, and startup reconciliation can later prove a surviving process is (or is not) the child it was told about.
The write is fenced like every other holder's write: only the executor that owns the run's lease may name processes for it, so a replaced executor cannot relabel someone else's process as one of ours.
func (*Store) RecordMissedTicks ¶
func (s *Store) RecordMissedTicks(ctx context.Context, sessionID string, outageID int64, ticks []MissedTick) (int, error)
RecordMissedTicks writes the synthetic ticks for one outage and adds the count it actually inserted to the outage's total. Every insert is INSERT OR IGNORE: a slot that already has a tick row was really evaluated before the crash, and reconciliation never rewrites real history. That same ignore makes a repeated pass over the same slots a no-op, which is what makes the whole sweep safe to run twice.
The work is chunked so a fourteen day gap across a hundred schedules cannot hold the write lock for one long stretch.
func (*Store) RecordOrphanKill ¶
RecordOrphanKill writes the audit event that says this process group was signalled and why. A kill nobody can read about later is indistinguishable from sabotage, so the sweep's every shot lands here first.
The event carries the run's current state on both ends of the transition, because a kill changes no state: it only explains a signal that was sent. Stamping the live state also keeps the event chain (I15) continuous even when another actor moved the run between the scan and this write.
func (*Store) RecordOutage ¶
RecordOutage stores the outage and returns its rowid. The detected_at stamp is this store's clock reading at write time, which is what bounds the SLO promise: an outage longer than the noise threshold exists within seconds of the startup that noticed it.
func (*Store) RecordStepOutcome ¶
func (s *Store) RecordStepOutcome(ctx context.Context, runID, name string, out StepOutcome, ref LeaseRef) error
RecordStepOutcome applies one event to one step. The machine decides whether the step may take it and what the transition demands; this writes the verdict, its log facts and its event in one transaction. A step that fails with attempts left goes back to pending for its next attempt: parked at next_attempt_at when the caller attached a RetryPlan, runnable again at once when it did not. The claim gate, not this method, decides when the next attempt may start.
The ref is the fence. A holder writes only while its token still matches; recovery passes the zero ref, which is refused against any live lease. A writer that lost the lease gets ErrLeaseLost and nothing on the row moves.
func (*Store) ReleaseLease ¶
ReleaseLease deletes the named lease row when holder still owns it, which is the clean shutdown path: the next process can take over immediately instead of waiting out the ttl.
released is false when there was nothing of holder's to delete: either the lease was never held, or it expired and another holder took it over. Neither case is an error, and neither may touch the new holder's row.
func (*Store) RenewRunLeases ¶
func (s *Store) RenewRunLeases(ctx context.Context, owner string, ttl time.Duration) ([]LeaseRenewal, error)
RenewRunLeases is the batch heartbeat: one transaction for all the runs the owner holds, no matter how many there are, so the write cost of staying alive does not scale with the workload. Callers diff the answer against what they believe they hold; a run missing from the answer, or answering with a different token, is a lease lost and must be self-fenced.
func (*Store) RequestCancel ¶
RequestCancel records that somebody wants the run stopped, durably and before anything is killed. The first request stands: a second one changes nothing, so two people asking at once cannot disagree about who asked or why. Observing the request is a different act, done by whoever holds the lease, and that is the transition with the event.
func (*Store) RequeueCrashedRun ¶
RequeueCrashedRun puts a run whose executor died back in the queue. It is the store half of the restart story the crash harness (#75) proves: a run left running by a SIGKILL is requeued through the machine's own lease_expired transition, never reset behind its back.
The refusal is the safety catch. A lease that has not expired yet belongs to a process that may still be alive, and requeuing under it would let two executors drive one run. Only an expired lease counts as evidence that the previous owner is gone; the state directory lock adds its own guarantee on this platform, but the machine's guard does not rely on it.
The transition writes what the machine demands: the epoch goes up so any writer from the dead attempt is fenced out, crash_count counts the loss of the executor against the run, and defer_reason records why the requeued run sits waiting (I14). One event row tells the story, in the same transaction.
func (*Store) ResetSensor ¶
func (s *Store) ResetSensor(ctx context.Context, in ResetSensorInput) (ResetResult, error)
ResetSensor atomically raises the sensor's dedup_epoch by one, so every run key this sensor ever registered becomes a new fingerprint in the fresh epoch. It is the store-level backing for `sensors reset`: the CLI surface lands in M3-06, and this is the method it will call.
No read happens before the write: the bump is one UPDATE with a RETURNING readback, so two resets can never collide on the same new value (insert- first discipline, 11 section 5.2).
func (*Store) ResumeSchedule ¶
func (s *Store) ResumeSchedule(ctx context.Context, jobName, name string, nextTickAt time.Time) (ScheduleRow, error)
ResumeSchedule sets paused=0, writes the caller's pre-computed next_tick_at, and returns the new row. The caller computes nextTickAt from the cursor and cron expression to avoid re-parsing inside the transaction.
func (*Store) RunEvents ¶
RunEvents returns a run's transition history, oldest first. This is the explain backbone: one row per state change, written in the transaction that made the change.
func (*Store) RunExists ¶
RunExists reports whether this database has ever heard of the run. The orphan sweep checks before it believes an environment variable: a foreign program that happens to carry PACEQ_RUN_ID must never be mistaken for one of ours.
func (*Store) RunIDByRunKey ¶
RunIDByRunKey names the run a registered dedup key points at. The crash harness uses it to find the run behind an already-committed tick decision; explain will use it to answer "which run did this fire-time become".
func (*Store) ScheduleCursor ¶
func (s *Store) ScheduleCursor(ctx context.Context, jobName, name string) (*time.Time, time.Time, error)
ScheduleCursor returns a schedule's progress stamps: last_tick_at (nil when the schedule has never been evaluated) and next_tick_at.
func (*Store) ScheduleTicks ¶
ScheduleTicks lists one schedule's recorded ticks, oldest fire-time first. This is the read side of everything MaterializeTick writes, and the table explain (M5-01) will read.
func (*Store) SchemaVersion ¶
SchemaVersion is the schema version recorded in this database file. It is the number doctor reports, and the one that decides whether a binary may write to the file at all.
func (*Store) SetJobPaused ¶
SetJobPaused flips the pause flag. It exists for the doctor and the CLI pause command; tests use it to stage paused jobs.
func (*Store) SetSensorCursor ¶
func (s *Store) SetSensorCursor(ctx context.Context, in CursorInput) error
SetSensorCursor moves a sensor's cursor without touching its dedup epoch. The old run keys keep dedupping, because the epoch they are tagged with is still the current one. This is the "cursor set" row of the reset table in the issue: spool the cursor without replay, the dedup gate still stops old keys (10 section 5 F4c).
func (*Store) StartSession ¶
StartSession opens this process's session row and closes whatever the last run left behind.
Three things happen in one transaction, because a reader that saw them apart would draw the wrong conclusion. Sessions still open belong to a run that never got to say goodbye, so they are marked crashed. The new row records who is running, on which boot, from when. The boot id is written to meta, so the next start can tell a restart of paceq from a restart of the machine.
A changed boot id is the strongest evidence this system has: the machine restarted, so no process paceq started can still be alive. BootChanged reports it after this call returns.
func (*Store) StartStep ¶
StartStep opens the first attempt of a pending step: running, attempt up by one, started_at stamped, its event in the same transaction. The run has to be running and still held by the writer at the writer's fencing token: a step may not move while its run is queued, whatever the step machine alone would allow, and it may not move under a lease the writer has lost.
func (*Store) StopSession ¶
StopSession records a clean shutdown. A session that ends any other way keeps its open row, which is what the next start reads as a crash.
func (*Store) SyncSensors ¶
func (s *Store) SyncSensors(ctx context.Context, job string, sensors []spec.Sensor) (SyncResult, error)
SyncSensors makes the sensor rows of one job identical to the job's spec, atomically, without touching drift state. New sensors become rows with cursor NULL, dedup_epoch 0, consecutive_failures 0 and next_eval_at now; a re-apply of an unchanged spec is a no-op against the definition columns, so no updated_at moves.
func (*Store) TouchSession ¶
TouchSession moves the session's heartbeat forward. Nothing here decides how often that happens; the daemon owns the interval.
The heartbeat is what turns a crashed session into a bounded outage: the gap runs from the last heartbeat to the restart, and without it the whole run would be unaccounted for.
func (*Store) UnexplainedReasons ¶
func (s *Store) UnexplainedReasons(ctx context.Context) ([]UnexplainedReason, error)
UnexplainedReasons runs the audit query and returns every terminal run, step, tick and trigger stored without a usable reason code. A healthy database returns nothing, and `paceq fsck` prints exactly this list.
It reads through the read only pool like every other read: fsck has to run while another process holds the state lock, and a checker that needed the single writer would be part of whatever it is checking.
func (*Store) UpsertJobVersion ¶
func (s *Store) UpsertJobVersion(ctx context.Context, in JobVersionInput) (JobVersion, bool, error)
UpsertJobVersion records a job and the spec it currently holds, and reports whether that spec is new. Loading the same file twice is not an error and does not invent a version: the second load conflicts on (job_name, spec_hash), keeps the version that is already there and points the job at it.
The insert comes first and the conflict decides, rather than a read deciding whether to insert. Reading first would be a check with a write after it, and the answer can be stale by the time the write lands.
Pausing is left alone. It is an operator decision about a job, not a property of the file, and a reload that silently resumed a paused job would be the worst kind of surprise.
func (*Store) UpsertSchedule ¶
func (s *Store) UpsertSchedule(ctx context.Context, in ScheduleInput) (ScheduleRow, error)
UpsertSchedule inserts a schedule or replaces its definition in place, keeping id and timestamps stable across re-apply.
type SyncResult ¶
SyncResult says what SyncSensors did, so a caller can tell the story of an apply without re-reading the table. Created and Updated both mean the row's definition changed; Unchanged means it was the same on arrival; Removed names the sensors that left the job's spec and were deleted.
type TickEvidence ¶
TickEvidence is one stored tick's explanation triple: what ran, what code explains it, and what data points at its cause.
type TickInput ¶
type TickInput struct {
// Schedule names the row the evaluation belongs to.
Schedule ScheduleRow
// ScheduledFor is the fire-time in UTC. Together with source_kind
// 'schedule' and the schedule's name it IS the idempotency key.
ScheduledFor time.Time
// Outcome is OutcomeTriggered, OutcomeSkipped or OutcomeError.
Outcome string
// ReasonCode explains every outcome that produced no run.
ReasonCode reason.Code
// ReasonText and ReasonData carry the human line and its JSON detail.
ReasonText string
ReasonData string
// RunKey is required when Outcome is triggered.
RunKey string
// NextTickAt is where progress should move to when UpdateProgress holds.
NextTickAt time.Time
// UpdateProgress moves last_tick_at/next_tick_at to this fire-time. A
// config failure sets it false: the schedule must stay due so a fixed
// definition picks the work back up instead of skipping past it.
UpdateProgress bool
// Actor lands on the queued run event. Empty becomes "system".
Actor string
}
TickInput is one decided evaluation: the loop has already computed what happened at this fire-time and why; this transaction only records it.
type TickResult ¶
type TickResult struct {
// Claimed is true when this call recorded the evaluation: a new tick
// row, or an identical earlier skip absorbing it. False means the
// fire-time was already materialised by someone else; nothing was
// written and no error is reported.
Claimed bool
// Coalesced is true when an identical previous skip took this
// evaluation in as another repeat_count step instead of a new row.
Coalesced bool
// Deferred is true when the run was materialised under overlap: queue
// with every concurrency slot held: queued, but available_at points
// into the future and defer_reason says why.
Deferred bool
// Run describes the queued run when Claimed and the outcome triggered.
Run Run
}
TickResult says what one decided evaluation became.
type TickView ¶
type TickView struct {
ScheduledFor time.Time // zero when the tick carries no fire-time
Outcome string
ReasonCode string
RepeatCount int
TriggerCount int
}
TickView is one recorded schedule tick as a reader wants it: what fired, what it became, and why it did not.
type UnexplainedReason ¶
type UnexplainedReason struct {
// Kind is which table the row lives in: "run", "step", "tick" or
// "trigger".
Kind string
// Key names the row the way an operator would: the id, or run and step
// name together for a step.
Key string
}
UnexplainedReason is one row the audit query returns: a terminal object sitting without a usable reason code.
type Violation ¶
type Violation struct {
// Check names the invariant: "I2", "I10", "I13", "I14", "I15" or
// "reason" for the catalogue rule.
Check string
// Subject names the row: "run <id>" or "run <id> step <name>".
Subject string
// Detail says what was expected and what was found.
Detail string
}
Violation is one broken invariant: which check caught it and on what row.
Source Files
¶
- admission.go
- apply.go
- autovacuum.go
- bootid_linux.go
- diagnose.go
- doc.go
- flock_unix.go
- fs_linux.go
- fsck.go
- inject.go
- inspect.go
- jobs.go
- jobstatus.go
- leases.go
- materialize.go
- migrate.go
- open.go
- openreadonly.go
- openstate.go
- process_unix.go
- procticks_linux.go
- quickfsck.go
- reasons.go
- reconcile.go
- runlease.go
- runs.go
- schedules.go
- sensor_reset.go
- sensor_tick.go
- sensors.go
- session.go
- shutdown.go
- statelock.go
- transitions.go
- tx.go