Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 89 additions & 63 deletions src/execution/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import type {Failure} from '../event.js';
import type {Result} from '../error.js';

type ServiceState =
| {id: 'initial'}
| {
id: 'initial';
entireExecutionAborted: Promise<void>;
}
| {
id: 'executingDeps';
fingerprint: Deferred<ExecutionResult>;
Expand Down Expand Up @@ -90,13 +93,24 @@ function unexpectedState(state: ServiceState) {
* │ │ │
* ▼ fingerprinted ▼
* │ │ │
* │ ┌─────▼─────┐ │
* ├─◄─ abort ───┤ UNSTARTED │ │
* │ └─────┬─────┘ │
* │ │ │
* │ start ╭─╮ │
* │ │ │ start │
* │ ┌───────▼────▼─┴┐ │
* │ ╔══════════════════════╗ │
* │ ║ is directly invoked? ╟── yes ──╮ │
* │ ╚══════════╤═══════════╝ │ │
* │ │ │ │
* │ no │ │
* │ │ │ │
* │ ┌─────▼─────┐ │ │
* ├─◄─ abort ───┤ UNSTARTED │ ▼ │
* │ └─────┬─────┘ │ │
* │ │ │ │
* │ start │ │
* │ │ │ │
* │ │ ╭─────────◄────────╯ │
* │ │ │ │
* │ │ │ ╭─╮ │
* │ │ │ │start │
* │ ┌───────▼──▼─▼─┴┐ │
* ├─◄─ abort ─┤ DEPS_STARTING ├───── depStartErr ───►───┤
* │ └───────┬───────┘ │
* │ │ │
Expand All @@ -116,18 +130,14 @@ function unexpectedState(state: ServiceState) {
* │ │ │ │ start │ │
* │ │ ┌────▼─▼─┴┐ │ │
* │ ├◄─ abort ┤ STARTED ├── exit ────────────────────┤
* │ │ └────┬─┬─┬┘ │ │
* │ │ │ │ │ │ │
* │ │ │ │ ╰── depServiceExit ─►─┤ │
* │ │ │ │ │ │
* │ │ │ ╰───── detach ──╮ │ │
* │ ▼ │ │ │ │
* │ │ allConsumersDone │ │ │
* │ │ (unless directly invoked) │ │ │
* │ │ │ ▼ │ ▼
* ▼ │ │ ╭─╮ │ │ │
* │ │ │ │ start │ │ │
* │ │ ┌────▼──▼─┴┐ │ ┌────▼────┐ │
* │ │ └──────┬─┬┘ │ │
* │ │ │ │ │ │
* │ │ │ ╰── depServiceExit ─►─┤ │
* │ │ │ │ │
* │ │ ╰───── detach ──╮ │ │
* │ │ │ │ │
* ▼ │ ▼ │ ▼
* │ │ ┌──────────┐ │ ┌────▼────┐ │
* │ ╰─────────► STOPPING │ │ │ FAILING │ │
* │ └┬─▲─┬─────┘ │ └────┬────┘ │
* │ abort │ │ │ │ │
Expand All @@ -143,7 +153,7 @@ function unexpectedState(state: ServiceState) {
* ```
*/
export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScriptConfig> {
private _state: ServiceState = {id: 'initial'};
private _state: ServiceState;
private readonly _terminated = new Deferred<Result<void, Failure>>();

/**
Expand All @@ -160,10 +170,13 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
config: ServiceScriptConfig,
executor: Executor,
logger: Logger,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_abort: Promise<void>
entireExecutionAborted: Promise<void>
) {
super(config, executor, logger);
this._state = {
id: 'initial',
entireExecutionAborted,
};
}

/**
Expand All @@ -173,6 +186,19 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
protected override _execute(): Promise<ExecutionResult> {
switch (this._state.id) {
case 'initial': {
const allConsumersDone = Promise.all(
this._config.serviceConsumers.map(
(consumer) =>
this._executor.getExecution(consumer).servicesNotNeeded
)
);
const abort = this._config.isDirectlyInvoked
? Promise.all([this._state.entireExecutionAborted, allConsumersDone])
: allConsumersDone;
void abort.then(() => {
this._onAbort();
});

this._state = {
id: 'executingDeps',
fingerprint: new Deferred(),
Expand Down Expand Up @@ -220,6 +246,7 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
);
return;
}
case 'stopped':
case 'failed': {
return;
}
Expand All @@ -230,7 +257,6 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
case 'starting':
case 'started':
case 'stopping':
case 'stopped':
case 'failing': {
throw unexpectedState(this._state);
}
Expand All @@ -246,6 +272,7 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
this._state.fingerprint.resolve(result);
return;
}
case 'stopped':
case 'failed': {
return;
}
Expand All @@ -256,7 +283,6 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
case 'starting':
case 'started':
case 'stopping':
case 'stopped':
case 'failing': {
throw unexpectedState(this._state);
}
Expand All @@ -271,9 +297,13 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
case 'fingerprinting': {
this._state.fingerprint.resolve({ok: true, value: fingerprint});
this._state = {id: 'unstarted'};
if (this._config.isDirectlyInvoked) {
void this.start();
}
return;
}
case 'failed': {
case 'failed':
case 'stopped': {
return;
}
case 'initial':
Expand All @@ -283,7 +313,6 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
case 'starting':
case 'started':
case 'stopping':
case 'stopped':
case 'failing': {
throw unexpectedState(this._state);
}
Expand Down Expand Up @@ -398,14 +427,16 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
};
return;
}
case 'stopped': {
return;
}
case 'depsStarting':
case 'initial':
case 'executingDeps':
case 'fingerprinting':
case 'unstarted':
case 'starting':
case 'stopping':
case 'stopped':
case 'failing':
case 'failed': {
throw unexpectedState(this._state);
Expand All @@ -429,15 +460,6 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
id: 'started',
child: this._state.child,
};
const allConsumersDone = Promise.all(
this._config.serviceConsumers.map(
(consumer) =>
this._executor.getExecution(consumer).servicesNotNeeded
)
);
void allConsumersDone.then(() => {
this._onAllConsumersDone();
});
return;
}
case 'initial':
Expand All @@ -458,33 +480,6 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
}
}

private _onAllConsumersDone() {
switch (this._state.id) {
case 'started': {
this._state.child.kill();
this._state = {id: 'stopping'};
return;
}
case 'failed': {
return;
}
case 'initial':
case 'executingDeps':
case 'fingerprinting':
case 'unstarted':
case 'depsStarting':
case 'starting':
case 'stopping':
case 'stopped':
case 'failing': {
throw unexpectedState(this._state);
}
default: {
throw unknownState(this._state);
}
}
}

private _onChildExited() {
switch (this._state.id) {
case 'stopping': {
Expand Down Expand Up @@ -530,6 +525,37 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
}
}

private _onAbort() {
switch (this._state.id) {
case 'started': {
this._state.child.kill();
this._state = {id: 'stopping'};
return;
}
case 'starting': {
this._state = {id: 'stopping'};
return;
}
case 'initial':
case 'executingDeps':
case 'fingerprinting':
case 'unstarted':
case 'depsStarting': {
this._state = {id: 'stopped'};
return;
}
case 'stopping':
case 'stopped':
case 'failing':
case 'failed': {
return;
}
default: {
throw unknownState(this._state);
}
}
}

private _fail(failure: Failure) {
this._state = {
id: 'failed',
Expand Down
9 changes: 6 additions & 3 deletions src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,15 @@ export class Executor {
private readonly _logger: Logger;
private readonly _workerPool: WorkerPool;
private readonly _cache?: Cache;
private readonly _abort: Deferred<void>;

/** Resolves when the first failure occurs in any script. */
private readonly _failureOccured = new Deferred<void>();
/** Resolves when we decide that new scripts should not be started. */
private readonly _stopStartingNewScripts = new Deferred<void>();
/** Resolves when we decide that running scripts should be killed. */
private readonly _killRunningScripts = new Deferred<void>();
/** Resolves when we decide that services should be stopped. */
private readonly _stopServices = new Deferred<void>();

constructor(
rootConfig: ScriptConfig,
Expand All @@ -76,19 +77,21 @@ export class Executor {
this._logger = logger;
this._workerPool = workerPool;
this._cache = cache;
this._abort = abort;

// If this entire execution is aborted because e.g. the user sent a SIGINT
// to the Wireit process, then dont start new scripts, and kill running
// ones.
void abort.promise.then(() => {
this._stopStartingNewScripts.resolve();
this._killRunningScripts.resolve();
this._stopServices.resolve();
});

// If a failure occurs, then whether we stop starting new scripts or kill
// running ones depends on the failure mode setting.
void this._failureOccured.promise.then(() => {
// Services should stop in any mode.
this._stopServices.resolve();
switch (failureMode) {
case 'continue': {
break;
Expand Down Expand Up @@ -165,7 +168,7 @@ export class Executor {
config,
this,
this._logger,
this._abort.promise
this._stopServices.promise
);
this._allServices.push(execution);
} else {
Expand Down
Loading