@shipfox/api-runners 12.6.0 → 13.0.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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +20 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/job-executions.d.ts +8 -21
- package/dist/db/job-executions.d.ts.map +1 -1
- package/dist/db/job-executions.js +63 -60
- package/dist/db/job-executions.js.map +1 -1
- package/dist/db/provisioner-tokens.d.ts.map +1 -1
- package/dist/db/provisioner-tokens.js +14 -2
- package/dist/db/provisioner-tokens.js.map +1 -1
- package/dist/db/reservations.d.ts +10 -0
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +139 -17
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +8 -5
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts +0 -1
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +85 -100
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/runner-states.d.ts +3 -1
- package/dist/db/runner-states.d.ts.map +1 -1
- package/dist/db/runner-states.js +6 -4
- package/dist/db/runner-states.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/presentation/index.d.ts +2 -1
- package/dist/presentation/index.d.ts.map +1 -1
- package/dist/presentation/index.js +2 -1
- package/dist/presentation/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +0 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +2 -25
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/subscribers/on-workflows-job-execution-queued.d.ts +3 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-queued.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-queued.js +21 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-queued.js.map +1 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-terminated.d.ts +3 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-terminated.d.ts.map +1 -0
- package/dist/presentation/subscribers/{on-workflows-job-execution-timed-out.js → on-workflows-job-execution-terminated.js} +5 -4
- package/dist/presentation/subscribers/on-workflows-job-execution-terminated.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/db/index.ts +0 -2
- package/src/db/job-executions.test.ts +106 -217
- package/src/db/job-executions.ts +71 -70
- package/src/db/provisioner-tokens.test.ts +19 -0
- package/src/db/provisioner-tokens.ts +13 -2
- package/src/db/reservations.test.ts +138 -0
- package/src/db/reservations.ts +314 -35
- package/src/db/runner-assignments.test.ts +20 -0
- package/src/db/runner-assignments.ts +6 -7
- package/src/db/runner-instances.test.ts +350 -15
- package/src/db/runner-instances.ts +136 -198
- package/src/db/runner-states.test.ts +16 -0
- package/src/db/runner-states.ts +9 -4
- package/src/index.test.ts +1 -0
- package/src/index.ts +8 -7
- package/src/presentation/index.ts +2 -1
- package/src/presentation/inter-module.ts +2 -40
- package/src/presentation/subscribers/on-workflows-job-execution-queued.test.ts +57 -0
- package/src/presentation/subscribers/on-workflows-job-execution-queued.ts +26 -0
- package/src/presentation/subscribers/on-workflows-job-execution-terminated.test.ts +142 -0
- package/src/presentation/subscribers/{on-workflows-job-execution-timed-out.ts → on-workflows-job-execution-terminated.ts} +5 -4
- package/test/factories/pending-job.ts +3 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.d.ts +0 -3
- package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.d.ts.map +0 -1
- package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.js.map +0 -1
- package/src/presentation/inter-module.test.ts +0 -15
- package/src/presentation/subscribers/on-workflows-job-execution-timed-out.test.ts +0 -163
package/src/db/reservations.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isNotNull,
|
|
11
11
|
isNull,
|
|
12
12
|
lt,
|
|
13
|
+
lte,
|
|
13
14
|
notExists,
|
|
14
15
|
notInArray,
|
|
15
16
|
or,
|
|
@@ -27,6 +28,7 @@ import {reservations} from './schema/reservations.js';
|
|
|
27
28
|
import {runnerActivationTokens} from './schema/runner-activation-tokens.js';
|
|
28
29
|
import {runnerControlSessions} from './schema/runner-control-sessions.js';
|
|
29
30
|
import {type providerRunnerLaunchKindEnum, providerRunners} from './schema/runner-instances.js';
|
|
31
|
+
import {runningJobExecutions} from './schema/running-job-executions.js';
|
|
30
32
|
|
|
31
33
|
export interface ReservationTemplate {
|
|
32
34
|
templateKey: string;
|
|
@@ -427,7 +429,6 @@ async function listActiveProvisionerReservationRowsTx(
|
|
|
427
429
|
id: providerRunners.id,
|
|
428
430
|
reservationId: providerRunners.reservationId,
|
|
429
431
|
intendedReservationId: providerRunners.intendedReservationId,
|
|
430
|
-
state: providerRunners.state,
|
|
431
432
|
})
|
|
432
433
|
.from(providerRunners)
|
|
433
434
|
.where(
|
|
@@ -435,7 +436,10 @@ async function listActiveProvisionerReservationRowsTx(
|
|
|
435
436
|
eq(providerRunners.provisionerId, params.provisionerId),
|
|
436
437
|
isNull(providerRunners.reservationReleasedAt),
|
|
437
438
|
or(
|
|
438
|
-
|
|
439
|
+
and(
|
|
440
|
+
inArray(providerRunners.reservationId, activeIds),
|
|
441
|
+
notInArray(providerRunners.state, [...terminalStates]),
|
|
442
|
+
),
|
|
439
443
|
and(
|
|
440
444
|
inArray(providerRunners.intendedReservationId, activeIds),
|
|
441
445
|
notInArray(providerRunners.state, [...terminalStates]),
|
|
@@ -448,11 +452,7 @@ async function listActiveProvisionerReservationRowsTx(
|
|
|
448
452
|
if (runner.reservationId && activeIdSet.has(runner.reservationId)) {
|
|
449
453
|
addUsedRunner(usedByReservation, runner.reservationId, runner.id);
|
|
450
454
|
}
|
|
451
|
-
if (
|
|
452
|
-
runner.intendedReservationId &&
|
|
453
|
-
activeIdSet.has(runner.intendedReservationId) &&
|
|
454
|
-
!terminalStates.includes(runner.state as (typeof terminalStates)[number])
|
|
455
|
-
) {
|
|
455
|
+
if (runner.intendedReservationId && activeIdSet.has(runner.intendedReservationId)) {
|
|
456
456
|
addUsedRunner(usedByReservation, runner.intendedReservationId, runner.id);
|
|
457
457
|
}
|
|
458
458
|
}
|
|
@@ -863,46 +863,325 @@ export async function releaseReservationUnits(
|
|
|
863
863
|
releases: Array<{reservationId: string; count: number}>;
|
|
864
864
|
},
|
|
865
865
|
): Promise<number> {
|
|
866
|
-
|
|
867
|
-
|
|
866
|
+
const releaseByReservationId = new Map<string, number>();
|
|
868
867
|
for (const release of params.releases) {
|
|
869
868
|
if (release.count <= 0) continue;
|
|
869
|
+
releaseByReservationId.set(
|
|
870
|
+
release.reservationId,
|
|
871
|
+
(releaseByReservationId.get(release.reservationId) ?? 0) + release.count,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
const reservationIds = [...releaseByReservationId.keys()];
|
|
875
|
+
if (reservationIds.length === 0) return 0;
|
|
876
|
+
|
|
877
|
+
const releaseCount = sql<number>`CASE ${reservations.id}
|
|
878
|
+
${sql.join(
|
|
879
|
+
[...releaseByReservationId].map(
|
|
880
|
+
([reservationId, count]) => sql`WHEN ${reservationId} THEN ${count}`,
|
|
881
|
+
),
|
|
882
|
+
sql` `,
|
|
883
|
+
)}
|
|
884
|
+
ELSE 0
|
|
885
|
+
END`;
|
|
886
|
+
const scope = and(
|
|
887
|
+
eq(reservations.workspaceId, params.workspaceId),
|
|
888
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
889
|
+
inArray(reservations.id, reservationIds),
|
|
890
|
+
gt(reservations.expiresAt, sql`now()`),
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
const decremented = await tx
|
|
894
|
+
.update(reservations)
|
|
895
|
+
.set({count: sql`${reservations.count} - ${releaseCount}`})
|
|
896
|
+
.where(and(scope, gt(reservations.count, releaseCount)))
|
|
897
|
+
.returning({id: reservations.id});
|
|
898
|
+
const releasedFromDecremented = decremented.reduce(
|
|
899
|
+
(total, row) => total + (releaseByReservationId.get(row.id) ?? 0),
|
|
900
|
+
0,
|
|
901
|
+
);
|
|
870
902
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
903
|
+
const decrementedIds = decremented.map((row) => row.id);
|
|
904
|
+
const deleted = await tx
|
|
905
|
+
.delete(reservations)
|
|
906
|
+
.where(
|
|
907
|
+
and(
|
|
908
|
+
scope,
|
|
909
|
+
sql`${reservations.count} <= ${releaseCount}`,
|
|
910
|
+
decrementedIds.length > 0 ? notInArray(reservations.id, decrementedIds) : undefined,
|
|
911
|
+
),
|
|
912
|
+
)
|
|
913
|
+
.returning({count: reservations.count});
|
|
914
|
+
const releasedFromDeleted = deleted.reduce((total, row) => total + row.count, 0);
|
|
915
|
+
|
|
916
|
+
return releasedFromDeleted + releasedFromDecremented;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export async function releaseTerminalRunnerInstanceReservationsByIds(
|
|
920
|
+
tx: Tx,
|
|
921
|
+
params: {
|
|
922
|
+
workspaceId: string | null;
|
|
923
|
+
provisionerId: string;
|
|
924
|
+
providerRunnerIds?: string[];
|
|
925
|
+
runnerInstanceIds?: string[];
|
|
926
|
+
reportedAtByProviderRunnerId?: ReadonlyMap<string, Date>;
|
|
927
|
+
requireUnlinkedSession?: boolean;
|
|
928
|
+
/** Lock linked runners before rechecking terminal state for lease finalization. */
|
|
929
|
+
requireTerminalState?: boolean;
|
|
930
|
+
},
|
|
931
|
+
): Promise<number> {
|
|
932
|
+
if (
|
|
933
|
+
(params.providerRunnerIds?.length ?? 0) === 0 &&
|
|
934
|
+
(params.runnerInstanceIds?.length ?? 0) === 0
|
|
935
|
+
)
|
|
936
|
+
return 0;
|
|
937
|
+
|
|
938
|
+
const reservationWorkspacePredicate =
|
|
939
|
+
params.workspaceId === null
|
|
940
|
+
? sql``
|
|
941
|
+
: sql`and ${eq(reservations.workspaceId, params.workspaceId)}`;
|
|
942
|
+
// Cancellation keeps the lease row so the provisioner can observe its terminate intent.
|
|
943
|
+
// Once the runner is terminal, only an uncancelled lease should block reservation release.
|
|
944
|
+
const noUncancelledRunningJobPredicate = notExists(
|
|
945
|
+
tx
|
|
946
|
+
.select({id: runningJobExecutions.id})
|
|
947
|
+
.from(runningJobExecutions)
|
|
874
948
|
.where(
|
|
875
949
|
and(
|
|
876
|
-
eq(
|
|
877
|
-
eq(
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
950
|
+
eq(runningJobExecutions.provisionerId, params.provisionerId),
|
|
951
|
+
eq(runningJobExecutions.providerRunnerId, providerRunners.providerRunnerId),
|
|
952
|
+
isNull(runningJobExecutions.cancellationRequestedAt),
|
|
953
|
+
params.workspaceId === null
|
|
954
|
+
? undefined
|
|
955
|
+
: eq(runningJobExecutions.workspaceId, params.workspaceId),
|
|
881
956
|
),
|
|
882
|
-
)
|
|
883
|
-
|
|
957
|
+
),
|
|
958
|
+
);
|
|
959
|
+
const reportFreshnessPredicate =
|
|
960
|
+
params.reportedAtByProviderRunnerId && params.reportedAtByProviderRunnerId.size > 0
|
|
961
|
+
? or(
|
|
962
|
+
...[...params.reportedAtByProviderRunnerId].map(([providerRunnerId, reportedAt]) =>
|
|
963
|
+
and(
|
|
964
|
+
eq(providerRunners.providerRunnerId, providerRunnerId),
|
|
965
|
+
lte(providerRunners.reportedAt, reportedAt),
|
|
966
|
+
),
|
|
967
|
+
),
|
|
968
|
+
)
|
|
969
|
+
: undefined;
|
|
970
|
+
const runnerIdentityPredicate = or(
|
|
971
|
+
params.providerRunnerIds && params.providerRunnerIds.length > 0
|
|
972
|
+
? inArray(providerRunners.providerRunnerId, params.providerRunnerIds)
|
|
973
|
+
: undefined,
|
|
974
|
+
params.runnerInstanceIds && params.runnerInstanceIds.length > 0
|
|
975
|
+
? inArray(providerRunners.id, params.runnerInstanceIds)
|
|
976
|
+
: undefined,
|
|
977
|
+
);
|
|
884
978
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
979
|
+
// Assignment locks these keys before changing runner links. Acquire them before locking or
|
|
980
|
+
// marking terminal runners so an assignment cannot consume a reservation concurrently with
|
|
981
|
+
// this release.
|
|
982
|
+
const reservationRowsToLock = await tx
|
|
983
|
+
.select({
|
|
984
|
+
reservationId: providerRunners.reservationId,
|
|
985
|
+
intendedReservationId: providerRunners.intendedReservationId,
|
|
986
|
+
})
|
|
987
|
+
.from(providerRunners)
|
|
988
|
+
.where(
|
|
989
|
+
and(
|
|
990
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
991
|
+
runnerIdentityPredicate,
|
|
992
|
+
params.requireTerminalState === false
|
|
993
|
+
? undefined
|
|
994
|
+
: inArray(providerRunners.state, terminalStates),
|
|
995
|
+
or(
|
|
996
|
+
isNotNull(providerRunners.reservationId),
|
|
997
|
+
isNotNull(providerRunners.intendedReservationId),
|
|
998
|
+
),
|
|
999
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
1000
|
+
),
|
|
1001
|
+
);
|
|
1002
|
+
const reservationIdsToLock = [
|
|
1003
|
+
...new Set(
|
|
1004
|
+
reservationRowsToLock.flatMap((row) =>
|
|
1005
|
+
[row.reservationId, row.intendedReservationId].filter(
|
|
1006
|
+
(reservationId): reservationId is string => reservationId !== null,
|
|
1007
|
+
),
|
|
1008
|
+
),
|
|
1009
|
+
),
|
|
1010
|
+
];
|
|
1011
|
+
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
1012
|
+
provisionerId: params.provisionerId,
|
|
1013
|
+
reservationIds: reservationIdsToLock,
|
|
1014
|
+
});
|
|
889
1015
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
.
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1016
|
+
const rows = await tx
|
|
1017
|
+
.select({
|
|
1018
|
+
id: providerRunners.id,
|
|
1019
|
+
state: providerRunners.state,
|
|
1020
|
+
releaseReservationId: sql<string | null>`coalesce(
|
|
1021
|
+
(select ${reservations.id}
|
|
1022
|
+
from ${reservations}
|
|
1023
|
+
where ${reservations.id} = ${providerRunners.intendedReservationId}
|
|
1024
|
+
and ${reservations.provisionerId} = ${params.provisionerId}
|
|
1025
|
+
${reservationWorkspacePredicate}),
|
|
1026
|
+
(select ${reservations.id}
|
|
1027
|
+
from ${reservations}
|
|
1028
|
+
where ${reservations.id} = ${providerRunners.reservationId}
|
|
1029
|
+
and ${reservations.provisionerId} = ${params.provisionerId}
|
|
1030
|
+
${reservationWorkspacePredicate})
|
|
1031
|
+
)`,
|
|
1032
|
+
releaseReservationWorkspaceId: sql<string | null>`coalesce(
|
|
1033
|
+
(select ${reservations.workspaceId}
|
|
1034
|
+
from ${reservations}
|
|
1035
|
+
where ${reservations.id} = ${providerRunners.intendedReservationId}
|
|
1036
|
+
and ${reservations.provisionerId} = ${params.provisionerId}
|
|
1037
|
+
${reservationWorkspacePredicate}),
|
|
1038
|
+
(select ${reservations.workspaceId}
|
|
1039
|
+
from ${reservations}
|
|
1040
|
+
where ${reservations.id} = ${providerRunners.reservationId}
|
|
1041
|
+
and ${reservations.provisionerId} = ${params.provisionerId}
|
|
1042
|
+
${reservationWorkspacePredicate})
|
|
1043
|
+
)`,
|
|
1044
|
+
})
|
|
1045
|
+
.from(providerRunners)
|
|
1046
|
+
.where(
|
|
1047
|
+
and(
|
|
1048
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
1049
|
+
runnerIdentityPredicate,
|
|
1050
|
+
params.requireTerminalState === false
|
|
1051
|
+
? undefined
|
|
1052
|
+
: inArray(providerRunners.state, terminalStates),
|
|
1053
|
+
or(
|
|
1054
|
+
isNotNull(providerRunners.reservationId),
|
|
1055
|
+
isNotNull(providerRunners.intendedReservationId),
|
|
899
1056
|
),
|
|
900
|
-
|
|
901
|
-
|
|
1057
|
+
params.workspaceId === null
|
|
1058
|
+
? undefined
|
|
1059
|
+
: or(
|
|
1060
|
+
and(
|
|
1061
|
+
eq(providerRunners.workspaceId, params.workspaceId),
|
|
1062
|
+
or(
|
|
1063
|
+
exists(
|
|
1064
|
+
tx
|
|
1065
|
+
.select({id: reservations.id})
|
|
1066
|
+
.from(reservations)
|
|
1067
|
+
.where(
|
|
1068
|
+
and(
|
|
1069
|
+
eq(reservations.workspaceId, params.workspaceId),
|
|
1070
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
1071
|
+
eq(reservations.id, providerRunners.reservationId),
|
|
1072
|
+
),
|
|
1073
|
+
),
|
|
1074
|
+
),
|
|
1075
|
+
exists(
|
|
1076
|
+
tx
|
|
1077
|
+
.select({id: reservations.id})
|
|
1078
|
+
.from(reservations)
|
|
1079
|
+
.where(
|
|
1080
|
+
and(
|
|
1081
|
+
eq(reservations.workspaceId, params.workspaceId),
|
|
1082
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
1083
|
+
eq(reservations.id, providerRunners.intendedReservationId),
|
|
1084
|
+
),
|
|
1085
|
+
),
|
|
1086
|
+
),
|
|
1087
|
+
),
|
|
1088
|
+
),
|
|
1089
|
+
and(
|
|
1090
|
+
isNull(providerRunners.workspaceId),
|
|
1091
|
+
isNotNull(providerRunners.intendedReservationId),
|
|
1092
|
+
exists(
|
|
1093
|
+
tx
|
|
1094
|
+
.select({id: reservations.id})
|
|
1095
|
+
.from(reservations)
|
|
1096
|
+
.where(
|
|
1097
|
+
and(
|
|
1098
|
+
eq(reservations.workspaceId, params.workspaceId),
|
|
1099
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
1100
|
+
eq(reservations.id, providerRunners.intendedReservationId),
|
|
1101
|
+
),
|
|
1102
|
+
),
|
|
1103
|
+
),
|
|
1104
|
+
),
|
|
1105
|
+
),
|
|
1106
|
+
params.requireUnlinkedSession === false
|
|
1107
|
+
? undefined
|
|
1108
|
+
: isNull(providerRunners.runnerSessionId),
|
|
1109
|
+
noUncancelledRunningJobPredicate,
|
|
1110
|
+
reportFreshnessPredicate,
|
|
1111
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
1112
|
+
),
|
|
1113
|
+
)
|
|
1114
|
+
.for('update');
|
|
1115
|
+
|
|
1116
|
+
if (rows.length === 0) return 0;
|
|
1117
|
+
|
|
1118
|
+
// The lease-finalization path deliberately locks active rows too. If a terminal report is
|
|
1119
|
+
// concurrently projecting the runner state, PostgreSQL rechecks this row after the lock and
|
|
1120
|
+
// lets cleanup observe the committed terminal state without a cross-module advisory lock.
|
|
1121
|
+
const terminalRows =
|
|
1122
|
+
params.requireTerminalState === false
|
|
1123
|
+
? rows.filter((row) => terminalStates.some((terminalState) => terminalState === row.state))
|
|
1124
|
+
: rows;
|
|
1125
|
+
if (terminalRows.length === 0) return 0;
|
|
902
1126
|
|
|
903
|
-
|
|
1127
|
+
const updated = await tx
|
|
1128
|
+
.update(providerRunners)
|
|
1129
|
+
.set({
|
|
1130
|
+
intendedReservationId: null,
|
|
1131
|
+
reservationReleasedAt: sql`now()`,
|
|
1132
|
+
updatedAt: sql`now()`,
|
|
1133
|
+
})
|
|
1134
|
+
.where(
|
|
1135
|
+
and(
|
|
1136
|
+
inArray(
|
|
1137
|
+
providerRunners.id,
|
|
1138
|
+
terminalRows.map((row) => row.id),
|
|
1139
|
+
),
|
|
1140
|
+
params.requireUnlinkedSession === false
|
|
1141
|
+
? undefined
|
|
1142
|
+
: isNull(providerRunners.runnerSessionId),
|
|
1143
|
+
noUncancelledRunningJobPredicate,
|
|
1144
|
+
reportFreshnessPredicate,
|
|
1145
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
1146
|
+
),
|
|
1147
|
+
)
|
|
1148
|
+
.returning({id: providerRunners.id});
|
|
1149
|
+
|
|
1150
|
+
const releasesByReservationId = new Map<
|
|
1151
|
+
string,
|
|
1152
|
+
{workspaceId: string; reservationId: string; count: number}
|
|
1153
|
+
>();
|
|
1154
|
+
const updatedIds = new Set(updated.map((row) => row.id));
|
|
1155
|
+
for (const row of terminalRows) {
|
|
1156
|
+
if (!updatedIds.has(row.id)) continue;
|
|
1157
|
+
if (!row.releaseReservationId || !row.releaseReservationWorkspaceId) continue;
|
|
1158
|
+
const key = `${row.releaseReservationWorkspaceId}:${row.releaseReservationId}`;
|
|
1159
|
+
const release = releasesByReservationId.get(key) ?? {
|
|
1160
|
+
workspaceId: row.releaseReservationWorkspaceId,
|
|
1161
|
+
reservationId: row.releaseReservationId,
|
|
1162
|
+
count: 0,
|
|
1163
|
+
};
|
|
1164
|
+
release.count += 1;
|
|
1165
|
+
releasesByReservationId.set(key, release);
|
|
904
1166
|
}
|
|
905
1167
|
|
|
1168
|
+
if (releasesByReservationId.size === 0) return 0;
|
|
1169
|
+
|
|
1170
|
+
const releasesByWorkspaceId = new Map<string, Array<{reservationId: string; count: number}>>();
|
|
1171
|
+
for (const release of releasesByReservationId.values()) {
|
|
1172
|
+
const releases = releasesByWorkspaceId.get(release.workspaceId) ?? [];
|
|
1173
|
+
releases.push({reservationId: release.reservationId, count: release.count});
|
|
1174
|
+
releasesByWorkspaceId.set(release.workspaceId, releases);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
let released = 0;
|
|
1178
|
+
for (const [workspaceId, releases] of releasesByWorkspaceId) {
|
|
1179
|
+
released += await releaseReservationUnits(tx, {
|
|
1180
|
+
workspaceId,
|
|
1181
|
+
provisionerId: params.provisionerId,
|
|
1182
|
+
releases,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
906
1185
|
return released;
|
|
907
1186
|
}
|
|
908
1187
|
|
|
@@ -304,6 +304,26 @@ describe('assignRunnerInstances', () => {
|
|
|
304
304
|
).resolves.toEqual([runner.id]);
|
|
305
305
|
});
|
|
306
306
|
|
|
307
|
+
it('does not charge capacity for a terminal reservation assignment before release bookkeeping', async () => {
|
|
308
|
+
const reservation = await createReservation();
|
|
309
|
+
await db().insert(providerRunners).values({
|
|
310
|
+
provisionerId,
|
|
311
|
+
reservationId: reservation.id,
|
|
312
|
+
providerRunnerId: crypto.randomUUID(),
|
|
313
|
+
state: 'failed',
|
|
314
|
+
reportedAt: new Date(),
|
|
315
|
+
});
|
|
316
|
+
const runner = await createEnrolledRunner();
|
|
317
|
+
|
|
318
|
+
await expect(
|
|
319
|
+
assignRunnerInstances({
|
|
320
|
+
provisionerId,
|
|
321
|
+
reservationId: reservation.id,
|
|
322
|
+
runnerInstanceIds: [runner.id],
|
|
323
|
+
}),
|
|
324
|
+
).resolves.toEqual([runner.id]);
|
|
325
|
+
});
|
|
326
|
+
|
|
307
327
|
async function createReservation(
|
|
308
328
|
overrides: Partial<{expiresAt: Date; requiredLabels: string[]; kind: 'bound' | 'launch'}> = {},
|
|
309
329
|
) {
|
|
@@ -165,6 +165,7 @@ export async function assignRunnerInstancesTx(
|
|
|
165
165
|
and(
|
|
166
166
|
eq(providerRunners.reservationId, reservation.id),
|
|
167
167
|
isNull(providerRunners.reservationReleasedAt),
|
|
168
|
+
notInArray(providerRunners.state, [...terminalStates]),
|
|
168
169
|
),
|
|
169
170
|
and(
|
|
170
171
|
eq(providerRunners.intendedReservationId, reservation.id),
|
|
@@ -308,7 +309,6 @@ export async function validateRunnerReservationCapacityTx(
|
|
|
308
309
|
id: providerRunners.id,
|
|
309
310
|
reservationId: providerRunners.reservationId,
|
|
310
311
|
intendedReservationId: providerRunners.intendedReservationId,
|
|
311
|
-
state: providerRunners.state,
|
|
312
312
|
})
|
|
313
313
|
.from(providerRunners)
|
|
314
314
|
.where(
|
|
@@ -316,7 +316,10 @@ export async function validateRunnerReservationCapacityTx(
|
|
|
316
316
|
eq(providerRunners.provisionerId, params.provisionerId),
|
|
317
317
|
isNull(providerRunners.reservationReleasedAt),
|
|
318
318
|
or(
|
|
319
|
-
|
|
319
|
+
and(
|
|
320
|
+
inArray(providerRunners.reservationId, reservationIds),
|
|
321
|
+
notInArray(providerRunners.state, [...terminalStates]),
|
|
322
|
+
),
|
|
320
323
|
and(
|
|
321
324
|
inArray(providerRunners.intendedReservationId, reservationIds),
|
|
322
325
|
notInArray(providerRunners.state, [...terminalStates]),
|
|
@@ -331,11 +334,7 @@ export async function validateRunnerReservationCapacityTx(
|
|
|
331
334
|
usedRunnerIds.add(runner.id);
|
|
332
335
|
usedRunnerIdsByReservation.set(runner.reservationId, usedRunnerIds);
|
|
333
336
|
}
|
|
334
|
-
if (
|
|
335
|
-
runner.intendedReservationId &&
|
|
336
|
-
requestedReservationIds.has(runner.intendedReservationId) &&
|
|
337
|
-
!terminalStates.includes(runner.state as (typeof terminalStates)[number])
|
|
338
|
-
) {
|
|
337
|
+
if (runner.intendedReservationId && requestedReservationIds.has(runner.intendedReservationId)) {
|
|
339
338
|
const usedRunnerIds =
|
|
340
339
|
usedRunnerIdsByReservation.get(runner.intendedReservationId) ?? new Set<string>();
|
|
341
340
|
usedRunnerIds.add(runner.id);
|