@highstate/backend 0.9.16 → 0.9.18
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/dist/chunk-NAAIDR4U.js +8499 -0
- package/dist/chunk-NAAIDR4U.js.map +1 -0
- package/dist/chunk-OU5OQBLB.js +74 -0
- package/dist/chunk-OU5OQBLB.js.map +1 -0
- package/dist/{chunk-WHALQHEZ.js → chunk-Y7DXREVO.js} +502 -774
- package/dist/chunk-Y7DXREVO.js.map +1 -0
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +2979 -2233
- package/dist/index.js.map +1 -1
- package/dist/library/package-resolution-worker.js +7 -5
- package/dist/library/package-resolution-worker.js.map +1 -1
- package/dist/library/worker/main.js +40 -41
- package/dist/library/worker/main.js.map +1 -1
- package/dist/magic-string.es-5ABAC4JN.js +1292 -0
- package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
- package/dist/shared/index.js +3 -216
- package/dist/shared/index.js.map +1 -1
- package/package.json +9 -6
- package/src/artifact/encryption.ts +47 -7
- package/src/artifact/factory.ts +2 -2
- package/src/artifact/local.ts +2 -6
- package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
- package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
- package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
- package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
- package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
- package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
- package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
- package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
- package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
- package/src/business/artifact.ts +2 -1
- package/src/business/index.ts +1 -0
- package/src/business/instance-lock.ts +3 -2
- package/src/business/instance-state.ts +202 -60
- package/src/business/project-unlock.ts +41 -23
- package/src/business/project.ts +299 -0
- package/src/business/secret.test.ts +178 -0
- package/src/business/secret.ts +139 -45
- package/src/business/worker.test.ts +614 -0
- package/src/business/worker.ts +289 -52
- package/src/common/clock.ts +18 -0
- package/src/common/index.ts +3 -0
- package/src/common/random.ts +68 -0
- package/src/common/test/index.ts +2 -0
- package/src/common/test/render.ts +98 -0
- package/src/common/test/tracer.ts +359 -0
- package/src/config.ts +5 -1
- package/src/hotstate/manager.ts +8 -8
- package/src/hotstate/validation.ts +0 -1
- package/src/library/abstractions.ts +20 -11
- package/src/library/local.ts +6 -13
- package/src/library/worker/evaluator.ts +30 -34
- package/src/library/worker/loader.lite.ts +13 -0
- package/src/library/worker/main.ts +8 -8
- package/src/library/worker/protocol.ts +0 -11
- package/src/lock/index.ts +1 -0
- package/src/lock/manager.ts +17 -2
- package/src/lock/test.ts +108 -0
- package/src/orchestrator/manager.ts +17 -36
- package/src/orchestrator/operation-workset.ts +34 -37
- package/src/orchestrator/operation.ts +129 -74
- package/src/project/abstractions.ts +27 -51
- package/src/project/evaluation.ts +248 -0
- package/src/project/index.ts +1 -1
- package/src/project/local.ts +75 -127
- package/src/pubsub/manager.ts +21 -13
- package/src/runner/abstractions.ts +29 -9
- package/src/runner/artifact-env.ts +3 -3
- package/src/runner/local.ts +29 -19
- package/src/runner/pulumi.ts +4 -1
- package/src/services.ts +77 -24
- package/src/shared/models/backend/library.ts +4 -4
- package/src/shared/models/backend/project.ts +25 -6
- package/src/shared/models/backend/unlock-method.ts +1 -1
- package/src/shared/models/base.ts +1 -84
- package/src/shared/models/project/api-key.ts +5 -2
- package/src/shared/models/project/artifact.ts +3 -33
- package/src/shared/models/project/index.ts +1 -2
- package/src/shared/models/project/lock.ts +3 -3
- package/src/shared/models/project/model.ts +14 -0
- package/src/shared/models/project/operation.ts +3 -3
- package/src/shared/models/project/page.ts +3 -3
- package/src/shared/models/project/secret.ts +4 -18
- package/src/shared/models/project/service-account.ts +2 -2
- package/src/shared/models/project/state.ts +32 -15
- package/src/shared/models/project/terminal.ts +4 -5
- package/src/shared/models/project/trigger.ts +1 -1
- package/src/shared/models/project/unlock-method.ts +9 -2
- package/src/shared/models/project/worker.ts +9 -7
- package/src/shared/resolvers/graph-resolver.ts +41 -26
- package/src/shared/resolvers/input.ts +47 -5
- package/src/shared/resolvers/validation.ts +23 -7
- package/src/shared/utils/args.ts +25 -0
- package/src/shared/utils/index.ts +1 -0
- package/src/state/abstractions.ts +98 -259
- package/src/state/encryption.ts +39 -0
- package/src/state/index.ts +1 -0
- package/src/state/local/backend.ts +29 -222
- package/src/state/local/collection.ts +105 -86
- package/src/state/manager.ts +358 -287
- package/src/state/memory/backend.ts +70 -0
- package/src/state/memory/collection.ts +270 -0
- package/src/state/memory/index.ts +2 -0
- package/src/state/repository/repository.index.ts +1 -1
- package/src/state/repository/repository.ts +71 -22
- package/src/state/test.ts +457 -0
- package/src/unlock/abstractions.ts +49 -0
- package/src/unlock/index.ts +2 -0
- package/src/unlock/memory.ts +32 -0
- package/src/worker/manager.ts +28 -0
- package/dist/chunk-RCB4AFGD.js +0 -159
- package/dist/chunk-RCB4AFGD.js.map +0 -1
- package/dist/chunk-WHALQHEZ.js.map +0 -1
- package/src/project/manager.ts +0 -574
- package/src/shared/models/project/component.ts +0 -45
- package/src/shared/models/project/instance.ts +0 -74
package/src/business/worker.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import type { Logger } from "pino"
|
|
2
2
|
import type { LockManager } from "../lock"
|
|
3
3
|
import type { WorkerManager } from "../worker"
|
|
4
|
-
import type {
|
|
5
|
-
import { randomBytes } from "node:crypto"
|
|
6
|
-
import { v7 as uuidv7 } from "uuid"
|
|
4
|
+
import type { RandomProvider } from "../common"
|
|
7
5
|
import {
|
|
8
6
|
getWorkerIdentity,
|
|
9
|
-
type InstanceState,
|
|
10
7
|
type ProjectApiKey,
|
|
11
8
|
type ServiceAccount,
|
|
12
9
|
type UnitWorker,
|
|
@@ -20,66 +17,186 @@ export class WorkerService {
|
|
|
20
17
|
private readonly stateManager: StateManager,
|
|
21
18
|
private readonly workerManager: WorkerManager,
|
|
22
19
|
private readonly lockManager: LockManager,
|
|
23
|
-
private readonly
|
|
20
|
+
private readonly random: RandomProvider,
|
|
24
21
|
private readonly logger: Logger,
|
|
25
22
|
) {}
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Updates the worker registrations for the given project and instance.
|
|
26
|
+
* It creates new registrations for each unit worker, updates existing ones,
|
|
27
|
+
* and deletes registrations that are no longer present.
|
|
28
|
+
*
|
|
29
|
+
* @param projectId The ID of the project.
|
|
30
|
+
* @param instanceId The ID of the instance.
|
|
31
|
+
* @param existingRegistrationIds A mapping of unit worker names to existing registration IDs.
|
|
32
|
+
* @param unitWorkers The list of unit workers to register.
|
|
33
|
+
* @returns A new mapping of unit worker names to their registration IDs.
|
|
34
|
+
*/
|
|
35
|
+
async updateUnitRegistrations(
|
|
28
36
|
projectId: string,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
instanceId: string,
|
|
38
|
+
existingRegistrationIds: Record<string, string>,
|
|
39
|
+
unitWorkers: UnitWorker[],
|
|
40
|
+
): Promise<Record<string, string>> {
|
|
41
|
+
// we will lock on the worker images to ensure that
|
|
42
|
+
// concurrent units cannot create duplicate workers or leave dangling worker resources when removing them
|
|
43
|
+
// we will not lock on the registrations
|
|
44
|
+
// since it they are only updated within an operation
|
|
45
|
+
const lockKeys = unitWorkers.map(
|
|
46
|
+
unitWorker => ["worker-image", projectId, unitWorker.image] as const,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const newRegistrationIds: Record<string, string> = {}
|
|
50
|
+
|
|
51
|
+
return await this.lockManager.acquire(lockKeys, async () => {
|
|
33
52
|
const existingRegistrations = await this.stateManager
|
|
34
53
|
.getWorkerRegistrationRepository(projectId)
|
|
35
|
-
.
|
|
54
|
+
.getManyRecord(Object.values(existingRegistrationIds))
|
|
36
55
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
return
|
|
56
|
+
const nameToExistingRegistration = new Map<string, WorkerUnitRegistration>()
|
|
57
|
+
for (const [name, registrationId] of Object.entries(existingRegistrationIds)) {
|
|
58
|
+
const existingRegistration = existingRegistrations[registrationId]
|
|
59
|
+
if (existingRegistration) {
|
|
60
|
+
nameToExistingRegistration.set(name, existingRegistration)
|
|
61
|
+
}
|
|
45
62
|
}
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
if (!workerId) {
|
|
49
|
-
workerId = await this.ensureWorkerCreated(projectId, unitWorker)
|
|
50
|
-
}
|
|
64
|
+
const batch = this.stateManager.batch()
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
params: unitWorker.params,
|
|
57
|
-
instanceId: state.id,
|
|
58
|
-
workerId,
|
|
59
|
-
}
|
|
66
|
+
// create or update registrations for each unit worker
|
|
67
|
+
for (const unitWorker of unitWorkers) {
|
|
68
|
+
const workerId = await this.ensureWorkerCreated(projectId, unitWorker)
|
|
69
|
+
const existingRegistration = nameToExistingRegistration.get(unitWorker.name)
|
|
60
70
|
|
|
61
|
-
|
|
71
|
+
if (
|
|
72
|
+
existingRegistration &&
|
|
73
|
+
existingRegistration.workerId === workerId &&
|
|
74
|
+
JSON.stringify(existingRegistration.params) === JSON.stringify(unitWorker.params)
|
|
75
|
+
) {
|
|
76
|
+
// no changes
|
|
77
|
+
newRegistrationIds[unitWorker.name] = existingRegistration.id
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
62
80
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
const registration: WorkerUnitRegistration = {
|
|
82
|
+
id: existingRegistration?.id ?? this.random.uuidv7(),
|
|
83
|
+
meta: existingRegistration?.meta ?? {},
|
|
84
|
+
image: unitWorker.image,
|
|
85
|
+
params: unitWorker.params,
|
|
86
|
+
instanceId,
|
|
87
|
+
workerId,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isNewRegistration = !existingRegistration
|
|
91
|
+
const isWorkerChange = existingRegistration?.workerId !== workerId
|
|
92
|
+
const isParamsChange =
|
|
93
|
+
existingRegistration &&
|
|
94
|
+
JSON.stringify(existingRegistration.params) !== JSON.stringify(unitWorker.params)
|
|
95
|
+
|
|
96
|
+
if (isNewRegistration) {
|
|
97
|
+
this.logger.info(
|
|
98
|
+
{
|
|
99
|
+
projectId,
|
|
100
|
+
registrationId: registration.id,
|
|
101
|
+
unitWorkerName: unitWorker.name,
|
|
102
|
+
workerId,
|
|
103
|
+
image: unitWorker.image,
|
|
104
|
+
},
|
|
105
|
+
`creating worker registration "%s" for unit worker "%s" in project "%s"`,
|
|
106
|
+
registration.id,
|
|
107
|
+
unitWorker.name,
|
|
108
|
+
projectId,
|
|
109
|
+
)
|
|
110
|
+
} else if (isWorkerChange) {
|
|
111
|
+
this.logger.info(
|
|
112
|
+
{
|
|
113
|
+
projectId,
|
|
114
|
+
registrationId: registration.id,
|
|
115
|
+
unitWorkerName: unitWorker.name,
|
|
116
|
+
oldWorkerId: existingRegistration?.workerId,
|
|
117
|
+
newWorkerId: workerId,
|
|
118
|
+
oldImage: existingRegistration?.image,
|
|
119
|
+
newImage: unitWorker.image,
|
|
120
|
+
},
|
|
121
|
+
`updating worker registration "%s" for unit worker "%s" in project "%s" (worker changed from "%s" to "%s")`,
|
|
122
|
+
registration.id,
|
|
123
|
+
unitWorker.name,
|
|
124
|
+
projectId,
|
|
125
|
+
existingRegistration?.workerId,
|
|
126
|
+
workerId,
|
|
127
|
+
)
|
|
128
|
+
} else if (isParamsChange) {
|
|
129
|
+
this.logger.info(
|
|
130
|
+
{
|
|
131
|
+
projectId,
|
|
132
|
+
registrationId: registration.id,
|
|
133
|
+
unitWorkerName: unitWorker.name,
|
|
134
|
+
workerId,
|
|
135
|
+
},
|
|
136
|
+
`updating worker registration "%s" for unit worker "%s" in project "%s" (params changed)`,
|
|
137
|
+
registration.id,
|
|
138
|
+
unitWorker.name,
|
|
139
|
+
projectId,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
66
142
|
|
|
67
|
-
if (!existingReg) {
|
|
68
|
-
// update the index if this is a new registration
|
|
69
143
|
await this.stateManager
|
|
70
|
-
.
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
// update the
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
144
|
+
.getWorkerRegistrationRepository(projectId)
|
|
145
|
+
.putItem(registration, batch)
|
|
146
|
+
|
|
147
|
+
// update the registration indexes for new and old workers
|
|
148
|
+
if (existingRegistration?.workerId !== workerId) {
|
|
149
|
+
await this.stateManager
|
|
150
|
+
.getWorkerRegistrationIndexRepository(projectId, workerId)
|
|
151
|
+
.indexRepository.put(registration.id, SAME_KEY)
|
|
152
|
+
|
|
153
|
+
if (existingRegistration?.workerId) {
|
|
154
|
+
await this.stateManager
|
|
155
|
+
.getWorkerRegistrationIndexRepository(projectId, existingRegistration.workerId)
|
|
156
|
+
.indexRepository.delete(registration.id)
|
|
157
|
+
|
|
158
|
+
// check if the old worker should be deleted
|
|
159
|
+
await this.deleteWorkerIfHasNoRegistrations(projectId, existingRegistration.workerId)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
newRegistrationIds[unitWorker.name] = registration.id
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// delete registrations that are no longer present
|
|
167
|
+
for (const [name, existingRegistration] of nameToExistingRegistration.entries()) {
|
|
168
|
+
if (unitWorkers.some(unitWorker => unitWorker.name === name)) {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.logger.info(
|
|
173
|
+
{
|
|
174
|
+
projectId,
|
|
175
|
+
registrationId: existingRegistration.id,
|
|
176
|
+
unitWorkerName: name,
|
|
177
|
+
workerId: existingRegistration.workerId,
|
|
78
178
|
},
|
|
79
|
-
|
|
179
|
+
`deleting worker registration "%s" for unit worker "%s" in project "%s" (unit worker no longer present)`,
|
|
180
|
+
existingRegistration.id,
|
|
181
|
+
name,
|
|
182
|
+
projectId,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
await this.stateManager
|
|
186
|
+
.getWorkerRegistrationRepository(projectId)
|
|
187
|
+
.delete(existingRegistration.id, batch)
|
|
188
|
+
|
|
189
|
+
await this.stateManager
|
|
190
|
+
.getWorkerRegistrationIndexRepository(projectId, existingRegistration.workerId)
|
|
191
|
+
.indexRepository.delete(existingRegistration.id)
|
|
192
|
+
|
|
193
|
+
// ensure the worker is deleted if it has no registrations left
|
|
194
|
+
await this.deleteWorkerIfHasNoRegistrations(projectId, existingRegistration.workerId)
|
|
80
195
|
}
|
|
81
196
|
|
|
82
197
|
await batch.write()
|
|
198
|
+
|
|
199
|
+
return newRegistrationIds
|
|
83
200
|
})
|
|
84
201
|
}
|
|
85
202
|
|
|
@@ -92,10 +209,22 @@ export class WorkerService {
|
|
|
92
209
|
|
|
93
210
|
const existingWorker = workers.find(worker => worker.image === unitWorker.image)
|
|
94
211
|
if (existingWorker) {
|
|
95
|
-
this.logger.debug(
|
|
212
|
+
this.logger.debug(
|
|
213
|
+
{ projectId, workerId: existingWorker.id, image: unitWorker.image },
|
|
214
|
+
`worker with image "%s" already exists, reusing it`,
|
|
215
|
+
unitWorker.image,
|
|
216
|
+
)
|
|
96
217
|
return existingWorker.id
|
|
97
218
|
}
|
|
98
219
|
|
|
220
|
+
this.logger.info(
|
|
221
|
+
{ projectId, image: unitWorker.image, identity },
|
|
222
|
+
`creating new worker for image "%s" with identity "%s" in project "%s"`,
|
|
223
|
+
unitWorker.image,
|
|
224
|
+
identity,
|
|
225
|
+
projectId,
|
|
226
|
+
)
|
|
227
|
+
|
|
99
228
|
let serviceAccountId: string | undefined
|
|
100
229
|
|
|
101
230
|
const siblingWorker = workers.find(worker => worker.identity === identity)
|
|
@@ -114,20 +243,29 @@ export class WorkerService {
|
|
|
114
243
|
// the meta of the account will be updated by the worker itself
|
|
115
244
|
if (!serviceAccountId) {
|
|
116
245
|
const serviceAccount: ServiceAccount = {
|
|
117
|
-
id: uuidv7(),
|
|
246
|
+
id: this.random.uuidv7(),
|
|
118
247
|
meta: {},
|
|
119
248
|
}
|
|
120
249
|
|
|
121
250
|
serviceAccountId = serviceAccount.id
|
|
251
|
+
|
|
252
|
+
this.logger.info(
|
|
253
|
+
{ projectId, serviceAccountId, identity },
|
|
254
|
+
`creating service account "%s" for worker identity "%s" in project "%s"`,
|
|
255
|
+
serviceAccountId,
|
|
256
|
+
identity,
|
|
257
|
+
projectId,
|
|
258
|
+
)
|
|
259
|
+
|
|
122
260
|
await this.stateManager
|
|
123
261
|
.getServiceAccountRepository(projectId)
|
|
124
262
|
.putItem(serviceAccount, batch)
|
|
125
263
|
}
|
|
126
264
|
|
|
127
265
|
const apiKey: ProjectApiKey = {
|
|
128
|
-
id: uuidv7(),
|
|
266
|
+
id: this.random.uuidv7(),
|
|
129
267
|
meta: {},
|
|
130
|
-
token:
|
|
268
|
+
token: Buffer.from(this.random.bytes(32)).toString("hex"),
|
|
131
269
|
scopes: [
|
|
132
270
|
{
|
|
133
271
|
type: "service-account",
|
|
@@ -140,7 +278,7 @@ export class WorkerService {
|
|
|
140
278
|
await this.stateManager.getApiKeyRepository(projectId).putItem(apiKey, batch)
|
|
141
279
|
|
|
142
280
|
const worker: Worker = {
|
|
143
|
-
id: uuidv7(),
|
|
281
|
+
id: this.random.uuidv7(),
|
|
144
282
|
meta: {},
|
|
145
283
|
status: "starting",
|
|
146
284
|
failedStartAttempts: 5,
|
|
@@ -150,12 +288,111 @@ export class WorkerService {
|
|
|
150
288
|
apiKeyId: apiKey.id,
|
|
151
289
|
}
|
|
152
290
|
|
|
291
|
+
this.logger.info(
|
|
292
|
+
{ projectId, workerId: worker.id, image: unitWorker.image, identity, serviceAccountId },
|
|
293
|
+
`creating worker "%s" for image "%s" in project "%s"`,
|
|
294
|
+
worker.id,
|
|
295
|
+
unitWorker.image,
|
|
296
|
+
projectId,
|
|
297
|
+
)
|
|
298
|
+
|
|
153
299
|
await this.stateManager.getWorkerRepository(projectId).putItem(worker, batch)
|
|
154
300
|
await batch.write()
|
|
155
301
|
|
|
156
|
-
this.workerManager.startWorker(projectId, worker)
|
|
302
|
+
void this.workerManager.startWorker(projectId, worker.id)
|
|
157
303
|
|
|
158
304
|
return worker.id
|
|
159
305
|
})
|
|
160
306
|
}
|
|
307
|
+
|
|
308
|
+
private async deleteWorkerIfHasNoRegistrations(
|
|
309
|
+
projectId: string,
|
|
310
|
+
workerId: string,
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
await this.lockManager.acquire(["worker", workerId], async () => {
|
|
313
|
+
const registrations = await this.stateManager
|
|
314
|
+
.getWorkerRegistrationIndexRepository(projectId, workerId)
|
|
315
|
+
.getAllItems()
|
|
316
|
+
|
|
317
|
+
if (registrations.length > 0) {
|
|
318
|
+
// still has registrations, no need to delete
|
|
319
|
+
this.logger.debug(
|
|
320
|
+
{ projectId, workerId, registrationCount: registrations.length },
|
|
321
|
+
`worker "%s" still has %d registrations, not deleting`,
|
|
322
|
+
workerId,
|
|
323
|
+
registrations.length,
|
|
324
|
+
)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const worker = await this.stateManager.getWorkerRepository(projectId).get(workerId)
|
|
329
|
+
|
|
330
|
+
if (!worker) {
|
|
331
|
+
this.logger.warn(
|
|
332
|
+
{ projectId, workerId },
|
|
333
|
+
`worker "%s" not found in project "%s" while deleting`,
|
|
334
|
+
workerId,
|
|
335
|
+
projectId,
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const batch = this.stateManager.batch()
|
|
341
|
+
|
|
342
|
+
this.logger.info(
|
|
343
|
+
{ projectId, workerId, image: worker.image, identity: worker.identity },
|
|
344
|
+
`deleting worker "%s" with image "%s" in project "%s" (no registrations remaining)`,
|
|
345
|
+
workerId,
|
|
346
|
+
worker.image,
|
|
347
|
+
projectId,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
await this.stateManager.getWorkerRepository(projectId).delete(workerId, batch)
|
|
351
|
+
await this.stateManager.getApiKeyRepository(projectId).delete(worker.apiKeyId, batch)
|
|
352
|
+
|
|
353
|
+
const workers = await this.stateManager.getWorkerRepository(projectId).getAllItems()
|
|
354
|
+
const hasSiblingWorker = workers.some(
|
|
355
|
+
siblingWorker =>
|
|
356
|
+
siblingWorker.identity === worker.identity && siblingWorker.id !== workerId,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if (!hasSiblingWorker) {
|
|
360
|
+
this.logger.info(
|
|
361
|
+
{
|
|
362
|
+
projectId,
|
|
363
|
+
workerId,
|
|
364
|
+
serviceAccountId: worker.serviceAccountId,
|
|
365
|
+
identity: worker.identity,
|
|
366
|
+
},
|
|
367
|
+
`deleting service account "%s" for worker "%s" in project "%s" (no sibling workers remaining)`,
|
|
368
|
+
worker.serviceAccountId,
|
|
369
|
+
workerId,
|
|
370
|
+
projectId,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
await this.stateManager
|
|
374
|
+
.getServiceAccountRepository(projectId)
|
|
375
|
+
.delete(worker.serviceAccountId, batch)
|
|
376
|
+
} else {
|
|
377
|
+
this.logger.debug(
|
|
378
|
+
{
|
|
379
|
+
projectId,
|
|
380
|
+
workerId,
|
|
381
|
+
serviceAccountId: worker.serviceAccountId,
|
|
382
|
+
identity: worker.identity,
|
|
383
|
+
},
|
|
384
|
+
`not deleting service account "%s" for worker "%s" in project "%s" (has sibling workers)`,
|
|
385
|
+
worker.serviceAccountId,
|
|
386
|
+
workerId,
|
|
387
|
+
projectId,
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// for now, we will keep the service account even if the last sibling worker is deleted
|
|
392
|
+
await batch.write()
|
|
393
|
+
|
|
394
|
+
// stop the worker and clear logs after it
|
|
395
|
+
this.workerManager.stopWorker(projectId, workerId)
|
|
396
|
+
})
|
|
397
|
+
}
|
|
161
398
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ClockProvider {
|
|
2
|
+
/**
|
|
3
|
+
* Returns the current time in milliseconds since the Unix epoch.
|
|
4
|
+
*/
|
|
5
|
+
now(): number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class SystemClockProvider implements ClockProvider {
|
|
9
|
+
now(): number {
|
|
10
|
+
return Date.now()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ReproducibleClockProvider implements ClockProvider {
|
|
15
|
+
now(): number {
|
|
16
|
+
return 0
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/common/index.ts
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto"
|
|
2
|
+
import { v4 as uuidv4, v7 as uuidv7 } from "uuid"
|
|
3
|
+
|
|
4
|
+
export interface RandomProvider {
|
|
5
|
+
/**
|
|
6
|
+
* Generates a random string of the specified length.
|
|
7
|
+
*
|
|
8
|
+
* @param length The length of the random string to generate.
|
|
9
|
+
*/
|
|
10
|
+
bytes(length: number): Uint8Array
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a random UUID (version 4).
|
|
14
|
+
*/
|
|
15
|
+
uuidv4(): string
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates a random UUID (version 7).
|
|
19
|
+
*/
|
|
20
|
+
uuidv7(): string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class CryptoRandomProvider implements RandomProvider {
|
|
24
|
+
bytes(length: number): Uint8Array {
|
|
25
|
+
return randomBytes(length)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
uuidv4(): string {
|
|
29
|
+
return randomUUID()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
uuidv7(): string {
|
|
33
|
+
return uuidv7()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ReproducibleRandomProvider implements RandomProvider {
|
|
38
|
+
private readonly seed: Uint8Array
|
|
39
|
+
private counter = 0
|
|
40
|
+
|
|
41
|
+
constructor(seed: Uint8Array) {
|
|
42
|
+
this.seed = seed
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
bytes(length: number): Uint8Array {
|
|
46
|
+
const buffer = new Uint8Array(length)
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < length; i++) {
|
|
49
|
+
// use a simple counter to generate reproducible random bytes
|
|
50
|
+
buffer[i] = (this.seed[i % this.seed.length] + this.counter) % 256
|
|
51
|
+
this.counter += 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return buffer
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
uuidv4(): string {
|
|
58
|
+
return uuidv4({ random: this.bytes(16) })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
uuidv7(): string {
|
|
62
|
+
return uuidv7({
|
|
63
|
+
random: this.bytes(16),
|
|
64
|
+
seq: this.counter++,
|
|
65
|
+
msecs: this.counter++,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { camelCaseToHumanReadable, registerKnownAbbreviations } from "@highstate/contract"
|
|
2
|
+
import * as md from "ts-markdown-builder"
|
|
3
|
+
|
|
4
|
+
type RenderTraceEntryField = {
|
|
5
|
+
value: unknown
|
|
6
|
+
raw?: boolean
|
|
7
|
+
alwaysRender?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type RenderTraceEntryOptions = {
|
|
11
|
+
icon: string
|
|
12
|
+
title: string
|
|
13
|
+
code?: string
|
|
14
|
+
codeBlock?: string
|
|
15
|
+
body?: string
|
|
16
|
+
fields?: Record<string, RenderTraceEntryField>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function codeBlock(code: string): string {
|
|
20
|
+
return `\`\`\`js\n${code}\n\`\`\``
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isPrimitive(value: unknown): boolean {
|
|
24
|
+
return (
|
|
25
|
+
typeof value === "string" ||
|
|
26
|
+
typeof value === "number" ||
|
|
27
|
+
typeof value === "boolean" ||
|
|
28
|
+
value === null ||
|
|
29
|
+
value === undefined
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function renderMdValue(value: unknown): string {
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
return md.code(`"${value}"`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isPrimitive(value)) {
|
|
39
|
+
return md.code(String(value))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (value instanceof Error) {
|
|
43
|
+
return codeBlock(value.stack ?? value.message)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return codeBlock(JSON.stringify(value, null, 2))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
registerKnownAbbreviations(["ID"])
|
|
50
|
+
|
|
51
|
+
export function renderTraceEntry(options: RenderTraceEntryOptions): string {
|
|
52
|
+
const blocks: string[] = [`${options.icon} ${options.title}`]
|
|
53
|
+
|
|
54
|
+
if (options.code) {
|
|
55
|
+
blocks.push(md.code(options.code))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (options.codeBlock) {
|
|
59
|
+
blocks.push(codeBlock(options.codeBlock))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (options.fields) {
|
|
63
|
+
for (const [key, field] of Object.entries(options.fields)) {
|
|
64
|
+
const prefix = md.bold(camelCaseToHumanReadable(key) + ":")
|
|
65
|
+
|
|
66
|
+
if (field.raw && typeof field.value === "string") {
|
|
67
|
+
blocks.push(`${prefix} ${field.value}`)
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!field.alwaysRender && field.value === undefined) {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(field.value) && field.value.length > 0) {
|
|
76
|
+
blocks.push(prefix, ...field.value.map(item => renderMdValue(item)))
|
|
77
|
+
} else if (Array.isArray(field.value)) {
|
|
78
|
+
blocks.push(`${prefix} ${md.code("[]")}`)
|
|
79
|
+
} else if (
|
|
80
|
+
typeof field.value === "object" &&
|
|
81
|
+
field.value !== null &&
|
|
82
|
+
Object.keys(field.value).length === 0
|
|
83
|
+
) {
|
|
84
|
+
blocks.push(`${prefix} ${md.code("{}")}`)
|
|
85
|
+
} else if (isPrimitive(field.value)) {
|
|
86
|
+
blocks.push(`${prefix} ${renderMdValue(field.value)}`)
|
|
87
|
+
} else {
|
|
88
|
+
blocks.push(prefix, renderMdValue(field.value))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (options.body) {
|
|
94
|
+
blocks.push(options.body)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return md.joinBlocks(blocks)
|
|
98
|
+
}
|