@molecule/api-deploy-rollout-default

Provider bond · deploy-rollout · API (Node) · v1.1.3 · Apache-2.0

Default deploy-rollout provider for molecule.dev: grow-then-shrink and in-place instance replacement with a capacity floor, continuous soak, reverse rollback and idempotent resume.

npm install @molecule/api-deploy-rollout-default

npm · Source on GitHub · Implements @molecule/api-deploy-rollout

How it works

@molecule/api-deploy-rollout-default is a provider bond on the API (Node) side: it implements the deploy-rollout core interface (@molecule/api-deploy-rollout) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setProvider, runRollout } from '@molecule/api-deploy-rollout'
import { provider } from '@molecule/api-deploy-rollout-default'

setProvider(provider)

const outcome = await runRollout({
  provider: myInstanceProvider,
  current: oldInstances,
  policy: { replicas: 2, soakMs: 30_000 },
})

Works with: @molecule/api-deploy-rollout

Reference

Default deploy-rollout provider for molecule.dev.

Implements the two real deployment topologies over any instance provider: grow-then-shrink (create the new set, gate it ready, join it, then retire the old one at a time) and in-place (change the same instances one at a time, with a reverse restore on failure). No I/O of its own, no clock other than an injected one, no knowledge of where the instances live.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { setProvider, runRollout } from '@molecule/api-deploy-rollout'
import { provider } from '@molecule/api-deploy-rollout-default'

setProvider(provider)

const outcome = await runRollout({
  provider: myInstanceProvider,
  current: oldInstances,
  policy: { replicas: 2, soakMs: 30_000 },
})

Type

provider

Installation

npm install @molecule/api-deploy-rollout-default @molecule/api-deploy-rollout

API

Interfaces

ResolvedRolloutPolicy

The resolved policy the state machine runs with.

interface ResolvedRolloutPolicy {
  /** Target size of the new set (grow-then-shrink only). */
  replicas: number
  /** Floor of healthy instances. */
  minHealthy: number
  /** Continuous healthy time required after a change. */
  soakMs: number
  /** Probe cadence during the soak. */
  soakProbeMs: number
  /** Give-up bound for the soak. */
  soakGraceMs: number
  /** Bounded wait for a retiring instance. */
  drainMs: number
  /** Readiness budget handed to the provider. */
  readinessTimeoutMs: number
  /** What an abort does to instances already changed. */
  rollback: 'reverse' | 'none'
  /** Whether to reconcile against the provider's current instances. */
  resume: boolean
}

InstanceProvider

What a grow-then-shrink rollout needs from whatever hosts the instances.

The caller implements this over its hosting provider; the -sandbox bond does it over @molecule/api-code-sandbox, and tests drive it with a fake that records the order and can fail any step.

interface InstanceProvider<I extends RolloutInstance = RolloutInstance> {
  /** A human-readable provider name, for progress lines. */
  readonly name?: string

  /**
   * Every instance the deployment currently owns, when the provider can enumerate
   * them. Used to resume an interrupted rollout idempotently: an instance already
   * ready and joined is not created twice.
   */
  list?(): Promise<I[]>

  /**
   * Create the instance at `index` (0-based, above any primary the caller created
   * itself). It must not be routable until `InstanceProvider.join`.
   *
   * @param index - Which instance this is.
   * @returns The created instance.
   */
  create(index: number): Promise<I>

  /**
   * Resolve once the instance may take traffic.
   *
   * @param instance - The instance to gate.
   * @param timeoutMs - The policy's readiness budget, for providers whose probe
   *   accepts one.
   * @returns `false` when it never became ready (the rollout aborts).
   */
  waitReady(instance: I, timeoutMs?: number): Promise<boolean>

  /**
   * A single, fast readiness probe, for the soak window. Optional: a caller that
   * sets `soakMs` must provide it, because a blocking readiness wait cannot tell a
   * warm instance from a recovering one.
   *
   * @param instance - The instance to probe.
   * @returns Whether it is serving right now.
   */
  isServing?(instance: I): Promise<boolean>

  /**
   * Add the instance to the routable set, so the edge may send it new requests.
   *
   * @param instance - A ready instance.
   */
  join(instance: I): Promise<void>

  /**
   * Remove the instance from the routable set, so no NEW request is routed to it.
   * Requests already in flight are untouched.
   *
   * @param instance - The instance being retired.
   */
  leave(instance: I): Promise<void>

  /**
   * Tell the instance to drain: finish in-flight work and stop accepting new
   * connections. Resolve when it may be destroyed, and never later than `drainMs`.
   *
   * @param instance - The retired instance.
   * @param drainMs - The bounded wait.
   */
  drain(instance: I, drainMs: number): Promise<void>

  /**
   * Destroy the instance, releasing whatever it costs.
   *
   * @param instance - The instance to remove.
   */
  destroy(instance: I): Promise<void>
}

Types

RolloutCancelProbe

A cancellation probe: true means stop.

type RolloutCancelProbe = () => Promise<boolean> | boolean

Classes

RolloutAbortedError

A rollout that could not finish, carrying what it did so the caller can report it (and, where the topology allows, undo the rest itself).

The same class serves both topologies: grow-then-shrink fills role, created and outcome; in-place fills failed, rolledBack and rollbackFailures.

