import type * as ManagerPB from "@norskvideo/norsk-api/lib/manager_pb"; import type * as ManagerSdk from "@norskvideo/norsk-manager-sdk"; import type { JobId, JobWithHistory, NodeId, NorskManager, Role } from "@norskvideo/norsk-manager-sdk"; import type { AutoSettings, MigrationApplySummary, MigrationId, MigrationState, MigrationStrategy, PlacementOutcome } from "./automanager"; import type { Clock } from "./clock"; import type { JobBindings } from "./jobBinding"; import type { CapabilityRequirement } from "./types"; /** * The instance KEY a migration target wears: the source's slot plus the * first eight hex of the migration id, e.g. `primary\@3f9c1e2a`. Unique * per migration, so the source and the target of one migration — and * the targets of successive migrations of the same job — are distinct * `ActiveJobKey`s for the Manager's JobNodeMap. The job itself is still * told the SLOT (`primary` / `backup`) in `NORSK_RUNTIME_ROLE`. * @public */ export declare function migrationTargetRole(slot: Role, migrationId: MigrationId): Role; /** Decode the apply summary a target sent; `undefined` for none or for * bytes that are not one. Never throws: this rides the event stream. */ export declare function readApplySummary(payload: Uint8Array | undefined): MigrationApplySummary | undefined; /** * What `placeAndProvision` is asked to do differently for a migration * target: bind as the migration claim's target rather than as the job's * own node, dispatch StartJob with the migration fields, pin `targetNode` * when the operator named one, and never consider `excludeNodes` (the * source's node, plus whatever recovery would skip). Everything else — * pools, requirements, resilience, hot spares, provision-a-node — is the * same path every job takes. */ export interface MigrationPlacement { migrationId: MigrationId; targetRole: Role; runtimeRole: Role; deferStart: boolean; targetNode?: NodeId; excludeNodes: Set; /** Capabilities the target node must advertise on top of the job's * own — a handover needs the worker's job socket, which a worker * running with it disabled does not advertise. */ extraCapabilities?: CapabilityRequirement[]; /** Phase G — this move may take an exclusively reserved hot spare. The * operator's own decision, per migration, not a policy read off the * job. */ overrideReservation?: boolean; } /** What the saga needs from the AutoManager, handed in explicitly. */ export interface MigrationSagaDeps { settings: AutoSettings; clock: Clock; norsk: Pick; bindings: JobBindings; jobOf: (jobId: JobId) => JobWithHistory | undefined; /** The one placement path every job takes; a migration target goes * through it with the `migration` argument set. */ placeAndProvision: (jobId: JobId, tags: { [key: string]: string; }, recovery: { excludeNodes: Set; } | undefined, migration?: MigrationPlacement) => Promise; /** The nodes ordinary recovery would skip right now. */ currentExcludeSet: () => Set; anyOtherNodeAdvertises: (name: string, except: NodeId) => boolean; /** A target's StartJob was refused (the placement metrics count it). */ onDispatchFailed: () => void; /** A migration reached `done` or `aborted` (drains watch for theirs). */ onMigrationEnded: (state: MigrationState) => void; } export declare class MigrationSaga { private readonly deps; private readonly migrationStates; private readonly migrationPhaseTimers; private readonly migrationProposals; constructor(deps: MigrationSagaDeps); /** Every in-flight migration's deadline timer, cancelled (close). */ cancelTimers(): void; /** * Migrate a running job to another node. Placement is the same path * every other job takes: `targetNodeId`, when given, is a hard pin * (that node or nowhere, requirements still checked); otherwise the * engine chooses, with the job's current node excluded. The target is * started under a key unique to this migration * (`migrationTargetRole`) while being told the source's slot. * * Resolves once the target has been placed and its StartJob * dispatched (phase `launching`); the rest is event-driven — watch * `onMigrationPhase` / `onMigrationCompleted` / `onMigrationAborted`. * Rejects (and records the migration as aborted) if placement finds * nowhere for the target or the dispatch fails; throws without * recording anything if the job is unknown, not bound to a node, not * running, already migrating, or `targetNodeId` is its current node. * * `sourceRole` defaults to the job's current instance key (its slot, * or the key a previous migration left it under); pass it explicitly * only when addressing one replica of a multi-replica bundle whose * instances share a jobId (they do not, today). * * @public */ migrateJob(sourceJobId: JobId, opts?: { targetNodeId?: NodeId; strategy?: MigrationStrategy; sourceRole?: Role; /** Phase G — allow the target onto an EXCLUSIVELY reserved hot * spare, one set aside for other templates. Steve's case: two * spares held for `xyz`, and an `abc` that has to move now. */ overrideReservation?: boolean; }): Promise; /** * Abort an in-flight migration cleanly: MigrationAbort to both keys, * StopJob for the target, its reservation released, the source * untouched. No-op (with a debug log) for an unknown migration id or * one already done/aborted. * * Rejects once the migration is past the point of no return — the * source has already been told to stop, so there is nothing to roll * back to; the saga will complete into the target, or fall into * ordinary recovery if the target is lost. * * @public */ abortMigration(migrationId: MigrationId, reason: string): Promise; /** All known migrations (active + done + aborted). @public */ migrations(): MigrationState[]; /** Look up a single migration by id. @public */ migrationById(migrationId: MigrationId): MigrationState | undefined; /** Migration active for the given source jobId, if any. @public */ migrationForJob(jobId: JobId): MigrationState | undefined; /** * Forget a TERMINAL migration — `migrations()` keeps every migration * this AutoManager has ever run, which is an unbounded list an operator * has no other way to shorten. * * Refuses an in-flight one: dropping the record would leave the saga * running with nothing watching it. Abort it first (and, past the point * of no return, wait — it fails forward to `done`). The refusal is a * legitimate state, not a fault; the caller turns it into a 409, exactly * as it does `abortMigration`'s point-of-no-return refusal. * * Returns false for an id that is not (or is no longer) known, so a * double-clear is a no-op rather than an error. * * @public */ forgetMigration(migrationId: MigrationId): boolean; /** * Forget every terminal migration, leaving the in-flight ones alone. * Returns the ids forgotten. @public */ forgetTerminalMigrations(): MigrationId[]; inFlightMigrations(): MigrationState[]; /** * The job's runtime SLOT ("primary" / "backup"), from its replicaIndex * tag. What the job is told in NORSK_RUNTIME_ROLE, and the instance * key of a job that has never migrated. Undefined for un-managed jobs. */ slotForJob(jobId: JobId): Role | undefined; /** * The instance KEY of the job's current instance: the key its last * completed migration left it under, else its slot. This is the role * every RPC that addresses the RUNNING instance must use. */ roleForJob(jobId: JobId): Role; private advanceMigration; /** * Re-arm the per-phase timer for the migration. Expiry before the * point of no return aborts; `stoppingSource` expiry proceeds as if * the source had reported stopped (it was told to stop — waiting * longer helps nobody); `startingTarget` expiry fails forward. */ private armMigrationPhaseDeadline; private cfgMigrationPhaseDeadline; private markMigrationDone; markMigrationAborted(state: MigrationState, reason: string): void; /** * Has anything about this migration been put on the wire yet — i.e. is * there anybody to tell when it is abandoned? For a HANDOVER the answer * is yes from the very first phase: `proposing` asks the source for its * state, and a source that answered is LOCKED on it until a cutover or * an abort. The abort sent in `proposing` / `placing` / `launching` is * therefore the source's unlock, and skipping it strands a live * instance holding state nobody will ever collect. For the other two * strategies nothing reaches a worker until placement has dispatched * the target's StartJob, so `placing` really is silent. */ private migrationTouchedWire; /** * Phase F, the propose. Ask the SOURCE what it would hand over and park * until it answers, the phase deadline gives up, or the migration is * aborted from anywhere else. Nothing has been placed at this point and * nothing will be unless the answer is `accepted`. */ private propose; /** Hand the parked `migrateJob` its answer. Idempotent: the first * settler wins, so a refusal racing the phase deadline is harmless. */ private settleProposal; /** MigrationAbort to both keys, via Manager. Best effort. */ private dispatchMigrationAborts; /** * The clean rollback (pre point-of-no-return only): abort to both * keys and StopJob for the target when anything was put on the wire * (`notifyWire`), the target's reservation released, the migration * marked aborted. The source is never touched — it was never told to * stop. Local state changes first, so a caller iterating bindings * sees them gone even if the RPCs are slow. */ private performAbort; /** * StartJob for the migration target on `nodeId`, carrying the * migration fields. On failure the migration is treated as having * lost its target (abort before the point of no return; the only * caller after it is `startPreparedJob`, which has its own path). */ dispatchMigrationTarget(state: MigrationState, nodeId: NodeId): Promise; /** The target's containers exist and are running (or, for * stopThenStart, have been told to start): what "fail forward into * the target" needs to be true before it is preferable to recovery. */ private targetIsRunning; /** The target is gone (failed, rejected, its node dying, its provision * abandoned): a clean abort while the source is untouched; past the * point of no return, fail forward into ordinary recovery. */ handleTargetLost(state: MigrationState, reason: string): Promise; /** The source is gone (failed, its node dying) mid-migration. If the * target is already running, complete into it — the job carries on * there, which is exactly what a migration is for. Otherwise abort; * the caller then runs the ordinary recovery re-place for the job * (the job's binding still names the dead source, so recovery sees * it as needing a node). */ handleSourceLost(state: MigrationState, reason: string): Promise; /** * StopJob(source, migrationId) — the point of no return. If the RPC * itself fails there is no way back either (the Manager may or may * not have acted on it), so proceed as if the source had stopped: * for stopThenStart that starts the prepared target regardless. */ private beginStoppingSource; /** The source's JobStopped landed (or its deadline stood in for it). */ private onSourceStopped; /** * `done`: the job now lives on the target. Its binding rebinds to it, * the target's separate binding folds away, the instance key becomes * the one the running instance wears from here on, and the placement * of record becomes the target's own decision (`JobBindings. * completeMigration`). The source's reservation was released by the * rebind (the derived reservation reads the bindings); its JobNodeMap * entry went with its JobStopped. */ private completeMigration; /** * Past the point of no return and the target is lost: the source is * gone or going, so the job has nowhere to run. Mark the migration * aborted, drop both bindings, and hand the job to ordinary recovery * — the same re-place a node death would trigger, ceiling and policy * included — under its slot key again (a fresh instance is not the * migrated one). */ private failForward; /** * The saga's event intake for job history: every transition that is * not a handshake message arrives as the newest entry of a * `jobUpdated`, keyed by the entry's role — the instance key — which * is what tells the target's `running` from the source's. Returns * true when the entry was about a migrating instance and has been * fully handled, so `handleJobChanged` must NOT also run ordinary * failure recovery for it (a target failure is the saga's to handle; * a source failure that completed into the target needs no recovery). * Returns false to let the ordinary path run — including after a * source failure that aborted the migration, which is then recovered * like any other runtime failure. */ advanceMigrationOnJobHistory(jh: JobWithHistory): Promise; /** Look up the in-flight migration a handshake event belongs to, * dropping events for unknown jobs and for a migrationId other than * the one in flight (a predecessor's stragglers). */ private migrationForHandshake; /** * Phase F — the source's answer to the propose. Either its migratable * state (possibly empty: a job with nothing to hand over still * accepts) or a refusal, which ends the migration with no target ever * placed. The refusal is read off the ENVELOPE, so the coordinator * never has to open the opaque blob to know it was turned down. */ handleMigrationState(raw: ManagerPB.MigrationStateEvent): void; handleMigrationTargetReady(raw: ManagerPB.MigrationTargetReadyEvent): Promise; handleMigrationSourceStoppedOutput(raw: ManagerPB.MigrationSourceStoppedOutputEvent): Promise; /** The target has applied the handover and begun output — only now is * the source stopped. (Marking done on the apply RPC returning was * hole 4: a target that failed to apply left no source to go back to.) */ handleMigrationTargetApplied(raw: ManagerPB.MigrationTargetAppliedEvent): Promise; /** Task 90, fix-list 27: the worker has handed the source's state to the * target. `settingStateOnTarget` ends here; what follows is the target's * own ready round. Idempotent against a ready that raced it. */ handleMigrationInitialStateDelivered(raw: ManagerPB.MigrationInitialStateDeliveredEvent): void; handleMigrationAborted(raw: ManagerPB.MigrationAbortedEvent): Promise; /** * End every in-flight migration for `reason`: the clean rollback * (abort on the wire, target stopped) before the point of no return, * marked lost after it. Returns the wire work still settling, so the * caller can hold the channel open for it. Local state is final on * return either way. */ abortInFlightMigrations(reason: string): Promise[]; } export declare function roleFromReplicaIndex(replicaIndex: number): Role; /** * The newest history entry about a given instance key (running, * provisioned, prepared, stopping, stopped all carry the key; created / * updated / completed / rejected do not and are skipped). Undefined when * the key has no history — it never existed, or the history has been * trimmed. */ export declare function newestHistoryEntryForRole(jh: JobWithHistory, role: Role): ManagerSdk.JobHistoryJobRunning | ManagerSdk.JobHistoryJobProvisioned | ManagerSdk.JobHistoryJobPrepared | ManagerSdk.JobHistoryJobStopping | ManagerSdk.JobHistoryJobStopped | undefined; //# sourceMappingURL=migrationSaga.d.ts.map