Documentation
¶
Overview ¶
Package botworkspace owns the declarative lifecycle of a bot's workspace.
The API layer records intent (present / absent) and the reconciler in this package keeps the observed state converging toward it: provisioning, retrying with backoff, tearing down, and deriving bots.status. Every backend operation is idempotent and every write of an observation happens on a fresh context, so a lost request context or a crashed Server never strands a bot in a transitional status.
Index ¶
- Constants
- Variables
- func DeriveBotStatus(w Workspace) (string, bool)
- func FailureCode(step *StepError) apperror.Code
- func NextBackoff(now time.Time, attempts int32, base, capDuration time.Duration) time.Time
- type Action
- type Backend
- type BotStatusWriter
- type Inspection
- type ObservedWrite
- type Options
- type ProgressEvent
- type Repository
- type Service
- func (s *Service) Await(ctx context.Context, botID string, generation int64) (Workspace, error)
- func (s *Service) CancelRemoval(ctx context.Context, botID string) error
- func (s *Service) EnsurePresent(ctx context.Context, botID, image string) (Workspace, error)
- func (s *Service) Get(ctx context.Context, botID string) (Workspace, error)
- func (s *Service) Kick()
- func (s *Service) Observe(ctx context.Context, botID string) (Workspace, error)
- func (s *Service) Owner() string
- func (s *Service) ReconcileOnce(ctx context.Context) (int, error)
- func (s *Service) RequestAbsent(ctx context.Context, botID string, preserve bool) (Workspace, error)
- func (s *Service) SetBotStatusWriter(w BotStatusWriter)
- func (s *Service) Start(ctx context.Context) error
- func (s *Service) Stop(ctx context.Context) error
- func (s *Service) Subscribe(botID string) (<-chan ProgressEvent, func())
- type StepError
- type Workspace
Constants ¶
const ( DesiredPresent = "present" DesiredAbsent = "absent" )
Desired states.
const ( ObservedAbsent = "absent" ObservedProvisioning = "provisioning" ObservedRunning = "running" ObservedStopped = "stopped" ObservedFailed = "failed" ObservedRemoving = "removing" )
Observed states.
const ( PhaseImagePrepare = "image_prepare" PhaseStart = "start" PhaseBridge = "bridge" PhaseBootstrap = "bootstrap" PhaseTeardown = "teardown" )
Failure phases recorded in last_error_phase.
const ( BotStatusCreating = "creating" BotStatusReady = "ready" BotStatusFailed = "failed" )
Bot statuses derived from the workspace state. They mirror the values in internal/bots without importing it (bots depends on this package through an interface, so the dependency must not point back).
const ( RemovalRemoving = "removing" RemovalFailed = "failed" )
const ( EventReady = "ready" EventError = "error" )
Terminal event types emitted by the reconciler in addition to backend progress.
Variables ¶
var ( // ErrImageNotFound marks an image the registry or runtime does not have, // or whose name cannot be resolved. Retrying will not change the answer. ErrImageNotFound = errors.New("workspace image not found") // the registry. ErrImageRegistryUnavailable = errors.New("workspace image registry unavailable") )
var ErrNotFound = errors.New("bot workspace not found")
ErrNotFound is returned when the bot has no workspace row.
var ErrVersionConflict = errors.New("bot workspace changed concurrently")
ErrVersionConflict is returned by the repository when the row changed under the caller. The caller drops the pass; the next one re-reads and re-decides.
Functions ¶
func DeriveBotStatus ¶
DeriveBotStatus maps the workspace state onto bots.status. ok is false when the bot's status must not be touched (the workspace is absent on purpose or the bot is being deleted; those transitions belong to the bot lifecycle).
func FailureCode ¶ added in v0.22.0
FailureCode is the catalog code a failed provisioning step is recorded with in last_error_code. The step's error stays with the unit's result record; the row keeps only what the user can act on.
Types ¶
type Action ¶
type Action int
Action is what the reconciler does with a claimed row.
func Decide ¶
Decide compares intent with observation for a claimed row. It looks only at the present state, never at history, so an interrupted pass (lease expired mid-provisioning) simply continues from what the backend reports. Whether the row is due, a failed attempt's backoff included, the claim has already decided on the database's clock; checking it again here on the process clock would only disagree when the two clocks drift apart.
type Backend ¶
type Backend interface {
// Provision brings the bot's workspace to running: image, container,
// start, bridge readiness, template bootstrap. An empty image means "the
// backend's default for this bot". Failures are *StepError.
Provision(ctx context.Context, botID, image string, progress func(ProgressEvent)) error
// Teardown removes the workspace; a missing workspace is success. With
// preserve the data is exported before deletion and a failed export aborts
// the teardown.
Teardown(ctx context.Context, botID string, preserve bool) error
// Inspect reports the backend's view of the workspace.
Inspect(ctx context.Context, botID string) (Inspection, error)
// HasPreservedData reports whether an exported data archive exists.
HasPreservedData(botID string) bool
}
Backend performs the workspace operations for one runtime (containerd, docker, apple, Cloud builtin). Every method is idempotent.
type BotStatusWriter ¶
type BotStatusWriter interface {
SetBotStatusFromWorkspace(ctx context.Context, botID, status string) error
}
BotStatusWriter derives bots.status from the workspace state. Implemented by the bots service; injected as an interface so this package does not import it.
type Inspection ¶
type Inspection struct {
Exists bool
Running bool
// Image is the container's image reference when it exists.
Image string
}
Inspection is the backend's answer to "does this bot's workspace exist and is it running".
type ObservedWrite ¶
type ObservedWrite struct {
BotID string
Owner string
ExpectedVersion int64
Observed string
ObservedGeneration int64
MarkReady bool
LastError string
LastErrorCode string
LastErrorPhase string
Attempts int32
NextAttemptAt time.Time
ReleaseLease bool
}
ObservedWrite is one observation written by a lease holder.
type Options ¶
type Options struct {
Owner string
Interval time.Duration
Lease time.Duration
DriftInterval time.Duration
BackoffBase time.Duration
BackoffCap time.Duration
MaxAttempts int32
SlowRetryInterval time.Duration
Concurrency int
// TeardownConcurrency gives removals (rows whose desired state is absent)
// slots of their own beside Concurrency's, so a removal never waits
// behind provisionings. Zero, the default, keeps one pool of Concurrency
// slots for both: a removal that exports data competes with image pulls
// for the same host.
TeardownConcurrency int
ProvisionTimeout time.Duration
TeardownTimeout time.Duration
WriteTimeout time.Duration
}
Options tune the reconciler. Zero values take the defaults.
type ProgressEvent ¶
type ProgressEvent struct {
Type string
Image string
Message string
Layers []ctr.LayerStatus
ContainerID string
WorkspaceBackend string
RuntimeBackend string
ContainerPath string
CDIDevices []string
Snapshotter string
Started bool
DataRestored bool
HasPreservedData bool
// Terminal fields, set by the reconciler on "ready" / "error".
Phase string
Err error
Workspace *Workspace
}
ProgressEvent mirrors the workspace setup progress the SSE creation stream already exposes; the reconciler relays backend progress to in-process subscribers so the UI keeps its live pull/create feedback.
type Repository ¶
type Repository interface {
Upsert(ctx context.Context, botID, desired, image string, preserveData bool) (Workspace, error)
// CancelRemoval returns ErrNotFound when the bot has no failed removal.
CancelRemoval(ctx context.Context, botID, observed string) (Workspace, error)
Get(ctx context.Context, botID string) (Workspace, error)
// Claim leases up to limit due rows. A non-empty desired state claims only
// the rows that want it.
Claim(ctx context.Context, owner string, lease time.Duration, desired string, limit int32) ([]Workspace, error)
ClaimOne(ctx context.Context, botID, owner string, lease time.Duration) (Workspace, error)
Renew(ctx context.Context, botID, owner string, lease time.Duration) error
WriteObserved(ctx context.Context, w ObservedWrite) (Workspace, error)
Release(ctx context.Context, botID, owner string) error
ListByObserved(ctx context.Context, observed string, limit int32) ([]Workspace, error)
}
Repository is the persistence port. The postgres implementation is thin; the interface exists so the reconciler can be tested against an in-memory fake.
func NewRepository ¶
func NewRepository(queries dbstore.Queries) Repository
NewRepository returns the postgres-backed Repository.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the reconciler plus the intent API.
func (*Service) Await ¶
Await blocks until the workspace has a final answer for an intent at least as new as generation, polling the repository. A failure still inside its fast retry budget is not final: Await keeps waiting so a transient error that recovers on the next attempt never reaches the caller as a failure. It works across Server instances.
func (*Service) CancelRemoval ¶ added in v0.22.0
CancelRemoval withdraws a removal that failed; one still in flight is left alone, the backend may already be deleting. The workspace is recorded as it stands, not provisioned: that would restore a preserved archive over newer data and start a workspace the user had stopped.
func (*Service) EnsurePresent ¶
EnsurePresent records that the bot should have a running workspace built from image (empty keeps the previous / default image) and wakes the loop.
func (*Service) Observe ¶
Observe refreshes the observation for one bot from the backend (after a user-driven start or stop). It never provisions or tears down.
func (*Service) ReconcileOnce ¶
ReconcileOnce runs a single pass synchronously (tests, admin tooling).
func (*Service) RequestAbsent ¶
func (s *Service) RequestAbsent(ctx context.Context, botID string, preserve bool) (Workspace, error)
RequestAbsent records that the bot's workspace should be removed. preserve exports the data archive before deletion.
func (*Service) SetBotStatusWriter ¶
func (s *Service) SetBotStatusWriter(w BotStatusWriter)
SetBotStatusWriter wires bots.status derivation (setter injection avoids an import cycle with the bots service).
func (*Service) Stop ¶
Stop asks the loop to exit and waits for in-flight passes to release their leases (bounded by ctx).
func (*Service) Subscribe ¶
func (s *Service) Subscribe(botID string) (<-chan ProgressEvent, func())
Subscribe streams progress and terminal events for a bot to an in-process listener. Events are dropped when the listener falls behind; Await is the reliable way to learn the outcome.
type StepError ¶
StepError attributes a backend failure to a phase and says whether the reconciler should retry it. Non-retryable failures (an image that does not exist, a template bootstrap that cannot succeed without user action) go straight to the failed state without consuming the retry budget.
type Workspace ¶
type Workspace struct {
BotID string
TeamID string
Desired string
DesiredGeneration int64
Image string
PreserveData bool
Observed string
ObservedGeneration int64
EverReady bool
// LastError is the text an earlier server stored; new failures set
// LastErrorCode and leave it empty.
LastError string
LastErrorCode string
LastErrorPhase string
Attempts int32
NextAttemptAt time.Time
LeaseOwner string
LeaseUntil time.Time
Version int64
UpdatedAt time.Time
}
Workspace is one row of bot_workspaces.
func (Workspace) Final ¶
Final reports whether the observation is the last word on the current intent: settled, and not a failure the reconciler is about to retry soon. Callers that relay an outcome to a user wait for Final so a transient failure that recovers on the next attempt never surfaces as a failure.
func (Workspace) RemovalState ¶ added in v0.22.0
RemovalState is empty when no removal is pending.
func (Workspace) RetryPending ¶
RetryPending reports whether a failed observation is still inside its fast retry budget of maxAttempts. Attempts counts the budget consumed: a non-retryable failure consumes all of it at once. Beyond the budget the reconciler keeps retrying a provisioning at a slow cadence, but that is background self-healing and no longer holds up callers.