# stability-sim

A typed, dependency-free discrete-event simulation library for distributed-system stability experiments.
The simulator was extracted from [stability-sim](https://github.com/marcbrooker/stability-sim) into a standalone library with a [CDK](https://github.com/aws/aws-cdk)-like API.

Each distributed system component is a class that can be instantiated with a set of properties.
Components can be connected to other components, and failures can be added to the scenario to simulate different failure scenarios.
Once a scenario is created, it can be run synchronously.

## Install

Node.js 22.18 or newer is required.

```bash
npm install stability-sim
```

## Usage

```ts
import { Client, Distribution, LoadBalancer, LoadBalancerStrategy, Queue, Retry, Scenario, Server, ServerCrash, Traffic } from 'stability-sim';

const scenario = new Scenario({
  name: 'Metastable Retry Storm',
  endTime: 60,
  metricsWindowSize: 1,
  seed: 42,
});

const client = scenario.add(new Client('client-1', {
  trafficPattern: Traffic.openLoop({ meanArrivalRate: 70 }),
  retryStrategy: Retry.fixedN({ maxRetries: 3 }),
  timeout: 1,
}));

const loadBalancer = scenario.add(new LoadBalancer('lb-1', {
  strategy: LoadBalancerStrategy.ROUND_ROBIN,
}));

const queueA = scenario.add(new Queue('queue-a', {
  maxCapacity: 10_000,
  maxConcurrency: 5,
}));

const queueB = scenario.add(new Queue('queue-b', {
  maxCapacity: 10_000,
  maxConcurrency: 5,
}));

const serverA = scenario.add(new Server('srv-1', {
  serviceTimeDistribution: Distribution.exponential({ mean: 0.1 }),
  concurrencyLimit: 5,
}));

const serverB = scenario.add(new Server('srv-2', {
  serviceTimeDistribution: Distribution.exponential({ mean: 0.1 }),
  concurrencyLimit: 5,
}));

client.sendsTo(loadBalancer);
loadBalancer.routesTo(queueA);
loadBalancer.routesTo(queueB);
queueA.sendsTo(serverA);
queueB.sendsTo(serverB);

scenario.addFailure(new ServerCrash(serverA, {
  triggerTime: 5,
  recoveryTime: 10,
}));

const result = scenario.run({ snapshotInterval: 1 });
console.log(result.finalSnapshot);
```

In this distributed system simulation, a client connects to a load balancer, which routes requests to servers, whose buffers are represented by queues. 
A failure scenario, namely a short single server outage, is injected from T=5 to T=10, causing an amplified retry storm from clients that keeps both branches overloaded after the server recovers.

The `result` object contains information about the simulation which can be probed further:

```ts
const allSnapshots = result.snapshots; // an array of MetricSnapshot objects
const finalSnapshot = result.finalSnapshot; // the final MetricSnapshot object, for convenience
```

If we inspect the final snapshot, we can see metrics including the number of client requests successfully fulfilled, the number that were retried, the final size of the queues, as well as latency percentiles for the client:

```ts
console.log(finalSnapshot);
/*
{
  simTime: 59.99985197301004,
  componentMetrics: {
    'client-1': {
      completedCount: 662,
      failedCount: 13948,
      retriedCount: 10631,
      timedOutCount: 13474,
      inFlightCount: 281,
      tokenBucketTokens: 0
    },
    'lb-1': {
      tpsForwarded: 14891,
      totalFailed: 0,
      failedDownstreamCount: 0,
      errorRate: 0
    },
    'queue-a': {
      queueDepth: 4245,
      totalEnqueued: 7446,
      totalDequeued: 3201,
      totalRejected: 0
    },
    'queue-b': {
      queueDepth: 4437,
      totalEnqueued: 7445,
      totalDequeued: 3008,
      totalRejected: 0
    },
    'srv-1': {
      activeCount: 5,
      utilization: 1,
      tpsProcessed: 2722,
      totalRejected: 474,
      crashed: 0,
      latencySpikeMultiplier: 1,
      cpuReductionPercent: 0,
      errorRate: 0
    },
    'srv-2': {
      activeCount: 5,
      utilization: 1,
      tpsProcessed: 3003,
      totalRejected: 0,
      crashed: 0,
      latencySpikeMultiplier: 1,
      cpuReductionPercent: 0,
      errorRate: 0
    }
  },
  latencyPercentiles: {
    p50: 29.621504949269067,
    p95: 33.26896852844208,
    p99: 34.40729320856889,
    p999: 34.9146943670209
  },
  completedCount: 662,
  failedCount: 13948
}
*/
```

The docs below provide further information about the available APIs and how to use the simulator.
For more examples, see the [examples directory](./examples).

## Low-level JSON API

A simulation can also be created from a raw JSON object by calling `Scenario.fromJSON()`.

```ts
import type { ScenarioJSON } from 'stability-sim';

const document = {
  schemaVersion: 1,
  name: 'Low-level example',
  seed: 42,
  endTime: 5,
  metricsWindowSize: 1,
  components: [
    {
      id: 'client',
      type: 'client',
      config: {
        trafficPattern: { type: 'burst', count: 10, atTime: 0 },
        retryStrategy: { type: 'none' },
        targetComponentId: 'server',
        timeout: 1,
      },
    },
    {
      id: 'server',
      type: 'server',
      config: {
        serviceTimeDistribution: {
          type: 'uniform',
          min: 0.02,
          max: 0.04,
        },
        concurrencyLimit: 10,
      },
    },
  ],
  connections: [
    { id: 'client-to-server', sourceId: 'client', targetId: 'server' },
  ],
  failureScenarios: [],
};

const scenario = Scenario.fromJSON(document); // also accepts a JSON string
const result = scenario.run();
```

To serialize a builder scenario:

```ts
const document = scenario.toJSON();
const json = scenario.toJSONString(2);
```

## Validation

There are some ways that a simulation can be created incorrectly, especially if it's created directly from JSON.
The API provides two validation levels to help catch these issues:

- TypeScript rejects incompatible relationships, failure targets, and malformed component props at development time.
- Runtime validation checks numerical ranges, required connections, unique IDs, references, connection cardinality, and JSON discriminants.

Validation happens automatically before synthesis or execution. It is also directly accessible:

```ts
const issues = scenario.validate(); // readonly ValidationIssue[]
scenario.assertValid();             // throws ScenarioValidationError
```

Every runtime issue includes a stable code, JSON-style path, and message:

```ts
{
  code: ValidationIssueCode.INVALID_TOPOLOGY,
  path: '$.components[0]',
  message: 'client requires exactly one downstream connection',
}
```

## Simulation control

`scenario.run()` will run a full simulation.
For stepping or metric access, create a simulation session:

```ts
const simulation = scenario.createSimulation();

simulation.step();
simulation.currentTime;
simulation.queueSize;
simulation.snapshot();

const result = simulation.run({
  snapshotInterval: 0.5,
  latencyWindow: 10,
  maxEvents: 1_000_000,
  onSnapshot(snapshot) {
    console.log(snapshot.simTime, snapshot.completedCount);
  },
});

const points = simulation.getTimeSeries('api-a', 'utilization'); // TimeSeriesPoint[]
simulation.reset();
```

## API reference

### Public values

| API | Description |
| --- | --- |
| `Cache` | Pass-through cache component with probabilistic or keyed TTL behavior. |
| `Client` | Workload source component configured with traffic and retry policies. |
| `Component` | Abstract base class shared by builder component objects. |
| `Database` | Terminal database processor with read/write latency and a connection pool. |
| `LoadBalancer` | Pass-through router supporting round-robin, random, and least-connections selection. |
| `Queue` | Pass-through buffer with capacity, concurrency, shedding, and ordering controls. |
| `Server` | Terminal request processor with service-time and concurrency controls. |
| `Throttle` | Pass-through component that limits concurrency or request rate. |
| `Connection` | Typed directed relationship returned by component relationship methods. |
| `Traffic` | Immutable factories for open-loop, closed-loop, ramping, and burst traffic patterns. |
| `Retry` | Immutable factories for none, fixed-count, token-bucket, and circuit-breaker retries. |
| `Distribution` | Immutable distribution factories and the union type they produce. |
| `LoadDependentLatency` | Immutable load-scaling factories and the union type they produce. |
| `ThrottleMode` | Immutable throttle-mode factories and the union type they produce. |
| `CacheFlush` | Failure object that clears a cache at a configured simulation time. |
| `CpuReduction` | Failure object that temporarily reduces a server's available concurrency. |
| `Failure` | Abstract base class shared by typed failure objects. |
| `LatencySpike` | Failure object that temporarily multiplies server or database latency. |
| `NetworkPartition` | Failure object that temporarily disables a `Connection`. |
| `RandomError` | Failure object that temporarily injects errors into a server or load balancer. |
| `ServerCrash` | Failure object that crashes and later recovers a server or database. |
| `Scenario` | Builder, validator, serializer, and entry point for running a scenario. |
| `ScenarioValidationError` | Error containing every structured issue found during scenario validation. |
| `Simulation` | Stateful synchronous simulation session supporting run, step, metrics, and reset. |

### Enums

| Enum | Description |
| --- | --- |
| `CacheEvictionPolicy` | FIFO or LRU cache eviction policy. |
| `ComponentType` | Stable serialized type of a simulation component. |
| `DistributionType` | Uniform, exponential, or log-normal distribution discriminant. |
| `EventKind` | Arrival, departure, failure, recovery, or timeout event kind. |
| `FailureType` | Stable serialized type of an injectable failure. |
| `LoadBalancerStrategy` | Round-robin, random, or least-connections routing strategy. |
| `LoadDependentLatencyMode` | Linear, polynomial, or exponential load-scaling mode. |
| `QueueOrdering` | FIFO or LIFO queue ordering. |
| `RetryStrategyType` | Stable discriminant for a retry strategy. |
| `SimulationStatus` | Idle, running, paused, or completed execution status. |
| `ThrottleModeType` | Disabled, concurrency, or RPS throttle-mode discriminant. |
| `TrafficPatternType` | Stable discriminant for a traffic pattern. |
| `ValidationIssueCode` | Stable category assigned to a validation diagnostic. |

### Component and connection types

| Type | Description |
| --- | --- |
| `AnyComponent` | Union of every high-level component object. |
| `CacheProps` | Constructor properties for `Cache`, excluding its object-referenced downstream target. |
| `ClientProps` | Constructor properties for `Client`, excluding its object-referenced target. |
| `ConnectableComponent` | Union of component objects allowed as connection sources. |
| `DatabaseProps` | Constructor properties for `Database`. |
| `LoadBalancerProps` | Constructor properties for `LoadBalancer`. |
| `QueueProps` | Constructor properties for `Queue`. |
| `ServerProps` | Constructor properties for `Server`. |
| `TargetComponent` | Union of component objects allowed as connection targets. |
| `TerminalComponent` | Union of terminal `Server` and `Database` objects. |
| `ThrottleProps` | Constructor properties for `Throttle`. |
| `ConnectionOptions` | Optional relationship settings, currently the serialized connection ID. |

### Factory result and property types

| Type | Description |
| --- | --- |
| `OpenLoopTraffic` | Traffic value produced by `Traffic.openLoop()`. |
| `OpenLoopTrafficProps` | Properties accepted by `Traffic.openLoop()`. |
| `ClosedLoopTraffic` | Traffic value produced by `Traffic.closedLoop()`. |
| `ClosedLoopTrafficProps` | Properties accepted by `Traffic.closedLoop()`. |
| `RampingTraffic` | Traffic value produced by `Traffic.ramping()`. |
| `RampingTrafficProps` | Properties accepted by `Traffic.ramping()`. |
| `BurstTraffic` | Traffic value produced by `Traffic.burst()`. |
| `BurstTrafficProps` | Properties accepted by `Traffic.burst()`. |
| `NoRetry` | Retry value produced by `Retry.none()`. |
| `FixedNRetry` | Retry value produced by `Retry.fixedN()`. |
| `FixedNRetryProps` | Properties accepted by `Retry.fixedN()`. |
| `TokenBucketRetry` | Retry value produced by `Retry.tokenBucket()`. |
| `TokenBucketRetryProps` | Properties accepted by `Retry.tokenBucket()`. |
| `CircuitBreakerRetry` | Retry value produced by `Retry.circuitBreaker()`. |
| `CircuitBreakerRetryProps` | Properties accepted by `Retry.circuitBreaker()`. |
| `UniformDistribution` | Distribution value produced by `Distribution.uniform()`. |
| `UniformDistributionProps` | Properties accepted by `Distribution.uniform()`. |
| `ExponentialDistribution` | Distribution value produced by `Distribution.exponential()`. |
| `ExponentialDistributionProps` | Properties accepted by `Distribution.exponential()`. |
| `LogNormalDistribution` | Distribution value produced by `Distribution.logNormal()`. |
| `LogNormalDistributionProps` | Properties accepted by `Distribution.logNormal()`. |
| `LinearLoadDependentLatency` | Scaling value produced by `LoadDependentLatency.linear()`. |
| `LinearLoadDependentLatencyProps` | Properties accepted by `LoadDependentLatency.linear()`. |
| `PolynomialLoadDependentLatency` | Scaling value produced by `LoadDependentLatency.polynomial()`. |
| `PolynomialLoadDependentLatencyProps` | Properties accepted by `LoadDependentLatency.polynomial()`. |
| `ExponentialLoadDependentLatency` | Scaling value produced by `LoadDependentLatency.exponential()`. |
| `ExponentialLoadDependentLatencyProps` | Properties accepted by `LoadDependentLatency.exponential()`. |
| `DisabledThrottleMode` | Throttle value produced by `ThrottleMode.disabled()`. |
| `ConcurrencyThrottleMode` | Throttle value produced by `ThrottleMode.concurrency()`. |
| `ConcurrencyThrottleModeProps` | Properties accepted by `ThrottleMode.concurrency()`. |
| `RpsThrottleMode` | Throttle value produced by `ThrottleMode.rps()`. |
| `RpsThrottleModeProps` | Properties accepted by `ThrottleMode.rps()`. |

### Failure types

| Type | Description |
| --- | --- |
| `AnyFailure` | Union of every high-level failure object. |
| `CacheFlushProps` | Timing properties accepted by `CacheFlush`. |
| `CpuReductionProps` | Timing and reduction properties accepted by `CpuReduction`. |
| `LatencySpikeProps` | Timing and multiplier properties accepted by `LatencySpike`. |
| `NetworkPartitionProps` | Timing properties accepted by `NetworkPartition`. |
| `RandomErrorProps` | Timing and error-rate properties accepted by `RandomError`. |
| `ServerCrashProps` | Crash and recovery timing properties accepted by `ServerCrash`. |
| `FailureScenario` | Low-level discriminated union used to serialize failure scenarios by ID. |

### Scenario, simulation, and validation types

| Type | Description |
| --- | --- |
| `ScenarioOptions` | Name, seed, duration, and metrics-window settings for `Scenario`. |
| `ValidationIssue` | Structured validation diagnostic containing a code, path, and message. |
| `RunOptions` | Snapshot, latency-window, event-limit, and callback options for a run. |
| `SimulationResult` | Final status, counts, timing, snapshots, and metrics returned by a run. |
| `SnapshotOptions` | Options controlling an on-demand metrics snapshot. |

### Configuration, JSON, and metric types

| Type | Description |
| --- | --- |
| `CacheConfig` | Canonical low-level cache configuration including its downstream component ID. |
| `ClientConfig` | Canonical low-level client configuration including its target component ID. |
| `DatabaseConfig` | Canonical database latency, pool, and load-scaling configuration. |
| `Distribution` | Discriminated union of uniform, exponential, and log-normal distributions. |
| `LoadBalancerConfig` | Canonical load-balancer strategy configuration. |
| `LoadDependentLatency` | Discriminated union of linear, polynomial, and exponential load scaling. |
| `QueueConfig` | Canonical queue capacity, concurrency, shedding, and ordering configuration. |
| `RetryStrategy` | Discriminated union of supported retry strategies. |
| `ServerConfig` | Canonical server service-time, concurrency, and load-scaling configuration. |
| `ThrottleConfig` | Canonical throttle configuration containing a `ThrottleMode`. |
| `ThrottleMode` | Discriminated union of disabled, concurrency, and RPS throttle modes. |
| `TrafficPattern` | Discriminated union of supported client traffic patterns. |
| `ComponentJSON` | Low-level union of ID-based serialized component entries. |
| `SavedScenarioJSON` | Existing application save-document shape accepted by `Scenario.fromJSON()`. |
| `ScenarioJSON` | Canonical ID-based scenario document emitted by `Scenario.toJSON()`. |
| `LatencyPercentiles` | P50, P95, P99, and P99.9 latency values. |
| `MetricSnapshot` | Aggregate and per-component metrics captured at one simulation time. |
| `TimeSeriesPoint` | One time/value pair returned from a metric series. |

## Runnable TypeScript examples

Build the library once, then run any example directly with Node.js:

```bash
npm run build

node examples/metastable-retry.ts
node examples/gc-death-spiral.ts
node examples/connection-pool-exhaustion.ts
node examples/cache-stampede.ts
node examples/cache-flush.ts
node examples/timeout-cascade.ts
node examples/lb-sinkholing.ts
node examples/goodput-collapse.ts
node examples/from-json.ts
```

- [`examples/metastable-retry.ts`](./examples/metastable-retry.ts) — Metastable Failure (Retry Storm).
- [`examples/gc-death-spiral.ts`](./examples/gc-death-spiral.ts) — GC Pressure Death Spiral.
- [`examples/connection-pool-exhaustion.ts`](./examples/connection-pool-exhaustion.ts) — Connection Pool Exhaustion.
- [`examples/cache-stampede.ts`](./examples/cache-stampede.ts) — Cache Stampede.
- [`examples/cache-flush.ts`](./examples/cache-flush.ts) — Cache Flush Metastability.
- [`examples/timeout-cascade.ts`](./examples/timeout-cascade.ts) — Timeout Cascade at High Utilization.
- [`examples/lb-sinkholing.ts`](./examples/lb-sinkholing.ts) — Load Balancer Sinkholing.
- [`examples/goodput-collapse.ts`](./examples/goodput-collapse.ts) — Goodput Collapse (Stale Queue).

## Development

The library has no dependencies. TypeScript, `tsx`, and Node types are development-only.

```bash
npm run typecheck
npm test
npm pack --dry-run
```

The build emits ESM JavaScript, source maps, and `.d.ts` declarations under `dist/`. Advanced low-level engine classes are available from `stability-sim/engine`.
