botworkspace

package
v0.21.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

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

View Source
const (
	DesiredPresent = "present"
	DesiredAbsent  = "absent"
)

Desired states.

View Source
const (
	ObservedAbsent       = "absent"
	ObservedProvisioning = "provisioning"
	ObservedRunning      = "running"
	ObservedStopped      = "stopped"
	ObservedFailed       = "failed"
	ObservedRemoving     = "removing"
)

Observed states.

View Source
const (
	PhaseImagePrepare = "image_prepare"
	PhaseStart        = "start"
	PhaseBridge       = "bridge"
	PhaseBootstrap    = "bootstrap"
	PhaseTeardown     = "teardown"
)

Failure phases recorded in last_error_phase.

View Source
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).

View Source
const (
	EventReady = "ready"
	EventError = "error"
)

Terminal event types emitted by the reconciler in addition to backend progress.

Variables

View Source
var ErrNotFound = errors.New("bot workspace not found")

ErrNotFound is returned when the bot has no workspace row.

View Source
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

func DeriveBotStatus(w Workspace) (string, bool)

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 NextBackoff

func NextBackoff(now time.Time, attempts int32, base, capDuration time.Duration) time.Time

NextBackoff returns when the next attempt may run after attempt number `attempts` (1-based) failed. Exponential from base, capped.

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
)

func Decide

func Decide(w Workspace, now time.Time) Action

Decide compares intent with observation. 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.

func (Action) String

func (a Action) String() string

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 New

func New(repo Repository, backend Backend, log *slog.Logger, opts Options) *Service

New builds a Service. The loop starts with Start.

func (*Service) Await

func (s *Service) Await(ctx context.Context, botID string, generation int64) (Workspace, error)

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

func (s *Service) EnsurePresent(ctx context.Context, botID, image string) (Workspace, error)

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) Get

func (s *Service) Get(ctx context.Context, botID string) (Workspace, error)

Get returns the row.

func (*Service) Kick

func (s *Service) Kick()

Kick wakes the loop for an immediate pass.

func (*Service) Observe

func (s *Service) Observe(ctx context.Context, botID string) (Workspace, error)

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) Owner

func (s *Service) Owner() string

Owner is this instance's lease owner id.

func (*Service) ReconcileOnce

func (s *Service) ReconcileOnce(ctx context.Context) (int, error)

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) Start

func (s *Service) Start(ctx context.Context) error

Start launches the loop; it returns immediately.

func (*Service) Stop

func (s *Service) Stop(ctx context.Context) error

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

type StepError struct {
	Phase     string
	Retryable bool
	Err       error
}

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.

func (*StepError) Error

func (e *StepError) Error() string

func (*StepError) Unwrap

func (e *StepError) Unwrap() error

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

func (w Workspace) Final(maxAttempts int32) bool

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

func (w Workspace) RetryPending(maxAttempts int32) bool

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.

func (Workspace) Settled

func (w Workspace) Settled() bool

Settled reports whether the observation is a stable answer to the current intent: no transitional state, and the generation caught up.

func (Workspace) String

func (w Workspace) String() string

String renders a workspace for logs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL