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 ¶
- Variables
- type Broker
- func (b *Broker) Capacity() int
- func (b *Broker) CapacityFor(class device.Class) int
- func (b *Broker) Claim(ctx context.Context, o ClaimOptions) (store.Claim, error)
- func (b *Broker) Config() Config
- func (b *Broker) Discover(ctx context.Context) ([]device.Device, error)
- func (b *Broker) GC() (int, error)
- func (b *Broker) InPool(d device.Device) bool
- func (b *Broker) InUseByClass(claims []store.Claim) map[device.Class]int
- func (b *Broker) List(ctx context.Context) ([]PoolEntry, []store.Claim, error)
- func (b *Broker) LiveHolders(class device.Class) map[string]store.Claim
- func (b *Broker) Release(id string) (bool, error)
- func (b *Broker) Renew(id string, ttl time.Duration) (store.Claim, error)
- func (b *Broker) Store() *store.Store
- func (b *Broker) Usage(entries []PoolEntry, claims []store.Claim) []ClassUsage
- type ClaimOptions
- type ClassUsage
- type Config
- type PoolEntry
Constants ¶
This section is empty.
Variables ¶
var ( ErrCapacity = errors.New("capacity reached: all slots for this class are in use") ErrNoFreeDevice = errors.New("no free device available in the pool") 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 ¶
New builds a Broker with production clock/liveness over one or more controllers (one per class).
func (*Broker) Capacity ¶
Capacity is the iOS ceiling (kept for back-compat with single-class callers).
func (*Broker) CapacityFor ¶
CapacityFor is the configured concurrent-claim ceiling for a class.
func (*Broker) Claim ¶
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) Discover ¶
Discover returns the raw available devices across all classes (for `simbroker devices`).
func (*Broker) GC ¶
GC reclaims dead/expired claims and returns how many were removed, tearing down their devices (best-effort) outside the lock.
func (*Broker) InUseByClass ¶
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 ¶
List returns the pool (across all classes) annotated with holders, plus the raw claim list.
func (*Broker) LiveHolders ¶
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 ¶
Release drops a claim by id. It is idempotent: releasing an unknown id returns (false, nil).
func (*Broker) Renew ¶
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) 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 ¶
LoadConfig reads path, layering any present fields over DefaultConfig. A missing file is not an error.
func (Config) DefaultTTL ¶
DefaultTTL is the exported lease length, used by the MCP server.
func (*Config) UnmarshalJSON ¶
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).