@openlup/platform 0.29.0 → 0.30.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/CHANGELOG.md +150 -138
- package/README.md +80 -3
- package/dist/composition/nodeHost.d.ts +1 -1
- package/dist/composition/nodeHost.js +3 -2
- package/dist/lifecycle/appUpgrade.d.ts +52 -0
- package/dist/lifecycle/appUpgrade.js +180 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/lifecycle/postgresInstallQualification.d.ts +51 -0
- package/dist/lifecycle/postgresInstallQualification.js +74 -0
- package/dist/migrations/postgresRunner.d.ts +9 -0
- package/dist/migrations/postgresRunner.js +17 -10
- package/dist/operations/adminAlertsSchemas.d.ts +153 -0
- package/dist/operations/adminAlertsSchemas.js +69 -0
- package/dist/operations/adminAlertsView.d.ts +73 -0
- package/dist/operations/adminAlertsView.js +184 -0
- package/dist/operations/alertPagingPolicy.d.ts +15 -0
- package/dist/operations/alertPagingPolicy.js +43 -0
- package/dist/operations/alertThrottlePolicy.d.ts +3 -0
- package/dist/operations/alertThrottlePolicy.js +18 -0
- package/dist/operations/clients.d.ts +17 -0
- package/dist/operations/clients.js +32 -0
- package/dist/operations/contracts/admin.d.ts +93 -0
- package/dist/operations/contracts/admin.js +1 -0
- package/dist/operations/contracts/adminAlertsContracts.d.ts +40 -0
- package/dist/operations/contracts/adminAlertsContracts.js +1 -0
- package/dist/operations/contracts/alertHumanContext.d.ts +7 -0
- package/dist/operations/contracts/alertHumanContext.js +1 -0
- package/dist/operations/contracts/index.d.ts +6 -0
- package/dist/operations/contracts/index.js +6 -0
- package/dist/operations/contracts/observabilityContracts.d.ts +41 -0
- package/dist/operations/contracts/observabilityContracts.js +1 -0
- package/dist/operations/contracts/observabilityPorts.d.ts +18 -0
- package/dist/operations/contracts/observabilityPorts.js +1 -0
- package/dist/operations/contracts/ports.d.ts +19 -0
- package/dist/operations/contracts/ports.js +1 -0
- package/dist/operations/errors.d.ts +6 -0
- package/dist/operations/errors.js +6 -0
- package/dist/operations/index.d.ts +9 -0
- package/dist/operations/index.js +9 -0
- package/dist/operations/schemas.d.ts +138 -0
- package/dist/operations/schemas.js +104 -0
- package/dist/operations/server/adminAlertsHandlers.d.ts +16 -0
- package/dist/operations/server/adminAlertsHandlers.js +35 -0
- package/dist/operations/server/adminPipelineHandlers.d.ts +12 -0
- package/dist/operations/server/adminPipelineHandlers.js +48 -0
- package/dist/operations/server/adminSettingsHandlers.d.ts +14 -0
- package/dist/operations/server/adminSettingsHandlers.js +82 -0
- package/dist/operations/server/controlPlaneHandler.d.ts +7 -0
- package/dist/operations/server/controlPlaneHandler.js +54 -0
- package/dist/operations/server/controlPlaneStore.d.ts +12 -0
- package/dist/operations/server/controlPlaneStore.js +138 -0
- package/dist/operations/server/controlPlaneWatchdog.d.ts +7 -0
- package/dist/operations/server/controlPlaneWatchdog.js +20 -0
- package/dist/operations/server/index.d.ts +12 -0
- package/dist/operations/server/index.js +12 -0
- package/dist/operations/server/migrations.d.ts +14 -0
- package/dist/operations/server/migrations.js +7 -0
- package/dist/operations/server/platformWatchdogDecision.d.ts +34 -0
- package/dist/operations/server/platformWatchdogDecision.js +71 -0
- package/dist/operations/server/platformWatchdogDeliveryPolicy.d.ts +10 -0
- package/dist/operations/server/platformWatchdogDeliveryPolicy.js +53 -0
- package/dist/operations/server/platformWatchdogQueue.d.ts +11 -0
- package/dist/operations/server/platformWatchdogQueue.js +94 -0
- package/dist/operations/server/platformWatchdogService.d.ts +47 -0
- package/dist/operations/server/platformWatchdogService.js +105 -0
- package/dist/operations/validation.d.ts +2 -0
- package/dist/operations/validation.js +3 -0
- package/dist/portability/filesystemObjects.d.ts +12 -0
- package/dist/portability/filesystemObjects.js +191 -0
- package/dist/portability/index.d.ts +5 -0
- package/dist/portability/index.js +4 -0
- package/dist/portability/objects.d.ts +63 -0
- package/dist/portability/objects.js +138 -0
- package/dist/portability/postgresRows.d.ts +64 -0
- package/dist/portability/postgresRows.js +182 -0
- package/dist/scheduler/advisoryLock.js +20 -2
- package/dist/scheduler/nodeScheduler.d.ts +2 -2
- package/dist/scheduler/nodeScheduler.js +35 -6
- package/package.json +27 -2
- package/release-gates.json +73 -7
- package/sql/20260814010000_platform_control_plane.sql +286 -0
- package/src/composition/nodeHost.ts +4 -3
- package/src/lifecycle/appUpgrade.ts +186 -0
- package/src/lifecycle/index.ts +19 -0
- package/src/lifecycle/postgresInstallQualification.ts +125 -0
- package/src/migrations/postgresRunner.ts +24 -9
- package/src/operations/adminAlertsSchemas.ts +78 -0
- package/src/operations/adminAlertsView.ts +238 -0
- package/src/operations/alertPagingPolicy.ts +50 -0
- package/src/operations/alertThrottlePolicy.ts +22 -0
- package/src/operations/clients.ts +44 -0
- package/src/operations/contracts/admin.ts +24 -0
- package/src/operations/contracts/adminAlertsContracts.ts +13 -0
- package/src/operations/contracts/alertHumanContext.ts +17 -0
- package/src/operations/contracts/index.ts +6 -0
- package/src/operations/contracts/observabilityContracts.ts +40 -0
- package/src/operations/contracts/observabilityPorts.ts +30 -0
- package/src/operations/contracts/ports.ts +22 -0
- package/src/operations/errors.ts +6 -0
- package/src/operations/index.ts +9 -0
- package/src/operations/schemas.ts +122 -0
- package/src/operations/server/adminAlertsHandlers.ts +61 -0
- package/src/operations/server/adminPipelineHandlers.ts +73 -0
- package/src/operations/server/adminSettingsHandlers.ts +114 -0
- package/src/operations/server/controlPlaneHandler.ts +56 -0
- package/src/operations/server/controlPlaneStore.ts +150 -0
- package/src/operations/server/controlPlaneWatchdog.ts +28 -0
- package/src/operations/server/index.ts +12 -0
- package/src/operations/server/migrations.ts +7 -0
- package/src/operations/server/platformWatchdogDecision.ts +93 -0
- package/src/operations/server/platformWatchdogDeliveryPolicy.ts +92 -0
- package/src/operations/server/platformWatchdogQueue.ts +96 -0
- package/src/operations/server/platformWatchdogService.ts +209 -0
- package/src/operations/validation.ts +3 -0
- package/src/portability/filesystemObjects.ts +138 -0
- package/src/portability/index.ts +12 -0
- package/src/portability/objects.ts +160 -0
- package/src/portability/postgresRows.ts +169 -0
- package/src/scheduler/advisoryLock.ts +20 -5
- package/src/scheduler/nodeScheduler.ts +28 -8
|
@@ -32,16 +32,34 @@ export function advisoryLockKey(jobId) {
|
|
|
32
32
|
*/
|
|
33
33
|
export function createPgAdvisoryLock(client, closeClient = () => client.end?.() ?? Promise.resolve()) {
|
|
34
34
|
let closePromise = null;
|
|
35
|
+
// PostgreSQL grants reentrant session locks. Reserve the actual key locally
|
|
36
|
+
// before querying so pending/held acquisitions, including hash collisions, skip.
|
|
37
|
+
const reserved = new Set();
|
|
35
38
|
return {
|
|
36
39
|
durability: "cluster",
|
|
37
40
|
async tryAcquire(jobId) {
|
|
38
41
|
const key = advisoryLockKey(jobId);
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
if (reserved.has(key))
|
|
43
|
+
return false;
|
|
44
|
+
reserved.add(key);
|
|
45
|
+
try {
|
|
46
|
+
const { rows } = await client.query("SELECT pg_try_advisory_lock($1) AS locked", [key]);
|
|
47
|
+
const acquired = rows[0]?.locked === true;
|
|
48
|
+
if (!acquired)
|
|
49
|
+
reserved.delete(key);
|
|
50
|
+
return acquired;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
reserved.delete(key);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
41
56
|
},
|
|
42
57
|
async release(jobId) {
|
|
43
58
|
const key = advisoryLockKey(jobId);
|
|
44
59
|
await client.query("SELECT pg_advisory_unlock($1)", [key]);
|
|
60
|
+
// A failed unlock keeps the reservation: another query on this session
|
|
61
|
+
// could otherwise reenter the still-held PostgreSQL lock.
|
|
62
|
+
reserved.delete(key);
|
|
45
63
|
},
|
|
46
64
|
close() {
|
|
47
65
|
closePromise ??= closeClient();
|
|
@@ -14,8 +14,8 @@ export interface CronScheduler {
|
|
|
14
14
|
}
|
|
15
15
|
export interface NodeSchedulerAdapter extends SchedulerPort {
|
|
16
16
|
readonly jobs: readonly ScheduledJobDeclaration[];
|
|
17
|
-
/**
|
|
18
|
-
stopAll(): void
|
|
17
|
+
/** Fence/stop current timers synchronously, then await admitted callbacks and lock release. Later registration remains supported. */
|
|
18
|
+
stopAll(): Promise<void>;
|
|
19
19
|
}
|
|
20
20
|
export interface CreateNodeSchedulerOptions {
|
|
21
21
|
/** node-cron instance (inject `import("node-cron")` at the host boot site). */
|
|
@@ -45,6 +45,7 @@ export function createNodeScheduler(options) {
|
|
|
45
45
|
});
|
|
46
46
|
const scheduleById = new Map(jobs.map((job) => [job.jobId, job.schedule]));
|
|
47
47
|
const timers = [];
|
|
48
|
+
const active = new Set();
|
|
48
49
|
return {
|
|
49
50
|
jobs,
|
|
50
51
|
register(input) {
|
|
@@ -56,18 +57,46 @@ export function createNodeScheduler(options) {
|
|
|
56
57
|
throw new Error(`node-scheduler: job "${input.jobId}" is not declared in config/platform-runtime.json crons`);
|
|
57
58
|
}
|
|
58
59
|
const guarded = withAdvisoryLock(input.jobId, lock, input.handler, onError);
|
|
59
|
-
const
|
|
60
|
-
|
|
60
|
+
const registration = { jobId: input.jobId, stop: () => { }, stopped: false };
|
|
61
|
+
const timer = options.cron.schedule(schedule, () => {
|
|
62
|
+
if (registration.stopped)
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
// Own the callback before acquisition starts, including a pending acquisition.
|
|
65
|
+
const running = Promise.resolve().then(guarded);
|
|
66
|
+
active.add(running);
|
|
67
|
+
void running.then(() => active.delete(running), () => active.delete(running));
|
|
68
|
+
return running;
|
|
69
|
+
}, { timezone: options.timezone });
|
|
70
|
+
registration.stop = timer.stop;
|
|
71
|
+
timers.push(registration);
|
|
61
72
|
},
|
|
62
73
|
verifyInvocation(req) {
|
|
63
74
|
// Self-hosted invocations still present the shared CRON_SECRET; reuse the canonical check.
|
|
64
75
|
return verifyCronAuth(req, env);
|
|
65
76
|
},
|
|
66
|
-
stopAll() {
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
async stopAll() {
|
|
78
|
+
const stopped = timers.splice(0);
|
|
79
|
+
for (const timer of stopped)
|
|
80
|
+
timer.stopped = true;
|
|
81
|
+
const admitted = [...active];
|
|
82
|
+
const failures = [];
|
|
83
|
+
for (const timer of stopped) {
|
|
84
|
+
try {
|
|
85
|
+
timer.stop();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
failures.push(error);
|
|
89
|
+
}
|
|
69
90
|
}
|
|
70
|
-
|
|
91
|
+
const results = await Promise.allSettled(admitted);
|
|
92
|
+
for (const result of results) {
|
|
93
|
+
if (result.status === "rejected")
|
|
94
|
+
failures.push(result.reason);
|
|
95
|
+
}
|
|
96
|
+
if (failures.length === 1)
|
|
97
|
+
throw failures[0];
|
|
98
|
+
if (failures.length > 1)
|
|
99
|
+
throw new AggregateError(failures, "node_scheduler_shutdown_failed");
|
|
71
100
|
},
|
|
72
101
|
};
|
|
73
102
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlup/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Runnable server substrate: Node HTTP runtime, BFF dispatch, scheduler, storage, data gateways and bundle composition; development preview.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"api:update": "npm run build && node --experimental-strip-types ./scripts/api-contract.ts --update"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@openlup/core": "0.
|
|
34
|
+
"@openlup/core": "0.30.0",
|
|
35
35
|
"pg": "^8.22.0",
|
|
36
36
|
"@aws-sdk/client-s3": "^3.1144.0",
|
|
37
37
|
"@types/pg": "^8.20.0"
|
|
@@ -261,6 +261,21 @@
|
|
|
261
261
|
"import": "./dist/account-support/contracts/index.js",
|
|
262
262
|
"default": "./dist/account-support/contracts/index.js"
|
|
263
263
|
},
|
|
264
|
+
"./operations": {
|
|
265
|
+
"types": "./dist/operations/index.d.ts",
|
|
266
|
+
"import": "./dist/operations/index.js",
|
|
267
|
+
"default": "./dist/operations/index.js"
|
|
268
|
+
},
|
|
269
|
+
"./operations/contracts": {
|
|
270
|
+
"types": "./dist/operations/contracts/index.d.ts",
|
|
271
|
+
"import": "./dist/operations/contracts/index.js",
|
|
272
|
+
"default": "./dist/operations/contracts/index.js"
|
|
273
|
+
},
|
|
274
|
+
"./operations/server": {
|
|
275
|
+
"types": "./dist/operations/server/index.d.ts",
|
|
276
|
+
"import": "./dist/operations/server/index.js",
|
|
277
|
+
"default": "./dist/operations/server/index.js"
|
|
278
|
+
},
|
|
264
279
|
"./marketing": {
|
|
265
280
|
"types": "./dist/marketing/index.d.ts",
|
|
266
281
|
"import": "./dist/marketing/index.js",
|
|
@@ -280,6 +295,16 @@
|
|
|
280
295
|
"types": "./dist/marketing/server/postgres/index.d.ts",
|
|
281
296
|
"import": "./dist/marketing/server/postgres/index.js",
|
|
282
297
|
"default": "./dist/marketing/server/postgres/index.js"
|
|
298
|
+
},
|
|
299
|
+
"./portability": {
|
|
300
|
+
"types": "./dist/portability/index.d.ts",
|
|
301
|
+
"import": "./dist/portability/index.js",
|
|
302
|
+
"default": "./dist/portability/index.js"
|
|
303
|
+
},
|
|
304
|
+
"./lifecycle": {
|
|
305
|
+
"types": "./dist/lifecycle/index.d.ts",
|
|
306
|
+
"import": "./dist/lifecycle/index.js",
|
|
307
|
+
"default": "./dist/lifecycle/index.js"
|
|
283
308
|
}
|
|
284
309
|
},
|
|
285
310
|
"repository": {
|
package/release-gates.json
CHANGED
|
@@ -600,6 +600,45 @@
|
|
|
600
600
|
"entrypoint": "dist/account-support/contracts/index.d.ts",
|
|
601
601
|
"snapshot": "api/account-support-contracts.api.md"
|
|
602
602
|
},
|
|
603
|
+
"./operations": {
|
|
604
|
+
"role": "capability",
|
|
605
|
+
"maturity": "experimental",
|
|
606
|
+
"compatibilityPolicy": "internal-candidate",
|
|
607
|
+
"packageSmokeEvidence": [
|
|
608
|
+
"test/packageBoundary.test.ts",
|
|
609
|
+
"test/operations/controlPlaneHandler.test.ts",
|
|
610
|
+
"test/operations/platformWatchdogService.test.ts",
|
|
611
|
+
"test/operations/adminAlertsView.test.ts"
|
|
612
|
+
],
|
|
613
|
+
"entrypoint": "dist/operations/index.d.ts",
|
|
614
|
+
"snapshot": "api/operations.api.md"
|
|
615
|
+
},
|
|
616
|
+
"./operations/contracts": {
|
|
617
|
+
"role": "capability",
|
|
618
|
+
"maturity": "experimental",
|
|
619
|
+
"compatibilityPolicy": "internal-candidate",
|
|
620
|
+
"packageSmokeEvidence": [
|
|
621
|
+
"test/packageBoundary.test.ts",
|
|
622
|
+
"test/operations/controlPlaneHandler.test.ts",
|
|
623
|
+
"test/operations/platformWatchdogService.test.ts",
|
|
624
|
+
"test/operations/adminAlertsView.test.ts"
|
|
625
|
+
],
|
|
626
|
+
"entrypoint": "dist/operations/contracts/index.d.ts",
|
|
627
|
+
"snapshot": "api/operations-contracts.api.md"
|
|
628
|
+
},
|
|
629
|
+
"./operations/server": {
|
|
630
|
+
"role": "capability",
|
|
631
|
+
"maturity": "experimental",
|
|
632
|
+
"compatibilityPolicy": "internal-candidate",
|
|
633
|
+
"packageSmokeEvidence": [
|
|
634
|
+
"test/packageBoundary.test.ts",
|
|
635
|
+
"test/operations/controlPlaneHandler.test.ts",
|
|
636
|
+
"test/operations/platformWatchdogService.test.ts",
|
|
637
|
+
"test/operations/adminAlertsView.test.ts"
|
|
638
|
+
],
|
|
639
|
+
"entrypoint": "dist/operations/server/index.d.ts",
|
|
640
|
+
"snapshot": "api/operations-server.api.md"
|
|
641
|
+
},
|
|
603
642
|
"./marketing": {
|
|
604
643
|
"role": "capability",
|
|
605
644
|
"maturity": "experimental",
|
|
@@ -666,23 +705,48 @@
|
|
|
666
705
|
],
|
|
667
706
|
"entrypoint": "dist/marketing/server/postgres/index.d.ts",
|
|
668
707
|
"snapshot": "api/marketing-server-postgres.api.md"
|
|
708
|
+
},
|
|
709
|
+
"./portability": {
|
|
710
|
+
"role": "rail",
|
|
711
|
+
"maturity": "experimental",
|
|
712
|
+
"compatibilityPolicy": "internal-candidate",
|
|
713
|
+
"packageSmokeEvidence": [
|
|
714
|
+
"test/packageBoundary.test.ts",
|
|
715
|
+
"test/portability/objects.test.ts",
|
|
716
|
+
"test/portability/filesystemObjects.test.ts",
|
|
717
|
+
"test/payments/tpay/effectAuthority.test.ts"
|
|
718
|
+
],
|
|
719
|
+
"entrypoint": "dist/portability/index.d.ts",
|
|
720
|
+
"snapshot": "api/portability.api.md"
|
|
721
|
+
},
|
|
722
|
+
"./lifecycle": {
|
|
723
|
+
"role": "rail",
|
|
724
|
+
"maturity": "experimental",
|
|
725
|
+
"compatibilityPolicy": "internal-candidate",
|
|
726
|
+
"packageSmokeEvidence": [
|
|
727
|
+
"test/packageBoundary.test.ts",
|
|
728
|
+
"test/lifecycle/appUpgrade.test.ts",
|
|
729
|
+
"test/lifecycle/postgresInstallQualification.test.ts"
|
|
730
|
+
],
|
|
731
|
+
"entrypoint": "dist/lifecycle/index.d.ts",
|
|
732
|
+
"snapshot": "api/lifecycle.api.md"
|
|
669
733
|
}
|
|
670
734
|
},
|
|
671
735
|
"pack": {
|
|
672
736
|
"code": {
|
|
673
|
-
"maxPackedBytes":
|
|
674
|
-
"maxFiles":
|
|
737
|
+
"maxPackedBytes": 1743872,
|
|
738
|
+
"maxFiles": 2574
|
|
675
739
|
},
|
|
676
740
|
"docs": {
|
|
677
741
|
"maxPackedBytes": 73728,
|
|
678
742
|
"maxFiles": 10
|
|
679
743
|
},
|
|
680
|
-
"maxPackedBytes":
|
|
681
|
-
"maxUnpackedBytes":
|
|
682
|
-
"maxFiles":
|
|
744
|
+
"maxPackedBytes": 2029568,
|
|
745
|
+
"maxUnpackedBytes": 9447424,
|
|
746
|
+
"maxFiles": 2628,
|
|
683
747
|
"baseline": {
|
|
684
|
-
"version": "0.
|
|
685
|
-
"packedBytes":
|
|
748
|
+
"version": "0.29.0",
|
|
749
|
+
"packedBytes": 1870812
|
|
686
750
|
},
|
|
687
751
|
"browserEntries": {
|
|
688
752
|
"./catalog/contracts": 4096,
|
|
@@ -699,6 +763,8 @@
|
|
|
699
763
|
"./commerce/contracts": 32,
|
|
700
764
|
"./pricing/browser": 25600,
|
|
701
765
|
"./account-support/contracts": 32,
|
|
766
|
+
"./operations/contracts": 32,
|
|
767
|
+
"./operations": 30720,
|
|
702
768
|
"./marketing/contracts": 32
|
|
703
769
|
}
|
|
704
770
|
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
-- Provider-neutral platform operations control plane.
|
|
2
|
+
--
|
|
3
|
+
-- WHAT THIS FORWARD SHIPS. A capability-local operator allowlist, opaque
|
|
4
|
+
-- boolean operational controls, an idempotent operation ledger, the portable
|
|
5
|
+
-- watchdog alert ledger and fixed invoker-rights routines for control mutation,
|
|
6
|
+
-- heartbeat persistence and bounded readback.
|
|
7
|
+
--
|
|
8
|
+
-- WHAT IT DELIBERATELY DOES NOT SHIP. No private tester, email, auth, provider,
|
|
9
|
+
-- payment, fulfilment or rich observability snapshot tables; no message or
|
|
10
|
+
-- template content; no raw provider payload; no browser grant; no scheduler;
|
|
11
|
+
-- and no platform-specific seed value.
|
|
12
|
+
|
|
13
|
+
CREATE TABLE public.platform_control_operators (
|
|
14
|
+
principal_id uuid PRIMARY KEY,
|
|
15
|
+
active boolean NOT NULL DEFAULT true,
|
|
16
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
17
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
18
|
+
CONSTRAINT platform_control_operators_timestamp_check CHECK (updated_at >= created_at)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE TABLE public.platform_operational_controls (
|
|
22
|
+
control_key text PRIMARY KEY,
|
|
23
|
+
enabled boolean NOT NULL DEFAULT false,
|
|
24
|
+
revision integer NOT NULL DEFAULT 1,
|
|
25
|
+
updated_by uuid NOT NULL REFERENCES public.platform_control_operators(principal_id),
|
|
26
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
27
|
+
CONSTRAINT platform_operational_controls_key_check
|
|
28
|
+
CHECK (control_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
|
|
29
|
+
CONSTRAINT platform_operational_controls_revision_check CHECK (revision >= 1)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE public.platform_control_operations (
|
|
33
|
+
idempotency_key text PRIMARY KEY,
|
|
34
|
+
command_fingerprint text NOT NULL,
|
|
35
|
+
action text NOT NULL CHECK (action IN ('set-control', 'record-heartbeat')),
|
|
36
|
+
operator_id uuid NOT NULL REFERENCES public.platform_control_operators(principal_id),
|
|
37
|
+
response jsonb NOT NULL,
|
|
38
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
39
|
+
CONSTRAINT platform_control_operations_key_check
|
|
40
|
+
CHECK (idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'),
|
|
41
|
+
CONSTRAINT platform_control_operations_fingerprint_check
|
|
42
|
+
CHECK (command_fingerprint ~ '^[a-f0-9]{64}$')
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS public.platform_alerts (
|
|
46
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
47
|
+
dedupe_key text NOT NULL UNIQUE,
|
|
48
|
+
severity text NOT NULL CHECK (severity IN ('p0', 'p1', 'p2', 'p3')),
|
|
49
|
+
status text NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'acknowledged', 'resolved')),
|
|
50
|
+
first_seen_at timestamptz NOT NULL DEFAULT now(),
|
|
51
|
+
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
|
52
|
+
last_notified_at timestamptz,
|
|
53
|
+
last_notification_attempt_at timestamptz,
|
|
54
|
+
last_notification_status text CHECK (last_notification_status IS NULL OR last_notification_status IN ('sent', 'failed', 'skipped')),
|
|
55
|
+
next_notification_attempt_at timestamptz,
|
|
56
|
+
notification_failure_count integer NOT NULL DEFAULT 0 CHECK (notification_failure_count >= 0),
|
|
57
|
+
snoozed_until timestamptz,
|
|
58
|
+
acknowledged_at timestamptz,
|
|
59
|
+
resolved_at timestamptz,
|
|
60
|
+
support_code text NOT NULL,
|
|
61
|
+
owner text NOT NULL,
|
|
62
|
+
runbook_url text NOT NULL,
|
|
63
|
+
title text NOT NULL,
|
|
64
|
+
message text NOT NULL,
|
|
65
|
+
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
66
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
67
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS public.platform_alert_notifications (
|
|
71
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
72
|
+
alert_id uuid NOT NULL REFERENCES public.platform_alerts(id) ON DELETE CASCADE,
|
|
73
|
+
channel text NOT NULL CHECK (channel IN ('webhook', 'resend')),
|
|
74
|
+
status text NOT NULL CHECK (status IN ('sent', 'failed', 'skipped')),
|
|
75
|
+
provider text,
|
|
76
|
+
provider_response jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
77
|
+
error text,
|
|
78
|
+
notified_at timestamptz NOT NULL DEFAULT now(),
|
|
79
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE INDEX platform_control_operations_created_idx
|
|
83
|
+
ON public.platform_control_operations(created_at DESC, idempotency_key);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_platform_alerts_status_seen
|
|
85
|
+
ON public.platform_alerts(status, last_seen_at DESC);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_platform_alert_notifications_alert
|
|
87
|
+
ON public.platform_alert_notifications(alert_id, notified_at DESC);
|
|
88
|
+
|
|
89
|
+
REVOKE ALL ON TABLE
|
|
90
|
+
public.platform_control_operators,
|
|
91
|
+
public.platform_operational_controls,
|
|
92
|
+
public.platform_control_operations,
|
|
93
|
+
public.platform_alerts,
|
|
94
|
+
public.platform_alert_notifications
|
|
95
|
+
FROM PUBLIC, anon, authenticated;
|
|
96
|
+
|
|
97
|
+
CREATE FUNCTION public.platform_control_operator_is_active(p_principal_id uuid)
|
|
98
|
+
RETURNS boolean
|
|
99
|
+
LANGUAGE sql
|
|
100
|
+
STABLE
|
|
101
|
+
SET search_path = pg_catalog
|
|
102
|
+
AS $$
|
|
103
|
+
SELECT EXISTS (
|
|
104
|
+
SELECT 1 FROM public.platform_control_operators AS operator_row
|
|
105
|
+
WHERE operator_row.principal_id = p_principal_id AND operator_row.active IS TRUE
|
|
106
|
+
);
|
|
107
|
+
$$;
|
|
108
|
+
|
|
109
|
+
CREATE FUNCTION public.platform_control_require_operator(p_operator_id uuid)
|
|
110
|
+
RETURNS void
|
|
111
|
+
LANGUAGE plpgsql
|
|
112
|
+
STABLE
|
|
113
|
+
SET search_path = pg_catalog
|
|
114
|
+
AS $$
|
|
115
|
+
BEGIN
|
|
116
|
+
IF p_operator_id IS NULL OR NOT public.platform_control_operator_is_active(p_operator_id) THEN
|
|
117
|
+
RAISE EXCEPTION 'platform_control_operator_inactive' USING ERRCODE = '42501';
|
|
118
|
+
END IF;
|
|
119
|
+
END;
|
|
120
|
+
$$;
|
|
121
|
+
|
|
122
|
+
CREATE FUNCTION public.platform_control_set_control(
|
|
123
|
+
p_operator_id uuid,
|
|
124
|
+
p_idempotency_key text,
|
|
125
|
+
p_command_fingerprint text,
|
|
126
|
+
p_control_key text,
|
|
127
|
+
p_enabled boolean
|
|
128
|
+
)
|
|
129
|
+
RETURNS TABLE(action text, replayed boolean, revision integer, recorded_at timestamptz)
|
|
130
|
+
LANGUAGE plpgsql
|
|
131
|
+
SET search_path = pg_catalog
|
|
132
|
+
AS $$
|
|
133
|
+
DECLARE
|
|
134
|
+
v_existing public.platform_control_operations%ROWTYPE;
|
|
135
|
+
v_revision integer;
|
|
136
|
+
v_recorded_at timestamptz := clock_timestamp();
|
|
137
|
+
BEGIN
|
|
138
|
+
PERFORM public.platform_control_require_operator(p_operator_id);
|
|
139
|
+
SELECT * INTO v_existing
|
|
140
|
+
FROM public.platform_control_operations
|
|
141
|
+
WHERE idempotency_key = p_idempotency_key
|
|
142
|
+
FOR UPDATE;
|
|
143
|
+
IF FOUND THEN
|
|
144
|
+
IF v_existing.command_fingerprint <> p_command_fingerprint OR v_existing.action <> 'set-control' THEN
|
|
145
|
+
RAISE EXCEPTION 'platform_control_idempotency_conflict' USING ERRCODE = '23505';
|
|
146
|
+
END IF;
|
|
147
|
+
RETURN QUERY SELECT 'set-control'::text, true,
|
|
148
|
+
(v_existing.response->>'revision')::integer, v_existing.created_at;
|
|
149
|
+
RETURN;
|
|
150
|
+
END IF;
|
|
151
|
+
|
|
152
|
+
INSERT INTO public.platform_operational_controls AS control_row
|
|
153
|
+
(control_key, enabled, revision, updated_by, updated_at)
|
|
154
|
+
VALUES (p_control_key, p_enabled, 1, p_operator_id, v_recorded_at)
|
|
155
|
+
ON CONFLICT (control_key) DO UPDATE
|
|
156
|
+
SET enabled = EXCLUDED.enabled,
|
|
157
|
+
revision = control_row.revision + 1,
|
|
158
|
+
updated_by = EXCLUDED.updated_by,
|
|
159
|
+
updated_at = EXCLUDED.updated_at
|
|
160
|
+
RETURNING control_row.revision INTO v_revision;
|
|
161
|
+
|
|
162
|
+
INSERT INTO public.platform_control_operations
|
|
163
|
+
(idempotency_key, command_fingerprint, action, operator_id, response, created_at)
|
|
164
|
+
VALUES (
|
|
165
|
+
p_idempotency_key, p_command_fingerprint, 'set-control', p_operator_id,
|
|
166
|
+
jsonb_build_object('revision', v_revision, 'recordedAt', v_recorded_at), v_recorded_at
|
|
167
|
+
);
|
|
168
|
+
RETURN QUERY SELECT 'set-control'::text, false, v_revision, v_recorded_at;
|
|
169
|
+
END;
|
|
170
|
+
$$;
|
|
171
|
+
|
|
172
|
+
CREATE FUNCTION public.platform_control_record_heartbeat(
|
|
173
|
+
p_operator_id uuid,
|
|
174
|
+
p_idempotency_key text,
|
|
175
|
+
p_command_fingerprint text,
|
|
176
|
+
p_health text,
|
|
177
|
+
p_firing_count integer,
|
|
178
|
+
p_max_severity text,
|
|
179
|
+
p_promotion_ready boolean
|
|
180
|
+
)
|
|
181
|
+
RETURNS TABLE(action text, replayed boolean, revision integer, recorded_at timestamptz)
|
|
182
|
+
LANGUAGE plpgsql
|
|
183
|
+
SET search_path = pg_catalog
|
|
184
|
+
AS $$
|
|
185
|
+
DECLARE
|
|
186
|
+
v_existing public.platform_control_operations%ROWTYPE;
|
|
187
|
+
v_recorded_at timestamptz := clock_timestamp();
|
|
188
|
+
BEGIN
|
|
189
|
+
PERFORM public.platform_control_require_operator(p_operator_id);
|
|
190
|
+
IF p_health NOT IN ('healthy', 'degraded', 'failed')
|
|
191
|
+
OR p_firing_count < 0
|
|
192
|
+
OR (p_max_severity IS NOT NULL AND p_max_severity NOT IN ('p0', 'p1', 'p2', 'p3')) THEN
|
|
193
|
+
RAISE EXCEPTION 'platform_control_heartbeat_invalid' USING ERRCODE = '22023';
|
|
194
|
+
END IF;
|
|
195
|
+
SELECT * INTO v_existing
|
|
196
|
+
FROM public.platform_control_operations
|
|
197
|
+
WHERE idempotency_key = p_idempotency_key
|
|
198
|
+
FOR UPDATE;
|
|
199
|
+
IF FOUND THEN
|
|
200
|
+
IF v_existing.command_fingerprint <> p_command_fingerprint OR v_existing.action <> 'record-heartbeat' THEN
|
|
201
|
+
RAISE EXCEPTION 'platform_control_idempotency_conflict' USING ERRCODE = '23505';
|
|
202
|
+
END IF;
|
|
203
|
+
RETURN QUERY SELECT 'record-heartbeat'::text, true, NULL::integer, v_existing.created_at;
|
|
204
|
+
RETURN;
|
|
205
|
+
END IF;
|
|
206
|
+
|
|
207
|
+
INSERT INTO public.platform_job_controls AS job_row
|
|
208
|
+
(job_name, enabled, active_driver, last_started_at, last_finished_at,
|
|
209
|
+
last_success_at, last_status, metadata, updated_at)
|
|
210
|
+
VALUES (
|
|
211
|
+
'platform-watchdog', true, 'operator', v_recorded_at, v_recorded_at,
|
|
212
|
+
v_recorded_at, 'success',
|
|
213
|
+
jsonb_build_object('health', p_health, 'firingCount', p_firing_count,
|
|
214
|
+
'maxSeverity', p_max_severity, 'promotionReady', p_promotion_ready),
|
|
215
|
+
v_recorded_at
|
|
216
|
+
)
|
|
217
|
+
ON CONFLICT (job_name) DO UPDATE
|
|
218
|
+
SET last_started_at = EXCLUDED.last_started_at,
|
|
219
|
+
last_finished_at = EXCLUDED.last_finished_at,
|
|
220
|
+
last_success_at = EXCLUDED.last_success_at,
|
|
221
|
+
last_status = EXCLUDED.last_status,
|
|
222
|
+
metadata = EXCLUDED.metadata,
|
|
223
|
+
updated_at = EXCLUDED.updated_at;
|
|
224
|
+
|
|
225
|
+
INSERT INTO public.platform_control_operations
|
|
226
|
+
(idempotency_key, command_fingerprint, action, operator_id, response, created_at)
|
|
227
|
+
VALUES (
|
|
228
|
+
p_idempotency_key, p_command_fingerprint, 'record-heartbeat', p_operator_id,
|
|
229
|
+
jsonb_build_object('recordedAt', v_recorded_at), v_recorded_at
|
|
230
|
+
);
|
|
231
|
+
RETURN QUERY SELECT 'record-heartbeat'::text, false, NULL::integer, v_recorded_at;
|
|
232
|
+
END;
|
|
233
|
+
$$;
|
|
234
|
+
|
|
235
|
+
CREATE FUNCTION public.platform_control_list_controls(p_operator_id uuid)
|
|
236
|
+
RETURNS TABLE(control_key text, enabled boolean, revision integer, updated_at timestamptz)
|
|
237
|
+
LANGUAGE plpgsql
|
|
238
|
+
STABLE
|
|
239
|
+
SET search_path = pg_catalog
|
|
240
|
+
AS $$
|
|
241
|
+
BEGIN
|
|
242
|
+
PERFORM public.platform_control_require_operator(p_operator_id);
|
|
243
|
+
RETURN QUERY SELECT row.control_key, row.enabled, row.revision, row.updated_at
|
|
244
|
+
FROM public.platform_operational_controls AS row ORDER BY row.control_key;
|
|
245
|
+
END;
|
|
246
|
+
$$;
|
|
247
|
+
|
|
248
|
+
CREATE FUNCTION public.platform_control_list_jobs(p_operator_id uuid)
|
|
249
|
+
RETURNS TABLE(job_name text, enabled boolean, active_driver text, last_status text,
|
|
250
|
+
last_started_at timestamptz, last_finished_at timestamptz, last_success_at timestamptz)
|
|
251
|
+
LANGUAGE plpgsql
|
|
252
|
+
STABLE
|
|
253
|
+
SET search_path = pg_catalog
|
|
254
|
+
AS $$
|
|
255
|
+
BEGIN
|
|
256
|
+
PERFORM public.platform_control_require_operator(p_operator_id);
|
|
257
|
+
RETURN QUERY SELECT row.job_name, row.enabled, row.active_driver, row.last_status,
|
|
258
|
+
row.last_started_at, row.last_finished_at, row.last_success_at
|
|
259
|
+
FROM public.platform_job_controls AS row ORDER BY row.job_name;
|
|
260
|
+
END;
|
|
261
|
+
$$;
|
|
262
|
+
|
|
263
|
+
CREATE FUNCTION public.platform_control_list_alerts(p_operator_id uuid)
|
|
264
|
+
RETURNS TABLE(dedupe_key text, severity text, status text, owner text, title text,
|
|
265
|
+
first_seen_at timestamptz, last_seen_at timestamptz, resolved_at timestamptz)
|
|
266
|
+
LANGUAGE plpgsql
|
|
267
|
+
STABLE
|
|
268
|
+
SET search_path = pg_catalog
|
|
269
|
+
AS $$
|
|
270
|
+
BEGIN
|
|
271
|
+
PERFORM public.platform_control_require_operator(p_operator_id);
|
|
272
|
+
RETURN QUERY SELECT row.dedupe_key, row.severity, row.status, row.owner, row.title,
|
|
273
|
+
row.first_seen_at, row.last_seen_at, row.resolved_at
|
|
274
|
+
FROM public.platform_alerts AS row
|
|
275
|
+
ORDER BY row.last_seen_at DESC, row.dedupe_key
|
|
276
|
+
LIMIT 200;
|
|
277
|
+
END;
|
|
278
|
+
$$;
|
|
279
|
+
|
|
280
|
+
REVOKE ALL ON FUNCTION public.platform_control_operator_is_active(uuid) FROM PUBLIC, anon, authenticated;
|
|
281
|
+
REVOKE ALL ON FUNCTION public.platform_control_require_operator(uuid) FROM PUBLIC, anon, authenticated;
|
|
282
|
+
REVOKE ALL ON FUNCTION public.platform_control_set_control(uuid, text, text, text, boolean) FROM PUBLIC, anon, authenticated;
|
|
283
|
+
REVOKE ALL ON FUNCTION public.platform_control_record_heartbeat(uuid, text, text, text, integer, text, boolean) FROM PUBLIC, anon, authenticated;
|
|
284
|
+
REVOKE ALL ON FUNCTION public.platform_control_list_controls(uuid) FROM PUBLIC, anon, authenticated;
|
|
285
|
+
REVOKE ALL ON FUNCTION public.platform_control_list_jobs(uuid) FROM PUBLIC, anon, authenticated;
|
|
286
|
+
REVOKE ALL ON FUNCTION public.platform_control_list_alerts(uuid) FROM PUBLIC, anon, authenticated;
|
|
@@ -120,7 +120,7 @@ export interface ServeNodeResult {
|
|
|
120
120
|
bundleId: string;
|
|
121
121
|
httpRuntime: HttpRuntimePort;
|
|
122
122
|
registeredJobs: string[];
|
|
123
|
-
/**
|
|
123
|
+
/** Fence timers, drain admitted jobs, then close their lock session; also close the local listener. Safe to call repeatedly. */
|
|
124
124
|
stop(): Promise<void>;
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -276,14 +276,15 @@ async function stopNodeHost(
|
|
|
276
276
|
advisoryLock: AdvisoryLock | undefined,
|
|
277
277
|
httpRuntime: HttpRuntimePort,
|
|
278
278
|
): Promise<void> {
|
|
279
|
-
const timers =
|
|
279
|
+
const timers = scheduler?.stopAll() ?? Promise.resolve();
|
|
280
280
|
const server = (httpRuntime as HttpRuntimePort & {
|
|
281
281
|
server?: { close(callback: (error?: Error) => void): void } | null;
|
|
282
282
|
}).server;
|
|
283
283
|
const listener = server
|
|
284
284
|
? new Promise<void>((resolveClose, reject) => server.close((error) => error ? reject(error) : resolveClose()))
|
|
285
285
|
: Promise.resolve();
|
|
286
|
-
const
|
|
286
|
+
const lock = timers.then(() => advisoryLock?.close(), () => advisoryLock?.close());
|
|
287
|
+
const results = await Promise.allSettled([timers, lock, listener]);
|
|
287
288
|
const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
288
289
|
if (failures.length > 0) throw new AggregateError(failures, "node_host_shutdown_failed");
|
|
289
290
|
}
|