@sshkeda/task 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -2
- package/package.json +41 -4
- package/src/index.ts +2 -0
- package/src/lib/errors/index.ts +15 -0
- package/src/lib/errors/internal/errors.ts +147 -0
- package/src/lib/kill-leftover-processes/index.ts +17 -0
- package/src/lib/kill-leftover-processes/internal/command-words.ts +16 -0
- package/src/lib/kill-leftover-processes/internal/find-process-ids.ts +36 -0
- package/src/lib/kill-leftover-processes/internal/kill-process-groups.ts +270 -0
- package/src/lib/kill-leftover-processes/internal/list-linux-processes-from-proc.ts +191 -0
- package/src/lib/kill-leftover-processes/internal/list-macos-processes-with-sysctl.ts +335 -0
- package/src/lib/kill-leftover-processes/internal/list-processes.ts +58 -0
- package/src/lib/kill-leftover-processes/internal/mark-run-processes.ts +367 -0
- package/src/lib/kill-leftover-processes/internal/process-cleanup-errors.ts +17 -0
- package/src/lib/kill-leftover-processes/internal/send-signal.ts +32 -0
- package/src/lib/kill-leftover-processes/internal/signal-run-processes.ts +48 -0
- package/src/lib/kill-leftover-processes/internal/started-process-groups.ts +107 -0
- package/src/lib/main-thread/index.ts +3 -0
- package/src/lib/main-thread/internal/listen-for-stop-requests.ts +102 -0
- package/src/lib/main-thread/internal/run-main-thread.ts +81 -0
- package/src/lib/main-thread/internal/run-root-task-fiber.ts +191 -0
- package/src/lib/main-thread/internal/stops-at-first-failure.ts +20 -0
- package/src/lib/main-thread/internal/trace-the-run.ts +107 -0
- package/src/lib/main-thread/lib/find-code-each-task-depends-on/index.ts +7 -0
- package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/follow-imports-from-entry-files.ts +306 -0
- package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/load-all-reporting-first-failure.ts +11 -0
- package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/make-module-graph.ts +48 -0
- package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/read-configured-loaders.ts +64 -0
- package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/resolve-module-specifiers.ts +60 -0
- package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/rewrite-worker-imports.ts +118 -0
- package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/worker-import-errors.ts +19 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/index.ts +7 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/internal/handle-worker-events.ts +348 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/internal/make-task-runs.ts +207 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/index.ts +1 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/await-within-counted-limit.ts +29 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/execute-task.ts +272 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/make-task-setup-clock.ts +129 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/run-requested-tasks.ts +135 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/shared/define-task-run-state.ts +288 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/shared/make-stopwatch.ts +39 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/shared/share-standard-input-with-root-task.ts +36 -0
- package/src/lib/main-thread/lib/run-tasks-on-workers/shared/stop-task.ts +166 -0
- package/src/lib/main-thread/lib/scan-module-source/index.ts +1 -0
- package/src/lib/main-thread/lib/scan-module-source/internal/find-lexical-bindings.ts +154 -0
- package/src/lib/main-thread/lib/scan-module-source/internal/find-task-declarations.ts +205 -0
- package/src/lib/main-thread/lib/scan-module-source/internal/parse-module-text.ts +34 -0
- package/src/lib/main-thread/lib/scan-module-source/internal/read-literal-string.ts +15 -0
- package/src/lib/main-thread/lib/scan-module-source/internal/read-module-loads.ts +316 -0
- package/src/lib/main-thread/lib/scan-module-source/internal/scan-module-text.ts +70 -0
- package/src/lib/main-thread/lib/scan-module-source/internal/unwrap-type-assertions.ts +10 -0
- package/src/lib/main-thread/lib/scan-module-source/internal/visit-non-reference-identifiers.ts +21 -0
- package/src/lib/main-thread/shared/fail-run.ts +36 -0
- package/src/lib/main-thread/shared/find-failure-reason.ts +43 -0
- package/src/lib/main-thread/shared/find-package-directories.ts +34 -0
- package/src/lib/main-thread/shared/find-tasks-reachable-from-root-task-file.ts +246 -0
- package/src/lib/main-thread/shared/list-directory-paths-up-to-root.ts +13 -0
- package/src/lib/main-thread/shared/main-thread-errors.ts +373 -0
- package/src/lib/main-thread/shared/package-json-errors.ts +93 -0
- package/src/lib/main-thread/shared/provide-discovered-tasks.ts +26 -0
- package/src/lib/main-thread/shared/provide-process-groups.ts +63 -0
- package/src/lib/main-thread/shared/provide-run-root-task-config.ts +66 -0
- package/src/lib/main-thread/shared/provide-stop-state.ts +150 -0
- package/src/lib/main-thread/shared/provide-worker-pool.ts +607 -0
- package/src/lib/main-thread/shared/read-bare-package-name.ts +3 -0
- package/src/lib/main-thread/shared/read-package-json.ts +149 -0
- package/src/lib/main-thread/shared/report-task-failure.ts +31 -0
- package/src/lib/preload-rewritten-imports-into-each-worker/index.ts +5 -0
- package/src/lib/preload-rewritten-imports-into-each-worker/internal/load-rewritten-imports.ts +34 -0
- package/src/lib/supervise-the-run/index.ts +15 -0
- package/src/lib/supervise-the-run/internal/adopt-orphans-on-linux.ts +73 -0
- package/src/lib/supervise-the-run/internal/check-root-task-file.ts +8 -0
- package/src/lib/supervise-the-run/internal/report-ready.ts +12 -0
- package/src/lib/supervise-the-run/internal/share-process-groups.ts +25 -0
- package/src/lib/supervise-the-run/internal/start-watching-the-supervisor.ts +6 -0
- package/src/lib/supervise-the-run/internal/supervise-run.ts +155 -0
- package/src/lib/supervise-the-run/internal/talk-to-the-supervisor.ts +53 -0
- package/src/lib/supervise-the-run/internal/watch-the-supervisor.ts +61 -0
- package/src/lib/task/index.ts +5 -0
- package/src/lib/task/internal/make-tasks.ts +82 -0
- package/src/lib/task/internal/run-command.ts +194 -0
- package/src/lib/task/internal/run-root-task.ts +263 -0
- package/src/lib/task/internal/run-task-lists.ts +51 -0
- package/src/lib/task/internal/trust-output-mode-names.ts +18 -0
- package/src/lib/task/internal/trust-task-results.ts +35 -0
- package/src/lib/task/internal/use-task-services.ts +4 -0
- package/src/lib/worker-thread/index.ts +3 -0
- package/src/lib/worker-thread/internal/answer-main-thread-messages.ts +141 -0
- package/src/lib/worker-thread/internal/describe-task-body-result.ts +91 -0
- package/src/lib/worker-thread/internal/name-config-key-not-set.ts +57 -0
- package/src/lib/worker-thread/internal/run-task-body.ts +117 -0
- package/src/lib/worker-thread/internal/serve-on-the-parent-port.ts +63 -0
- package/src/lib/worker-thread/internal/start-worker-thread.ts +41 -0
- package/src/lib/worker-thread/lib/provide-task-services/index.ts +1 -0
- package/src/lib/worker-thread/lib/provide-task-services/internal/make-task-logger.ts +34 -0
- package/src/lib/worker-thread/lib/provide-task-services/internal/provide-task-services.ts +86 -0
- package/src/lib/worker-thread/lib/provide-task-services/internal/read-standard-input-from-main-thread.ts +58 -0
- package/src/lib/worker-thread/lib/provide-task-services/internal/resolve-paths-from-package-directory.ts +61 -0
- package/src/lib/worker-thread/lib/provide-task-services/internal/run-commands-in-package-directory.ts +24 -0
- package/src/lib/worker-thread/lib/provide-task-services/internal/spawn-commands-with-bun.ts +545 -0
- package/src/lib/worker-thread/lib/provide-task-services/internal/spawn-reported-process.ts +151 -0
- package/src/lib/worker-thread/shared/await-main-thread-answers.ts +76 -0
- package/src/lib/worker-thread/shared/decode-task-result-json.ts +114 -0
- package/src/lib/worker-thread/shared/define-task-body-record.ts +11 -0
- package/src/lib/worker-thread/shared/describe-issue-input.ts +52 -0
- package/src/lib/worker-thread/shared/plain-object.ts +8 -0
- package/src/lib/worker-thread/shared/provide-worker-state.ts +160 -0
- package/src/lib/worker-thread/shared/worker-thread-errors.ts +41 -0
- package/src/shared/attribute-reported-failures.ts +25 -0
- package/src/shared/claim-processes-to-report.ts +22 -0
- package/src/shared/command-errors.ts +44 -0
- package/src/shared/complete-discarded-calls-once-sent.ts +12 -0
- package/src/shared/cut-to-graphemes.ts +12 -0
- package/src/shared/environment-config-errors.ts +74 -0
- package/src/shared/error-fields.ts +13 -0
- package/src/shared/find-bun-loader-for-script.ts +32 -0
- package/src/shared/find-node-modules-package-directory.ts +35 -0
- package/src/shared/find-user-locations-in-stack-traces.ts +64 -0
- package/src/shared/format-error-lines.ts +53 -0
- package/src/shared/format-task-lines.ts +21 -0
- package/src/shared/hold-task-logs.ts +24 -0
- package/src/shared/invariant-errors.ts +28 -0
- package/src/shared/mark-task-worker-thread-contexts.ts +29 -0
- package/src/shared/module-graph-errors.ts +49 -0
- package/src/shared/name-config-key.ts +42 -0
- package/src/shared/names-from-package-json.ts +7 -0
- package/src/shared/print-to-standard-error.ts +48 -0
- package/src/shared/process-errors.ts +191 -0
- package/src/shared/read-environment-config.ts +239 -0
- package/src/shared/record-tasks-made.ts +10 -0
- package/src/shared/report-cause.ts +140 -0
- package/src/shared/report-errors.ts +121 -0
- package/src/shared/resolve-task-options.ts +20 -0
- package/src/shared/rpc.ts +161 -0
- package/src/shared/run-in-child-fiber-and-join.ts +16 -0
- package/src/shared/run-marker.ts +7 -0
- package/src/shared/save-standard-stream-flags.ts +57 -0
- package/src/shared/select-cause-reasons.ts +14 -0
- package/src/shared/signal-run-called-twice.ts +11 -0
- package/src/shared/standard-input-streams.ts +41 -0
- package/src/shared/stop-signals.ts +41 -0
- package/src/shared/task-errors.ts +224 -0
- package/src/shared/task-package.ts +5 -0
- package/src/shared/task-results.ts +55 -0
- package/src/shared/task-services.ts +30 -0
- package/src/shared/write-trace-file.ts +112 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Cause,
|
|
6
|
+
Context,
|
|
7
|
+
Deferred,
|
|
8
|
+
Duration,
|
|
9
|
+
Effect,
|
|
10
|
+
Exit,
|
|
11
|
+
FiberSet,
|
|
12
|
+
Layer,
|
|
13
|
+
Option,
|
|
14
|
+
Queue,
|
|
15
|
+
Ref,
|
|
16
|
+
Schema,
|
|
17
|
+
Scope,
|
|
18
|
+
Semaphore,
|
|
19
|
+
Stream,
|
|
20
|
+
Tracer,
|
|
21
|
+
} from "effect";
|
|
22
|
+
import { RpcClient, RpcSerialization, RpcServer, RpcWorker } from "effect/rpc";
|
|
23
|
+
import type { RpcClientError, RpcMessage } from "effect/rpc";
|
|
24
|
+
import { Worker as EffectWorker } from "effect/workers";
|
|
25
|
+
|
|
26
|
+
import type { ProcessIdentity } from "../../../shared/claim-processes-to-report.ts";
|
|
27
|
+
import { findUserLocationInStack } from "../../../shared/find-user-locations-in-stack-traces.ts";
|
|
28
|
+
import { dieOnInvariantFailure } from "../../../shared/invariant-errors.ts";
|
|
29
|
+
import { TASK_TRACE_LABEL_PREFIX } from "../../../shared/names-from-package-json.ts";
|
|
30
|
+
import { UncaughtWorkerError } from "../../../shared/process-errors.ts";
|
|
31
|
+
import { bunArguments, commandLineArguments } from "../../../shared/read-environment-config.ts";
|
|
32
|
+
import { describeCauseMessage, ErrorStack } from "../../../shared/report-errors.ts";
|
|
33
|
+
import {
|
|
34
|
+
MainThreadRpcs,
|
|
35
|
+
MessageFromMainThreadRpcClient,
|
|
36
|
+
TASK_EVENT_FIELDS,
|
|
37
|
+
TaskWorkerRpcs,
|
|
38
|
+
WorkerInitialMessage,
|
|
39
|
+
} from "../../../shared/rpc.ts";
|
|
40
|
+
import type { StandardInputRead, StartTaskFiber, WorkerData } from "../../../shared/rpc.ts";
|
|
41
|
+
import type { TaskRequestFailure } from "../../../shared/task-errors.ts";
|
|
42
|
+
import { TASK_PACKAGE } from "../../../shared/task-package.ts";
|
|
43
|
+
import { TraceFile } from "../../../shared/write-trace-file.ts";
|
|
44
|
+
import { ModuleGraph } from "../lib/find-code-each-task-depends-on/index.ts";
|
|
45
|
+
import { DiscoveredTasks } from "./provide-discovered-tasks.ts";
|
|
46
|
+
import { RunRootTaskConfig } from "./provide-run-root-task-config.ts";
|
|
47
|
+
import { StopState } from "./provide-stop-state.ts";
|
|
48
|
+
|
|
49
|
+
const WORKER_EXIT_GRACE_SECONDS = 2;
|
|
50
|
+
|
|
51
|
+
const WORKER_EXIT_GRACE = Duration.seconds(WORKER_EXIT_GRACE_SECONDS);
|
|
52
|
+
|
|
53
|
+
const FORCED_STOP_REASON = "without waiting for it to exit because the stop was forced";
|
|
54
|
+
|
|
55
|
+
export const FromWorker = Schema.TaggedUnion({
|
|
56
|
+
...TASK_EVENT_FIELDS,
|
|
57
|
+
TaskFiberRequestedTask: {
|
|
58
|
+
dependencyRequestId: Schema.Int,
|
|
59
|
+
taskName: Schema.String,
|
|
60
|
+
dependencyTaskName: Schema.String,
|
|
61
|
+
},
|
|
62
|
+
TaskFiberCancelledRequest: { taskName: Schema.String, dependencyRequestId: Schema.Int },
|
|
63
|
+
TaskFiberRequestedStandardInput: { taskName: Schema.String, readId: Schema.Int },
|
|
64
|
+
TaskFiberStartedProcess: {
|
|
65
|
+
taskName: Schema.String,
|
|
66
|
+
recordId: Schema.Int,
|
|
67
|
+
processId: Schema.Int,
|
|
68
|
+
startTime: Schema.NullOr(Schema.String),
|
|
69
|
+
},
|
|
70
|
+
TaskFiberClaimingProcessReports: {
|
|
71
|
+
taskName: Schema.String,
|
|
72
|
+
claimId: Schema.Int,
|
|
73
|
+
processes: Schema.Array(Schema.Struct({ processId: Schema.Int, startTime: Schema.String })),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const decodeMessageFromMainThreadRpcClient = Schema.decodeUnknownExit(
|
|
78
|
+
MessageFromMainThreadRpcClient,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const holdMessagesUntilListened = (thread: Worker) => {
|
|
82
|
+
const inbox = {
|
|
83
|
+
heldMessages: new Array<EffectWorker.PlatformMessage>(),
|
|
84
|
+
deliver: (message: EffectWorker.PlatformMessage) => {
|
|
85
|
+
inbox.heldMessages.push(message);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
thread.on("message", (message: EffectWorker.PlatformMessage) => {
|
|
89
|
+
inbox.deliver(message);
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
postMessage: (...posting: Parameters<Worker["postMessage"]>) => {
|
|
93
|
+
thread.postMessage(...posting);
|
|
94
|
+
},
|
|
95
|
+
listen: (emit: (message: EffectWorker.PlatformMessage) => void) => {
|
|
96
|
+
inbox.deliver = emit;
|
|
97
|
+
for (const message of inbox.heldMessages.splice(0)) {
|
|
98
|
+
emit(message);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const workerThreadPlatform = EffectWorker.makePlatform<
|
|
105
|
+
ReturnType<typeof holdMessagesUntilListened>
|
|
106
|
+
>()({
|
|
107
|
+
setup: ({ worker }) => Effect.succeed(worker),
|
|
108
|
+
listen: ({ port, emit }) =>
|
|
109
|
+
Effect.sync(() => {
|
|
110
|
+
port.listen(emit);
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const WorkerEvent = Schema.TaggedUnion({
|
|
115
|
+
MessageReceived: { workerId: Schema.Int, message: FromWorker },
|
|
116
|
+
MessageUnreadable: { workerId: Schema.Int, reason: Schema.String },
|
|
117
|
+
ExitedWithCode: {
|
|
118
|
+
workerId: Schema.Int,
|
|
119
|
+
exitCode: Schema.Int,
|
|
120
|
+
terminationRequested: Schema.Boolean,
|
|
121
|
+
uncaughtError: Schema.NullOr(UncaughtWorkerError),
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const answerPendingRequest = <Success, Failure>(
|
|
126
|
+
pendingAnswers: Map<number, Deferred.Deferred<Success, Failure>>,
|
|
127
|
+
answerId: number,
|
|
128
|
+
exit: Exit.Exit<Success, Failure>,
|
|
129
|
+
) =>
|
|
130
|
+
Effect.sync(() => {
|
|
131
|
+
const pendingAnswer = pendingAnswers.get(answerId);
|
|
132
|
+
if (pendingAnswer !== undefined) {
|
|
133
|
+
Deferred.doneUnsafe(pendingAnswer, exit);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const makeWorkerPool = Effect.gen(function* () {
|
|
138
|
+
const shutdown = yield* StopState;
|
|
139
|
+
const runRootTaskConfig = yield* RunRootTaskConfig;
|
|
140
|
+
const moduleGraph = yield* ModuleGraph;
|
|
141
|
+
const traceFile = yield* TraceFile;
|
|
142
|
+
const parentSpan = yield* Tracer.ParentSpan;
|
|
143
|
+
const { tasksByName } = yield* DiscoveredTasks;
|
|
144
|
+
const taskFilePaths =
|
|
145
|
+
tasksByName === null
|
|
146
|
+
? []
|
|
147
|
+
: Array.from(tasksByName.values(), (discoveredTask) => discoveredTask.filePath);
|
|
148
|
+
const workerLimit = Math.min(runRootTaskConfig.workerCount, taskFilePaths.length);
|
|
149
|
+
const workersById = new Map<
|
|
150
|
+
number,
|
|
151
|
+
{
|
|
152
|
+
readonly thread: Worker;
|
|
153
|
+
readonly client: RpcClient.FromGroup<typeof TaskWorkerRpcs, RpcClientError.RpcClientError>;
|
|
154
|
+
readonly exitDeferred: Deferred.Deferred<void>;
|
|
155
|
+
terminationRequested: boolean;
|
|
156
|
+
terminateCalled: boolean;
|
|
157
|
+
uncaughtError: Option.Option<typeof UncaughtWorkerError.Type>;
|
|
158
|
+
}
|
|
159
|
+
>();
|
|
160
|
+
const workerSelectionSemaphore = yield* Semaphore.make(1);
|
|
161
|
+
const workerEvents = yield* Queue.unbounded<typeof WorkerEvent.Type, Cause.Done>();
|
|
162
|
+
const workerConnectionsScope = yield* Scope.fork(yield* Effect.scope);
|
|
163
|
+
const callsToWorkers = yield* FiberSet.make().pipe(Scope.provide(workerConnectionsScope));
|
|
164
|
+
const nextAnswerId = yield* Ref.make(0);
|
|
165
|
+
const pendingDependencyResults = new Map<
|
|
166
|
+
number,
|
|
167
|
+
Deferred.Deferred<{ readonly value?: Schema.Json }, typeof TaskRequestFailure.Type>
|
|
168
|
+
>();
|
|
169
|
+
const pendingStandardInputReads = new Map<
|
|
170
|
+
number,
|
|
171
|
+
Deferred.Deferred<typeof StandardInputRead.Type>
|
|
172
|
+
>();
|
|
173
|
+
const pendingProcessRecords = new Map<number, Deferred.Deferred<void>>();
|
|
174
|
+
const pendingReportClaims = new Map<
|
|
175
|
+
number,
|
|
176
|
+
Deferred.Deferred<readonly (typeof ProcessIdentity.Type)[]>
|
|
177
|
+
>();
|
|
178
|
+
const receiveMessage = (workerId: number, message: typeof FromWorker.Type) => {
|
|
179
|
+
Queue.offerUnsafe(workerEvents, WorkerEvent.cases.MessageReceived.make({ workerId, message }));
|
|
180
|
+
};
|
|
181
|
+
const reportAndAwaitAnswer = <Success, Failure>(
|
|
182
|
+
workerId: number,
|
|
183
|
+
pendingAnswers: Map<number, Deferred.Deferred<Success, Failure>>,
|
|
184
|
+
makeMessage: (answerId: number) => typeof FromWorker.Type,
|
|
185
|
+
makeCancelledMessage: ((answerId: number) => typeof FromWorker.Type) | null,
|
|
186
|
+
) =>
|
|
187
|
+
Effect.gen(function* () {
|
|
188
|
+
const answerId = yield* Ref.getAndUpdate(nextAnswerId, (id) => id + 1);
|
|
189
|
+
const answer = Deferred.makeUnsafe<Success, Failure>();
|
|
190
|
+
pendingAnswers.set(answerId, answer);
|
|
191
|
+
receiveMessage(workerId, makeMessage(answerId));
|
|
192
|
+
return yield* Deferred.await(answer).pipe(
|
|
193
|
+
Effect.onInterrupt(() =>
|
|
194
|
+
Effect.sync(() => {
|
|
195
|
+
if (makeCancelledMessage !== null) {
|
|
196
|
+
receiveMessage(workerId, makeCancelledMessage(answerId));
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
Effect.ensuring(
|
|
201
|
+
Effect.sync(() => {
|
|
202
|
+
pendingAnswers.delete(answerId);
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
const makeWorkerRpcServerProtocol = (workerId: number, workerPort: MessagePort) =>
|
|
208
|
+
RpcServer.Protocol.make((writeRequest) =>
|
|
209
|
+
Effect.gen(function* () {
|
|
210
|
+
const runInPoolScope = yield* FiberSet.makeRuntime<never, void, never>();
|
|
211
|
+
const onMessage = (event: { readonly data: unknown }) => {
|
|
212
|
+
Exit.match(decodeMessageFromMainThreadRpcClient(event.data), {
|
|
213
|
+
onSuccess: (request) => {
|
|
214
|
+
runInPoolScope(writeRequest(0, request));
|
|
215
|
+
},
|
|
216
|
+
onFailure: (cause) => {
|
|
217
|
+
Queue.offerUnsafe(
|
|
218
|
+
workerEvents,
|
|
219
|
+
WorkerEvent.cases.MessageUnreadable.make({ workerId, reason: Cause.pretty(cause) }),
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
yield* Effect.acquireRelease(
|
|
225
|
+
Effect.sync(() => {
|
|
226
|
+
workerPort.addEventListener("message", onMessage);
|
|
227
|
+
workerPort.start();
|
|
228
|
+
}),
|
|
229
|
+
() =>
|
|
230
|
+
Effect.sync(() => {
|
|
231
|
+
workerPort.removeEventListener("message", onMessage);
|
|
232
|
+
workerPort.close();
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
const workerRpcServerProtocol = {
|
|
236
|
+
disconnects: yield* Queue.make<number>(),
|
|
237
|
+
send: (_clientId: number, response: RpcMessage.FromServerEncoded) =>
|
|
238
|
+
Effect.sync(() => {
|
|
239
|
+
workerPort.postMessage(response);
|
|
240
|
+
}),
|
|
241
|
+
end: () => Effect.void,
|
|
242
|
+
clientIds: Effect.succeed(new Set([0])),
|
|
243
|
+
initialMessage: Effect.succeedNone,
|
|
244
|
+
supportsAck: false,
|
|
245
|
+
supportsTransferables: false,
|
|
246
|
+
supportsSpanPropagation: false,
|
|
247
|
+
supportsNotifications: false,
|
|
248
|
+
codecFor: RpcSerialization.json.codecFor,
|
|
249
|
+
};
|
|
250
|
+
return workerRpcServerProtocol;
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
const serveWorker = (workerId: number, workerPort: MessagePort) =>
|
|
254
|
+
Effect.gen(function* () {
|
|
255
|
+
const handlers = yield* MainThreadRpcs.toHandlers({
|
|
256
|
+
ReportTaskEvent: (event) =>
|
|
257
|
+
Effect.sync(() => {
|
|
258
|
+
receiveMessage(workerId, event);
|
|
259
|
+
}),
|
|
260
|
+
RequestTask: (requested) =>
|
|
261
|
+
reportAndAwaitAnswer(
|
|
262
|
+
workerId,
|
|
263
|
+
pendingDependencyResults,
|
|
264
|
+
(dependencyRequestId) =>
|
|
265
|
+
FromWorker.cases.TaskFiberRequestedTask.make({ ...requested, dependencyRequestId }),
|
|
266
|
+
(dependencyRequestId) =>
|
|
267
|
+
FromWorker.cases.TaskFiberCancelledRequest.make({
|
|
268
|
+
taskName: requested.taskName,
|
|
269
|
+
dependencyRequestId,
|
|
270
|
+
}),
|
|
271
|
+
),
|
|
272
|
+
ReadStandardInput: (requested) =>
|
|
273
|
+
reportAndAwaitAnswer(
|
|
274
|
+
workerId,
|
|
275
|
+
pendingStandardInputReads,
|
|
276
|
+
(readId) =>
|
|
277
|
+
FromWorker.cases.TaskFiberRequestedStandardInput.make({
|
|
278
|
+
taskName: requested.taskName,
|
|
279
|
+
readId,
|
|
280
|
+
}),
|
|
281
|
+
null,
|
|
282
|
+
),
|
|
283
|
+
RecordStartedProcess: (started) =>
|
|
284
|
+
reportAndAwaitAnswer(
|
|
285
|
+
workerId,
|
|
286
|
+
pendingProcessRecords,
|
|
287
|
+
(recordId) => FromWorker.cases.TaskFiberStartedProcess.make({ ...started, recordId }),
|
|
288
|
+
null,
|
|
289
|
+
),
|
|
290
|
+
ClaimProcessReports: (claiming) =>
|
|
291
|
+
reportAndAwaitAnswer(
|
|
292
|
+
workerId,
|
|
293
|
+
pendingReportClaims,
|
|
294
|
+
(claimId) =>
|
|
295
|
+
FromWorker.cases.TaskFiberClaimingProcessReports.make({ ...claiming, claimId }),
|
|
296
|
+
null,
|
|
297
|
+
),
|
|
298
|
+
});
|
|
299
|
+
yield* RpcServer.make(MainThreadRpcs, { disableTracing: true }).pipe(
|
|
300
|
+
Effect.provideContext(handlers),
|
|
301
|
+
Effect.provideServiceEffect(
|
|
302
|
+
RpcServer.Protocol,
|
|
303
|
+
makeWorkerRpcServerProtocol(workerId, workerPort),
|
|
304
|
+
),
|
|
305
|
+
Effect.interruptible,
|
|
306
|
+
Effect.forkScoped,
|
|
307
|
+
Scope.provide(workerConnectionsScope),
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
const connectToWorker = (
|
|
311
|
+
thread: ReturnType<typeof holdMessagesUntilListened>,
|
|
312
|
+
mainThreadPort: MessagePort,
|
|
313
|
+
) =>
|
|
314
|
+
RpcClient.make(TaskWorkerRpcs, { disableTracing: true }).pipe(
|
|
315
|
+
Effect.provideServiceEffect(
|
|
316
|
+
RpcClient.Protocol,
|
|
317
|
+
RpcClient.makeProtocolWorker({ size: 1, concurrency: Number.POSITIVE_INFINITY }),
|
|
318
|
+
),
|
|
319
|
+
Effect.provideService(EffectWorker.WorkerPlatform, workerThreadPlatform),
|
|
320
|
+
Effect.provideService(EffectWorker.Spawner, () => thread),
|
|
321
|
+
Effect.provideService(
|
|
322
|
+
RpcWorker.InitialMessage,
|
|
323
|
+
RpcWorker.makeInitialMessage(WorkerInitialMessage, Effect.succeed({ mainThreadPort })).pipe(
|
|
324
|
+
dieOnInvariantFailure("worker startup", "the main thread's RPC port can be handed over"),
|
|
325
|
+
),
|
|
326
|
+
),
|
|
327
|
+
Scope.provide(workerConnectionsScope),
|
|
328
|
+
dieOnInvariantFailure("worker startup", "a started worker can be called over RPC"),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const startWorker = (
|
|
332
|
+
workerId: number,
|
|
333
|
+
rewrittenFiles: (typeof WorkerData.Type)["rewrittenFiles"],
|
|
334
|
+
) =>
|
|
335
|
+
Effect.gen(function* () {
|
|
336
|
+
const workerData = {
|
|
337
|
+
workerId,
|
|
338
|
+
traceFilePath: traceFile.filePath,
|
|
339
|
+
projectDirectoryPath: runRootTaskConfig.rootTaskPackageJsonDirectoryPath,
|
|
340
|
+
rewrittenFiles,
|
|
341
|
+
taskLogHold: shutdown.taskLogHold,
|
|
342
|
+
runMarker: runRootTaskConfig.runMarker,
|
|
343
|
+
} satisfies typeof WorkerData.Type;
|
|
344
|
+
const rpcChannel = new MessageChannel();
|
|
345
|
+
yield* serveWorker(workerId, rpcChannel.port1);
|
|
346
|
+
const thread = new Worker(new URL("../../worker-thread/index.ts", import.meta.url), {
|
|
347
|
+
workerData,
|
|
348
|
+
argv: yield* commandLineArguments,
|
|
349
|
+
execArgv: yield* bunArguments,
|
|
350
|
+
...(Object.keys(rewrittenFiles).length === 0
|
|
351
|
+
? {}
|
|
352
|
+
: {
|
|
353
|
+
preload: [
|
|
354
|
+
fileURLToPath(
|
|
355
|
+
new URL(
|
|
356
|
+
"../../preload-rewritten-imports-into-each-worker/index.ts",
|
|
357
|
+
import.meta.url,
|
|
358
|
+
),
|
|
359
|
+
),
|
|
360
|
+
],
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
const threadMessages = holdMessagesUntilListened(thread);
|
|
364
|
+
const workerThread = {
|
|
365
|
+
thread,
|
|
366
|
+
client: yield* connectToWorker(threadMessages, rpcChannel.port2),
|
|
367
|
+
exitDeferred: yield* Deferred.make<void>(),
|
|
368
|
+
terminationRequested: false,
|
|
369
|
+
terminateCalled: false,
|
|
370
|
+
uncaughtError: Option.none(),
|
|
371
|
+
} satisfies Parameters<typeof workersById.set>[1];
|
|
372
|
+
workersById.set(workerId, workerThread);
|
|
373
|
+
workerThread.thread.on("error", (error) => {
|
|
374
|
+
workerThread.uncaughtError = Option.some({
|
|
375
|
+
message: describeCauseMessage(error),
|
|
376
|
+
location: Option.getOrNull(
|
|
377
|
+
Option.flatMap(Schema.decodeUnknownOption(ErrorStack)(error), (withStack) =>
|
|
378
|
+
Option.fromUndefinedOr(findUserLocationInStack(withStack.stack)),
|
|
379
|
+
),
|
|
380
|
+
),
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
workerThread.thread.on("exit", (exitCode) => {
|
|
384
|
+
Deferred.doneUnsafe(workerThread.exitDeferred, Effect.void);
|
|
385
|
+
Queue.offerUnsafe(
|
|
386
|
+
workerEvents,
|
|
387
|
+
WorkerEvent.cases.ExitedWithCode.make({
|
|
388
|
+
workerId,
|
|
389
|
+
exitCode,
|
|
390
|
+
terminationRequested: workerThread.terminationRequested,
|
|
391
|
+
uncaughtError: Option.getOrNull(workerThread.uncaughtError),
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
const readRewrittenWorkerImports = yield* Effect.cached(
|
|
397
|
+
moduleGraph.rewriteBareImports([
|
|
398
|
+
fileURLToPath(new URL("../../worker-thread/index.ts", import.meta.url)),
|
|
399
|
+
...taskFilePaths,
|
|
400
|
+
]),
|
|
401
|
+
);
|
|
402
|
+
const startNextWorker = Effect.gen(function* () {
|
|
403
|
+
const workerId = workersById.size;
|
|
404
|
+
const rewrittenWorkerImports = yield* readRewrittenWorkerImports;
|
|
405
|
+
if (yield* shutdown.isStopping) {
|
|
406
|
+
return yield* Effect.interrupt;
|
|
407
|
+
}
|
|
408
|
+
yield* startWorker(workerId, rewrittenWorkerImports);
|
|
409
|
+
return workerId;
|
|
410
|
+
});
|
|
411
|
+
const terminateWorker = (
|
|
412
|
+
workerId: number,
|
|
413
|
+
workerThread: Parameters<typeof workersById.set>[1],
|
|
414
|
+
reason: string,
|
|
415
|
+
) =>
|
|
416
|
+
Effect.gen(function* () {
|
|
417
|
+
const isWorkerEnding =
|
|
418
|
+
workerThread.terminateCalled || Deferred.isDoneUnsafe(workerThread.exitDeferred);
|
|
419
|
+
if (isWorkerEnding) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
workerThread.terminateCalled = true;
|
|
423
|
+
workerThread.terminationRequested = true;
|
|
424
|
+
yield* Effect.logInfo(`worker terminated ${reason}`).pipe(
|
|
425
|
+
Effect.annotateLogs(`${TASK_TRACE_LABEL_PREFIX}.worker`, workerId),
|
|
426
|
+
);
|
|
427
|
+
void workerThread.thread.terminate();
|
|
428
|
+
}).pipe(Effect.uninterruptible);
|
|
429
|
+
const requestExitOfAllWorkers = Effect.forEach(
|
|
430
|
+
workersById.values(),
|
|
431
|
+
(workerThread) =>
|
|
432
|
+
Effect.gen(function* () {
|
|
433
|
+
const isWorkerLeaving =
|
|
434
|
+
workerThread.terminationRequested || Deferred.isDoneUnsafe(workerThread.exitDeferred);
|
|
435
|
+
if (isWorkerLeaving) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
workerThread.terminationRequested = true;
|
|
439
|
+
yield* FiberSet.run(
|
|
440
|
+
callsToWorkers,
|
|
441
|
+
workerThread.client
|
|
442
|
+
.ExitWorker()
|
|
443
|
+
.pipe(Effect.catchTag("RpcClientError", () => Effect.void)),
|
|
444
|
+
);
|
|
445
|
+
}),
|
|
446
|
+
{ discard: true },
|
|
447
|
+
);
|
|
448
|
+
const terminateAllWorkers = Effect.forEach(
|
|
449
|
+
workersById,
|
|
450
|
+
([workerId, workerThread]) => terminateWorker(workerId, workerThread, FORCED_STOP_REASON),
|
|
451
|
+
{ discard: true },
|
|
452
|
+
);
|
|
453
|
+
const awaitWorkerShutdown = Effect.forEach(
|
|
454
|
+
workersById,
|
|
455
|
+
([workerId, workerThread]) =>
|
|
456
|
+
Effect.gen(function* () {
|
|
457
|
+
const exited = yield* Deferred.await(workerThread.exitDeferred).pipe(
|
|
458
|
+
Effect.timeoutOption(WORKER_EXIT_GRACE),
|
|
459
|
+
Effect.raceFirst(shutdown.awaitForcedStop.pipe(Effect.as(Option.none()))),
|
|
460
|
+
);
|
|
461
|
+
if (Option.isSome(exited)) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
yield* terminateWorker(
|
|
465
|
+
workerId,
|
|
466
|
+
workerThread,
|
|
467
|
+
(yield* shutdown.isForced)
|
|
468
|
+
? FORCED_STOP_REASON
|
|
469
|
+
: `because it did not exit within ${Duration.format(WORKER_EXIT_GRACE)} of being asked to`,
|
|
470
|
+
);
|
|
471
|
+
yield* Deferred.await(workerThread.exitDeferred);
|
|
472
|
+
}),
|
|
473
|
+
{ concurrency: "unbounded", discard: true },
|
|
474
|
+
).pipe(Effect.andThen(Queue.end(workerEvents)));
|
|
475
|
+
const findReachableWorker = (workerId: number, rpcTag: string) =>
|
|
476
|
+
Effect.gen(function* () {
|
|
477
|
+
const workerThread = yield* Effect.fromNullishOr(workersById.get(workerId)).pipe(
|
|
478
|
+
dieOnInvariantFailure("worker pool", "messages go to a started worker"),
|
|
479
|
+
);
|
|
480
|
+
const isWorkerGone =
|
|
481
|
+
workerThread.terminationRequested || Deferred.isDoneUnsafe(workerThread.exitDeferred);
|
|
482
|
+
if (isWorkerGone) {
|
|
483
|
+
yield* Effect.logDebug(
|
|
484
|
+
"message not sent because its worker is gone, and that worker's exit ends its tasks",
|
|
485
|
+
).pipe(
|
|
486
|
+
Effect.annotateLogs({
|
|
487
|
+
[`${TASK_TRACE_LABEL_PREFIX}.worker`]: workerId,
|
|
488
|
+
[`${TASK_TRACE_LABEL_PREFIX}.message`]: rpcTag,
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
return workerThread;
|
|
494
|
+
});
|
|
495
|
+
const callWorker = (
|
|
496
|
+
workerId: number,
|
|
497
|
+
rpcTag: string,
|
|
498
|
+
call: (
|
|
499
|
+
client: RpcClient.FromGroup<typeof TaskWorkerRpcs, RpcClientError.RpcClientError>,
|
|
500
|
+
) => Effect.Effect<void, RpcClientError.RpcClientError>,
|
|
501
|
+
) =>
|
|
502
|
+
Effect.gen(function* () {
|
|
503
|
+
const workerThread = yield* findReachableWorker(workerId, rpcTag);
|
|
504
|
+
if (workerThread === null) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
yield* FiberSet.run(
|
|
508
|
+
callsToWorkers,
|
|
509
|
+
call(workerThread.client).pipe(Effect.catchTag("RpcClientError", () => Effect.void)),
|
|
510
|
+
);
|
|
511
|
+
return true;
|
|
512
|
+
});
|
|
513
|
+
yield* Effect.addFinalizer(() => Effect.andThen(requestExitOfAllWorkers, awaitWorkerShutdown));
|
|
514
|
+
return {
|
|
515
|
+
events: Stream.fromQueue(workerEvents).pipe(
|
|
516
|
+
Stream.provideService(Tracer.ParentSpan, parentSpan),
|
|
517
|
+
),
|
|
518
|
+
selectWorkerIdForTask: (
|
|
519
|
+
readWorkerLoad: (workerId: number) => {
|
|
520
|
+
readonly occupyingTaskCount: number;
|
|
521
|
+
readonly servingTaskCount: number;
|
|
522
|
+
},
|
|
523
|
+
recordAssignment: (workerId: number) => Effect.Effect<void>,
|
|
524
|
+
) =>
|
|
525
|
+
Effect.gen(function* () {
|
|
526
|
+
if (yield* shutdown.isStopping) {
|
|
527
|
+
return yield* Effect.interrupt;
|
|
528
|
+
}
|
|
529
|
+
const usableWorkerIds = [...workersById]
|
|
530
|
+
.filter(
|
|
531
|
+
([, workerThread]) =>
|
|
532
|
+
!workerThread.terminationRequested &&
|
|
533
|
+
!Deferred.isDoneUnsafe(workerThread.exitDeferred),
|
|
534
|
+
)
|
|
535
|
+
.map(([workerId]) => workerId);
|
|
536
|
+
const usableWorkerLoads = usableWorkerIds.map(
|
|
537
|
+
(usableWorkerId) => [usableWorkerId, readWorkerLoad(usableWorkerId)] as const,
|
|
538
|
+
);
|
|
539
|
+
const idleWorkerId = usableWorkerLoads.find(
|
|
540
|
+
([, load]) => load.occupyingTaskCount === 0 && load.servingTaskCount === 0,
|
|
541
|
+
)?.[0];
|
|
542
|
+
const workerId =
|
|
543
|
+
idleWorkerId ??
|
|
544
|
+
(workersById.size < workerLimit
|
|
545
|
+
? yield* startNextWorker
|
|
546
|
+
: usableWorkerLoads.toSorted(
|
|
547
|
+
([, left], [, right]) =>
|
|
548
|
+
left.occupyingTaskCount - right.occupyingTaskCount ||
|
|
549
|
+
left.servingTaskCount - right.servingTaskCount,
|
|
550
|
+
)[0]?.[0]);
|
|
551
|
+
if (workerId === undefined) {
|
|
552
|
+
yield* shutdown.awaitFailure;
|
|
553
|
+
return yield* Effect.interrupt;
|
|
554
|
+
}
|
|
555
|
+
yield* recordAssignment(workerId);
|
|
556
|
+
return workerId;
|
|
557
|
+
}).pipe(workerSelectionSemaphore.withPermits(1)),
|
|
558
|
+
startTaskFiber: (workerId: number, start: typeof StartTaskFiber.Type) =>
|
|
559
|
+
callWorker(workerId, "StartTaskFiber", (client) => client.StartTaskFiber(start)),
|
|
560
|
+
stopTaskFiber: (workerId: number, taskName: string) =>
|
|
561
|
+
callWorker(workerId, "StopTaskFiber", (client) => client.StopTaskFiber({ taskName })),
|
|
562
|
+
answerDependencyRequest: (
|
|
563
|
+
dependencyRequestId: number,
|
|
564
|
+
exit: Exit.Exit<Schema.Json | undefined, typeof TaskRequestFailure.Type>,
|
|
565
|
+
) =>
|
|
566
|
+
answerPendingRequest(
|
|
567
|
+
pendingDependencyResults,
|
|
568
|
+
dependencyRequestId,
|
|
569
|
+
Exit.map(exit, (value) => (value === undefined ? {} : { value })),
|
|
570
|
+
),
|
|
571
|
+
answerStandardInputRead: (readId: number, read: typeof StandardInputRead.Type) =>
|
|
572
|
+
answerPendingRequest(pendingStandardInputReads, readId, Exit.succeed(read)),
|
|
573
|
+
acknowledgeProcessRecorded: (recordId: number) =>
|
|
574
|
+
answerPendingRequest(pendingProcessRecords, recordId, Exit.void),
|
|
575
|
+
answerReportClaim: (claimId: number, claimed: readonly (typeof ProcessIdentity.Type)[]) =>
|
|
576
|
+
answerPendingRequest(pendingReportClaims, claimId, Exit.succeed(claimed)),
|
|
577
|
+
awaitAnswer: (workerId: number) =>
|
|
578
|
+
Effect.gen(function* () {
|
|
579
|
+
const reachableWorker = yield* findReachableWorker(workerId, "AnswerIfResponsive");
|
|
580
|
+
if (reachableWorker === null) {
|
|
581
|
+
return "gone" as const;
|
|
582
|
+
}
|
|
583
|
+
return yield* reachableWorker.client.AnswerIfResponsive().pipe(
|
|
584
|
+
Effect.as("answered" as const),
|
|
585
|
+
Effect.catchTag("RpcClientError", () => Effect.succeed("gone" as const)),
|
|
586
|
+
Effect.raceFirst(
|
|
587
|
+
Deferred.await(reachableWorker.exitDeferred).pipe(Effect.as("gone" as const)),
|
|
588
|
+
),
|
|
589
|
+
);
|
|
590
|
+
}),
|
|
591
|
+
requestWorkerTermination: (workerId: number, reason: string) =>
|
|
592
|
+
Effect.fromNullishOr(workersById.get(workerId)).pipe(
|
|
593
|
+
dieOnInvariantFailure("worker pool", "a worker whose termination is requested was started"),
|
|
594
|
+
Effect.flatMap((workerThread) => terminateWorker(workerId, workerThread, reason)),
|
|
595
|
+
),
|
|
596
|
+
requestExitOfAllWorkers,
|
|
597
|
+
terminateAllWorkers,
|
|
598
|
+
awaitWorkerShutdown,
|
|
599
|
+
startedWorkerCount: Effect.sync(() => workersById.size),
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
export class WorkerPool extends Context.Service<
|
|
604
|
+
WorkerPool,
|
|
605
|
+
Effect.Success<typeof makeWorkerPool>
|
|
606
|
+
>()(`${TASK_PACKAGE.name}/WorkerPool`) {}
|
|
607
|
+
export const WorkerPoolLive = Layer.effect(WorkerPool, makeWorkerPool);
|