@highstate/backend 0.9.16 → 0.9.19
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-5WVU2AK4.js +1535 -0
- package/dist/chunk-5WVU2AK4.js.map +1 -0
- package/dist/chunk-I7BWSAN6.js +49 -0
- package/dist/chunk-I7BWSAN6.js.map +1 -0
- package/dist/{chunk-RCB4AFGD.js → chunk-VB4YL327.js} +51 -71
- package/dist/chunk-VB4YL327.js.map +1 -0
- package/dist/database/local/prisma.config.js +26 -0
- package/dist/database/local/prisma.config.js.map +1 -0
- package/dist/highstate.manifest.json +5 -4
- package/dist/index.js +7676 -6634
- package/dist/index.js.map +1 -1
- package/dist/library/package-resolution-worker.js +8 -6
- package/dist/library/package-resolution-worker.js.map +1 -1
- package/dist/library/worker/main.js +63 -58
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +3 -216
- package/dist/shared/index.js.map +1 -1
- package/package.json +23 -11
- package/prisma/backend/_schema/layout.prisma +7 -0
- package/prisma/backend/_schema/library.prisma +17 -0
- package/prisma/backend/_schema/project.prisma +101 -0
- package/prisma/backend/_schema/pulumi.prisma +17 -0
- package/prisma/backend/postgresql/main.prisma +17 -0
- package/prisma/backend/sqlite/main.prisma +17 -0
- package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +34 -0
- package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +59 -0
- package/prisma/backend/sqlite/migrations/20250818082732_add_models/migration.sql +41 -0
- package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +19 -0
- package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +1 -0
- package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +5 -0
- package/prisma/backend/sqlite/migrations/migration_lock.toml +3 -0
- package/prisma/project/api-key.prisma +27 -0
- package/prisma/project/artifact.prisma +52 -0
- package/prisma/project/custom-status.prisma +46 -0
- package/prisma/project/evaluation.prisma +35 -0
- package/prisma/project/instance.prisma +160 -0
- package/prisma/project/layout.prisma +23 -0
- package/prisma/project/lock.prisma +18 -0
- package/prisma/project/main.prisma +17 -0
- package/prisma/project/migrations/20250816081310_initial/migration.sql +300 -0
- package/prisma/project/migrations/20250816082523_test/migration.sql +72 -0
- package/prisma/project/migrations/20250818065643_update/migration.sql +42 -0
- package/prisma/project/migrations/20250818070758_a/migration.sql +8 -0
- package/prisma/project/migrations/20250818070913_a/migration.sql +8 -0
- package/prisma/project/migrations/20250818082720_add_motels/migration.sql +11 -0
- package/prisma/project/migrations/20250818112523_hello/migration.sql +35 -0
- package/prisma/project/migrations/20250819082305_a/migration.sql +14 -0
- package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +216 -0
- package/prisma/project/migrations/20250819171309_a/migration.sql +22 -0
- package/prisma/project/migrations/20250820113949_a/migration.sql +66 -0
- package/prisma/project/migrations/20250820144256_b/migration.sql +31 -0
- package/prisma/project/migrations/20250820145547_a/migration.sql +24 -0
- package/prisma/project/migrations/20250820182517_b/migration.sql +2 -0
- package/prisma/project/migrations/20250821172324_a/migration.sql +2 -0
- package/prisma/project/migrations/20250822081339_a/migration.sql +219 -0
- package/prisma/project/migrations/20250822083742_b/migration.sql +1 -0
- package/prisma/project/migrations/20250822105134_boom/migration.sql +1 -0
- package/prisma/project/migrations/20250822141028_b/migration.sql +1 -0
- package/prisma/project/migrations/20250822142342_b/migration.sql +16 -0
- package/prisma/project/migrations/20250824072720_a/migration.sql +1 -0
- package/prisma/project/migrations/20250824093656_b/migration.sql +21 -0
- package/prisma/project/migrations/20250825082518_a/migration.sql +1 -0
- package/prisma/project/migrations/20250825085343_b/migration.sql +1 -0
- package/prisma/project/migrations/20250825091312_a/migration.sql +1 -0
- package/prisma/project/migrations/20250903095431_hi/migration.sql +44 -0
- package/prisma/project/migrations/20250903174255_a/migration.sql +24 -0
- package/prisma/project/migrations/20250908095205_hi/migration.sql +18 -0
- package/prisma/project/migrations/20250909155857_hi/migration.sql +15 -0
- package/prisma/project/migrations/migration_lock.toml +3 -0
- package/prisma/project/model.prisma +37 -0
- package/prisma/project/operation.prisma +148 -0
- package/prisma/project/page.prisma +41 -0
- package/prisma/project/secret.prisma +42 -0
- package/prisma/project/service-account.prisma +36 -0
- package/prisma/project/terminal.prisma +90 -0
- package/prisma/project/trigger.prisma +31 -0
- package/prisma/project/unlock-method.prisma +32 -0
- package/prisma/project/worker.prisma +138 -0
- package/src/artifact/abstractions.ts +13 -13
- package/src/artifact/encryption.ts +31 -15
- package/src/artifact/factory.ts +7 -10
- package/src/artifact/local.ts +33 -50
- package/src/business/api-key.ts +24 -36
- package/src/business/artifact.test.ts +978 -0
- package/src/business/artifact.ts +136 -215
- package/src/business/evaluation.ts +328 -0
- package/src/business/index.ts +5 -1
- package/src/business/instance-lock.test.ts +1060 -0
- package/src/business/instance-lock.ts +387 -77
- package/src/business/instance-state.test.ts +735 -0
- package/src/business/instance-state.ts +604 -217
- package/src/business/operation.test.ts +439 -0
- package/src/business/operation.ts +174 -208
- package/src/business/project-model.ts +258 -0
- package/src/business/project-unlock.ts +172 -112
- package/src/business/project.ts +407 -0
- package/src/business/secret.test.ts +513 -0
- package/src/business/secret.ts +194 -131
- package/src/business/settings.test.ts +695 -0
- package/src/business/settings.ts +855 -0
- package/src/business/terminal-session.ts +90 -0
- package/src/business/unit-extra.test.ts +539 -0
- package/src/business/unit-extra.ts +160 -0
- package/src/business/worker.test.ts +391 -0
- package/src/business/worker.ts +250 -114
- package/src/common/codebase.ts +65 -0
- package/src/common/index.ts +3 -2
- package/src/common/logger.ts +5 -0
- package/src/common/utils.ts +4 -3
- package/src/config.ts +15 -12
- package/src/database/_generated/backend/postgresql/client.ts +72 -0
- package/src/database/_generated/backend/postgresql/commonInputTypes.ts +350 -0
- package/src/database/_generated/backend/postgresql/enums.ts +13 -0
- package/src/database/_generated/backend/postgresql/internal/class.ts +320 -0
- package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +1238 -0
- package/src/database/_generated/backend/postgresql/models/Library.ts +1263 -0
- package/src/database/_generated/backend/postgresql/models/Project.ts +2175 -0
- package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +1263 -0
- package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +1602 -0
- package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +1263 -0
- package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +1065 -0
- package/src/database/_generated/backend/postgresql/models.ts +16 -0
- package/src/database/_generated/backend/postgresql/pjtg.ts +182 -0
- package/src/database/_generated/backend/sqlite/client.ts +72 -0
- package/src/database/_generated/backend/sqlite/commonInputTypes.ts +331 -0
- package/src/database/_generated/backend/sqlite/enums.ts +13 -0
- package/src/database/_generated/backend/sqlite/internal/class.ts +318 -0
- package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +1207 -0
- package/src/database/_generated/backend/sqlite/models/Library.ts +1261 -0
- package/src/database/_generated/backend/sqlite/models/Project.ts +2169 -0
- package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +1261 -0
- package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +1599 -0
- package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +1261 -0
- package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +1063 -0
- package/src/database/_generated/backend/sqlite/models.ts +16 -0
- package/src/database/_generated/backend/sqlite/pjtg.ts +182 -0
- package/src/database/_generated/project/client.ts +204 -0
- package/src/database/_generated/project/commonInputTypes.ts +827 -0
- package/src/database/_generated/project/enums.ts +104 -0
- package/src/database/_generated/project/internal/class.ts +479 -0
- package/src/database/_generated/project/internal/prismaNamespace.ts +2974 -0
- package/src/database/_generated/project/models/ApiKey.ts +1506 -0
- package/src/database/_generated/project/models/Artifact.ts +2051 -0
- package/src/database/_generated/project/models/HubModel.ts +1125 -0
- package/src/database/_generated/project/models/InstanceCustomStatus.ts +1713 -0
- package/src/database/_generated/project/models/InstanceEvaluationState.ts +1312 -0
- package/src/database/_generated/project/models/InstanceLock.ts +1268 -0
- package/src/database/_generated/project/models/InstanceModel.ts +1125 -0
- package/src/database/_generated/project/models/InstanceOperationState.ts +1707 -0
- package/src/database/_generated/project/models/InstanceState.ts +4613 -0
- package/src/database/_generated/project/models/Operation.ts +1647 -0
- package/src/database/_generated/project/models/OperationLog.ts +1455 -0
- package/src/database/_generated/project/models/Page.ts +1838 -0
- package/src/database/_generated/project/models/Secret.ts +1692 -0
- package/src/database/_generated/project/models/ServiceAccount.ts +2165 -0
- package/src/database/_generated/project/models/Terminal.ts +2038 -0
- package/src/database/_generated/project/models/TerminalSession.ts +1454 -0
- package/src/database/_generated/project/models/TerminalSessionLog.ts +1280 -0
- package/src/database/_generated/project/models/Trigger.ts +1430 -0
- package/src/database/_generated/project/models/UnlockMethod.ts +1220 -0
- package/src/database/_generated/project/models/UserCompositeViewport.ts +1280 -0
- package/src/database/_generated/project/models/UserProjectViewport.ts +1059 -0
- package/src/database/_generated/project/models/Worker.ts +1459 -0
- package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1524 -0
- package/src/database/_generated/project/models/WorkerVersion.ts +1974 -0
- package/src/database/_generated/project/models/WorkerVersionLog.ts +1318 -0
- package/src/database/_generated/project/models.ts +35 -0
- package/src/database/_generated/project/pjtg.ts +182 -0
- package/src/database/abstractions.ts +19 -0
- package/src/database/factory.ts +37 -0
- package/src/database/index.ts +6 -0
- package/src/database/local/backend.ts +134 -0
- package/src/database/local/index.ts +3 -0
- package/src/database/local/meta.ts +46 -0
- package/src/database/local/prisma.config.ts +25 -0
- package/src/database/local/project.ts +39 -0
- package/src/database/manager.ts +181 -0
- package/src/database/migrate.ts +35 -0
- package/src/database/prisma.ts +56 -0
- package/src/database/well-known.ts +38 -0
- package/src/index.ts +4 -4
- package/src/library/abstractions.ts +21 -14
- package/src/library/factory.ts +1 -1
- package/src/library/local.ts +86 -38
- package/src/library/package-resolution-worker.ts +1 -1
- package/src/library/worker/evaluator.ts +61 -48
- package/src/library/worker/loader.lite.ts +14 -1
- package/src/library/worker/main.ts +9 -16
- package/src/library/worker/protocol.ts +0 -12
- package/src/lock/manager.ts +12 -7
- package/src/orchestrator/manager.ts +198 -131
- package/src/orchestrator/operation-context.ts +357 -0
- package/src/orchestrator/operation-plan.destroy.test.md +357 -0
- package/src/orchestrator/operation-plan.destroy.test.ts +775 -0
- package/src/orchestrator/operation-plan.fixtures.ts +213 -0
- package/src/orchestrator/operation-plan.md +198 -0
- package/src/orchestrator/operation-plan.refresh.test.md +199 -0
- package/src/orchestrator/operation-plan.refresh.test.ts +367 -0
- package/src/orchestrator/operation-plan.ts +709 -0
- package/src/orchestrator/operation-plan.update.test.md +485 -0
- package/src/orchestrator/operation-plan.update.test.ts +1066 -0
- package/src/orchestrator/operation-workset.ts +235 -583
- package/src/orchestrator/operation.ts +446 -904
- package/src/orchestrator/plan-test-builder.ts +267 -0
- package/src/project-model/abstractions.ts +118 -0
- package/src/project-model/backends/codebase.ts +365 -0
- package/src/project-model/backends/database.ts +440 -0
- package/src/project-model/errors.ts +81 -0
- package/src/project-model/factory.ts +24 -0
- package/src/project-model/index.ts +4 -0
- package/src/project-model/utils.test.ts +544 -0
- package/src/project-model/utils.ts +242 -0
- package/src/pubsub/abstractions.ts +10 -1
- package/src/pubsub/factory.ts +4 -4
- package/src/pubsub/index.ts +1 -0
- package/src/pubsub/manager.ts +49 -25
- package/src/pubsub/memory.ts +31 -0
- package/src/runner/abstractions.ts +38 -26
- package/src/runner/artifact-env.ts +17 -6
- package/src/runner/factory.ts +6 -6
- package/src/runner/force-abort.ts +3 -6
- package/src/runner/local.ts +79 -72
- package/src/runner/pulumi.ts +26 -63
- package/src/services.ts +214 -103
- package/src/shared/models/backend/index.ts +3 -1
- package/src/shared/models/backend/library.ts +12 -4
- package/src/shared/models/backend/project.ts +43 -23
- package/src/shared/models/backend/pulumi.ts +14 -0
- package/src/shared/models/backend/unlock-method.ts +1 -1
- package/src/shared/models/backend/well-known.ts +58 -0
- package/src/shared/models/base.ts +40 -109
- package/src/shared/models/errors.ts +82 -1
- package/src/shared/models/index.ts +3 -2
- package/src/shared/models/prisma.ts +36 -0
- package/src/shared/models/project/api-key.ts +37 -56
- package/src/shared/models/project/artifact.ts +15 -105
- package/src/shared/models/project/custom-status.ts +12 -0
- package/src/shared/models/project/index.ts +9 -9
- package/src/shared/models/project/lock.ts +10 -78
- package/src/shared/models/project/model.ts +32 -0
- package/src/shared/models/project/operation.ts +222 -99
- package/src/shared/models/project/page.ts +37 -48
- package/src/shared/models/project/secret.ts +29 -103
- package/src/shared/models/project/service-account.ts +12 -17
- package/src/shared/models/project/state.ts +100 -390
- package/src/shared/models/project/terminal.ts +75 -89
- package/src/shared/models/project/trigger.ts +13 -49
- package/src/shared/models/project/unlock-method.ts +21 -20
- package/src/shared/models/project/worker.ts +89 -88
- package/src/shared/resolvers/graph-resolver.ts +62 -26
- package/src/shared/resolvers/index.ts +1 -1
- package/src/shared/resolvers/input-hash.ts +24 -14
- package/src/shared/resolvers/input.ts +48 -6
- package/src/shared/resolvers/registry.ts +5 -4
- package/src/shared/resolvers/state.ts +12 -1
- package/src/shared/resolvers/validation.ts +29 -9
- package/src/shared/utils/index.ts +1 -1
- package/src/shared/utils/promise-tracker.ts +30 -3
- package/src/terminal/abstractions.ts +1 -1
- package/src/terminal/docker.ts +3 -3
- package/src/terminal/manager.ts +102 -118
- package/src/test-utils/database.ts +119 -0
- package/src/test-utils/index.ts +2 -0
- package/src/test-utils/services.ts +134 -0
- package/src/unlock/abstractions.ts +31 -0
- package/src/unlock/index.ts +2 -0
- package/src/unlock/memory.ts +27 -0
- package/src/worker/abstractions.ts +7 -4
- package/src/worker/docker.ts +14 -19
- package/src/worker/manager.ts +376 -79
- package/dist/chunk-RCB4AFGD.js.map +0 -1
- package/dist/chunk-WHALQHEZ.js +0 -2017
- package/dist/chunk-WHALQHEZ.js.map +0 -1
- package/src/business/backend-unlock.ts +0 -10
- package/src/common/performance.ts +0 -44
- package/src/hotstate/abstractions.ts +0 -48
- package/src/hotstate/factory.ts +0 -17
- package/src/hotstate/index.ts +0 -3
- package/src/hotstate/manager.ts +0 -192
- package/src/hotstate/memory.ts +0 -100
- package/src/hotstate/validation.ts +0 -101
- package/src/project/abstractions.ts +0 -102
- package/src/project/factory.ts +0 -11
- package/src/project/index.ts +0 -3
- package/src/project/local.ts +0 -469
- package/src/project/manager.ts +0 -574
- package/src/pubsub/local.ts +0 -36
- package/src/pubsub/validation.ts +0 -33
- package/src/shared/models/project/component.ts +0 -45
- package/src/shared/models/project/instance.ts +0 -74
- package/src/state/abstractions.ts +0 -450
- package/src/state/encryption.ts +0 -59
- package/src/state/factory.ts +0 -20
- package/src/state/index.ts +0 -6
- package/src/state/local/backend.ts +0 -299
- package/src/state/local/collection.ts +0 -342
- package/src/state/local/index.ts +0 -2
- package/src/state/manager.ts +0 -819
- package/src/state/repository/index.ts +0 -2
- package/src/state/repository/repository.index.ts +0 -193
- package/src/state/repository/repository.ts +0 -458
- /package/src/{state → database/local}/keyring.ts +0 -0
|
@@ -1,124 +1,434 @@
|
|
|
1
|
+
import type { CommonObjectMeta } from "@highstate/contract"
|
|
1
2
|
import type { Logger } from "pino"
|
|
2
|
-
import type {
|
|
3
|
+
import type { DatabaseManager, InstanceLock, ProjectTransaction } from "../database"
|
|
3
4
|
import type { PubSubManager } from "../pubsub"
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
5
|
+
import { createId } from "@paralleldrive/cuid2"
|
|
6
|
+
import { type InstanceLockEvent, InstanceLockLostError } from "../shared"
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Service for managing instance locks within projects.
|
|
10
|
+
* Handles atomic lock operations using database transactions.
|
|
11
|
+
*/
|
|
7
12
|
export class InstanceLockService {
|
|
8
13
|
constructor(
|
|
9
|
-
private readonly
|
|
10
|
-
private readonly lockManager: LockManager,
|
|
14
|
+
private readonly database: DatabaseManager,
|
|
11
15
|
private readonly pubsubManager: PubSubManager,
|
|
12
16
|
private readonly logger: Logger,
|
|
13
17
|
) {}
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
20
|
+
* Attempts to acquire locks on the specified instances.
|
|
21
|
+
* Uses database transactions to ensure atomicity.
|
|
18
22
|
*
|
|
19
|
-
* @param projectId The project ID
|
|
20
|
-
* @param
|
|
21
|
-
* @param lockMeta The metadata for the
|
|
22
|
-
* @param
|
|
23
|
-
* @param allowPartialLock Whether to allow partial locking
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* @return A tuple containing a boolean indicating if the lock was acquired,
|
|
27
|
-
* and an array of instance IDs that were successfully locked.
|
|
28
|
-
* If `allowPartialLock` is false and some instances are already locked,
|
|
29
|
-
* it will return false and an empty array.
|
|
23
|
+
* @param projectId The project ID containing the instances.
|
|
24
|
+
* @param stateIds The instance state IDs to lock.
|
|
25
|
+
* @param lockMeta The metadata for the locks.
|
|
26
|
+
* @param action Optional async action to execute once locks are acquired in the same transaction with the locks.
|
|
27
|
+
* @param allowPartialLock Whether to allow partial locking when some instances are already locked.
|
|
28
|
+
* @param customToken Optional custom token to use instead of auto-generating one.
|
|
29
|
+
* @returns A tuple containing the token and array of successfully locked state IDs.
|
|
30
30
|
*/
|
|
31
31
|
async tryLockInstances(
|
|
32
32
|
projectId: string,
|
|
33
|
-
|
|
34
|
-
lockMeta:
|
|
35
|
-
|
|
33
|
+
stateIds: string[],
|
|
34
|
+
lockMeta: CommonObjectMeta,
|
|
35
|
+
action?: (tx: ProjectTransaction, stateIds: string[]) => Promise<void>,
|
|
36
36
|
allowPartialLock = false,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// check if the instances are already locked
|
|
43
|
-
const existingLocks = await this.stateManager
|
|
44
|
-
.getInstanceLockRepository(projectId)
|
|
45
|
-
.getManyItems(instanceIds)
|
|
46
|
-
|
|
47
|
-
// only the locks with another spec are considered as the conflicting locks
|
|
48
|
-
const conflictingLocks = existingLocks.filter(
|
|
49
|
-
lock => !compareInstanceLockSpecs(lock.spec, spec),
|
|
50
|
-
)
|
|
37
|
+
customToken?: string,
|
|
38
|
+
): Promise<[token: string, lockedStateIds: string[]]> {
|
|
39
|
+
if (stateIds.length === 0) {
|
|
40
|
+
return ["", []]
|
|
41
|
+
}
|
|
51
42
|
|
|
52
|
-
|
|
43
|
+
// use custom token or generate a cuid token for this lock operation
|
|
44
|
+
const token = customToken ?? createId()
|
|
45
|
+
const database = await this.database.forProject(projectId)
|
|
46
|
+
|
|
47
|
+
return await database.$transaction(async tx => {
|
|
48
|
+
// check for existing locks on requested instances
|
|
49
|
+
const existingLocks = await tx.instanceLock.findMany({
|
|
50
|
+
where: { stateId: { in: stateIds } },
|
|
51
|
+
select: { stateId: true },
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const lockedStateIds = existingLocks.map(lock => lock.stateId)
|
|
55
|
+
const availableStateIds = stateIds.filter(id => !lockedStateIds.includes(id))
|
|
56
|
+
|
|
57
|
+
if (lockedStateIds.length > 0) {
|
|
53
58
|
this.logger.debug(
|
|
54
59
|
{
|
|
55
60
|
projectId,
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
conflictingInstances: lockedStateIds.length,
|
|
62
|
+
totalRequested: stateIds.length,
|
|
58
63
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
projectId,
|
|
64
|
+
"found %s conflicting locks when attempting to lock %s instances",
|
|
65
|
+
lockedStateIds.length,
|
|
66
|
+
stateIds.length,
|
|
63
67
|
)
|
|
64
68
|
|
|
65
69
|
if (!allowPartialLock) {
|
|
66
|
-
|
|
67
|
-
return [false, []]
|
|
70
|
+
return ["", []]
|
|
68
71
|
}
|
|
72
|
+
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
if (availableStateIds.length === 0) {
|
|
75
|
+
// when custom token is provided and no instances are locked, don't return the token
|
|
76
|
+
// when allowPartialLock is true and no custom token, return a token for consistency
|
|
77
|
+
return [allowPartialLock && !customToken ? token : "", []]
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
// create locks for available instances with the generated token
|
|
81
|
+
const lockData: InstanceLock[] = availableStateIds.map(stateId => ({
|
|
82
|
+
stateId,
|
|
83
|
+
meta: lockMeta,
|
|
84
|
+
token,
|
|
85
|
+
acquiredAt: new Date(),
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
await tx.instanceLock.createMany({ data: lockData })
|
|
75
89
|
|
|
76
|
-
|
|
77
|
-
await this.stateManager.getInstanceLockRepository(projectId).putManyItems(newLocks)
|
|
90
|
+
await action?.(tx, availableStateIds)
|
|
78
91
|
|
|
79
92
|
this.logger.debug(
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
spec,
|
|
84
|
-
},
|
|
85
|
-
`locked %s instances in project "%s"`,
|
|
86
|
-
newLocks.length,
|
|
87
|
-
projectId,
|
|
93
|
+
{ projectId, lockedCount: availableStateIds.length, token },
|
|
94
|
+
"locked %s instances",
|
|
95
|
+
availableStateIds.length,
|
|
88
96
|
)
|
|
89
97
|
|
|
90
|
-
// publish
|
|
98
|
+
// publish lock event
|
|
91
99
|
await this.pubsubManager.publish(["instance-lock", projectId], {
|
|
92
100
|
type: "locked",
|
|
93
|
-
locks:
|
|
101
|
+
locks: lockData,
|
|
94
102
|
})
|
|
95
103
|
|
|
96
|
-
return [
|
|
104
|
+
return [token, availableStateIds]
|
|
97
105
|
})
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
/**
|
|
101
|
-
*
|
|
102
|
-
* This will remove the locks regardless of their current state.
|
|
109
|
+
* Checks if an instance is currently locked.
|
|
103
110
|
*
|
|
104
|
-
* @param projectId The project ID
|
|
105
|
-
* @param
|
|
111
|
+
* @param projectId The project ID containing the instance.
|
|
112
|
+
* @param stateId The instance state ID to check.
|
|
113
|
+
* @returns True if the instance is locked, false otherwise.
|
|
106
114
|
*/
|
|
107
|
-
async
|
|
108
|
-
this.
|
|
109
|
-
{ projectId, instanceIds },
|
|
110
|
-
`unconditionally unlocking %s instances in project "%s"`,
|
|
111
|
-
instanceIds.length,
|
|
112
|
-
projectId,
|
|
113
|
-
)
|
|
115
|
+
async isInstanceLocked(projectId: string, stateId: string): Promise<boolean> {
|
|
116
|
+
const database = await this.database.forProject(projectId)
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
const lock = await database.instanceLock.findUnique({
|
|
119
|
+
where: { stateId },
|
|
120
|
+
})
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
return lock !== null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Removes locks from the specified instances using the provided token.
|
|
127
|
+
* Executes an optional unlock action within the transaction if the lock is still valid.
|
|
128
|
+
*
|
|
129
|
+
* @param projectId The project ID containing the instances.
|
|
130
|
+
* @param stateIds The instance state IDs to unlock.
|
|
131
|
+
* @param token The token that was returned when the locks were created.
|
|
132
|
+
* @param unlockAction Optional async action to execute within the unlock transaction if the lock is still valid.
|
|
133
|
+
* @throws {InstanceLockLostError} When the lock with the given token is not found.
|
|
134
|
+
*/
|
|
135
|
+
async unlockInstances(
|
|
136
|
+
projectId: string,
|
|
137
|
+
stateIds: string[],
|
|
138
|
+
token: string,
|
|
139
|
+
unlockAction?: (tx: ProjectTransaction) => Promise<void>,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
if (stateIds.length === 0) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!token) {
|
|
146
|
+
throw new Error("Token is required to unlock instances")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const database = await this.database.forProject(projectId)
|
|
150
|
+
|
|
151
|
+
await database.$transaction(async tx => {
|
|
152
|
+
// verify that locks with the given token exist for all requested instances
|
|
153
|
+
const existingLocks = await tx.instanceLock.findMany({
|
|
154
|
+
where: {
|
|
155
|
+
stateId: { in: stateIds },
|
|
156
|
+
token: token,
|
|
157
|
+
},
|
|
158
|
+
select: { stateId: true },
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const lockedStateIds = existingLocks.map(lock => lock.stateId)
|
|
162
|
+
const missingLocks = stateIds.filter(id => !lockedStateIds.includes(id))
|
|
163
|
+
|
|
164
|
+
if (missingLocks.length > 0) {
|
|
165
|
+
throw new InstanceLockLostError(projectId, missingLocks, token)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// execute the optional unlock action if provided
|
|
169
|
+
if (unlockAction) {
|
|
170
|
+
await unlockAction(tx)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// remove the locks
|
|
174
|
+
const { count } = await tx.instanceLock.deleteMany({
|
|
175
|
+
where: {
|
|
176
|
+
stateId: { in: stateIds },
|
|
177
|
+
token: token,
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
if (count > 0) {
|
|
182
|
+
this.logger.debug(
|
|
183
|
+
{ projectId, unlockedCount: count, token },
|
|
184
|
+
"unlocked %s instances",
|
|
185
|
+
count,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// publish unlock event
|
|
189
|
+
await this.pubsubManager.publish(["instance-lock", projectId], {
|
|
190
|
+
type: "unlocked",
|
|
191
|
+
stateIds: lockedStateIds,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Unconditionally removes locks from the specified instances.
|
|
199
|
+
* This will remove locks regardless of their current state or ownership.
|
|
200
|
+
*
|
|
201
|
+
* @param projectId The project ID containing the instances.
|
|
202
|
+
* @param stateIds The instance state IDs to unlock.
|
|
203
|
+
*/
|
|
204
|
+
async unlockInstancesUnconditionally(projectId: string, stateIds: string[]): Promise<void> {
|
|
205
|
+
if (stateIds.length === 0) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const database = await this.database.forProject(projectId)
|
|
210
|
+
|
|
211
|
+
await database.$transaction(async tx => {
|
|
212
|
+
const { count } = await tx.instanceLock.deleteMany({
|
|
213
|
+
where: { stateId: { in: stateIds } },
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
if (count > 0) {
|
|
217
|
+
this.logger.info({ projectId, unlockedCount: count }, "unlocked %s instances", count)
|
|
218
|
+
|
|
219
|
+
// publish unlock event
|
|
220
|
+
await this.pubsubManager.publish(["instance-lock", projectId], {
|
|
221
|
+
type: "unlocked",
|
|
222
|
+
stateIds,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
122
225
|
})
|
|
123
226
|
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Attempts to acquire locks on the specified instances with retry logic.
|
|
230
|
+
* Subscribes to unlock events and retries lock acquisition until successful or aborted.
|
|
231
|
+
*
|
|
232
|
+
* @param projectId The project ID containing the instances.
|
|
233
|
+
* @param stateIds The instance state IDs to lock.
|
|
234
|
+
* @param lockMeta The metadata for the locks.
|
|
235
|
+
* @param action Optional async action to execute when instances are locked.
|
|
236
|
+
* @param allowPartialLock Whether to allow partial locking when some instances are already locked.
|
|
237
|
+
* @param abortSignal Optional abort signal to interrupt lock operations.
|
|
238
|
+
* @param eventWaitTime Optional time in milliseconds to wait for unlock events before retrying (default: 60000ms).
|
|
239
|
+
* @param customToken Optional custom token to use instead of auto-generating one.
|
|
240
|
+
* @returns A tuple containing the token and array of successfully locked state IDs.
|
|
241
|
+
*/
|
|
242
|
+
async lockInstances(
|
|
243
|
+
projectId: string,
|
|
244
|
+
stateIds: string[],
|
|
245
|
+
lockMeta: CommonObjectMeta,
|
|
246
|
+
action?: (tx: ProjectTransaction, stateIds: string[]) => Promise<void>,
|
|
247
|
+
allowPartialLock = false,
|
|
248
|
+
abortSignal?: AbortSignal,
|
|
249
|
+
eventWaitTime = 60000,
|
|
250
|
+
customToken?: string,
|
|
251
|
+
): Promise<[token: string, lockedStateIds: string[]]> {
|
|
252
|
+
if (stateIds.length === 0) {
|
|
253
|
+
return ["", []]
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// generate a single token for all locks
|
|
257
|
+
const token = customToken ?? createId()
|
|
258
|
+
|
|
259
|
+
// track which instances still need to be locked
|
|
260
|
+
let remainingStateIds = [...stateIds]
|
|
261
|
+
const lockedStateIds: string[] = []
|
|
262
|
+
|
|
263
|
+
// create abort controller for managing event subscription
|
|
264
|
+
const subscriptionController = new AbortController()
|
|
265
|
+
|
|
266
|
+
// set up event subscription first before attempting any locks to reduce probability of missing events
|
|
267
|
+
const eventIterable = await this.pubsubManager.subscribe(
|
|
268
|
+
["instance-lock", projectId],
|
|
269
|
+
subscriptionController.signal,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
while (remainingStateIds.length > 0) {
|
|
274
|
+
if (abortSignal?.aborted) {
|
|
275
|
+
throw new Error("Lock operation was aborted")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.logger.debug(
|
|
279
|
+
{
|
|
280
|
+
projectId,
|
|
281
|
+
remainingCount: remainingStateIds.length,
|
|
282
|
+
lockedCount: lockedStateIds.length,
|
|
283
|
+
},
|
|
284
|
+
"attempting to lock %s remaining instances",
|
|
285
|
+
remainingStateIds.length,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// try to acquire locks on remaining instances using the same token
|
|
289
|
+
const [_, newlyLockedStateIds] = await this.tryLockInstances(
|
|
290
|
+
projectId,
|
|
291
|
+
remainingStateIds,
|
|
292
|
+
lockMeta,
|
|
293
|
+
action,
|
|
294
|
+
allowPartialLock,
|
|
295
|
+
token,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if (newlyLockedStateIds.length === 0) {
|
|
299
|
+
// no instances were locked, wait for unlock events
|
|
300
|
+
this.logger.debug(
|
|
301
|
+
{ projectId, remainingCount: remainingStateIds.length },
|
|
302
|
+
"waiting for unlock events for %s remaining instances",
|
|
303
|
+
remainingStateIds.length,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
await this.waitForUnlockEvent(
|
|
307
|
+
projectId,
|
|
308
|
+
remainingStateIds,
|
|
309
|
+
eventIterable,
|
|
310
|
+
abortSignal,
|
|
311
|
+
eventWaitTime,
|
|
312
|
+
)
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// remove newly locked instances from remaining list
|
|
317
|
+
remainingStateIds = remainingStateIds.filter(id => !newlyLockedStateIds.includes(id))
|
|
318
|
+
lockedStateIds.push(...newlyLockedStateIds)
|
|
319
|
+
|
|
320
|
+
// if partial locking is not allowed, we should have all instances by now
|
|
321
|
+
if (!allowPartialLock && remainingStateIds.length > 0) {
|
|
322
|
+
this.logger.error(
|
|
323
|
+
{ projectId, remaining: remainingStateIds.length },
|
|
324
|
+
"partial lock not allowed but %s instances remain unlocked",
|
|
325
|
+
remainingStateIds.length,
|
|
326
|
+
)
|
|
327
|
+
throw new Error("Failed to acquire all required locks")
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return [token, lockedStateIds]
|
|
332
|
+
} finally {
|
|
333
|
+
// clean up event subscription
|
|
334
|
+
subscriptionController.abort()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Waits for an unlock event that affects any of the specified state IDs,
|
|
340
|
+
* or times out to trigger the next retry attempt.
|
|
341
|
+
*
|
|
342
|
+
* @param projectId The project ID to monitor for events.
|
|
343
|
+
* @param stateIds The state IDs we're waiting to become available.
|
|
344
|
+
* @param eventIterable The async iterable for event subscription.
|
|
345
|
+
* @param abortSignal Optional abort signal to interrupt waiting.
|
|
346
|
+
* @param eventWaitTime Time in milliseconds to wait before timing out and retrying.
|
|
347
|
+
*/
|
|
348
|
+
private async waitForUnlockEvent(
|
|
349
|
+
projectId: string,
|
|
350
|
+
stateIds: string[],
|
|
351
|
+
eventIterable: AsyncIterable<InstanceLockEvent>,
|
|
352
|
+
abortSignal?: AbortSignal,
|
|
353
|
+
eventWaitTime = 60000,
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
const eventController = new AbortController()
|
|
356
|
+
|
|
357
|
+
// combine abort signals
|
|
358
|
+
if (abortSignal?.aborted) {
|
|
359
|
+
throw new Error("Lock operation was aborted")
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const abortHandler = () => eventController.abort()
|
|
363
|
+
abortSignal?.addEventListener("abort", abortHandler)
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
await Promise.race([
|
|
367
|
+
// timeout promise - triggers retry attempt, does not abort
|
|
368
|
+
new Promise<void>(resolve => {
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
this.logger.debug(
|
|
371
|
+
{ projectId, eventWaitTime },
|
|
372
|
+
"unlock wait timed out after %s ms, will retry",
|
|
373
|
+
eventWaitTime,
|
|
374
|
+
)
|
|
375
|
+
resolve()
|
|
376
|
+
}, eventWaitTime)
|
|
377
|
+
}),
|
|
378
|
+
|
|
379
|
+
// event listener promise
|
|
380
|
+
this.listenForUnlockEvents(projectId, stateIds, eventIterable, eventController.signal),
|
|
381
|
+
|
|
382
|
+
// abort promise - only this can interrupt the operation
|
|
383
|
+
new Promise<void>((_, reject) => {
|
|
384
|
+
if (abortSignal) {
|
|
385
|
+
abortSignal.addEventListener("abort", () => {
|
|
386
|
+
reject(new Error("Lock operation was aborted"))
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
}),
|
|
390
|
+
])
|
|
391
|
+
} finally {
|
|
392
|
+
eventController.abort()
|
|
393
|
+
abortSignal?.removeEventListener("abort", abortHandler)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Listens for unlock events using async iteration.
|
|
399
|
+
*
|
|
400
|
+
* @param projectId The project ID to monitor for events.
|
|
401
|
+
* @param stateIds The state IDs we're waiting to become available.
|
|
402
|
+
* @param eventIterable The async iterable for event subscription.
|
|
403
|
+
* @param signal Abort signal to stop listening.
|
|
404
|
+
*/
|
|
405
|
+
private async listenForUnlockEvents(
|
|
406
|
+
projectId: string,
|
|
407
|
+
stateIds: string[],
|
|
408
|
+
eventIterable: AsyncIterable<InstanceLockEvent>,
|
|
409
|
+
signal: AbortSignal,
|
|
410
|
+
): Promise<void> {
|
|
411
|
+
for await (const event of eventIterable) {
|
|
412
|
+
if (signal.aborted) {
|
|
413
|
+
break
|
|
414
|
+
}
|
|
415
|
+
if (event.type !== "unlocked") {
|
|
416
|
+
continue // only interested in unlock events
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const relevantUnlocks = event.stateIds.filter((id: string) => stateIds.includes(id))
|
|
420
|
+
if (relevantUnlocks.length === 0) {
|
|
421
|
+
continue // keep waiting for relevant unlocks
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (relevantUnlocks.length > 0) {
|
|
425
|
+
this.logger.debug(
|
|
426
|
+
{ projectId, relevantUnlocks: relevantUnlocks.length },
|
|
427
|
+
"found relevant unlock event for %s instances",
|
|
428
|
+
relevantUnlocks.length,
|
|
429
|
+
)
|
|
430
|
+
return // this will resolve the promise and clean up the subscription
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
124
434
|
}
|