@knpkv/codecommit-core 0.14.0 → 0.16.0
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/README.md +7 -3
- package/dist/CacheService/Database.d.ts +1 -1
- package/dist/CacheService/Database.d.ts.map +1 -1
- package/dist/CacheService/Database.js +12 -2
- package/dist/CacheService/Database.js.map +1 -1
- package/dist/CacheService/diff.d.ts +5 -2
- package/dist/CacheService/diff.d.ts.map +1 -1
- package/dist/CacheService/diff.js +31 -13
- package/dist/CacheService/diff.js.map +1 -1
- package/dist/CacheService/migrations/0017_sandbox_region.d.ts +5 -0
- package/dist/CacheService/migrations/0017_sandbox_region.d.ts.map +1 -0
- package/dist/CacheService/migrations/0017_sandbox_region.js +4 -0
- package/dist/CacheService/migrations/0017_sandbox_region.js.map +1 -0
- package/dist/CacheService/migrations/0018_pull_request_coordinates.d.ts +6 -0
- package/dist/CacheService/migrations/0018_pull_request_coordinates.d.ts.map +1 -0
- package/dist/CacheService/migrations/0018_pull_request_coordinates.js +82 -0
- package/dist/CacheService/migrations/0018_pull_request_coordinates.js.map +1 -0
- package/dist/CacheService/migrations/0019_dependent_pr_coordinates.d.ts +6 -0
- package/dist/CacheService/migrations/0019_dependent_pr_coordinates.d.ts.map +1 -0
- package/dist/CacheService/migrations/0019_dependent_pr_coordinates.js +57 -0
- package/dist/CacheService/migrations/0019_dependent_pr_coordinates.js.map +1 -0
- package/dist/CacheService/migrations/0020_notification_coordinates.d.ts +6 -0
- package/dist/CacheService/migrations/0020_notification_coordinates.d.ts.map +1 -0
- package/dist/CacheService/migrations/0020_notification_coordinates.js +22 -0
- package/dist/CacheService/migrations/0020_notification_coordinates.js.map +1 -0
- package/dist/CacheService/migrations/0021_sandbox_legacy_retirement.d.ts +6 -0
- package/dist/CacheService/migrations/0021_sandbox_legacy_retirement.d.ts.map +1 -0
- package/dist/CacheService/migrations/0021_sandbox_legacy_retirement.js +5 -0
- package/dist/CacheService/migrations/0021_sandbox_legacy_retirement.js.map +1 -0
- package/dist/CacheService/repos/CommentRepo.d.ts +6 -2
- package/dist/CacheService/repos/CommentRepo.d.ts.map +1 -1
- package/dist/CacheService/repos/CommentRepo.js +46 -7
- package/dist/CacheService/repos/CommentRepo.js.map +1 -1
- package/dist/CacheService/repos/NotificationRepo.d.ts +2 -0
- package/dist/CacheService/repos/NotificationRepo.d.ts.map +1 -1
- package/dist/CacheService/repos/NotificationRepo.js +10 -3
- package/dist/CacheService/repos/NotificationRepo.js.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/index.d.ts +42 -7
- package/dist/CacheService/repos/PullRequestRepo/index.d.ts.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/index.js +2 -0
- package/dist/CacheService/repos/PullRequestRepo/index.js.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/mutations.d.ts +9 -5
- package/dist/CacheService/repos/PullRequestRepo/mutations.d.ts.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/mutations.js +61 -17
- package/dist/CacheService/repos/PullRequestRepo/mutations.js.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/queries.d.ts +48 -4
- package/dist/CacheService/repos/PullRequestRepo/queries.d.ts.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/queries.js +40 -5
- package/dist/CacheService/repos/PullRequestRepo/queries.js.map +1 -1
- package/dist/CacheService/repos/SandboxRepo.d.ts +53 -1
- package/dist/CacheService/repos/SandboxRepo.d.ts.map +1 -1
- package/dist/CacheService/repos/SandboxRepo.js +55 -8
- package/dist/CacheService/repos/SandboxRepo.js.map +1 -1
- package/dist/CacheService/repos/StatsRepo/index.d.ts +3 -0
- package/dist/CacheService/repos/StatsRepo/index.d.ts.map +1 -1
- package/dist/CacheService/repos/StatsRepo/internal.d.ts +6 -0
- package/dist/CacheService/repos/StatsRepo/internal.d.ts.map +1 -1
- package/dist/CacheService/repos/StatsRepo/internal.js +5 -5
- package/dist/CacheService/repos/StatsRepo/internal.js.map +1 -1
- package/dist/CacheService/repos/StatsRepo/queries.d.ts +1 -0
- package/dist/CacheService/repos/StatsRepo/queries.d.ts.map +1 -1
- package/dist/CacheService/repos/StatsRepo/queries.js +5 -4
- package/dist/CacheService/repos/StatsRepo/queries.js.map +1 -1
- package/dist/CacheService/repos/StatsRepo/reviewerData.d.ts +2 -0
- package/dist/CacheService/repos/StatsRepo/reviewerData.d.ts.map +1 -1
- package/dist/CacheService/repos/StatsRepo/reviewerData.js +38 -15
- package/dist/CacheService/repos/StatsRepo/reviewerData.js.map +1 -1
- package/dist/CacheService/repos/SubscriptionRepo.d.ts +11 -5
- package/dist/CacheService/repos/SubscriptionRepo.d.ts.map +1 -1
- package/dist/CacheService/repos/SubscriptionRepo.js +106 -12
- package/dist/CacheService/repos/SubscriptionRepo.js.map +1 -1
- package/dist/ChildEnv.d.ts.map +1 -1
- package/dist/ChildEnv.js +1 -0
- package/dist/ChildEnv.js.map +1 -1
- package/dist/ConfigService/index.d.ts +1 -1
- package/dist/ConfigService/index.d.ts.map +1 -1
- package/dist/ConfigService/index.js +1 -1
- package/dist/ConfigService/index.js.map +1 -1
- package/dist/ConfigService/internal.d.ts +3 -3
- package/dist/ConfigService/load.d.ts +3 -3
- package/dist/ConfigService/reset.d.ts +3 -3
- package/dist/ConfigService/save.d.ts +3 -3
- package/dist/Domain.d.ts +1 -0
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +1 -0
- package/dist/Domain.js.map +1 -1
- package/dist/PRService/index.d.ts +15 -8
- package/dist/PRService/index.d.ts.map +1 -1
- package/dist/PRService/index.js +5 -5
- package/dist/PRService/index.js.map +1 -1
- package/dist/PRService/refreshDiffs.d.ts.map +1 -1
- package/dist/PRService/refreshDiffs.js +12 -3
- package/dist/PRService/refreshDiffs.js.map +1 -1
- package/dist/PRService/refreshEnrich.d.ts.map +1 -1
- package/dist/PRService/refreshEnrich.js +28 -11
- package/dist/PRService/refreshEnrich.js.map +1 -1
- package/dist/PRService/refreshFetch.d.ts.map +1 -1
- package/dist/PRService/refreshFetch.js +25 -13
- package/dist/PRService/refreshFetch.js.map +1 -1
- package/dist/PRService/refreshHistory.d.ts +1 -1
- package/dist/PRService/refreshHistory.d.ts.map +1 -1
- package/dist/PRService/refreshHistory.js +6 -4
- package/dist/PRService/refreshHistory.js.map +1 -1
- package/dist/PRService/refreshResolve.d.ts +4 -1
- package/dist/PRService/refreshResolve.d.ts.map +1 -1
- package/dist/PRService/refreshResolve.js +19 -6
- package/dist/PRService/refreshResolve.js.map +1 -1
- package/dist/PRService/refreshScore.d.ts.map +1 -1
- package/dist/PRService/refreshScore.js +4 -1
- package/dist/PRService/refreshScore.js.map +1 -1
- package/dist/PRService/refreshSinglePR.d.ts +9 -2
- package/dist/PRService/refreshSinglePR.d.ts.map +1 -1
- package/dist/PRService/refreshSinglePR.js +99 -33
- package/dist/PRService/refreshSinglePR.js.map +1 -1
- package/dist/ReviewProfile.d.ts +14 -12
- package/dist/ReviewProfile.d.ts.map +1 -1
- package/dist/ReviewProfile.js +10 -3
- package/dist/ReviewProfile.js.map +1 -1
- package/dist/SandboxService/DockerService.d.ts +4 -0
- package/dist/SandboxService/DockerService.d.ts.map +1 -1
- package/dist/SandboxService/DockerService.js +60 -16
- package/dist/SandboxService/DockerService.js.map +1 -1
- package/dist/SandboxService/SandboxService.d.ts +8 -0
- package/dist/SandboxService/SandboxService.d.ts.map +1 -1
- package/dist/SandboxService/SandboxService.js +459 -77
- package/dist/SandboxService/SandboxService.js.map +1 -1
- package/dist/SandboxService/SandboxWorkerScope.d.ts +6 -1
- package/dist/SandboxService/SandboxWorkerScope.d.ts.map +1 -1
- package/dist/SandboxService/SandboxWorkerScope.js +6 -2
- package/dist/SandboxService/SandboxWorkerScope.js.map +1 -1
- package/dist/StatsService/WeeklyStats.d.ts +7 -0
- package/dist/StatsService/WeeklyStats.d.ts.map +1 -1
- package/dist/StatsService/WeeklyStats.js +3 -0
- package/dist/StatsService/WeeklyStats.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { Clock, Config, Context, Crypto, Duration, Effect, Layer, Option, Predicate, Random, Result, Schedule, Stream } from "effect";
|
|
9
|
+
import { Clock, Config, Context, Crypto, Duration, Effect, Fiber, Layer, Option, Predicate, Random, Ref, Result, Schedule, Semaphore, Stream } from "effect";
|
|
10
10
|
import * as FileSystem from "effect/FileSystem";
|
|
11
11
|
import { ChildProcess } from "effect/unstable/process";
|
|
12
12
|
import { SandboxRepo } from "../CacheService/repos/SandboxRepo.js";
|
|
@@ -14,7 +14,7 @@ import * as ChildEnv from "../ChildEnv.js";
|
|
|
14
14
|
import { ConfigService, defaultSandboxConfig, validateSandboxConfig } from "../ConfigService/index.js";
|
|
15
15
|
import { PullRequestId, RepositoryName, SandboxId } from "../Domain.js";
|
|
16
16
|
import { SandboxError } from "../Errors.js";
|
|
17
|
-
import { DockerService } from "./DockerService.js";
|
|
17
|
+
import { DockerService, isAlreadyStoppedContainerError, isMissingContainerError } from "./DockerService.js";
|
|
18
18
|
import { PluginService } from "./PluginService.js";
|
|
19
19
|
import { SandboxWorkerScope } from "./SandboxWorkerScope.js";
|
|
20
20
|
/** Keep the container non-root while matching the bind-mounted workspace owner when available. */
|
|
@@ -26,6 +26,14 @@ export const sandboxContainerIdentityForWorkspaceOwner = (uid, gid) => uid === 0
|
|
|
26
26
|
const SANDBOX_BASE_PORT = 18080;
|
|
27
27
|
export const sandboxRuntimeHome = "/tmp";
|
|
28
28
|
export const sandboxRuntimeXdgDataHome = `${sandboxRuntimeHome}/.local/share`;
|
|
29
|
+
const isRegionlessSandbox = (row) => row.region === undefined || row.region === null || row.region === "";
|
|
30
|
+
const isPreContainerSandboxStatus = (status) => status === "creating" || status === "cloning" || status === "starting";
|
|
31
|
+
const isTerminalSandboxStatus = (status) => status === "stopped" || status === "error";
|
|
32
|
+
const isConfirmedStoppedContainer = (state) => state === "exited" || state === "dead";
|
|
33
|
+
const isCompletedLegacyRetirement = (row) => row.accessPassword !== null && row.status === "stopped" && row.legacyRetiredAt !== null;
|
|
34
|
+
const isPendingLegacyRetirement = (row) => row.accessPassword !== null && row.status !== "stopped" && row.legacyRetiredAt !== null;
|
|
35
|
+
const isOrdinaryStoppingSandbox = (row) => row.status === "stopping" && row.legacyRetiredAt === null;
|
|
36
|
+
const isDiscoveredAwsAccountId = (value) => /^\d{12}$/u.test(value);
|
|
29
37
|
const homeDir = Config.string("HOME").pipe(Config.orElse(() => Config.string("USERPROFILE")));
|
|
30
38
|
const sandboxesDir = homeDir.pipe(Config.map((h) => `${h}/.codecommit/sandboxes`));
|
|
31
39
|
const expandHome = (p, home) => p.startsWith("~/") ? `${home}${p.slice(1)}` : p;
|
|
@@ -64,13 +72,101 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
64
72
|
const cryptoService = yield* Crypto.Crypto;
|
|
65
73
|
const homePath = yield* homeDir.pipe(Effect.orDie);
|
|
66
74
|
const basePath = yield* sandboxesDir.pipe(Effect.orDie);
|
|
75
|
+
const activeWorkerIds = yield* Ref.make(new Map());
|
|
76
|
+
const lifecycleAdmission = yield* Semaphore.make(1);
|
|
77
|
+
const containerAdmission = yield* Semaphore.make(1);
|
|
78
|
+
const createAdmission = yield* Semaphore.make(1);
|
|
79
|
+
const stopRequestedIds = yield* Ref.make(new Set());
|
|
80
|
+
const stopOwnerIds = yield* Ref.make(new Set());
|
|
81
|
+
const retiredLegacyIds = yield* Ref.make(new Set());
|
|
82
|
+
const markWorkerActive = (id) => Ref.update(activeWorkerIds, (workers) => {
|
|
83
|
+
const next = new Map(workers);
|
|
84
|
+
next.set(id, (next.get(id) ?? 0) + 1);
|
|
85
|
+
return next;
|
|
86
|
+
});
|
|
87
|
+
const releaseWorker = (id) => Ref.modify(activeWorkerIds, (workers) => {
|
|
88
|
+
const next = new Map(workers);
|
|
89
|
+
const count = next.get(id) ?? 0;
|
|
90
|
+
if (count <= 1)
|
|
91
|
+
next.delete(id);
|
|
92
|
+
else
|
|
93
|
+
next.set(id, count - 1);
|
|
94
|
+
return [count <= 1, next];
|
|
95
|
+
});
|
|
96
|
+
const hasActiveWorker = (id) => Ref.get(activeWorkerIds).pipe(Effect.map((workers) => (workers.get(id) ?? 0) > 0));
|
|
97
|
+
const isStopRequested = (id) => Ref.get(stopRequestedIds).pipe(Effect.map((ids) => ids.has(id)));
|
|
98
|
+
const requestWorkerStops = (ids) => Ref.update(stopRequestedIds, (requested) => new Set([...requested, ...ids]));
|
|
99
|
+
const claimStopOwners = (ids) => Ref.update(stopOwnerIds, (owners) => new Set([...owners, ...ids]));
|
|
100
|
+
const isStopOwned = (id) => Ref.get(stopOwnerIds).pipe(Effect.map((ids) => ids.has(id)));
|
|
101
|
+
const clearWorkerStopRequest = (id) => Ref.update(stopRequestedIds, (ids) => {
|
|
102
|
+
if (!ids.has(id))
|
|
103
|
+
return ids;
|
|
104
|
+
const next = new Set(ids);
|
|
105
|
+
next.delete(id);
|
|
106
|
+
return next;
|
|
107
|
+
});
|
|
108
|
+
const findFallbackCandidates = (params) => !isDiscoveredAwsAccountId(params.awsAccountId)
|
|
109
|
+
? Effect.gen(function* () {
|
|
110
|
+
const candidates = (yield* repo.findAll()).filter((row) => isDiscoveredAwsAccountId(row.awsAccountId) &&
|
|
111
|
+
row.awsAccountId !== params.profile &&
|
|
112
|
+
row.pullRequestId === params.pullRequestId &&
|
|
113
|
+
row.repositoryName === params.repositoryName &&
|
|
114
|
+
(row.region === params.region || row.region === null || row.region === undefined || row.region === ""));
|
|
115
|
+
const terminalCandidates = [];
|
|
116
|
+
for (const candidate of candidates) {
|
|
117
|
+
if (!isTerminalSandboxStatus(candidate.status)) {
|
|
118
|
+
return [{ row: candidate, blocksCreation: true }];
|
|
119
|
+
}
|
|
120
|
+
const containers = yield* docker.listContainersByLabel("codecommit.sandbox.id", candidate.id);
|
|
121
|
+
if (containers.some((container) => !isConfirmedStoppedContainer(container.State))) {
|
|
122
|
+
return [{ row: candidate, blocksCreation: true }];
|
|
123
|
+
}
|
|
124
|
+
terminalCandidates.push(candidate);
|
|
125
|
+
}
|
|
126
|
+
return terminalCandidates.map((row) => ({ row, blocksCreation: false }));
|
|
127
|
+
})
|
|
128
|
+
: Effect.succeed([]);
|
|
67
129
|
const loadSandboxConfig = configService.load.pipe(Effect.map((config) => config.sandbox), Effect.catch(() => Effect.succeed(defaultSandboxConfig)));
|
|
68
130
|
const updateStatus = (id, status, extra) => repo.updateStatus(id, status, extra);
|
|
69
|
-
const
|
|
131
|
+
const updateWorkerStatus = (id, status, extra) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
132
|
+
if (yield* isStopRequested(String(id)))
|
|
133
|
+
return;
|
|
134
|
+
yield* updateStatus(id, status, extra);
|
|
135
|
+
}));
|
|
136
|
+
const releaseWorkerReservation = (id) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
137
|
+
const lastWorker = yield* releaseWorker(id);
|
|
138
|
+
if (lastWorker && !(yield* isStopOwned(id))) {
|
|
139
|
+
if (yield* isStopRequested(id)) {
|
|
140
|
+
yield* updateStatus(SandboxId.make(id), "stopped").pipe(Effect.catch((error) => Effect.logError(`Failed to finalize stopped sandbox ${id}`, error)));
|
|
141
|
+
}
|
|
142
|
+
yield* clearWorkerStopRequest(id);
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
const releaseStopOwner = (id, finalizeUnowned = false) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
146
|
+
const hadStopRequest = yield* isStopRequested(id);
|
|
147
|
+
yield* Ref.update(stopOwnerIds, (ids) => {
|
|
148
|
+
if (!ids.has(id))
|
|
149
|
+
return ids;
|
|
150
|
+
const next = new Set(ids);
|
|
151
|
+
next.delete(id);
|
|
152
|
+
return next;
|
|
153
|
+
});
|
|
154
|
+
if (!(yield* hasActiveWorker(id))) {
|
|
155
|
+
if (finalizeUnowned && hadStopRequest) {
|
|
156
|
+
yield* repo.findById(SandboxId.make(id)).pipe(Effect.flatMap((row) => (row.containerId === null || row.containerId.length === 0) && isPreContainerSandboxStatus(row.status)
|
|
157
|
+
? updateStatus(SandboxId.make(id), "stopped")
|
|
158
|
+
: Effect.void), Effect.catch((error) => Effect.logError(`Failed to finalize stopped sandbox ${id}`, error)));
|
|
159
|
+
}
|
|
160
|
+
yield* clearWorkerStopRequest(id);
|
|
161
|
+
}
|
|
162
|
+
}));
|
|
163
|
+
const recordCreationFailure = (id, error) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
164
|
+
if (yield* isStopRequested(String(id)))
|
|
165
|
+
return;
|
|
70
166
|
yield* Effect.logError(`Sandbox ${id} creation failed`, error);
|
|
71
167
|
const errorDetail = Result.try(() => String(Predicate.isError(error) ? error.message : error)).pipe(Result.getOrElse(() => "Unknown error"));
|
|
72
168
|
yield* updateStatus(id, "error", { error: errorDetail.slice(0, 500) }).pipe(Effect.catch((statusError) => Effect.logError("Failed to update sandbox error status", statusError)), Effect.catchDefect((statusDefect) => Effect.logError("Defect while updating sandbox error status", statusDefect)));
|
|
73
|
-
});
|
|
169
|
+
}));
|
|
74
170
|
const progress = (id, detail) => Clock.currentTimeMillis.pipe(Effect.flatMap((ms) => {
|
|
75
171
|
const ts = new Date(ms).toISOString();
|
|
76
172
|
return repo.updateDetail(id, detail).pipe(Effect.tap(() => repo.appendLog(id, `[${ts}] ${detail}`)), Effect.tap(() => Effect.logInfo(`Sandbox ${id}: ${detail}`)));
|
|
@@ -87,12 +183,141 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
87
183
|
sourceBranch: row.sourceBranch
|
|
88
184
|
}
|
|
89
185
|
});
|
|
186
|
+
const retireLegacySandbox = (legacy) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
187
|
+
const activeWorker = yield* hasActiveWorker(legacy.id);
|
|
188
|
+
if (activeWorker) {
|
|
189
|
+
return yield* new SandboxError({
|
|
190
|
+
sandboxId: SandboxId.make(legacy.id),
|
|
191
|
+
message: "Legacy sandbox is still active; retry after its worker exits"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", legacy.id);
|
|
195
|
+
const containerIds = new Set([
|
|
196
|
+
...(legacy.containerId === null || legacy.containerId.length === 0 ? [] : [legacy.containerId]),
|
|
197
|
+
...discovered.map((container) => container.Id)
|
|
198
|
+
]);
|
|
199
|
+
if (legacy.accessPassword === null) {
|
|
200
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
201
|
+
yield* updateStatus(SandboxId.make(legacy.id), "error", {
|
|
202
|
+
error: "Legacy unauthenticated sandbox stopped; delete and recreate it"
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
yield* markLegacyRetirementStarted(SandboxId.make(legacy.id));
|
|
207
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.inspectContainer(containerId).pipe(Effect.flatMap((info) => info.State.Running ? docker.stopContainer(containerId) : Effect.void), Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
208
|
+
yield* Clock.currentTimeMillis.pipe(Effect.flatMap((ms) => updateStatus(SandboxId.make(legacy.id), "stopped", { legacyRetiredAt: new Date(ms).toISOString() })));
|
|
209
|
+
yield* Ref.update(retiredLegacyIds, (ids) => new Set(ids).add(legacy.id));
|
|
210
|
+
}));
|
|
211
|
+
const markLegacyRetired = (id) => Clock.currentTimeMillis.pipe(Effect.flatMap((ms) => updateStatus(id, "stopped", { legacyRetiredAt: new Date(ms).toISOString() })));
|
|
212
|
+
const markLegacyRetirementStarted = (id) => Clock.currentTimeMillis.pipe(Effect.flatMap((ms) => updateStatus(id, "stopping", { legacyRetiredAt: new Date(ms).toISOString() })));
|
|
90
213
|
const service = {
|
|
91
214
|
create: (params) => Effect.gen(function* () {
|
|
215
|
+
yield* Effect.uninterruptible(createAdmission.take(1));
|
|
92
216
|
// Singleton check — one active sandbox per PR
|
|
93
|
-
const existing = yield* repo.findByPr(params.awsAccountId, params.pullRequestId);
|
|
94
|
-
|
|
95
|
-
|
|
217
|
+
const existing = yield* repo.findByPr(params.awsAccountId, params.pullRequestId, params.repositoryName, params.region);
|
|
218
|
+
const exactCandidate = Option.isSome(existing) && existing.value.region === params.region
|
|
219
|
+
? existing.value
|
|
220
|
+
: undefined;
|
|
221
|
+
const exactExisting = exactCandidate !== undefined &&
|
|
222
|
+
!isCompletedLegacyRetirement(exactCandidate) &&
|
|
223
|
+
!isPendingLegacyRetirement(exactCandidate)
|
|
224
|
+
? exactCandidate
|
|
225
|
+
: undefined;
|
|
226
|
+
const pendingExact = exactCandidate !== undefined && isPendingLegacyRetirement(exactCandidate)
|
|
227
|
+
? [exactCandidate]
|
|
228
|
+
: [];
|
|
229
|
+
const emptyAccountRows = params.awsAccountId.length === 0
|
|
230
|
+
? []
|
|
231
|
+
: (yield* repo.findAll()).filter((row) => row.awsAccountId === "" &&
|
|
232
|
+
row.pullRequestId === params.pullRequestId &&
|
|
233
|
+
row.repositoryName === params.repositoryName &&
|
|
234
|
+
row.region === params.region);
|
|
235
|
+
const profileRows = isDiscoveredAwsAccountId(params.awsAccountId) &&
|
|
236
|
+
!isDiscoveredAwsAccountId(params.profile) &&
|
|
237
|
+
params.profile !== params.awsAccountId
|
|
238
|
+
? (yield* repo.findAll()).filter((row) => row.awsAccountId === params.profile &&
|
|
239
|
+
row.pullRequestId === params.pullRequestId &&
|
|
240
|
+
row.repositoryName === params.repositoryName &&
|
|
241
|
+
row.region === params.region)
|
|
242
|
+
: [];
|
|
243
|
+
const effectiveExisting = yield* Effect.forEach([...emptyAccountRows, ...profileRows, ...pendingExact]
|
|
244
|
+
.filter((row) => !isCompletedLegacyRetirement(row)), retireLegacySandbox, { discard: true }).pipe(Effect.as(exactExisting));
|
|
245
|
+
const regionlessKeys = Array.from(new Set([
|
|
246
|
+
params.awsAccountId,
|
|
247
|
+
isDiscoveredAwsAccountId(params.awsAccountId)
|
|
248
|
+
? params.profile
|
|
249
|
+
: undefined
|
|
250
|
+
])).filter((key) => key !== undefined && key.length > 0);
|
|
251
|
+
const regionlessCandidates = (yield* Effect.forEach(regionlessKeys, (key) => repo.findRegionlessByPrAll(key, params.pullRequestId, params.repositoryName), { concurrency: 1 })).flat()
|
|
252
|
+
.filter((row) => regionlessKeys.includes(row.awsAccountId) && !isCompletedLegacyRetirement(row));
|
|
253
|
+
const regionless = regionlessCandidates.filter((row, index, candidates) => candidates.findIndex((candidate) => candidate.id === row.id) === index);
|
|
254
|
+
const ambiguousNumericProfile = isDiscoveredAwsAccountId(params.profile)
|
|
255
|
+
? regionless.find((row) => row.awsAccountId === params.profile)
|
|
256
|
+
: undefined;
|
|
257
|
+
if (ambiguousNumericProfile !== undefined) {
|
|
258
|
+
return yield* new SandboxError({
|
|
259
|
+
sandboxId: SandboxId.make(ambiguousNumericProfile.id),
|
|
260
|
+
message: "Configured numeric profile matches an existing regionless sandbox; account identity is ambiguous"
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const legacyResults = yield* Effect.forEach(regionless, (legacy) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
264
|
+
if (legacy.accessPassword !== null && (yield* hasActiveWorker(legacy.id))) {
|
|
265
|
+
return yield* new SandboxError({
|
|
266
|
+
sandboxId: SandboxId.make(legacy.id),
|
|
267
|
+
message: "Regionless legacy sandbox is still active; retry after its worker exits"
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", legacy.id);
|
|
271
|
+
const containerIds = new Set([
|
|
272
|
+
...(legacy.containerId === null || legacy.containerId.length === 0 ? [] : [legacy.containerId]),
|
|
273
|
+
...discovered.map((container) => container.Id)
|
|
274
|
+
]);
|
|
275
|
+
if (legacy.accessPassword !== null && containerIds.size === 0 && isPreContainerSandboxStatus(legacy.status)) {
|
|
276
|
+
return yield* new SandboxError({
|
|
277
|
+
sandboxId: SandboxId.make(legacy.id),
|
|
278
|
+
message: "Regionless legacy sandbox is still starting; retry after its worker reports a container"
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (legacy.accessPassword !== null) {
|
|
282
|
+
yield* markLegacyRetirementStarted(SandboxId.make(legacy.id));
|
|
283
|
+
}
|
|
284
|
+
if (legacy.accessPassword === null) {
|
|
285
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
286
|
+
yield* updateStatus(SandboxId.make(legacy.id), "error", {
|
|
287
|
+
error: "Legacy unauthenticated sandbox stopped; delete and recreate it"
|
|
288
|
+
});
|
|
289
|
+
return Option.some(legacy);
|
|
290
|
+
}
|
|
291
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.inspectContainer(containerId).pipe(Effect.flatMap((info) => info.State.Running ? docker.stopContainer(containerId) : Effect.void), Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
292
|
+
yield* markLegacyRetired(SandboxId.make(legacy.id));
|
|
293
|
+
return Option.none();
|
|
294
|
+
})), { concurrency: 1 });
|
|
295
|
+
const unauthenticated = legacyResults.find(Option.isSome);
|
|
296
|
+
if (unauthenticated !== undefined && Option.isSome(unauthenticated)) {
|
|
297
|
+
return yield* new SandboxError({
|
|
298
|
+
sandboxId: SandboxId.make(unauthenticated.value.id),
|
|
299
|
+
message: "Regionless legacy sandbox requires explicit cleanup before recreation"
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (exactCandidate !== undefined && isOrdinaryStoppingSandbox(exactCandidate)) {
|
|
303
|
+
return yield* new SandboxError({
|
|
304
|
+
sandboxId: SandboxId.make(exactCandidate.id),
|
|
305
|
+
message: "Sandbox is still stopping; retry after it has stopped"
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (effectiveExisting !== undefined) {
|
|
309
|
+
if (effectiveExisting.containerId === null && isPreContainerSandboxStatus(effectiveExisting.status)) {
|
|
310
|
+
const activeWorker = yield* hasActiveWorker(effectiveExisting.id);
|
|
311
|
+
if (activeWorker) {
|
|
312
|
+
return effectiveExisting;
|
|
313
|
+
}
|
|
314
|
+
yield* updateStatus(SandboxId.make(effectiveExisting.id), "error", {
|
|
315
|
+
error: "Orphaned (no container)"
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
return effectiveExisting;
|
|
320
|
+
}
|
|
96
321
|
}
|
|
97
322
|
const sandboxCfg = yield* loadSandboxConfig;
|
|
98
323
|
yield* validateSandboxConfig(sandboxCfg, homePath);
|
|
@@ -103,26 +328,17 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
103
328
|
const accessPassword = yield* cryptoService.randomUUIDv4;
|
|
104
329
|
const workspacePath = `${basePath}/${id}`;
|
|
105
330
|
const now = new Date(nowMs).toISOString();
|
|
106
|
-
yield*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
awsAccountId: params.awsAccountId,
|
|
110
|
-
repositoryName: params.repositoryName,
|
|
111
|
-
sourceBranch: params.sourceBranch,
|
|
112
|
-
accessPassword,
|
|
113
|
-
workspacePath,
|
|
114
|
-
status: "creating",
|
|
115
|
-
createdAt: now,
|
|
116
|
-
lastActivityAt: now
|
|
117
|
-
});
|
|
118
|
-
// Fork daemon for async lifecycle
|
|
119
|
-
yield* ownerScope.fork(Effect.gen(function* () {
|
|
331
|
+
const workerTransferred = yield* Ref.make(false);
|
|
332
|
+
const releaseWorkerReservationForCreate = () => releaseWorkerReservation(String(id));
|
|
333
|
+
const worker = Effect.gen(function* () {
|
|
120
334
|
const fs = yield* FileSystem.FileSystem;
|
|
121
335
|
const host = yield* ChildEnv.HostEnvironment;
|
|
122
336
|
const log = (detail) => progress(id, detail);
|
|
337
|
+
if (yield* isStopRequested(String(id)))
|
|
338
|
+
return;
|
|
123
339
|
yield* log("Sandbox config validated");
|
|
124
340
|
// Clone via HTTPS + AWS credential helper
|
|
125
|
-
yield*
|
|
341
|
+
yield* updateWorkerStatus(id, "cloning");
|
|
126
342
|
yield* fs.makeDirectory(workspacePath, { recursive: true });
|
|
127
343
|
const cloneUrl = `https://git-codecommit.${params.region}.amazonaws.com/v1/repos/${params.repositoryName}`;
|
|
128
344
|
const branch = params.sourceBranch.replace(/^refs\/heads\//, "");
|
|
@@ -163,12 +379,14 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
163
379
|
if (cloneResult.exitCode !== 0) {
|
|
164
380
|
const stderrText = cloneResult.stderrText;
|
|
165
381
|
yield* log(`Clone failed: ${stderrText}`);
|
|
166
|
-
yield*
|
|
382
|
+
yield* updateWorkerStatus(id, "error", {
|
|
167
383
|
error: stderrText || `git clone failed (exit ${cloneResult.exitCode})`
|
|
168
384
|
});
|
|
169
385
|
return;
|
|
170
386
|
}
|
|
171
387
|
yield* log("Clone complete");
|
|
388
|
+
if (yield* isStopRequested(String(id)))
|
|
389
|
+
return;
|
|
172
390
|
const workspaceInfo = yield* fs.stat(workspacePath);
|
|
173
391
|
const containerIdentity = sandboxContainerIdentityForWorkspaceOwner(Option.getOrUndefined(workspaceInfo.uid), Option.getOrUndefined(workspaceInfo.gid));
|
|
174
392
|
if (containerIdentity.repairRootOwnedWorkspace) {
|
|
@@ -182,22 +400,40 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
182
400
|
}
|
|
183
401
|
}
|
|
184
402
|
// Pull image
|
|
185
|
-
yield*
|
|
403
|
+
yield* updateWorkerStatus(id, "starting");
|
|
186
404
|
yield* log(`Pulling image ${sandboxCfg.image}`);
|
|
187
405
|
yield* docker.pullImage(sandboxCfg.image).pipe(Effect.tap(() => log("Image ready")), Effect.catchIf(() => true, () => log("Image pull skipped (using cached)")));
|
|
188
|
-
// Create + start container
|
|
189
|
-
|
|
190
|
-
yield*
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
406
|
+
// Create + start container. Stop and provisioning share this gate so a
|
|
407
|
+
// stop request cannot pass while a worker is between create and start.
|
|
408
|
+
const cid = yield* lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
409
|
+
if (yield* isStopRequested(String(id)))
|
|
410
|
+
return undefined;
|
|
411
|
+
yield* log("Creating container");
|
|
412
|
+
yield* validateSandboxConfig(sandboxCfg, homePath);
|
|
413
|
+
const containerConfig = makeContainerConfig(workspacePath, port, id, params.pullRequestId, sandboxCfg, homePath, containerIdentity.user, accessPassword);
|
|
414
|
+
const containerId = yield* docker.createContainer(containerConfig);
|
|
415
|
+
const nextCid = containerId.trim();
|
|
416
|
+
if (yield* isStopRequested(String(id))) {
|
|
417
|
+
yield* updateStatus(id, "stopping", { containerId: nextCid, port });
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
yield* log(`Container ${nextCid.slice(0, 12)} created, starting`);
|
|
421
|
+
yield* docker.startContainer(nextCid);
|
|
422
|
+
yield* updateStatus(id, "starting", { containerId: nextCid, port });
|
|
423
|
+
yield* log(`Container started on port ${port}`);
|
|
424
|
+
if (yield* isStopRequested(String(id)))
|
|
425
|
+
return undefined;
|
|
426
|
+
return nextCid;
|
|
427
|
+
}));
|
|
428
|
+
if (cid === undefined)
|
|
429
|
+
return;
|
|
430
|
+
if (yield* isStopRequested(String(id)))
|
|
431
|
+
return;
|
|
198
432
|
// Wait for code-server to be ready (poll health)
|
|
199
433
|
yield* log("Waiting for code-server health check");
|
|
200
434
|
yield* docker.exec(cid, ["curl", "-sf", "http://localhost:8080/healthz"]).pipe(Effect.retry(Schedule.max([Schedule.recurs(30), Schedule.spaced(Duration.seconds(1))])), Effect.tap(() => log("code-server ready")), Effect.tapError((e) => log(`Health check failed: ${String(e)}`)), Effect.catchIf(() => true, () => Effect.void));
|
|
435
|
+
if (yield* isStopRequested(String(id)))
|
|
436
|
+
return;
|
|
201
437
|
// Run plugin hooks
|
|
202
438
|
const row = yield* repo.findById(id);
|
|
203
439
|
const ctx = makeSandboxContext(row);
|
|
@@ -207,62 +443,159 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
207
443
|
// Install configured extensions
|
|
208
444
|
if (sandboxCfg.extensions.length > 0) {
|
|
209
445
|
yield* log(`Installing ${sandboxCfg.extensions.length} extension(s)`);
|
|
210
|
-
yield* Effect.forEach(sandboxCfg.extensions, (ext) => log(`Installing extension: ${ext}`).pipe(Effect.andThen(docker.exec(cid, ["code-server", "--install-extension", ext])), Effect.tap((output) => log(`Extension installed: ${ext}${output ? `\n${output.trim()}` : ""}`)), Effect.tapError((e) => log(`Extension failed: ${ext} — ${String(e)}`)), Effect.catchIf(() => true, () => Effect.void)), { discard: true });
|
|
446
|
+
yield* Effect.forEach(sandboxCfg.extensions, (ext) => log(`Installing extension: ${ext}`).pipe(Effect.andThen(docker.exec(cid, ["code-server", "--install-extension", ext])), Effect.tap((output) => log(`Extension installed: ${ext}${output.length > 0 ? `\n${output.trim()}` : ""}`)), Effect.tapError((e) => log(`Extension failed: ${ext} — ${String(e)}`)), Effect.catchIf(() => true, () => Effect.void)), { discard: true });
|
|
211
447
|
}
|
|
212
448
|
// Run configured setup commands
|
|
213
449
|
if (sandboxCfg.setupCommands.length > 0) {
|
|
214
450
|
yield* log(`Running ${sandboxCfg.setupCommands.length} setup command(s)`);
|
|
215
|
-
yield* Effect.forEach(sandboxCfg.setupCommands, (cmd, i) => log(`[${i + 1}/${sandboxCfg.setupCommands.length}] ${cmd}`).pipe(Effect.andThen(docker.exec(cid, ["sh", "-c", cmd])), Effect.tap((output) => log(`Command done: ${cmd.slice(0, 60)}${output ? `\n${output.trim()}` : ""}`)), Effect.tapError((e) => log(`Command failed: ${cmd.slice(0, 60)} — ${String(e)}`)), Effect.catchIf(() => true, () => Effect.void)), { discard: true });
|
|
451
|
+
yield* Effect.forEach(sandboxCfg.setupCommands, (cmd, i) => log(`[${i + 1}/${sandboxCfg.setupCommands.length}] ${cmd}`).pipe(Effect.andThen(docker.exec(cid, ["sh", "-c", cmd])), Effect.tap((output) => log(`Command done: ${cmd.slice(0, 60)}${output.length > 0 ? `\n${output.trim()}` : ""}`)), Effect.tapError((e) => log(`Command failed: ${cmd.slice(0, 60)} — ${String(e)}`)), Effect.catchIf(() => true, () => Effect.void)), { discard: true });
|
|
216
452
|
}
|
|
217
|
-
yield*
|
|
453
|
+
yield* updateWorkerStatus(id, "running");
|
|
218
454
|
yield* log("Sandbox ready");
|
|
219
455
|
}).pipe(Effect.catch((error) => recordCreationFailure(id, error)),
|
|
220
456
|
// Observe and persist unexpected defects without recovering them.
|
|
221
457
|
// `tapDefect` leaves the original Cause / Exit unchanged.
|
|
222
|
-
Effect.tapDefect((defect) => recordCreationFailure(id, defect)))
|
|
458
|
+
Effect.tapDefect((defect) => recordCreationFailure(id, defect)));
|
|
459
|
+
yield* Effect.acquireUseRelease(lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
460
|
+
const fallbackCandidates = yield* findFallbackCandidates(params);
|
|
461
|
+
const blockingCandidate = fallbackCandidates.find((candidate) => candidate.blocksCreation);
|
|
462
|
+
if (blockingCandidate !== undefined) {
|
|
463
|
+
return yield* new SandboxError({
|
|
464
|
+
sandboxId: SandboxId.make(blockingCandidate.row.id),
|
|
465
|
+
message: "AWS account identity is unavailable; retry after account discovery"
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const fallbackStopGuards = fallbackCandidates.map((candidate) => candidate.row.id);
|
|
469
|
+
yield* claimStopOwners(fallbackStopGuards);
|
|
470
|
+
yield* requestWorkerStops(fallbackStopGuards);
|
|
471
|
+
return fallbackStopGuards;
|
|
472
|
+
})), () => Effect.gen(function* () {
|
|
473
|
+
yield* markWorkerActive(String(id));
|
|
474
|
+
yield* repo.insert({
|
|
475
|
+
id,
|
|
476
|
+
pullRequestId: params.pullRequestId,
|
|
477
|
+
awsAccountId: params.awsAccountId,
|
|
478
|
+
repositoryName: params.repositoryName,
|
|
479
|
+
region: params.region,
|
|
480
|
+
sourceBranch: params.sourceBranch,
|
|
481
|
+
accessPassword,
|
|
482
|
+
workspacePath,
|
|
483
|
+
status: "creating",
|
|
484
|
+
createdAt: now,
|
|
485
|
+
lastActivityAt: now
|
|
486
|
+
}).pipe(Effect.andThen(Effect.uninterruptible(ownerScope.fork(worker, releaseWorkerReservationForCreate()).pipe(Effect.flatMap(({ fiber, started }) => Effect.race(started.pipe(Effect.as(true)), Fiber.await(fiber).pipe(Effect.as(false)))), Effect.flatMap((started) => started ? Ref.set(workerTransferred, true) : Effect.void)))), Effect.ensuring(Ref.get(workerTransferred).pipe(Effect.flatMap((transferred) => transferred ? Effect.void : releaseWorkerReservationForCreate()))));
|
|
487
|
+
}), (fallbackStopGuards) => Effect.forEach(fallbackStopGuards, (guardId) => releaseStopOwner(guardId), { discard: true }));
|
|
223
488
|
return yield* repo.findById(id);
|
|
224
|
-
}).pipe(Effect.mapError((cause) =>
|
|
489
|
+
}).pipe(Effect.ensuring(createAdmission.release(1)), Effect.mapError((cause) => Predicate.isTagged(cause, "SandboxError")
|
|
490
|
+
? cause
|
|
491
|
+
: new SandboxError({ message: "Failed to create sandbox", cause }))),
|
|
225
492
|
get: (id) => repo.findById(id).pipe(Effect.mapError((cause) => new SandboxError({ sandboxId: id, message: "Sandbox not found", cause }))),
|
|
226
493
|
list: () => repo.findActive(),
|
|
227
494
|
listAll: () => repo.findAll(),
|
|
228
|
-
stop: (id) => Effect.
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
495
|
+
stop: (id) => Effect.uninterruptibleMask((restore) => {
|
|
496
|
+
const stopAdmissionAcquired = Effect.gen(function* () {
|
|
497
|
+
const acquired = yield* Ref.make(false);
|
|
498
|
+
return acquired;
|
|
499
|
+
});
|
|
500
|
+
const stopSandbox = lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
501
|
+
const row = yield* repo.findById(id);
|
|
502
|
+
yield* updateStatus(id, "stopping");
|
|
503
|
+
if (row.containerId !== null) {
|
|
504
|
+
const containerId = row.containerId;
|
|
505
|
+
const ctx = makeSandboxContext(row);
|
|
506
|
+
yield* plugins.executeHook("onSandboxDestroy", ctx);
|
|
507
|
+
yield* containerAdmission.withPermits(1)(Effect.gen(function* () {
|
|
508
|
+
const stop = row.legacyRetiredAt === null
|
|
509
|
+
? docker.stopContainer(containerId).pipe(Effect.catchIf(() => true, () => Effect.void))
|
|
510
|
+
: docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void));
|
|
511
|
+
yield* stop;
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
yield* updateStatus(id, "stopped");
|
|
515
|
+
yield* Effect.logInfo(`Sandbox ${id} stopped`);
|
|
516
|
+
}));
|
|
517
|
+
return Effect.flatMap(stopAdmissionAcquired, (admissionAcquired) => Effect.ensuring(Effect.gen(function* () {
|
|
518
|
+
// Publish the stop intent before waiting for create admission.
|
|
519
|
+
// A provisioning worker may hold that permit while it inserts its row.
|
|
520
|
+
yield* claimStopOwners([String(id)]);
|
|
521
|
+
yield* requestWorkerStops([String(id)]);
|
|
522
|
+
yield* restore(createAdmission.withPermits(1)(Effect.uninterruptible(Effect.gen(function* () {
|
|
523
|
+
yield* Ref.set(admissionAcquired, true);
|
|
524
|
+
yield* stopSandbox;
|
|
525
|
+
}))));
|
|
526
|
+
}), Ref.get(admissionAcquired).pipe(Effect.flatMap((acquired) => releaseStopOwner(String(id), !acquired)))));
|
|
238
527
|
}).pipe(Effect.mapError((cause) => new SandboxError({ sandboxId: id, message: "Failed to stop sandbox", cause }))),
|
|
239
|
-
restart: (id) => Effect.gen(function* () {
|
|
528
|
+
restart: (id) => Effect.uninterruptibleMask((restore) => Effect.acquireUseRelease(Effect.gen(function* () {
|
|
529
|
+
yield* lifecycleAdmission.take(1);
|
|
530
|
+
return {
|
|
531
|
+
permitReleased: yield* Ref.make(false),
|
|
532
|
+
workerMarked: yield* Ref.make(false)
|
|
533
|
+
};
|
|
534
|
+
}), ({ permitReleased, workerMarked }) => Effect.gen(function* () {
|
|
240
535
|
const row = yield* repo.findById(id);
|
|
536
|
+
if (yield* isStopRequested(String(id)))
|
|
537
|
+
return;
|
|
538
|
+
const wasRetired = yield* Ref.get(retiredLegacyIds).pipe(Effect.map((ids) => ids.has(String(id))));
|
|
539
|
+
if (wasRetired || row.legacyRetiredAt !== null) {
|
|
540
|
+
return yield* new SandboxError({
|
|
541
|
+
sandboxId: id,
|
|
542
|
+
message: "Legacy sandbox was retired; create a replacement"
|
|
543
|
+
});
|
|
544
|
+
}
|
|
241
545
|
if (row.accessPassword === null) {
|
|
242
|
-
return yield*
|
|
546
|
+
return yield* new SandboxError({
|
|
243
547
|
sandboxId: id,
|
|
244
548
|
message: "Legacy sandbox has no authenticated access credential; delete and recreate it"
|
|
245
|
-
})
|
|
549
|
+
});
|
|
246
550
|
}
|
|
247
|
-
if (
|
|
248
|
-
return yield*
|
|
551
|
+
if (row.containerId === null) {
|
|
552
|
+
return yield* new SandboxError({ sandboxId: id, message: "No container to restart" });
|
|
249
553
|
}
|
|
250
|
-
|
|
251
|
-
yield*
|
|
252
|
-
yield*
|
|
253
|
-
yield*
|
|
254
|
-
yield*
|
|
255
|
-
yield*
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
554
|
+
const containerId = row.containerId;
|
|
555
|
+
yield* markWorkerActive(String(id));
|
|
556
|
+
yield* Ref.set(workerMarked, true);
|
|
557
|
+
yield* lifecycleAdmission.release(1);
|
|
558
|
+
yield* Ref.set(permitReleased, true);
|
|
559
|
+
yield* restore(Effect.gen(function* () {
|
|
560
|
+
if (yield* isStopRequested(String(id)))
|
|
561
|
+
return;
|
|
562
|
+
yield* updateWorkerStatus(id, "starting");
|
|
563
|
+
if (yield* isStopRequested(String(id)))
|
|
564
|
+
return;
|
|
565
|
+
yield* progress(id, "Restarting container");
|
|
566
|
+
const started = yield* containerAdmission.withPermits(1)(Effect.gen(function* () {
|
|
567
|
+
if (yield* isStopRequested(String(id)))
|
|
568
|
+
return false;
|
|
569
|
+
yield* docker.startContainer(containerId);
|
|
570
|
+
yield* updateStatus(id, "starting", row.port !== null ? { port: row.port } : {});
|
|
571
|
+
return true;
|
|
572
|
+
}));
|
|
573
|
+
if (!started || (yield* isStopRequested(String(id))))
|
|
574
|
+
return;
|
|
575
|
+
yield* progress(id, "Waiting for code-server health check");
|
|
576
|
+
yield* docker.exec(containerId, ["curl", "-sf", "http://localhost:8080/healthz"]).pipe(Effect.retry(Schedule.max([Schedule.recurs(30), Schedule.spaced(Duration.seconds(1))])), Effect.tap(() => progress(id, "code-server ready")), Effect.catchIf(() => true, () => Effect.void));
|
|
577
|
+
if (yield* isStopRequested(String(id)))
|
|
578
|
+
return;
|
|
579
|
+
const ctx = makeSandboxContext(yield* repo.findById(id));
|
|
580
|
+
yield* plugins.executeHook("onSandboxReady", ctx);
|
|
581
|
+
if (yield* isStopRequested(String(id)))
|
|
582
|
+
return;
|
|
583
|
+
yield* updateWorkerStatus(id, "running");
|
|
584
|
+
if (yield* isStopRequested(String(id)))
|
|
585
|
+
return;
|
|
586
|
+
yield* progress(id, "Sandbox restarted");
|
|
587
|
+
yield* Effect.logInfo(`Sandbox ${id} restarted`);
|
|
588
|
+
}));
|
|
589
|
+
}), ({ permitReleased, workerMarked }) => Effect.gen(function* () {
|
|
590
|
+
if (!(yield* Ref.getAndSet(permitReleased, true)))
|
|
591
|
+
yield* lifecycleAdmission.release(1);
|
|
592
|
+
if (yield* Ref.getAndSet(workerMarked, false))
|
|
593
|
+
yield* releaseWorkerReservation(String(id));
|
|
594
|
+
}))).pipe(Effect.mapError((cause) => new SandboxError({ sandboxId: id, message: "Failed to restart sandbox", cause }))),
|
|
262
595
|
cleanup: (id) => Effect.gen(function* () {
|
|
263
596
|
const row = yield* repo.findById(id);
|
|
264
597
|
const fs = yield* FileSystem.FileSystem;
|
|
265
|
-
if (row.containerId) {
|
|
598
|
+
if (row.containerId !== null) {
|
|
266
599
|
yield* docker.removeContainer(row.containerId).pipe(Effect.catchIf(() => true, () => Effect.void));
|
|
267
600
|
}
|
|
268
601
|
yield* fs.remove(row.workspacePath, { recursive: true }).pipe(Effect.catchIf(() => true, () => Effect.void));
|
|
@@ -270,37 +603,86 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
270
603
|
yield* Effect.logInfo(`Sandbox ${id} cleaned up`);
|
|
271
604
|
}).pipe(Effect.mapError((cause) => new SandboxError({ sandboxId: id, message: "Failed to cleanup sandbox", cause }))),
|
|
272
605
|
reconcile: () => Effect.gen(function* () {
|
|
606
|
+
yield* Effect.uninterruptible(lifecycleAdmission.take(1));
|
|
273
607
|
const active = yield* repo.findActive();
|
|
274
608
|
const all = yield* repo.findAll();
|
|
275
609
|
const activeIds = new Set(active.map((row) => row.id));
|
|
276
|
-
const rows = all.filter((row) =>
|
|
610
|
+
const rows = all.filter((row) => !isCompletedLegacyRetirement(row) &&
|
|
611
|
+
(row.accessPassword === null || activeIds.has(row.id) || isRegionlessSandbox(row) ||
|
|
612
|
+
(row.legacyRetiredAt !== null && row.status !== "stopped")));
|
|
277
613
|
yield* Effect.forEach(rows, (row) => Effect.gen(function* () {
|
|
614
|
+
if (isRegionlessSandbox(row)) {
|
|
615
|
+
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", row.id);
|
|
616
|
+
const containerIds = new Set([
|
|
617
|
+
...(row.containerId === null || row.containerId.length === 0 ? [] : [row.containerId]),
|
|
618
|
+
...discovered.map((container) => container.Id)
|
|
619
|
+
]);
|
|
620
|
+
if (row.accessPassword !== null &&
|
|
621
|
+
containerIds.size === 0 &&
|
|
622
|
+
isPreContainerSandboxStatus(row.status)) {
|
|
623
|
+
const activeWorker = yield* hasActiveWorker(row.id);
|
|
624
|
+
if (activeWorker)
|
|
625
|
+
return;
|
|
626
|
+
yield* updateStatus(SandboxId.make(row.id), "error", { error: "Orphaned (no container)" });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (row.accessPassword === null) {
|
|
630
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
631
|
+
yield* updateStatus(SandboxId.make(row.id), "error", {
|
|
632
|
+
error: "Legacy unauthenticated sandbox stopped; delete and recreate it"
|
|
633
|
+
});
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (yield* hasActiveWorker(row.id)) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
yield* markLegacyRetirementStarted(SandboxId.make(row.id));
|
|
640
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.inspectContainer(containerId).pipe(Effect.flatMap((info) => info.State.Running ? docker.stopContainer(containerId) : Effect.void), Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
641
|
+
yield* markLegacyRetired(SandboxId.make(row.id));
|
|
642
|
+
yield* Effect.logInfo(`Reconciled regionless sandbox ${row.id}`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (row.accessPassword !== null && row.legacyRetiredAt !== null) {
|
|
646
|
+
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", row.id);
|
|
647
|
+
const containerIds = new Set([
|
|
648
|
+
...(row.containerId === null || row.containerId.length === 0 ? [] : [row.containerId]),
|
|
649
|
+
...discovered.map((container) => container.Id)
|
|
650
|
+
]);
|
|
651
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.inspectContainer(containerId).pipe(Effect.flatMap((info) => info.State.Running ? docker.stopContainer(containerId) : Effect.void), Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
652
|
+
yield* updateStatus(SandboxId.make(row.id), "stopped", { legacyRetiredAt: row.legacyRetiredAt });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
278
655
|
if (row.accessPassword === null) {
|
|
279
656
|
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", row.id);
|
|
280
657
|
const containerIds = new Set([
|
|
281
|
-
...(row.containerId === null ? [] : [row.containerId]),
|
|
658
|
+
...(row.containerId === null || row.containerId.length === 0 ? [] : [row.containerId]),
|
|
282
659
|
...discovered.map((container) => container.Id)
|
|
283
660
|
]);
|
|
284
661
|
// Do not consider any legacy row reconciled until every persisted
|
|
285
662
|
// or labeled passwordless container has confirmed shutdown.
|
|
286
|
-
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId), { discard: true });
|
|
663
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
287
664
|
yield* updateStatus(SandboxId.make(row.id), "error", {
|
|
288
665
|
error: "Legacy unauthenticated sandbox stopped; delete and recreate it"
|
|
289
666
|
});
|
|
290
667
|
return;
|
|
291
668
|
}
|
|
292
|
-
if (
|
|
669
|
+
if (row.containerId === null) {
|
|
670
|
+
if (isPreContainerSandboxStatus(row.status)) {
|
|
671
|
+
const activeWorker = yield* hasActiveWorker(row.id);
|
|
672
|
+
if (activeWorker)
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
293
675
|
yield* updateStatus(SandboxId.make(row.id), "error", { error: "Orphaned (no container)" });
|
|
294
676
|
return;
|
|
295
677
|
}
|
|
296
|
-
const info = yield* docker.inspectContainer(row.containerId).pipe(Effect.
|
|
297
|
-
if (
|
|
678
|
+
const info = yield* docker.inspectContainer(row.containerId).pipe(Effect.map(Option.some), Effect.catchIf(isMissingContainerError, () => Effect.succeed(Option.none())));
|
|
679
|
+
if (Option.isNone(info) || info.value.State.Running === false) {
|
|
298
680
|
yield* updateStatus(SandboxId.make(row.id), "stopped");
|
|
299
681
|
yield* Effect.logInfo(`Reconciled orphaned sandbox ${row.id}`);
|
|
300
682
|
}
|
|
301
683
|
}), { discard: true });
|
|
302
|
-
}).pipe(Effect.as(true), Effect.catch((cause) => Effect.logWarning("Sandbox reconcile failed", cause).pipe(Effect.as(false)))),
|
|
303
|
-
hasLegacyUnauthenticated: () => repo.findAll().pipe(Effect.map((rows) => rows.some((row) => row.accessPassword === null))),
|
|
684
|
+
}).pipe(Effect.as(true), Effect.catch((cause) => Effect.logWarning("Sandbox reconcile failed", cause).pipe(Effect.as(false))), Effect.ensuring(lifecycleAdmission.release(1))),
|
|
685
|
+
hasLegacyUnauthenticated: () => repo.findAll().pipe(Effect.map((rows) => rows.some((row) => row.accessPassword === null || (isRegionlessSandbox(row) && !isCompletedLegacyRetirement(row))))),
|
|
304
686
|
gcIdle: (idleTimeout = Duration.minutes(30), cleanupDelay = Duration.hours(24)) => Effect.gen(function* () {
|
|
305
687
|
const all = yield* repo.findAll();
|
|
306
688
|
const now = yield* Clock.currentTimeMillis;
|
|
@@ -310,7 +692,7 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
310
692
|
if (now - lastActivity > Duration.toMillis(idleTimeout)) {
|
|
311
693
|
return Effect.gen(function* () {
|
|
312
694
|
yield* Effect.logInfo(`GC: stopping idle sandbox ${row.id}`);
|
|
313
|
-
if (row.containerId) {
|
|
695
|
+
if (row.containerId !== null) {
|
|
314
696
|
yield* docker.stopContainer(row.containerId).pipe(Effect.catchIf(() => true, () => Effect.void));
|
|
315
697
|
}
|
|
316
698
|
yield* updateStatus(SandboxId.make(row.id), "stopped");
|
|
@@ -325,7 +707,7 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
325
707
|
if (now - lastActivity > Duration.toMillis(cleanupDelay)) {
|
|
326
708
|
return Effect.gen(function* () {
|
|
327
709
|
yield* Effect.logInfo(`GC: cleaning up sandbox ${row.id}`);
|
|
328
|
-
if (row.containerId) {
|
|
710
|
+
if (row.containerId !== null) {
|
|
329
711
|
yield* docker.removeContainer(row.containerId).pipe(Effect.catchIf(() => true, () => Effect.void));
|
|
330
712
|
}
|
|
331
713
|
yield* fs.remove(row.workspacePath, { recursive: true }).pipe(Effect.catchIf(() => true, () => Effect.void));
|