class RolloutAbortedError extends Error {
  /** The API layer's key for this abort. */
  readonly errorKey = 'deploy-rollout.error.aborted'

  /** The step that failed. */
  readonly role: RolloutStep | null

  /** The instance whose step failed, when one did. */
  readonly failed: string | null

  /** Instances restored to the old build, newest first. */
  readonly rolledBack: string[]

  /** Restores that themselves failed. */
  readonly rollbackFailures: RolloutFailure[]

  /** The grow-then-shrink outcome, when the abort happened there. */
  readonly outcome: RolloutOutcome | null

  /**
   * Build the abort with what the caller must report.
   *
   * @param message - What happened, in plain words.
   * @param details - The role, the failed instance, what was undone and the outcome.
   * @param cause - The underlying error, when there was one.
   */
  constructor(
    message: string,
    details: {
      role?: RolloutStep | null
      failed?: string | null
      rolledBack?: string[]
      rollbackFailures?: RolloutFailure[]
      outcome?: RolloutOutcome | null
    } = {},
    cause?: unknown,
  ) {
    super(message, cause === undefined ? undefined : { cause })
    this.name = 'RolloutAbortedError'
    this.role = details.role ?? null
    this.failed = details.failed ?? null
    this.rolledBack = details.rolledBack ?? []
    this.rollbackFailures = details.rollbackFailures ?? []
    this.outcome = details.outcome ?? null
  }
}

Functions

resolveRolloutPolicy(policy, instanceCount)

Fill the policy defaults.

maxUnavailable derives minHealthy only when it is set; without it the floor is one healthy instance, which is exactly the rule every existing rolling path has today.

function resolveRolloutPolicy(
  policy: RolloutPolicy | undefined,
  instanceCount: number,
): ResolvedRolloutPolicy
  • policy — The caller's policy, if any.
  • instanceCount — How many instances the topology has, for maxUnavailable.

Returns: The resolved policy.

runGrowThenShrink(options)

Run a grow-then-shrink replacement.

The caller has already created, gated and joined the PRIMARY instance of the new version when primaryServing is set; replicas is the size of the new set including that one, and this function creates the remaining replicas - 1.

function runGrowThenShrink<I extends RolloutInstance>(
  options: RolloutOptions<I>,
): Promise<RolloutOutcome<I>>
  • options — The set to build, the old set, and the provider.

Returns: The new set, the retired instances and non-fatal failures.

runInPlace(options)

Run an in-place replacement: the same instances changed one at a time.

The instance list comes from machines, or from the provider's own list(). The operations come from apply/rollback/probeServing, or from the provider's replace/restore/isServing. Before an instance is taken out of rotation, every OTHER instance must answer its probe, and the resulting healthy count must stay at or above the policy floor.

function runInPlace<I extends RolloutInstance>(
  options: InPlaceRolloutOptions<I>,
): Promise<InPlaceRolloutOutcome<I>>
  • options — The instances to roll and the provider or callbacks.

Returns: The instances changed, in roll order.

createProvider()

Create a fresh provider object (the same algorithm; a distinct identity for tests and for apps that register more than one).

function createProvider(): DeployRolloutProvider

Returns: A deploy-rollout provider.

describeRolloutResult(outcome, replicas)

One plain sentence about what a finished rollout did.

function describeRolloutResult<I extends RolloutInstance>(
  outcome: Pick<RolloutOutcome<I>, 'retired'>,
  replicas: number,
): string | null
  • outcome — What the rollout returned.
  • replicas — The set size, for the sentence.

Returns: The line, or null when there was nothing to retire or the set is one.

Constants

DEFAULT_ROLLOUT_DRAIN_MS

The default bounded drain wait — matches the lifecycle bond's own budget.

const DEFAULT_ROLLOUT_DRAIN_MS: 25000

DEFAULT_ROLLOUT_READINESS_TIMEOUT_MS

The default readiness budget handed to a provider.

const DEFAULT_ROLLOUT_READINESS_TIMEOUT_MS: 120000

DEFAULT_SOAK_PROBE_MS

The default cadence of the soak probe.

const DEFAULT_SOAK_PROBE_MS: 2000

DEFAULT_MIN_HEALTHY

The default floor of healthy instances: never blank the deployment.

const DEFAULT_MIN_HEALTHY: 1

provider

The provider implementation registered under deploy-rollout.

const provider: DeployRolloutProvider

Core Interface

Implements @molecule/api-deploy-rollout interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-deploy-rollout'
import { provider } from '@molecule/api-deploy-rollout-default'

export function setupDeployRolloutDefault(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-deploy-rollout ^1.0.0

Runtime Dependencies

  • @molecule/api-deploy-rollout

  • Capacity floor first. policy.minHealthy (default 1) is checked before every retirement; a step that would go below it aborts instead of running.

  • Soak is continuous. With soakMs > 0 the instance must keep answering isServing; a gap restarts the window (a fresh process can pass one probe while its event loop is still warming).

  • Rollback is reverse. Instances this run changed are restored newest first; a restore failure does not stop the remaining restores.

  • Resume is a no-op when complete. A re-run reconciles against the provider's own list() where it has one, so an interrupted rollout does not create a duplicate set.