Documentation
¶
Overview ¶
Package playground runs a BPMN model on the real Atlas engine inside a throwaway sandbox, on a clock the caller owns.
It is the machinery behind the Modeler's Playground tab: an author points it at a draft (or a deployed version), feeds it cases, and watches what the process actually does — without deploying, without durable state, and without any possibility of an external side effect.
What a sandbox is ¶
A Sandbox is a complete engine in a box: its own partition, its own write-ahead log and state store in a temporary directory, and its own engine.Processor. It compiles the model with the real compiler and executes it on the real processor, so control flow, FEEL, DMN, gateways, boundary events and multi-instance behave exactly as they do in production — there is no second implementation to drift (the reason ADR-0030 rejected a browser-side simulator).
Three things make it a playground rather than a second engine:
- A virtual clock. Simulated time is moved by the scheduler, never read from the wall: it jumps to the next stub completion or the next due timer. A model that waits three days finishes in microseconds.
- A stub policy (StubSet) supplied as *run configuration* rather than model content, so the model under test is byte-for-byte the model that deploys.
- Isolation by absence: a sandbox registers no workers, no vault, no mail transport and no HTTP client. A REST task cannot reach the network because nothing in a sandbox could dial it. That is a structural property, not a setting — see TestConnectorTaskIsAnsweredByTheStubAndNeverCalled.
What it deliberately is not ¶
Timings are *modelled*, not measured: a report is exactly as good as the stub durations someone configured. The sandbox answers "where does it pile up if the work takes this long", never "how fast is our endpoint".
Threading ¶
A Sandbox is owned by exactly one goroutine, like every other partition in Atlas (invariant I3). It carries no lock of its own; callers that serve concurrent requests put it behind a runloop.Loop, which is what Session does.
Index ¶
- Constants
- func ErrClosedSession(err error) bool
- func NewID() (string, error)
- type Arrival
- type ArrivalMode
- type ArrivalProfile
- type Bucket
- type Budget
- type Calendar
- type CaseResult
- type CaseRow
- type Check
- type Choice
- type Comparison
- type Dataset
- type Delta
- type Durations
- type ElementStat
- type ElementUse
- type Expectations
- type Field
- type FieldKind
- type FlowUse
- type HeatMap
- type Occurrence
- type OccurrenceKind
- type Options
- type Plan
- type Pool
- type PoolStat
- type Progress
- type Registry
- type Report
- type Rule
- type RuleOutcome
- type RunState
- type RunStatus
- type Sandbox
- func (s *Sandbox) Advance(d time.Duration) error
- func (s *Sandbox) ArrivalProfileOf(n int, a Arrival) (ArrivalProfile, error)
- func (s *Sandbox) Arrivals() []time.Time
- func (s *Sandbox) Case(piKey uint64) (CaseResult, error)
- func (s *Sandbox) Cases(offset, limit int) ([]CaseRow, int, error)
- func (s *Sandbox) Close() error
- func (s *Sandbox) CompleteTask(jobKey uint64, vars ...model.VariableValue) error
- func (s *Sandbox) Counts() (cases, completed int, err error)
- func (s *Sandbox) Dir() string
- func (s *Sandbox) ElementStats() map[string]ElementStat
- func (s *Sandbox) ElementVisits() (map[string]int64, error)
- func (s *Sandbox) HeatMap() (HeatMap, error)
- func (s *Sandbox) InjectUnreadableCase(piKey uint64) error
- func (s *Sandbox) JudgeRules(rules []Rule) ([]RuleOutcome, error)
- func (s *Sandbox) Now() time.Time
- func (s *Sandbox) OpenTasks() ([]Task, error)
- func (s *Sandbox) PoolStats() map[string]PoolStat
- func (s *Sandbox) ProcessID() string
- func (s *Sandbox) PublishMessage(name, correlationKey string, vars ...model.VariableValue) error
- func (s *Sandbox) Report() (Report, error)
- func (s *Sandbox) Run(b Budget) (Progress, error)
- func (s *Sandbox) Seed() int64
- func (s *Sandbox) StartCase(vars ...model.VariableValue) (uint64, error)
- func (s *Sandbox) StartPlan(p Plan) error
- func (s *Sandbox) StartedAt() time.Time
- func (s *Sandbox) Step() (Occurrence, bool, error)
- type Session
- func (s *Session) Budget(b Budget) Budget
- func (s *Session) Cancel()
- func (s *Session) Close() error
- func (s *Session) CreatedAt() time.Time
- func (s *Session) ID() string
- func (s *Session) LastUsed() time.Time
- func (s *Session) OwnedBy(principal string) bool
- func (s *Session) Pause()
- func (s *Session) Paused() bool
- func (s *Session) Resume()
- func (s *Session) RunStatus() RunStatus
- func (s *Session) StartRun(p Plan) error
- func (s *Session) With(fn func(*Sandbox) error) error
- type Stub
- type StubSet
- type Task
- type Timeline
- type Unit
- type Verdict
- type Window
Constants ¶
const PartitionBase uint16 = 0xF000
PartitionBase is the first partition id reserved for sandboxes. Every key a sandbox mints carries a partition at or above it, and the durable engine runs well below it (partition 1), so a sandbox key is recognisable on sight and can never be mistaken for — or collide with — a real one.
Variables ¶
This section is empty.
Functions ¶
func ErrClosedSession ¶
ErrClosedSession reports whether err says the session no longer exists, so an HTTP layer can answer 404 rather than 500.
Types ¶
type Arrival ¶
type Arrival struct {
Mode ArrivalMode
// Interval is the gap for ArrivalEvery.
Interval time.Duration
// PerHour is the mean rate for ArrivalPoisson.
PerHour float64
// Calendar confines arrivals to business hours: the stream pauses when it
// closes and resumes when it opens, so a day's worth of cases lands inside the
// day. The zero value is around the clock.
Calendar
}
Arrival says when a plan's cases start.
type ArrivalMode ¶
type ArrivalMode int
ArrivalMode is how a plan's cases are spread over simulated time.
const ( // ArrivalAllAtOnce starts every case at the run's first instant. It is the load // test for the model rather than for the server: fifty thousand tokens in the // graph, not fifty thousand requests per second. ArrivalAllAtOnce ArrivalMode = iota // ArrivalSequential starts the next case when the one before it has finished — // one case in flight at a time. ArrivalSequential // ArrivalEvery starts one case per Interval. ArrivalEvery // ArrivalPoisson draws the gaps between arrivals from an exponential // distribution around PerHour, which is what a stream of unrelated arrivals // actually looks like: bursty, not evenly spaced. ArrivalPoisson )
type ArrivalProfile ¶
type ArrivalProfile struct {
// Scheduled is false for a sequential plan, which has no schedule of its own:
// its next arrival depends on the run, so there is no shape to draw and saying
// so is better than drawing a flat line that means something else.
Scheduled bool
// Cases is how many the profile is of, and Start and End bound them.
Cases int
Start, End time.Time
// Buckets holds the arrivals per slice, oldest first. Empty when not Scheduled.
Buckets []int
}
ArrivalProfile is the shape of a planned arrival stream: how many cases land in each slice of the time it spans.
It exists so a panel can show the timing profile *before* the run rather than after it — and show it from the code that plans the run, not from a second implementation of the same arithmetic in a browser. A profile drawn by anything but the planner is a picture of a stream nobody is going to get.
func (ArrivalProfile) Peak ¶
func (p ArrivalProfile) Peak() int
Peak is the fullest slice — what a drawn profile is scaled against.
func (ArrivalProfile) Span ¶
func (p ArrivalProfile) Span() time.Duration
Span is how long the stream takes end to end.
type Bucket ¶
type Bucket struct {
// At is when the slice begins; it is Width long.
At time.Time
// Started and Completed are the cases that arrived and finished inside it.
Started, Completed int
// InFlight is how many cases were still running at its end — arrivals minus
// completions up to here, which is the work in progress a reader compares
// against the arrival rate.
InFlight int
}
Bucket is one slice of simulated time.
type Budget ¶
type Budget struct {
// MaxOccurrences caps how many scheduled things a single Run may carry out.
MaxOccurrences int
// Horizon caps how far simulated time may travel from where the Run started.
// A run stops *before* stepping past it, so the horizon is never overshot.
Horizon time.Duration
// Stop, when set, is asked before every occurrence whether to give up. It is
// how a pause reaches a run that is already in flight: the run holds the
// session's goroutine, so the answer cannot come through the loop. Like the
// other bounds, stopping this way is not quiescence.
Stop func() bool
}
Budget bounds a Run so an abandoned or pathological model cannot spin forever. A zero field means "no limit on this axis"; DefaultBudget is what a server should pass.
func DefaultBudget ¶
func DefaultBudget() Budget
DefaultBudget is a bound generous enough for any single case a person is stepping through and small enough that a runaway model gives up quickly.
type Calendar ¶
type Calendar struct {
// Open are the windows in a day. Empty means the whole day.
Open []Window
// Days selects the weekdays, indexed by time.Weekday. The zero value — no day
// selected — means every day.
Days [7]bool
}
Calendar says when something works: which weekdays, and which stretches of those days. It is shared by the pools that do the work and by the arrival plan that feeds them, because "business hours" means the same thing to both.
The zero value is always open, so a caller who does not care about hours does not have to say so.
type CaseResult ¶
type CaseResult struct {
InstanceKey uint64
State model.ProcessInstanceState
// Path is the BPMN element ids a token activated, in order.
Path []string
// Variables are the case's root-scope variables as text.
Variables map[string]string
// Incidents is how many of this case's tokens are parked behind an incident.
Incidents int
}
CaseResult is what became of one case.
type CaseRow ¶
type CaseRow struct {
// Index is the row's place in the dataset that produced it.
Index int
InstanceKey uint64
State model.ProcessInstanceState
Started time.Time
Ended time.Time
Duration time.Duration
Incidents int
// End is the BPMN id of the last element the case reached — the outcome a
// reader groups the table by.
End string
Variables map[string]string
}
CaseRow is one case in the results table.
type Check ¶
type Check struct {
Name string
Want string
Got string
Passed bool
// Rule marks a check that came from a per-case rule rather than from a bound on
// the run. Both decide the verdict alike; a reader is shown them differently,
// because a rule's own breakdown says more than a check line has room for.
Rule bool
}
Check is one expectation and what the run did about it. Want and Got are sentences rather than numbers: a check is a statement about the run, and the thing that computes on it is the verdict's Passed.
type Choice ¶
type Choice struct {
// Value is what the case carries. It is used as given, so a choice of numbers
// arrives as a number and a choice of labels as a string.
Value any
// Weight is how often this option comes up relative to the others. Zero on
// every option means they are equally likely; zero on one of several weighted
// options means that one never comes up, which is how an option is kept in the
// list while being switched off.
Weight int
}
Choice is one option of a FieldChoice field.
type Comparison ¶
type Comparison struct{ Deltas []Delta }
Comparison is one run set beside another — the answer to "did that change help?", which a single report cannot give however complete it is.
func Compare ¶
func Compare(before, after Report) Comparison
Compare measures one report against another. Like Expectations.Judge it is a pure function of the two reports, so a stored run can be compared with a fresh one without either sandbox still existing.
Anything present in only one of the runs is still listed, with zero on the side that lacks it: a branch the new data reached for the first time, or a pool that was removed, is exactly the change worth seeing.
type Dataset ¶
type Dataset struct {
// Count is how many cases to produce.
Count int
// Fields are the start variables each case carries. None is allowed: that is a
// run of N cases with no input, which is a load test of the model's own shape.
Fields []Field
}
Dataset is a dataset described rather than listed: how many cases, and how each field of one is produced.
It is what a list of cases cannot be at this scale. Fifty thousand cases typed into a panel are not a dataset anybody wrote, and fifty thousand rows uploaded as a file cannot be stored in a scenario — but the twenty lines that describe them can, which is what makes a generated run repeatable and reviewable.
func (Dataset) Generate ¶
Generate produces the whole dataset: one row per case, in the shape a case list sent inline or parsed out of a CSV already has, so a generated case is seeded exactly as a typed-in one is.
It is a pure function of the description, the seed and the run's start. Nothing here reads a global random source, so the same scenario run twice produces the same three hundred amounts — which is what makes a generated run something a review can quote and a build can compare against a baseline.
type Delta ¶
type Delta struct {
Name string
Unit Unit
Before, After int64
// HigherIsBetter is the polarity of this measure, not a judgement about this
// particular pair of runs — Better and Worse apply it to the numbers.
HigherIsBetter bool
// Neutral marks a measure that moved without that being good or bad, so it is
// shown and not coloured. Not every number has a direction, and inventing one
// for the sake of a green arrow is how a report starts misleading people.
Neutral bool
}
Delta is one measure of a run set beside the same measure of another.
It carries the raw numbers rather than rendered text, so the caller formats them the way the rest of its screen does — and it carries which direction is good, which is the half that makes a comparison readable. A reader should not have to know that more completions is progress and a longer p90 is not; that is what the report is for.
type Durations ¶
Durations summarises how long the cases took. The percentiles come from the values themselves rather than from a fitted curve, because a run's own numbers are what a reader will check them against.
type ElementStat ¶
type ElementStat struct {
Runs int
Work time.Duration
Wait time.Duration
MaxWait time.Duration
// Incidents is how many tokens are parked on this element behind a simulated
// failure. It is counted per element rather than only per run because "five
// incidents" says a run went wrong and "five incidents, all at the payout" says
// where — which is the question the run-wide total cannot answer.
Incidents int
}
ElementStat is what a run measured at one element: how often the sandbox answered a job there, how long that work took, and how long it waited for a seat first. The split is the point — elapsed time that is all queue is a capacity problem, and elapsed time that is all work is a different one.
type ElementUse ¶
ElementUse is how often the run passed through one element of the diagram.
type Expectations ¶
type Expectations struct {
// MinCompleted is how many cases must reach an end event. It counts completions,
// not starts: a run whose cases are all still parked has not completed them.
MinCompleted int
// MaxIncidents is how many tokens may be parked behind a simulated failure.
// Nil is not checked; a pointer to zero is "none at all".
MaxIncidents *int
// MaxP50, MaxP90 and MaxDuration bound how long a case may take: the median for
// the everyday case, p90 for the tail somebody actually waits through, and the
// maximum for the promise that nothing takes longer than a day. Zero is not
// checked — a bound of no time is not a thing anybody means.
MaxP50, MaxP90, MaxDuration time.Duration
// MinVisits and MaxVisits bound how many tokens passed through an element.
// Coverage and outcome are the same statement about the same counter: "the
// approval branch must be exercised" is a minimum of one, "the error branch must
// stay rare" is a maximum.
MinVisits, MaxVisits map[string]int64
// MaxQueue bounds the longest queue a pool ever had — the capacity question
// stated as a target rather than read off a table.
MaxQueue map[string]int
// Rules are the expectations stated per case rather than per run. They are the
// statement the fields above cannot make: "the median is under four hours" is
// true of a run, "an application under 50 000 from a grade-A customer is
// approved" is true of a case, and a run that holds it nine times in ten is not
// nine tenths right.
//
// They are judged from the cases, not from the report, so [Judge] takes their
// outcomes rather than computing them: see [Sandbox.JudgeRules].
Rules []Rule
}
Expectations are what a run has to show for a scenario to count as passed.
They are the difference between a Playground somebody looks at and one that can be run by something that is not a person: a number on a screen needs a reader to judge it, a verdict does not. The same expectations that make the panel say "passed" make a CI job exit non-zero.
Every field is optional and an omitted one is not checked, so a scenario asserts only what it means to assert. The two places where zero is itself a meaningful target — "no incidents at all", "this queue must never form" — are a pointer and a map entry rather than a plain number, because a zero value that silently asserts something is how an expectation ends up failing a run nobody aimed it at.
func (Expectations) Judge ¶
func (e Expectations) Judge(rep Report, rules []RuleOutcome) Verdict
Judge measures a report against the expectations. It is a pure function of the report — no sandbox, no clock — so a scenario's verdict can be recomputed from a stored run, and so the judging is testable without running anything.
Every expectation is evaluated even once one has failed: a run that misses three targets should say so once, rather than over three runs.
type Field ¶
type Field struct {
// Name is the start variable this field writes.
Name string
Kind FieldKind
// Min and Max bound FieldInt and FieldDecimal, inclusive. They are float64
// because that is what a JSON number is; FieldInt requires whole ones.
Min, Max float64
// Decimals is how many places FieldDecimal draws to.
Decimals int
// PercentTrue is how often FieldBool is true.
PercentTrue int
// Choices are the options of FieldChoice.
Choices []Choice
// Value is what FieldConstant writes.
Value any
// Prefix goes in front of FieldSequence's number.
Prefix string
// MinOffset and MaxOffset bound FieldTimestamp, as offsets from the run's own
// simulated start. They are relative because a Playground run happens on a
// virtual clock: an absolute date typed in once is stale the next time the
// scenario runs, while "some time in the last thirty days" stays true.
MinOffset, MaxOffset time.Duration
// OnlyDate writes a FieldTimestamp as a calendar date rather than an instant.
OnlyDate bool
}
Field is one variable of a generated dataset, and how its value is drawn.
It is one structure for every kind rather than one per kind because that is what it is on the wire and on screen: a row in a small table, with a name, a kind, and the parameters that kind reads. Each parameter below says which kinds read it; the rest are ignored.
type FieldKind ¶
type FieldKind string
FieldKind is how one field's value is produced.
const ( // FieldInt draws a whole number in [Min, Max]. FieldInt FieldKind = "int" // FieldDecimal draws a number in [Min, Max] to Decimals places. FieldDecimal FieldKind = "decimal" // FieldBool is true PercentTrue of the time. FieldBool FieldKind = "bool" // FieldChoice picks one of Choices, by weight. FieldChoice FieldKind = "choice" // FieldConstant writes Value into every case. FieldConstant FieldKind = "constant" // FieldSequence numbers the cases: Prefix and the case's position. FieldSequence FieldKind = "sequence" // FieldTimestamp draws an instant in a window around the run's own start. FieldTimestamp FieldKind = "timestamp" )
type FlowUse ¶
FlowUse is how often the run took one sequence flow, named by the two elements it joins rather than by its own BPMN id.
A compiled flow does not carry the diagram's id: the engine never needs it, and putting one there for a Playground feature would add a field to a structure every deployment builds. The pair identifies the flow just as well for the only caller there is — a client that already holds the diagram and can find the connection between two elements in its own registry.
type HeatMap ¶
type HeatMap struct {
Elements []ElementUse
Flows []FlowUse
}
HeatMap is how often the run used each part of the root process: every element and every sequence flow the model has, including the ones no token ever reached.
The zeroes are half the point. "This branch never ran with your data" is a statement the report can only make if it lists what it did *not* see, and a map drawn from the visit counters alone cannot: those exist only for elements a token has been to.
It covers the root process. A call activity's child runs on its own compiled graph, whose element indices mean nothing here — and the diagram on screen is the root's, which is what a heat map is drawn onto.
type Occurrence ¶
type Occurrence struct {
Kind OccurrenceKind
// Element is the BPMN element id the occurrence happened on; empty for a timer
// tick, which may fire several timers that came due at the same instant.
Element string
// At is the simulated instant it happened at.
At time.Time
}
Occurrence is one scheduled thing the scheduler carried out.
type OccurrenceKind ¶
type OccurrenceKind int
OccurrenceKind names what a step carried out.
const ( // OccJobCompleted: a stub answered a job the way a worker would. OccJobCompleted OccurrenceKind = iota // OccJobFailed: a stub failed a job into an incident. OccJobFailed // OccJobError: a stub threw a BPMN error from a job. OccJobError // OccTimer: simulated time reached a due timer and it fired. OccTimer // OccCaseStarted: a planned case reached its arrival time and was created. OccCaseStarted )
type Options ¶
type Options struct {
// ModelXML is the BPMN to run: a draft's XML, or a deployed definition's.
ModelXML []byte
// Root names the process to instantiate by its BPMN id. Empty picks the sole
// executable process, and is an error if the model has several.
Root string
// BaseDir is where the sandbox's temporary directory is created. Empty uses the
// host's temp dir.
BaseDir string
// StartTime is the instant simulated time starts at. The zero value starts at
// the Unix epoch, which is legal but reads badly in a report; a caller that
// shows times to a person should set it.
StartTime time.Time
// Seed makes the run reproducible: the same dataset, policy and seed replay the
// same durations and failures.
Seed int64
// Stubs is the answering policy. The zero value parks every job, which is what
// an environment with no workers does.
Stubs StubSet
}
Options configures a sandbox. Only ModelXML is required.
type Plan ¶
type Plan struct {
// Cases are the start variables of each case, in the order they were given.
// That order is the report's order.
Cases [][]model.VariableValue
// Arrival spreads them over simulated time. The zero value starts them all at
// once.
Arrival Arrival
}
Plan is a batch: the cases to run and when each of them starts.
type Pool ¶
type Pool struct {
// Capacity is how many cases the pool works on at once. It must be at least 1.
Capacity int
// Calendar is when the pool works. The zero value is always.
Calendar
}
Pool is a set of interchangeable workers — three clerks, two reviewers, one approver — that the elements assigned to it compete for.
A pool is what turns a stub duration into a waiting time. Without one, every case is worked on the instant it arrives and the report's "waiting" column is zero by construction, which makes a bottleneck ranking a restatement of the durations somebody typed in. With one, "do three clerks suffice for 200 applications a day" becomes a question the run answers.
type PoolStat ¶
type PoolStat struct {
// Served is how many jobs the pool worked on, BusyTime the seat time they took
// together (nine one-hour cases are nine hours of seat time however many seats
// shared them), and MaxQueue the longest the queue in front of it ever got.
Served int
BusyTime time.Duration
MaxQueue int
// Capacity is the pool's size, and Available the seat time its calendar offered
// over the run — capacity times the working time, not times the wall clock.
// Utilisation is BusyTime over Available, and the distinction is the whole
// point: counting the nights and the weekend as idle capacity reads as "we have
// room" on a pool with three hundred cases queued.
Capacity int
Available time.Duration
}
PoolStat is what a run measured at one pool.
type Progress ¶
type Progress struct {
// Occurrences is how many scheduled things the run carried out.
Occurrences int
// Quiescent is true when the run stopped because nothing was left to do, and
// false when it stopped on the budget — the difference between "the model is
// finished" and "we stopped looking".
Quiescent bool
// SimTime is where simulated time stands now.
SimTime time.Time
}
Progress reports how a Run ended.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the live sessions of a server: it hands them out by id, bounds how many may exist, and reclaims the ones nobody came back to.
func NewRegistry ¶
NewRegistry builds a registry that reclaims a session after ttl of disuse and refuses to hold more than max at once. Both are resource bounds, not tuning: a session is a live engine and a directory on disk.
func (*Registry) CloseAll ¶
func (r *Registry) CloseAll()
CloseAll ends every session, for server shutdown.
type Report ¶
type Report struct {
// Cases is how many were started, Completed how many reached an end event, and
// Incidents how many tokens are parked behind a simulated failure. A completion
// rate that quietly excluded the parked ones would be the useful-looking wrong
// number.
Cases, Completed, Incidents int
// SimStart and SimEnd bound the run in simulated time.
SimStart, SimEnd time.Time
// MaxInFlight is the most cases that were ever active at the same time — the
// work in progress a reader compares against the arrival rate.
MaxInFlight int
// Duration summarises the finished cases' elapsed time.
Duration Durations
// Visits counts every token that passed through an element (the heat map),
// Elements what the sandbox measured at the jobs it answered (the bottleneck
// ranking), and Pools what each pool did.
Visits map[string]int64
Elements map[string]ElementStat
Pools map[string]PoolStat
// Timeline is the same run laid out over simulated time, so a reader can see
// when the work arrived and when it drained rather than only what it summed to.
Timeline Timeline
}
Report is what a run measured. It is folded from the sandbox's state in one pass, holding no object per case, so it is the same size for fifty cases and fifty thousand.
type Rule ¶
type Rule struct {
// Name labels the rule in the verdict. Empty reads the expressions back.
Name string
// When selects the cases the rule speaks about. Empty means every case.
When string
// Then is what those cases must show: their variables as the run left them,
// plus "end" — the BPMN id of the last element the case reached — and
// "durationSeconds", how long it took in simulated time.
Then string
}
Rule is an expectation stated per case rather than per run.
It is the statement the run-wide expectations cannot make. "The median is under four hours" is true of a run; "an application under 50 000 from a grade-A customer is approved" is true of a *case*, and a run where nine tenths of the cases hold it is not nine tenths right — it is wrong for the tenth.
Both halves are FEEL, the language the diagram's own gateways are written in, so an author states the rule the way the model states the decision.
type RuleOutcome ¶
type RuleOutcome struct {
Rule Rule
// Cases is how many the run had; Matched how many the When selected.
Cases, Matched int
// Satisfied and Violated split the matched, finished cases.
Satisfied, Violated int
// Undecided is matched cases that never reached an end, so the Then could not
// be judged. They are neither: the run has not finished answering for them, and
// counting them either way would state something the run did not show.
Undecided int
// Examples are the first offending cases, by their position in the dataset, so
// a reader can go and look at one rather than at a number. Bounded; Truncated
// says the run had more.
Examples []int
Truncated bool
}
RuleOutcome is what a run did about one rule.
func (RuleOutcome) Got ¶
func (o RuleOutcome) Got() string
Got is the outcome as a sentence, for a verdict somebody reads in a build log.
func (RuleOutcome) Passed ¶
func (o RuleOutcome) Passed() bool
Passed reports whether the run held the rule. Only a violation fails it: an undecided case is a case the run did not finish, which is what the completion expectation is for — failing here as well would report one problem twice, and under a name that does not describe it.
type RunState ¶
type RunState string
RunState is what a session's batch is doing.
const ( // RunIdle: no batch has been started, or the last one was read and forgotten. RunIdle RunState = "idle" // RunRunning: a batch is being driven behind the request that started it. RunRunning RunState = "running" // RunFinished: the batch came to rest — every case ran to wherever it got to. RunFinished RunState = "finished" // RunCancelled: somebody stopped it. What it did up to that point is still // readable, which is usually why they stopped it. RunCancelled RunState = "cancelled" // RunFailed: the sandbox could not carry on. Err says what happened. RunFailed RunState = "failed" )
type RunStatus ¶
type RunStatus struct {
State RunState
// Occurrences is how many scheduled things the run has carried out, Cases how
// many have been created and Completed how many have finished. The last two
// come from the engine's maintained counters, so asking is O(1) however large
// the batch is.
Occurrences int
Cases, Completed int
// SimTime is where simulated time stands.
SimTime time.Time
// Err is why a failed run stopped.
Err string
}
RunStatus is a batch's progress, readable while it runs.
type Sandbox ¶
type Sandbox struct {
// contains filtered or unexported fields
}
Sandbox is a complete, throwaway engine: its own partition, log, store and processor over a virtual clock. See the package doc for what it guarantees.
It is owned by one goroutine and carries no lock (invariant I3).
func Open ¶
Open compiles the model and brings up a sandbox for it. The caller must Close it; Close removes everything the sandbox wrote.
func (*Sandbox) Advance ¶
Advance moves simulated time forward by d and fires whatever came due, without waiting for the scheduler to get there on its own — the "jump the clock" control an author uses when stepping through a case by hand.
func (*Sandbox) ArrivalProfileOf ¶
func (s *Sandbox) ArrivalProfileOf(n int, a Arrival) (ArrivalProfile, error)
ArrivalProfileOf lays out n cases the way Sandbox.StartPlan would and reports their shape, without seeding a plan or running anything.
It takes a count rather than the cases themselves because the timing does not depend on them: building fifty thousand rows to find out when they would arrive would be paying the dataset's cost to answer a question it has no part in.
func (*Sandbox) Arrivals ¶
Arrivals reports the instants the planned cases start at, for a caller that wants to see the stream before running it. It is empty for a sequential plan, which has no schedule of its own.
func (*Sandbox) Case ¶
func (s *Sandbox) Case(piKey uint64) (CaseResult, error)
Case reports what became of one case.
func (*Sandbox) Cases ¶
Cases reads one page of the results table, in arrival order, and reports how many rows there are in total. Fifty thousand cases are not handed over in one response, so this is the only way to read them.
func (*Sandbox) Close ¶
Close shuts the engine down and removes the sandbox's directory. Nothing a playground run wrote outlives this call.
func (*Sandbox) CompleteTask ¶
func (s *Sandbox) CompleteTask(jobKey uint64, vars ...model.VariableValue) error
CompleteTask completes a parked job the way the person or worker would have, and settles the engine — the author standing in for the human.
func (*Sandbox) Counts ¶
Counts is how many cases have been created and how many have finished, read from the engine's maintained per-definition counters (ADR-0080). It is O(1), which is what makes it safe to ask on every progress poll of a batch of fifty thousand.
func (*Sandbox) Dir ¶
Dir is the sandbox's temporary directory. Exported for tests and diagnostics; nothing outside the sandbox should write there.
func (*Sandbox) ElementStats ¶
func (s *Sandbox) ElementStats() map[string]ElementStat
ElementStats reports what the run measured at each element that ran a job. It is the bottleneck ranking's raw material; Sandbox.ElementVisits is the heat map's, and the two count different things — every token that passed through, against every job the sandbox answered.
func (*Sandbox) ElementVisits ¶
ElementVisits reports how many tokens have passed through each element of the root process, keyed by BPMN element id — the heat map's raw material, read from the same maintained counters the runtime overlay uses (ADR-0080).
func (*Sandbox) HeatMap ¶
HeatMap folds the run into the diagram's shape.
Element counts come from the maintained visit counters (ADR-0080), which are O(elements) to read. Flow counts have no counter — the engine aggregates elements, not edges — so they are folded out of the causal token history (ADR-0136) in a single scan, at the cost of one pass over the run's activations rather than one per case.
func (*Sandbox) InjectUnreadableCase ¶
InjectUnreadableCase writes an undecodable record under a case's key, so a caller in another package can exercise what every read path does when the sandbox's own state cannot be decoded — report it, rather than returning a report with rows silently missing.
It is a test/tooling affordance only, mirroring the store's own InjectCorruptProcessInstance, and nothing in the sandbox's normal operation writes a record this way.
func (*Sandbox) JudgeRules ¶
func (s *Sandbox) JudgeRules(rules []Rule) ([]RuleOutcome, error)
JudgeRules measures every case against the rules, in one pass over the run.
It is a pass of its own rather than part of Sandbox.Report because it costs what the report deliberately does not: the report reads each case's record, this reads its variables too. A run with no rules should not pay for that.
func (*Sandbox) OpenTasks ¶
OpenTasks lists every job waiting for a person: the ones the stub policy does not answer. A user task with no Human stub is the everyday case; a service task with no stub at all is the other, and both are exactly "nothing is going to answer this unless you do".
func (*Sandbox) PoolStats ¶
PoolStats reports what the run measured at each pool, including the seat time its calendar offered over the run so far.
func (*Sandbox) PublishMessage ¶
func (s *Sandbox) PublishMessage(name, correlationKey string, vars ...model.VariableValue) error
PublishMessage delivers a message into the sandbox, correlating whatever waits for it — the stand-in for the outside world sending one.
func (*Sandbox) Report ¶
Report folds the run into one summary. It reads each case's record once and keeps none of them, so its cost is linear in the cases and its size is not.
func (*Sandbox) Run ¶
Run carries out scheduled occurrences until the model comes to rest or the budget stops it.
func (*Sandbox) Seed ¶
Seed is the number every draw in this sandbox is derived from — the stub durations, the failures, the arrival stream and a generated dataset alike. It is published so a run can be repeated: quoting a figure from a run is only worth anything if somebody else can produce the same one.
func (*Sandbox) StartCase ¶
func (s *Sandbox) StartCase(vars ...model.VariableValue) (uint64, error)
StartCase creates one instance of the root process, seeded with the given variables, and settles the engine. It returns the new instance's key.
The key is found by looking for the newest instance rather than being returned by the engine, which does not hand one back: creation is a queued command and the key is minted when it is processed. That scan is cheap while a person is stepping through a handful of cases; a batch of tens of thousands will want a cheaper identity, and that is a problem for the batch stage, not this one.
func (*Sandbox) StartPlan ¶
StartPlan seeds a batch. It does not run it: the cases are released as simulated time reaches their arrival, so a plan and a run stay separable — the plan is reproducible input, the run is what happens to it.
func (*Sandbox) StartedAt ¶
StartedAt is where simulated time began. A generated dataset measures its dates from here rather than from the wall clock, so a scenario re-run next month still carries "some time in the last thirty days" rather than dates that have gone stale.
func (*Sandbox) Step ¶
func (s *Sandbox) Step() (Occurrence, bool, error)
Step carries out the next scheduled occurrence and settles the engine after it, reporting false when nothing was left to do. It is what "step" means in the Playground: exactly one thing happens, and simulated time moves to it.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is a Sandbox with an owner goroutine and a lifetime.
A sandbox is a partition, and a partition has exactly one writer (invariant I3). HTTP handlers are concurrent, so the sandbox is put behind a runloop.Loop and reached only through Session.With — the same boundary the rest of the API uses for the durable engine.
A session outlives a request on purpose: 50 000 cases will not finish inside an HTTP call, and stepping through one case by hand is a conversation rather than a call. It is closed explicitly, or reclaimed by its registry once nobody has touched it for the TTL.
func (*Session) Budget ¶
Budget returns b with this session's pause wired into it, so a run started through the session stops when somebody presses pause.
func (*Session) Cancel ¶
func (s *Session) Cancel()
Cancel stops a batch in flight at the end of its current slice. What it did stays in the sandbox and stays readable.
func (*Session) Close ¶
Close stops the session's goroutine and discards its sandbox. It is safe to call twice: the second call reports that there was nothing left to close.
func (*Session) LastUsed ¶
LastUsed is when Session.With last ran something. The TTL runs from here, so a session somebody is working in is not reclaimed under them.
func (*Session) OwnedBy ¶
OwnedBy reports whether principal is the one that opened this session. An unowned session (authentication off) belongs to everyone, which is the same reach every other route has in that mode.
func (*Session) Pause ¶
func (s *Session) Pause()
Pause asks a run in flight to stop at its next occurrence, and keeps later runs from starting. It is deliberately not dispatched onto the loop: the run it is stopping is holding that loop.
func (*Session) StartRun ¶
StartRun seeds a plan and drives it behind the caller, reporting progress through Session.RunStatus.
A batch is not a request: fifty thousand cases take longer than anyone will hold a connection open, and the point of watching one is to watch it. The driver goroutine hands work to the session's own loop in slices, so a status poll, a pause or a cancel gets in between them rather than behind the whole batch.
func (*Session) With ¶
With runs fn against the sandbox on the session's own goroutine and returns what fn returned. It is the only way to reach the sandbox, which is what makes the sandbox single-writer.
It refuses a closed session rather than returning fn's zero value, so a caller can tell "your session is gone" from "your closure produced nothing".
type Stub ¶
type Stub struct {
// Min and Max bound the simulated time the work takes. The draw between them
// is deterministic in the run's seed and the job's key, so a re-run with the
// same seed produces the same durations. Max below Min is treated as Min.
Min, Max time.Duration
// Outputs are the variables the answer writes, as a worker's completion would.
Outputs []model.VariableValue
// FailPerMillion is the failure probability in parts per million (0 never
// fails, 1_000_000 always). The draw is deterministic like the duration.
FailPerMillion int32
// FailMessage is the incident message a simulated failure raises. Empty uses a
// message naming the element, so an incident is never anonymous.
FailMessage string
// ErrorCode, when set, makes a simulated failure throw a BPMN error with this
// code — caught by a matching boundary or event subprocess — instead of raising
// an incident. That is how a *business* error path is exercised, not just a
// technical one.
ErrorCode string
}
Stub is how the sandbox answers one job — the stand-in for the worker, worker or person that would answer it in production.
It is deliberately the same vocabulary as the mockup service task (ADR-0120): a duration band, an optional result, an optional failure. The difference is where it lives. A mockup task is *in the model*, so testing with one means shipping something else than what was tested; a Stub is run configuration against an untouched model.
type StubSet ¶
type StubSet struct {
// Default answers every non-human job that ByElement does not name. Nil parks
// them.
Default *Stub
// Human answers user tasks. Nil — the default — parks them for the person at
// the keyboard: that is the interactive half of the Playground. A batch run
// sets it, so 50 000 cases do not wait for anybody.
Human *Stub
// ByElement answers one BPMN element by id, ahead of Default and Human alike.
ByElement map[string]Stub
// Pools are the sets of workers elements compete for, by name. An element with
// no pool is worked on the instant it arrives, however many cases are already
// in flight — which is the right model for a machine and the wrong one for a
// person.
Pools map[string]Pool
// PoolOf assigns a BPMN element to a pool by name. Naming a pool that does not
// exist is refused at [Open]: a silently ignored assignment would show up only
// as a waiting time that never appears.
PoolOf map[string]string
}
StubSet is the whole answering policy for a run: what answers a job, and what is left for a person.
A job that no entry covers **parks**. That is the deliberate default: a sandbox with no policy at all behaves like an environment with no workers, which is exactly what it is, rather than silently completing tasks nobody configured.
func DefaultStubs ¶
func DefaultStubs() StubSet
DefaultStubs is the policy a Playground session starts with: machine work is answered quickly, people are not answered at all. It is what makes "open the tab and press play" work on a model whose integrations do not exist yet.
type Task ¶
type Task struct {
JobKey uint64
InstanceKey uint64
// Element is the BPMN element id. The name a person reads is not carried here:
// the Modeler has the diagram, and the compiled graph only knows an element's
// name where its own detail record happens to keep one.
Element string
// Human is true for a user task — modelled human work, as opposed to a job
// whose worker simply has no stub configured.
Human bool
}
Task is a job waiting for a person: one that the stub policy does not answer.
type Timeline ¶
type Timeline struct {
Width time.Duration
Buckets []Bucket
// contains filtered or unexported fields
}
Timeline is the run over simulated time. Totals say what a run cost; this says when — that the queue built up on Tuesday morning and drained by Thursday is not a thing a mean and a p90 can tell anybody.
It is derived from the cases' own start and end instants rather than sampled during the run, so it costs no run-time accounting and cannot drift from the records it is folded out of.
type Unit ¶
type Unit int
Unit says what a delta's numbers are, so the thing rendering them does not have to guess whether 7200000 is a count or two hours.