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 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) 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 ( EventReady = "ready" EventError = "error" )
Terminal event types emitted by the reconciler in addition to backend progress.
Variables ¶
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).
Types ¶
type Action ¶
type Action int
Action is what the reconciler does with a claimed row.
const ( // ActionNone means observation already matches intent; the reconciler only // records that the generation is caught up. ActionNone Action = iota // ActionProvision brings the workspace to running. ActionProvision // ActionTeardown removes the workspace. ActionTeardown // ActionWait means the row is not due (backoff still running). ActionWait )
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
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
Batch int32
Concurrency 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)
Get(ctx context.Context, botID string) (Workspace, error)
Claim(ctx context.Context, owner string, lease time.Duration, 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) 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 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) 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 at a slow cadence, but that is background self-healing and no longer holds up callers.