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
33 changes: 33 additions & 0 deletions src/execution/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,37 @@ export abstract class BaseExecutionWithCommand<
* needed to run at all.
*/
readonly servicesNotNeeded = this._servicesNotNeeded.promise;

/**
* Resolves when any of the services this script depends on have terminated
* (see {@link ServiceScriptExecution.terminated} for exact definiton).
*/
readonly anyServiceTerminated = Promise.race(
this._config.services.map(
(service) => this._executor.getExecution(service).terminated
)
);

/**
* Ensure that all of the services this script depends on are running.
*/
protected async _startServices(): Promise<Result<void, Failure[]>> {
if (this._config.services.length > 0) {
const results = await Promise.all(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Promise.all() ok here (over Promise.allSettled()) because you have your own error results that resolved to, not rejected?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we throw only as a panic for an internal error, so in general we don't need to catch exceptions.

this._config.services.map((service) =>
this._executor.getExecution(service).start()
)
);
const errors: Failure[] = [];
for (const result of results) {
if (!result.ok) {
errors.push(...result.error);
}
}
if (errors.length > 0) {
return {ok: false, error: errors};
}
}
return {ok: true, value: undefined};
}
}
23 changes: 22 additions & 1 deletion src/execution/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,31 @@

import {BaseExecutionWithCommand} from './base.js';
import {Fingerprint} from '../fingerprint.js';
import {Deferred} from '../util/deferred.js';

import type {ExecutionResult} from './base.js';
import type {ServiceScriptConfig} from '../config.js';
import type {Executor} from '../executor.js';
import type {Logger} from '../logging/logger.js';
import type {Failure} from '../event.js';
import type {Result} from '../error.js';

/**
* Execution for a {@link ServiceScriptConfig}.
*/
export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScriptConfig> {
private readonly _terminated = new Deferred<Result<void, Failure>>();

/**
* Resolves as "ok" when this script decides it is no longer needed, and
* either has begun shutting down, or never needed to start in the first
* place.
*
* Resolves with an error if this service exited unexpectedly, or if any of
* its own service dependencies exited unexpectedly.
*/
readonly terminated = this._terminated.promise;

constructor(
config: ServiceScriptConfig,
executor: Executor,
Expand All @@ -42,5 +57,11 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
return {ok: true, value: fingerprint};
}

// TODO(aomarks) Implement service starting/stopping.
/**
* Start this service if it isn't already started.
*/
start(): Promise<Result<void, Failure[]>> {
// TODO(aomarks) Implement service starting/stopping.
throw new Error('Not implemented');
}
}
58 changes: 51 additions & 7 deletions src/execution/standard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type {StandardScriptConfig} from '../config.js';
import type {FingerprintString} from '../fingerprint.js';
import type {Logger} from '../logging/logger.js';
import type {Cache, CacheHit} from '../caching/cache.js';
import type {StartCancelled} from '../event.js';
import type {Failure, StartCancelled} from '../event.js';
import type {AbsoluteEntry} from '../util/glob.js';
import type {FileManifestEntry, FileManifestString} from '../util/manifest.js';

Expand Down Expand Up @@ -316,6 +316,41 @@ export class StandardScriptExecution extends BaseExecutionWithCommand<StandardSc
return {ok: false, error: this._startCancelledEvent};
}

let earlyServiceTermination: Failure | undefined;
if (this._config.services.length > 0) {
const servicesStarted = await this._startServices();
if (!servicesStarted.ok) {
return servicesStarted;
}

void this.anyServiceTerminated.then((result) => {
if (this._state === 'after-running') {
// This is expected after we're done.
return;
}
if (result.ok) {
// This should never happen and indicates an internal error. The
// service believed that nothing was depending on it anymore, but
// we're still running.
earlyServiceTermination = {
script: this._config,
type: 'failure',
reason: 'unknown-error-thrown',
error: new Error(
'Internal error: service dependency terminated unexpectedly'
),
};
} else {
// The service knows it exited too early. Propagate that error.
earlyServiceTermination = result.error;
}
// Stop running. If a service we depend on is down, then we know we're
// in an invalid state too.
child.kill();
this._executor.notifyFailure();
});
}

this._state = 'running';
this._logger.log({
script: this._config,
Expand Down Expand Up @@ -353,11 +388,15 @@ export class StandardScriptExecution extends BaseExecutionWithCommand<StandardSc

const result = await child.completed;
if (result.ok) {
this._logger.log({
script: this._config,
type: 'success',
reason: 'exit-zero',
});
if (earlyServiceTermination !== undefined) {
return {ok: false, error: earlyServiceTermination};
} else {
this._logger.log({
script: this._config,
type: 'success',
reason: 'exit-zero',
});
}
} else {
// This failure will propagate to the Executor eventually anyway, but
// asynchronously.
Expand All @@ -378,7 +417,12 @@ export class StandardScriptExecution extends BaseExecutionWithCommand<StandardSc
this._state = 'after-running';

if (!childResult.ok) {
return {ok: false, error: [childResult.error]};
return {
ok: false,
error: Array.isArray(childResult.error)
? childResult.error
: [childResult.error],
};
}

// Optimization: early signal that services are no longer needed while we're
Expand Down