-
Notifications
You must be signed in to change notification settings - Fork 123
Services: Support for very basic consumer/service test case #476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5b23fba
a29ebd3
5a4e5cd
76abfff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,14 +7,48 @@ | |
| import {BaseExecutionWithCommand} from './base.js'; | ||
| import {Fingerprint} from '../fingerprint.js'; | ||
| import {Deferred} from '../util/deferred.js'; | ||
| import {ScriptChildProcess} from '../script-child-process.js'; | ||
|
|
||
| import type {ExecutionResult} from './base.js'; | ||
| import type {ServiceScriptConfig} from '../config.js'; | ||
| import type {ScriptReference, 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'; | ||
|
|
||
| type ServiceState = | ||
| | {id: 'initial'} | ||
| | { | ||
| id: 'executingDeps'; | ||
| fingerprint: Deferred<ExecutionResult>; | ||
| } | ||
| | { | ||
| id: 'fingerprinting'; | ||
| fingerprint: Deferred<ExecutionResult>; | ||
| } | ||
| | {id: 'unstarted'} | ||
| | { | ||
| id: 'starting'; | ||
| child: ScriptChildProcess; | ||
| started: Deferred<Result<void, Failure[]>>; | ||
| } | ||
| | { | ||
| id: 'started'; | ||
| child: ScriptChildProcess; | ||
| } | ||
| | {id: 'stopping'} | ||
| | {id: 'stopped'}; | ||
|
|
||
| function unknownState(state: never) { | ||
| return new Error( | ||
| `Unknown service state ${String((state as ServiceState).id)}` | ||
| ); | ||
| } | ||
|
|
||
| function unexpectedState(state: ServiceState) { | ||
| return new Error(`Unexpected service state ${state.id}`); | ||
| } | ||
|
|
||
| /** | ||
| * Execution for a {@link ServiceScriptConfig}. | ||
| * | ||
|
|
@@ -33,7 +67,13 @@ import type {Result} from '../error.js'; | |
| * ▼ execute | ||
| * │ │ | ||
| * │ ┌───────▼────────┐ | ||
| * ├─◄─ abort ─┤ FINGERPRINTING ├──── depExecErr ────►───╮ | ||
| * ├─◄─ abort ─┤ EXECUTING_DEPS ├──── depExecErr ────►───╮ | ||
| * │ └───────┬────────┘ │ | ||
| * │ │ │ | ||
| * ▼ depsExecuted │ | ||
| * │ │ │ | ||
| * │ ┌───────▼────────┐ │ | ||
| * ├─◄─ abort ─┤ FINGERPRINTING │ │ | ||
| * │ └───────┬────────┘ │ | ||
| * │ │ │ | ||
| * ▼ fingerprinted │ | ||
|
|
@@ -82,6 +122,7 @@ import type {Result} from '../error.js'; | |
| * ``` | ||
| */ | ||
| export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScriptConfig> { | ||
| private _state: ServiceState = {id: 'initial'}; | ||
| private readonly _terminated = new Deferred<Result<void, Failure>>(); | ||
|
|
||
| /** | ||
|
|
@@ -108,23 +149,229 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri | |
| * Note `execute` is a bit of a misnomer here, because we don't actually | ||
| * execute the command at this stage in the case of services. | ||
| */ | ||
| protected override async _execute(): Promise<ExecutionResult> { | ||
| const dependencyFingerprints = await this._executeDependencies(); | ||
| if (!dependencyFingerprints.ok) { | ||
| return dependencyFingerprints; | ||
| protected override _execute(): Promise<ExecutionResult> { | ||
| switch (this._state.id) { | ||
| case 'initial': { | ||
| this._state = { | ||
| id: 'executingDeps', | ||
| fingerprint: new Deferred(), | ||
| }; | ||
| void this._executeDependencies().then((result) => { | ||
| if (result.ok) { | ||
| this._onDepsExecuted(result.value); | ||
| } else { | ||
| this._onDepExecErr(result); | ||
| } | ||
| }); | ||
| return this._state.fingerprint.promise; | ||
| } | ||
| case 'executingDeps': | ||
| case 'fingerprinting': | ||
| case 'unstarted': | ||
| case 'starting': | ||
| case 'started': | ||
| case 'stopping': | ||
| case 'stopped': { | ||
| throw unexpectedState(this._state); | ||
| } | ||
| default: { | ||
| throw unknownState(this._state); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private _onDepsExecuted( | ||
| depFingerprints: Array<[ScriptReference, Fingerprint]> | ||
| ): void { | ||
| switch (this._state.id) { | ||
| case 'executingDeps': { | ||
| this._state = { | ||
| id: 'fingerprinting', | ||
| fingerprint: this._state.fingerprint, | ||
| }; | ||
| void Fingerprint.compute(this._config, depFingerprints).then( | ||
| (result) => { | ||
| this._onFingerprinted(result); | ||
| } | ||
| ); | ||
| return; | ||
| } | ||
| case 'initial': | ||
| case 'fingerprinting': | ||
| case 'unstarted': | ||
| case 'starting': | ||
| case 'started': | ||
| case 'stopping': | ||
| case 'stopped': { | ||
| throw unexpectedState(this._state); | ||
| } | ||
| default: { | ||
| throw unknownState(this._state); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private _onDepExecErr(result: ExecutionResult & {ok: false}) { | ||
| switch (this._state.id) { | ||
| case 'executingDeps': { | ||
| this._state.fingerprint.resolve(result); | ||
| return; | ||
| } | ||
| case 'initial': | ||
| case 'fingerprinting': | ||
| case 'unstarted': | ||
| case 'starting': | ||
| case 'started': | ||
| case 'stopping': | ||
| case 'stopped': { | ||
| throw unexpectedState(this._state); | ||
| } | ||
| default: { | ||
| throw unknownState(this._state); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private _onFingerprinted(fingerprint: Fingerprint) { | ||
| switch (this._state.id) { | ||
| case 'fingerprinting': { | ||
| this._state.fingerprint.resolve({ok: true, value: fingerprint}); | ||
| this._state = {id: 'unstarted'}; | ||
| return; | ||
| } | ||
| case 'initial': | ||
| case 'executingDeps': | ||
| case 'unstarted': | ||
| case 'starting': | ||
| case 'started': | ||
| case 'stopping': | ||
| case 'stopped': { | ||
| throw unexpectedState(this._state); | ||
| } | ||
| default: { | ||
| throw unknownState(this._state); | ||
| } | ||
| } | ||
| const fingerprint = await Fingerprint.compute( | ||
| this._config, | ||
| dependencyFingerprints.value | ||
| ); | ||
| return {ok: true, value: fingerprint}; | ||
| } | ||
|
|
||
| /** | ||
| * 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'); | ||
| switch (this._state.id) { | ||
| case 'unstarted': { | ||
| this._state = { | ||
| id: 'starting', | ||
| child: new ScriptChildProcess(this._config), | ||
| started: new Deferred(), | ||
| }; | ||
| void this._state.child.started.then(() => { | ||
| this._onChildStarted(); | ||
| }); | ||
| void this._state.child.completed.then(() => { | ||
| this._onChildExited(); | ||
| }); | ||
| return this._state.started.promise; | ||
| } | ||
| case 'initial': | ||
| case 'executingDeps': | ||
| case 'fingerprinting': | ||
| case 'starting': | ||
| case 'started': | ||
| case 'stopping': | ||
| case 'stopped': { | ||
| throw unexpectedState(this._state); | ||
| } | ||
| default: { | ||
| throw unknownState(this._state); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private _onChildStarted() { | ||
| switch (this._state.id) { | ||
| case 'starting': { | ||
| this._state.started.resolve({ok: true, value: undefined}); | ||
| this._logger.log({ | ||
| script: this._config, | ||
| type: 'info', | ||
| detail: 'service-started', | ||
| }); | ||
| this._state = { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the state transition after the promises below settle?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. The new state is started, and we set up a handler for the "all consumers done" event, which allows the service to shut down. |
||
| id: 'started', | ||
| child: this._state.child, | ||
| }; | ||
| const allConsumersDone = Promise.all( | ||
| this._config.serviceConsumers.map( | ||
| (consumer) => | ||
| this._executor.getExecution(consumer).servicesNotNeeded | ||
| ) | ||
| ); | ||
| void allConsumersDone.then(() => { | ||
| this._allConsumersDone(); | ||
| }); | ||
| return; | ||
| } | ||
| case 'initial': | ||
| case 'executingDeps': | ||
| case 'fingerprinting': | ||
| case 'unstarted': | ||
| case 'started': | ||
| case 'stopping': | ||
| case 'stopped': { | ||
| throw unexpectedState(this._state); | ||
| } | ||
| default: { | ||
| throw unknownState(this._state); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private _allConsumersDone() { | ||
| switch (this._state.id) { | ||
| case 'started': { | ||
| this._state.child.kill(); | ||
| this._state = {id: 'stopping'}; | ||
| return; | ||
| } | ||
| case 'initial': | ||
| case 'executingDeps': | ||
| case 'fingerprinting': | ||
| case 'unstarted': | ||
| case 'starting': | ||
| case 'stopping': | ||
| case 'stopped': { | ||
| throw unexpectedState(this._state); | ||
| } | ||
| default: { | ||
| throw unknownState(this._state); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private _onChildExited() { | ||
| switch (this._state.id) { | ||
| case 'stopping': { | ||
| this._state = {id: 'stopped'}; | ||
| this._logger.log({ | ||
| script: this._config, | ||
| type: 'info', | ||
| detail: 'service-stopped', | ||
| }); | ||
| return; | ||
| } | ||
| case 'initial': | ||
| case 'executingDeps': | ||
| case 'fingerprinting': | ||
| case 'unstarted': | ||
| case 'starting': | ||
| case 'started': | ||
| case 'stopped': { | ||
| throw unexpectedState(this._state); | ||
| } | ||
| default: { | ||
| throw unknownState(this._state); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,7 +75,7 @@ export class StandardScriptExecution extends BaseExecutionWithCommand<StandardSc | |
| return {ok: false, error: [this._startCancelledEvent]}; | ||
| } | ||
|
|
||
| return this._acquireSystemLockIfNeeded(async () => { | ||
| return await this._acquireSystemLockIfNeeded(async () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit subtle! |
||
| // Note we must wait for dependencies to finish before generating the | ||
| // cache key, because a dependency could create or modify an input file to | ||
| // this script, which would affect the key. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this state still have an associated child process?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's needed, but if it turns out to be then I will add it. I'm only adding state data as needed.