broker

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package broker is simbroker's allocation logic: it hands out simulators from a capped pool and reclaims them when their holders are gone. It deliberately does NOT build or install apps — provisioning stays with the consumer.

The heart of the design is the reclaim rule (see reclaimable). An earlier draft used "(pid dead) OR (TTL expired)", which would reclaim a busy agent's device the instant its TTL lapsed mid-build and hand it to a second claimant — two agents driving one simulator. The corrected rule makes a provably-live holder immune to TTL: TTL only governs claims whose liveness can't be proven.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrCapacity          = errors.New("capacity reached: all slots for this class are in use")
	ErrNoFreeDevice      = errors.New("no free device available in the pool")
	ErrDeviceUnavailable = errors.New("requested device is unavailable or already claimed")
	ErrClaimNotFound     = errors.New("claim not found")
	ErrUnknownClass      = errors.New("unknown device class")
)

Sentinel errors callers can match with errors.Is.

Functions

This section is empty.

Types

type Broker

type Broker struct {
	// contains filtered or unexported fields
}

Broker allocates devices from per-class capped pools against a Store and one or more device.Controllers (each serving a class; injectable for tests).

func New

func New(st *store.Store, cfg Config, ctrls ...device.Controller) *Broker

New builds a Broker with production clock/liveness over one or more controllers (one per class).

func (*Broker) Capacity

func (b *Broker) Capacity() int

Capacity is the iOS ceiling (kept for back-compat with single-class callers).

func (*Broker) CapacityFor

func (b *Broker) CapacityFor(class device.Class) int

CapacityFor is the configured concurrent-claim ceiling for a class.

func (*Broker) Claim

func (b *Broker) Claim(ctx context.Context, o ClaimOptions) (store.Claim, error)

Claim leases a device. Discovery happens outside the lock; the per-class capacity check, gc, selection, and write all happen atomically under it.

func (*Broker) Config

func (b *Broker) Config() Config

func (*Broker) Discover

func (b *Broker) Discover(ctx context.Context) ([]device.Device, error)

Discover returns the raw available devices across all classes (for `simbroker devices`).

func (*Broker) GC

func (b *Broker) GC() (int, error)

GC reclaims dead/expired claims and returns how many were removed, tearing down their devices (best-effort) outside the lock.

func (*Broker) InPool

func (b *Broker) InPool(d device.Device) bool

InPool reports whether a device is part of its class's configured pool.

func (*Broker) InUseByClass

func (b *Broker) InUseByClass(claims []store.Claim) map[device.Class]int

InUseByClass tallies live (non-reclaimable) claims per class from a claim list, so callers can render a per-class "in use" count consistent with the capacity check.

func (*Broker) List

func (b *Broker) List(ctx context.Context) ([]PoolEntry, []store.Claim, error)

List returns the pool (across all classes) annotated with holders, plus the raw claim list.

func (*Broker) LiveHolders

func (b *Broker) LiveHolders(class device.Class) map[string]store.Claim

LiveHolders returns the live (non-reclaimable) claim holding each device id of a class, keyed by id. Used by `doctor` to find orphaned-looking slots without re-running device discovery.

func (*Broker) Release

func (b *Broker) Release(id string) (bool, error)

Release drops a claim by id. It is idempotent: releasing an unknown id returns (false, nil).

func (*Broker) Renew

func (b *Broker) Renew(id string, ttl time.Duration) (store.Claim, error)

Renew pushes a claim's expiry forward. A claim already reclaimed by gc (e.g. its holder died) cannot be renewed and returns ErrClaimNotFound.

func (*Broker) Store

func (b *Broker) Store() *store.Store

func (*Broker) Usage

func (b *Broker) Usage(entries []PoolEntry, claims []store.Claim) []ClassUsage

Usage returns per-class {capacity, in-use} across the classes worth showing, using the SAME class-normalization and count predicate as the capacity check — so what `list` reports always matches what `claim` enforces.

type ClaimOptions

type ClaimOptions struct {
	Class  string        // device class: "ios" (default) or "android"
	Label  string        // free-form holder label (e.g. worktree path)
	PID    int           // holder pid; pass a LONG-LIVED process's pid, never the ephemeral CLI's
	TTL    time.Duration // 0 -> config default
	Device string        // force a specific UDID (manual override; bypasses the model pool)
	Boot   bool          // best-effort boot after claiming
}

ClaimOptions are the inputs to a claim.

type ClassUsage

type ClassUsage struct {
	Class    string `json:"class"`
	Capacity int    `json:"capacity"`
	InUse    int    `json:"in_use"`
}

ClassUsage is a per-class capacity/usage summary, the unit both the CLI and the MCP server render so their per-class accounting can never diverge.

type Config

type Config struct {
	// Capacity is the iOS capacity when the config uses the legacy scalar form
	// (or when set directly, e.g. in tests). It is a fallback for the iOS entry
	// of the normalized per-class map below.
	Capacity int
	// Models is the iOS model pool when the config uses the legacy array form
	// (or set directly). Fallback for the iOS entry of the per-class map below.
	Models []string

	// GraceSeconds is added to a claim's TTL before a pid-less claim is eligible
	// for reclaim, absorbing small clock differences.
	GraceSeconds int `json:"grace_seconds,omitempty"`
	// DefaultTTLSeconds is the lease length when a claim doesn't specify one.
	// It is a safety net for ungraceful death, NOT a work-duration estimate:
	// a claim whose holder pid is alive is never reclaimed by TTL.
	DefaultTTLSeconds int `json:"default_ttl_seconds,omitempty"`
	// KeepAndroidEmulatorOnRelease disables the best-effort `adb emu kill` that
	// otherwise runs when an Android claim is released or reclaimed. Default
	// false (kill, to reclaim the emulator's RAM). Set true if you boot an
	// emulator once and reuse it across claims and don't want it torn down.
	KeepAndroidEmulatorOnRelease bool `json:"android_keep_emulator_on_release,omitempty"`
	// contains filtered or unexported fields
}

Config is the broker's policy, loaded from ~/.simbroker/config.json. Capacity and Models are PER CLASS but accept a back-compatible scalar/array shorthand that means "iOS" (see UnmarshalJSON):

{"capacity": 3, "models": ["iPhone 17 Pro"]}                  // legacy: iOS
{"capacity": {"ios": 3, "android": 1},
 "models":   {"ios": ["iPhone 17 Pro"], "android": ["Pixel_8"]}}  // per-class

All fields are optional; missing/zero values fall back to the defaults.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig is the policy used when no config file is present.

func LoadConfig

func LoadConfig(path string) (Config, error)

LoadConfig reads path, layering any present fields over DefaultConfig. A missing file is not an error.

func (Config) DefaultTTL

func (c Config) DefaultTTL() time.Duration

DefaultTTL is the exported lease length, used by the MCP server.

func (*Config) UnmarshalJSON

func (c *Config) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes config.json, accepting either the legacy scalar/array shapes or the per-class object shapes for `capacity` and `models`. A bare int stays the iOS capacity; an object becomes the per-class map. This is the one piece of config parsing that must tolerate both shapes — a plain `int` field would hard-error on the object form and abort every command at startup.

type PoolEntry

type PoolEntry struct {
	UDID        string
	Class       string
	Name        string
	OS          string
	Version     string
	State       string
	Claimed     bool
	ClaimID     string
	HolderLabel string
	HolderPID   int
	HolderAlive bool
	ExpiresAt   time.Time
}

PoolEntry is one device in the pool annotated with its current holder (if any live claim holds it).

Jump to

Keyboard shortcuts

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