@uns-kit/core 3.0.34 → 3.0.36
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/MIGRATIONS.md +41 -0
- package/dist/config/app-config.d.ts +1 -1
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js.map +1 -1
- package/dist/runtime/mqtt-handover-state.d.ts +49 -0
- package/dist/runtime/mqtt-handover-state.d.ts.map +1 -0
- package/dist/runtime/mqtt-handover-state.js +63 -0
- package/dist/runtime/mqtt-handover-state.js.map +1 -0
- package/dist/uns/handover-manager.d.ts +10 -1
- package/dist/uns/handover-manager.d.ts.map +1 -1
- package/dist/uns/handover-manager.js +113 -5
- package/dist/uns/handover-manager.js.map +1 -1
- package/dist/uns/uns-interfaces.d.ts +4 -0
- package/dist/uns/uns-interfaces.d.ts.map +1 -1
- package/dist/uns/uns-interfaces.js.map +1 -1
- package/dist/uns/uns-proxy-process.d.ts +3 -0
- package/dist/uns/uns-proxy-process.d.ts.map +1 -1
- package/dist/uns/uns-proxy-process.js +8 -2
- package/dist/uns/uns-proxy-process.js.map +1 -1
- package/dist/uns-config/config-schema.d.ts +4 -1
- package/dist/uns-config/config-schema.d.ts.map +1 -1
- package/dist/uns-config/uns-core-schema.d.ts +4 -1
- package/dist/uns-config/uns-core-schema.d.ts.map +1 -1
- package/dist/uns-config/uns-core-schema.js +1 -1
- package/dist/uns-config/uns-core-schema.js.map +1 -1
- package/dist/uns-mqtt/mqtt-worker.d.ts.map +1 -1
- package/dist/uns-mqtt/mqtt-worker.js +13 -1
- package/dist/uns-mqtt/mqtt-worker.js.map +1 -1
- package/dist/uns-mqtt/throttled-queue.d.ts +8 -1
- package/dist/uns-mqtt/throttled-queue.d.ts.map +1 -1
- package/dist/uns-mqtt/throttled-queue.js +32 -4
- package/dist/uns-mqtt/throttled-queue.js.map +1 -1
- package/dist/uns-mqtt/uns-mqtt-proxy.d.ts +20 -2
- package/dist/uns-mqtt/uns-mqtt-proxy.d.ts.map +1 -1
- package/dist/uns-mqtt/uns-mqtt-proxy.js +68 -6
- package/dist/uns-mqtt/uns-mqtt-proxy.js.map +1 -1
- package/package.json +1 -1
package/MIGRATIONS.md
CHANGED
|
@@ -9,6 +9,47 @@ Agents must inspect the application's existing ownership and shutdown flow
|
|
|
9
9
|
before editing it. The examples below describe the intended behavior, not a
|
|
10
10
|
mechanical search-and-replace operation.
|
|
11
11
|
|
|
12
|
+
## 3.0.36 - Unitless state and passive background publishers
|
|
13
|
+
|
|
14
|
+
When crossing `<3.0.36` to `>=3.0.36`, MQTT handover preserves absent units
|
|
15
|
+
for strings and unitless numeric data. It never substitutes an empty string or
|
|
16
|
+
an invented physical unit. The optional-unit state format is bound into the
|
|
17
|
+
compatibility offer: mixed 3.0.35/3.0.36 stateful peers reject before freezing the
|
|
18
|
+
source. Review an initial stop-before-start upgrade, then use matching updated
|
|
19
|
+
peers for live moves. Do not bypass this refusal by relabeling a contract.
|
|
20
|
+
|
|
21
|
+
A passive successor must not publish startup metadata or poll results before
|
|
22
|
+
state import. Gate background work with `UnsProxyProcess.isActive()` and stop it
|
|
23
|
+
in `handoverShutdown.onRelease`; keep tracking all accepted asynchronous work.
|
|
24
|
+
This getter is an ownership signal, not broker connectivity or archive readiness.
|
|
25
|
+
Input callbacks already delivered by the SDK must still finish during drain.
|
|
26
|
+
Legacy configurations and persistent-output ownership rules are unchanged.
|
|
27
|
+
|
|
28
|
+
## 3.0.35 - Opt-in state-preserving MQTT handover
|
|
29
|
+
|
|
30
|
+
When crossing `<3.0.35` to `>=3.0.35`, existing applications keep their current
|
|
31
|
+
handover behavior. State continuity is opt-in: declare `mqttHandover` protocol2
|
|
32
|
+
and `mqttDrain` protocol1, pass `uns.handoverProtocol: "correlated-v2"` to
|
|
33
|
+
`UnsProxyProcess`, declare each proxy's semantic `handoverStateContract`, and
|
|
34
|
+
register migration drain after installing application input handlers.
|
|
35
|
+
|
|
36
|
+
Use an updated host that understands this capability and its bound drain/exit
|
|
37
|
+
receipt. Both peers must support the protocol; persistent output and other
|
|
38
|
+
runtime-owned state remain separate reviewed transfers. No package-name logic
|
|
39
|
+
or application callbacks may be executed by the host to inspect compatibility.
|
|
40
|
+
|
|
41
|
+
The SDK transfers its counter baselines and publication sequence state only after
|
|
42
|
+
accepted work and output flush. The target advertises compatibility before the
|
|
43
|
+
source freezes. Different destination/subscriptions/proxy sets/semantic revisions
|
|
44
|
+
block the handover while the source remains active. New paths start fresh;
|
|
45
|
+
removed paths do not erase history; changed unit/type must not reuse arithmetic
|
|
46
|
+
baselines. Matching path strings alone are not a compatibility guarantee.
|
|
47
|
+
|
|
48
|
+
Unknown outcomes must be reconciled under the same operation. Return ownership
|
|
49
|
+
with a fresh reverse handover from the current owner, never by restarting stale
|
|
50
|
+
RAM. This is not crash durability or universal exactly-once MQTT delivery.
|
|
51
|
+
See [example, protocol and limits](https://github.com/uns-openhub/uns-kit/blob/master/docs/mqtt-stateful-handover.md).
|
|
52
|
+
|
|
12
53
|
## 3.0.34 - Optional paired-input retirement to one direct process
|
|
13
54
|
|
|
14
55
|
When crossing `<3.0.34` to `>=3.0.34`, ordinary applications require no change.
|
|
@@ -13,7 +13,7 @@ export interface ProjectAppConfig {
|
|
|
13
13
|
processName: string;
|
|
14
14
|
handover?: boolean;
|
|
15
15
|
/** Opt-in controller-correlated MQTT handover protocol. */
|
|
16
|
-
handoverProtocol?: "correlated-v1" | undefined;
|
|
16
|
+
handoverProtocol?: ("correlated-v1" | "correlated-v2") | undefined;
|
|
17
17
|
/** Optional PM2/controller supervisor guard settings for this RTT instance. */
|
|
18
18
|
supervisor?: {
|
|
19
19
|
/** Enable controller/PM2 supervisor handling for this RTT instance. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE;QACD,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,2EAA2E;QAC3E,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC3B,8EAA8E;QAC9E,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC3B,0DAA0D;QAC1D,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC9B,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;QAC7C,iDAAiD;QACjD,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,2DAA2D;QAC3D,gBAAgB,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config.js","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"","sourcesContent":["/* Auto-generated. Do not edit by hand. */\nexport interface ProjectAppConfig {\n uns: {\n graphql: string;\n rest: string;\n /** Bearer token used for service-to-service access to the UNS instance. */\n token?: string | undefined;\n /** Email used when authenticating to graphql endpoint of the UNS instance. */\n email?: string | undefined;\n /** Password or secret value paired with the UNS email. */\n password?: string | undefined;\n instanceMode?: \"wait\" | \"force\" | \"handover\";\n /** Process name used in MQTT topics and logs. */\n processName: string;\n handover?: boolean;\n /** Opt-in controller-correlated MQTT handover protocol. */\n handoverProtocol?: \"correlated-v1\" | undefined;\n /** Optional PM2/controller supervisor guard settings for this RTT instance. */\n supervisor?: {\n /** Enable controller/PM2 supervisor handling for this RTT instance. */\n enabled?: boolean;\n /** Let PM2 restart the process when it exits unexpectedly. */\n restartOnExit?: boolean;\n /** Optional PM2 memory restart limit in megabytes. */\n maxMemoryMb?: number | undefined;\n /** Let the controller auto-start this instance when required system-service runtime signals are absent. */\n restartOnUnhealthy?: boolean;\n /** How long runtime signals must stay unhealthy before the controller supervisor can act. */\n unhealthyAfterMs?: number;\n /** Minimum time between controller supervisor restart attempts for this instance. */\n restartCooldownMs?: number;\n } | undefined;\n jwksWellKnownUrl?: string | undefined;\n kidWellKnownUrl?: string | undefined;\n env?: \"dev\" | \"staging\" | \"test\" | \"prod\";\n };\n logging?: {\n adapter?: string;\n host: string;\n port?: number;\n level?: \"error\" | \"warn\" | \"info\" | \"http\" | \"verbose\" | \"debug\" | \"silly\";\n } | undefined;\n input?: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n } | undefined;\n output?: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n /** Opt-in MQTT output persistence; does not persist input subscriptions. */\n publishQueue?: {\n enabled?: boolean;\n mode: \"buffered\" | \"durable\";\n storage?: \"local\" | \"runtime\";\n directory: string;\n maxRecords?: number;\n maxBytes?: number;\n memoryRecords?: number;\n memoryBytes?: number;\n maxMessageBytes?: number;\n maxDiskBytes?: number;\n minFreeBytes?: number;\n retryIntervalMs?: number;\n publishTimeoutMs?: number;\n } | undefined;\n } | undefined;\n infra: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n };\n devops?: {\n provider?: \"azure-devops\";\n organization: string;\n project?: string | undefined;\n } | undefined;\n}\n\nexport interface AppConfig extends ProjectAppConfig {}\n"]}
|
|
1
|
+
{"version":3,"file":"app-config.js","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"","sourcesContent":["/* Auto-generated. Do not edit by hand. */\nexport interface ProjectAppConfig {\n uns: {\n graphql: string;\n rest: string;\n /** Bearer token used for service-to-service access to the UNS instance. */\n token?: string | undefined;\n /** Email used when authenticating to graphql endpoint of the UNS instance. */\n email?: string | undefined;\n /** Password or secret value paired with the UNS email. */\n password?: string | undefined;\n instanceMode?: \"wait\" | \"force\" | \"handover\";\n /** Process name used in MQTT topics and logs. */\n processName: string;\n handover?: boolean;\n /** Opt-in controller-correlated MQTT handover protocol. */\n handoverProtocol?: (\"correlated-v1\" | \"correlated-v2\") | undefined;\n /** Optional PM2/controller supervisor guard settings for this RTT instance. */\n supervisor?: {\n /** Enable controller/PM2 supervisor handling for this RTT instance. */\n enabled?: boolean;\n /** Let PM2 restart the process when it exits unexpectedly. */\n restartOnExit?: boolean;\n /** Optional PM2 memory restart limit in megabytes. */\n maxMemoryMb?: number | undefined;\n /** Let the controller auto-start this instance when required system-service runtime signals are absent. */\n restartOnUnhealthy?: boolean;\n /** How long runtime signals must stay unhealthy before the controller supervisor can act. */\n unhealthyAfterMs?: number;\n /** Minimum time between controller supervisor restart attempts for this instance. */\n restartCooldownMs?: number;\n } | undefined;\n jwksWellKnownUrl?: string | undefined;\n kidWellKnownUrl?: string | undefined;\n env?: \"dev\" | \"staging\" | \"test\" | \"prod\";\n };\n logging?: {\n adapter?: string;\n host: string;\n port?: number;\n level?: \"error\" | \"warn\" | \"info\" | \"http\" | \"verbose\" | \"debug\" | \"silly\";\n } | undefined;\n input?: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n } | undefined;\n output?: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n /** Opt-in MQTT output persistence; does not persist input subscriptions. */\n publishQueue?: {\n enabled?: boolean;\n mode: \"buffered\" | \"durable\";\n storage?: \"local\" | \"runtime\";\n directory: string;\n maxRecords?: number;\n maxBytes?: number;\n memoryRecords?: number;\n memoryBytes?: number;\n maxMessageBytes?: number;\n maxDiskBytes?: number;\n minFreeBytes?: number;\n retryIntervalMs?: number;\n publishTimeoutMs?: number;\n } | undefined;\n } | undefined;\n infra: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n };\n devops?: {\n provider?: \"azure-devops\";\n organization: string;\n project?: string | undefined;\n } | undefined;\n}\n\nexport interface AppConfig extends ProjectAppConfig {}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const MQTT_HANDOVER_STATE_FORMAT = 2;
|
|
3
|
+
export declare const MQTT_HANDOVER_STATE_MAX_BYTES: number;
|
|
4
|
+
export declare const mqttHandoverOfferSchema: z.ZodArray<z.ZodObject<{
|
|
5
|
+
instanceName: z.ZodString;
|
|
6
|
+
compatibility: z.ZodString;
|
|
7
|
+
}, z.core.$strict>>;
|
|
8
|
+
export declare const mqttProxyHandoverStateSchema: z.ZodObject<{
|
|
9
|
+
instanceName: z.ZodString;
|
|
10
|
+
compatibility: z.ZodString;
|
|
11
|
+
values: z.ZodArray<z.ZodObject<{
|
|
12
|
+
topic: z.ZodString;
|
|
13
|
+
value: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodArray<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>]>;
|
|
14
|
+
uom: z.ZodOptional<z.ZodString>;
|
|
15
|
+
timestamp: z.ZodString;
|
|
16
|
+
}, z.core.$strict>>;
|
|
17
|
+
sequences: z.ZodArray<z.ZodTuple<[z.ZodString, z.ZodNumber], null>>;
|
|
18
|
+
cursor: z.ZodObject<{
|
|
19
|
+
batchSize: z.ZodNumber;
|
|
20
|
+
referenceHash: z.ZodString;
|
|
21
|
+
}, z.core.$strict>;
|
|
22
|
+
}, z.core.$strict>;
|
|
23
|
+
export declare const mqttHandoverStateSchema: z.ZodObject<{
|
|
24
|
+
protocol: z.ZodLiteral<1>;
|
|
25
|
+
proxies: z.ZodArray<z.ZodObject<{
|
|
26
|
+
instanceName: z.ZodString;
|
|
27
|
+
compatibility: z.ZodString;
|
|
28
|
+
values: z.ZodArray<z.ZodObject<{
|
|
29
|
+
topic: z.ZodString;
|
|
30
|
+
value: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodArray<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>]>;
|
|
31
|
+
uom: z.ZodOptional<z.ZodString>;
|
|
32
|
+
timestamp: z.ZodString;
|
|
33
|
+
}, z.core.$strict>>;
|
|
34
|
+
sequences: z.ZodArray<z.ZodTuple<[z.ZodString, z.ZodNumber], null>>;
|
|
35
|
+
cursor: z.ZodObject<{
|
|
36
|
+
batchSize: z.ZodNumber;
|
|
37
|
+
referenceHash: z.ZodString;
|
|
38
|
+
}, z.core.$strict>;
|
|
39
|
+
}, z.core.$strict>>;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
export type MqttProxyHandoverState = z.infer<typeof mqttProxyHandoverStateSchema>;
|
|
42
|
+
export type MqttHandoverState = z.infer<typeof mqttHandoverStateSchema>;
|
|
43
|
+
export declare function parseMqttHandoverState(input: unknown): MqttHandoverState;
|
|
44
|
+
export declare function mqttHandoverStateDigest(state: MqttHandoverState): string;
|
|
45
|
+
export declare const mqttHandoverCapabilitySchema: z.ZodObject<{
|
|
46
|
+
protocolVersion: z.ZodLiteral<2>;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
export declare function readMqttHandoverCapability(packageRoot: string): 2 | undefined;
|
|
49
|
+
//# sourceMappingURL=mqtt-handover-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mqtt-handover-state.d.ts","sourceRoot":"","sources":["../../src/runtime/mqtt-handover-state.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAC5C,eAAO,MAAM,6BAA6B,QAAc,CAAC;AAIzD,eAAO,MAAM,uBAAuB;;;mBAG8E,CAAC;AACnH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;kBASvC,CAAC;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;kBAMlC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAClF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAIxE;AACD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAExE;AACD,eAAO,MAAM,4BAA4B;;kBAAoD,CAAC;AAC9F,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAS7E"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
// Same contract for every SDK MQTT consumer. No application/profile selectors.
|
|
6
|
+
// Bound into peer compatibility so older required-unit readers reject before freeze.
|
|
7
|
+
export const MQTT_HANDOVER_STATE_FORMAT = 2;
|
|
8
|
+
export const MQTT_HANDOVER_STATE_MAX_BYTES = 1024 * 1024;
|
|
9
|
+
const text = z.string().min(1).max(65535).refine(v => !v.includes('\0'));
|
|
10
|
+
const scalar = z.union([z.number().finite(), z.string().max(65535)]);
|
|
11
|
+
const value = z.union([scalar, z.array(scalar).max(4096)]);
|
|
12
|
+
export const mqttHandoverOfferSchema = z.array(z.strictObject({
|
|
13
|
+
instanceName: z.string().min(1).max(256),
|
|
14
|
+
compatibility: z.string().regex(/^[a-f0-9]{64}$/),
|
|
15
|
+
})).max(64).refine(offer => new Set(offer.map(p => p.instanceName)).size === offer.length, 'Duplicate MQTT proxy');
|
|
16
|
+
export const mqttProxyHandoverStateSchema = z.strictObject({
|
|
17
|
+
instanceName: z.string().min(1).max(256),
|
|
18
|
+
compatibility: z.string().regex(/^[a-f0-9]{64}$/),
|
|
19
|
+
values: z.array(z.strictObject({ topic: text, value, uom: z.string().max(256).optional(), timestamp: z.string().datetime() })).max(10000),
|
|
20
|
+
sequences: z.array(z.tuple([text, z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)])).max(10000),
|
|
21
|
+
cursor: z.strictObject({ batchSize: z.number().int().min(0).max(100), referenceHash: z.string().regex(/^[a-f0-9]{64}$/) }),
|
|
22
|
+
}).superRefine((state, ctx) => {
|
|
23
|
+
for (const keys of [state.values.map(v => v.topic), state.sequences.map(v => v[0])])
|
|
24
|
+
if (new Set(keys).size !== keys.length)
|
|
25
|
+
ctx.addIssue({ code: 'custom', message: 'Duplicate MQTT state key' });
|
|
26
|
+
});
|
|
27
|
+
export const mqttHandoverStateSchema = z.strictObject({
|
|
28
|
+
protocol: z.literal(1),
|
|
29
|
+
proxies: z.array(mqttProxyHandoverStateSchema).max(64),
|
|
30
|
+
}).superRefine((state, ctx) => {
|
|
31
|
+
if (new Set(state.proxies.map(p => p.instanceName)).size !== state.proxies.length)
|
|
32
|
+
ctx.addIssue({ code: 'custom', message: 'Duplicate MQTT proxy' });
|
|
33
|
+
});
|
|
34
|
+
export function parseMqttHandoverState(input) {
|
|
35
|
+
const json = JSON.stringify(input);
|
|
36
|
+
if (!json || Buffer.byteLength(json) > MQTT_HANDOVER_STATE_MAX_BYTES)
|
|
37
|
+
throw Error('MQTT handover state exceeds its bound');
|
|
38
|
+
return mqttHandoverStateSchema.parse(input);
|
|
39
|
+
}
|
|
40
|
+
export function mqttHandoverStateDigest(state) {
|
|
41
|
+
return createHash('sha256').update(JSON.stringify(parseMqttHandoverState(state))).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
export const mqttHandoverCapabilitySchema = z.strictObject({ protocolVersion: z.literal(2) });
|
|
44
|
+
export function readMqttHandoverCapability(packageRoot) {
|
|
45
|
+
const file = path.join(packageRoot, 'package.json');
|
|
46
|
+
let stat;
|
|
47
|
+
try {
|
|
48
|
+
stat = fs.lstatSync(file);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT')
|
|
52
|
+
return undefined;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
56
|
+
throw Error('Unsafe MQTT handover descriptor');
|
|
57
|
+
const descriptor = JSON.parse(fs.readFileSync(file, 'utf8'))?.unsDatahub?.mqttHandover;
|
|
58
|
+
if (descriptor === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
mqttHandoverCapabilitySchema.parse(descriptor);
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=mqtt-handover-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mqtt-handover-state.js","sourceRoot":"","sources":["../../src/runtime/mqtt-handover-state.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { z } from 'zod';\n\n// Same contract for every SDK MQTT consumer. No application/profile selectors.\n// Bound into peer compatibility so older required-unit readers reject before freeze.\nexport const MQTT_HANDOVER_STATE_FORMAT = 2;\nexport const MQTT_HANDOVER_STATE_MAX_BYTES = 1024 * 1024;\nconst text = z.string().min(1).max(65535).refine(v => !v.includes('\\0'));\nconst scalar = z.union([z.number().finite(), z.string().max(65535)]);\nconst value = z.union([scalar, z.array(scalar).max(4096)]);\nexport const mqttHandoverOfferSchema = z.array(z.strictObject({\n instanceName: z.string().min(1).max(256),\n compatibility: z.string().regex(/^[a-f0-9]{64}$/),\n})).max(64).refine(offer => new Set(offer.map(p => p.instanceName)).size === offer.length, 'Duplicate MQTT proxy');\nexport const mqttProxyHandoverStateSchema = z.strictObject({\n instanceName: z.string().min(1).max(256),\n compatibility: z.string().regex(/^[a-f0-9]{64}$/),\n values: z.array(z.strictObject({ topic: text, value, uom: z.string().max(256).optional(), timestamp: z.string().datetime() })).max(10000),\n sequences: z.array(z.tuple([text, z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)])).max(10000),\n cursor: z.strictObject({ batchSize: z.number().int().min(0).max(100), referenceHash: z.string().regex(/^[a-f0-9]{64}$/) }),\n}).superRefine((state, ctx) => {\n for (const keys of [state.values.map(v => v.topic), state.sequences.map(v => v[0])])\n if (new Set(keys).size !== keys.length) ctx.addIssue({ code: 'custom', message: 'Duplicate MQTT state key' });\n});\nexport const mqttHandoverStateSchema = z.strictObject({\n protocol: z.literal(1),\n proxies: z.array(mqttProxyHandoverStateSchema).max(64),\n}).superRefine((state, ctx) => {\n if (new Set(state.proxies.map(p => p.instanceName)).size !== state.proxies.length)\n ctx.addIssue({ code: 'custom', message: 'Duplicate MQTT proxy' });\n});\nexport type MqttProxyHandoverState = z.infer<typeof mqttProxyHandoverStateSchema>;\nexport type MqttHandoverState = z.infer<typeof mqttHandoverStateSchema>;\nexport function parseMqttHandoverState(input: unknown): MqttHandoverState {\n const json = JSON.stringify(input);\n if (!json || Buffer.byteLength(json) > MQTT_HANDOVER_STATE_MAX_BYTES) throw Error('MQTT handover state exceeds its bound');\n return mqttHandoverStateSchema.parse(input);\n}\nexport function mqttHandoverStateDigest(state: MqttHandoverState): string {\n return createHash('sha256').update(JSON.stringify(parseMqttHandoverState(state))).digest('hex');\n}\nexport const mqttHandoverCapabilitySchema = z.strictObject({ protocolVersion: z.literal(2) });\nexport function readMqttHandoverCapability(packageRoot: string): 2 | undefined {\n const file = path.join(packageRoot, 'package.json');\n let stat: fs.Stats;\n try { stat = fs.lstatSync(file); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; throw error; }\n if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576) throw Error('Unsafe MQTT handover descriptor');\n const descriptor = JSON.parse(fs.readFileSync(file, 'utf8'))?.unsDatahub?.mqttHandover;\n if (descriptor === undefined) return undefined;\n mqttHandoverCapabilitySchema.parse(descriptor);\n return 2;\n}\n"]}
|
|
@@ -10,6 +10,7 @@ import { UnsEvents } from "./uns-interfaces.js";
|
|
|
10
10
|
* and processing handover responses.
|
|
11
11
|
*/
|
|
12
12
|
export declare class HandoverManager {
|
|
13
|
+
private readonly statefulHandover;
|
|
13
14
|
event: HandoverManagerEventEmitter<HandoverManagerEvents>;
|
|
14
15
|
private processName;
|
|
15
16
|
private processId;
|
|
@@ -26,10 +27,16 @@ export declare class HandoverManager {
|
|
|
26
27
|
private sourcePreparation;
|
|
27
28
|
private sourceExit;
|
|
28
29
|
private readonly shutdownHooks;
|
|
30
|
+
private expectedSourceId;
|
|
31
|
+
private preparedState;
|
|
32
|
+
private stateDigest;
|
|
33
|
+
private acceptedStateDigest;
|
|
34
|
+
private targetPreparation;
|
|
35
|
+
private finRetry;
|
|
29
36
|
handoverRequestEnabled: boolean;
|
|
30
37
|
handoverEnabled: boolean;
|
|
31
38
|
forceStartEnabled: boolean;
|
|
32
|
-
constructor(processName: string, processId: string, mqttProxy: MqttProxy, unsMqttProxies: UnsMqttProxy[], handoverRequestEnabled: boolean, handoverEnabled: boolean, forceStartEnabled: boolean, handoverId?: string, shutdownHooks?: HandoverShutdownHooks);
|
|
39
|
+
constructor(processName: string, processId: string, mqttProxy: MqttProxy, unsMqttProxies: UnsMqttProxy[], handoverRequestEnabled: boolean, handoverEnabled: boolean, forceStartEnabled: boolean, handoverId?: string, shutdownHooks?: HandoverShutdownHooks, statefulHandover?: boolean);
|
|
33
40
|
private getUserProperties;
|
|
34
41
|
private handoverPayload;
|
|
35
42
|
private getHandoverId;
|
|
@@ -47,6 +54,8 @@ export declare class HandoverManager {
|
|
|
47
54
|
*/
|
|
48
55
|
private exitSource;
|
|
49
56
|
private prepareSourceHandover;
|
|
57
|
+
private sendPreparedState;
|
|
58
|
+
private acceptPreparedState;
|
|
50
59
|
private finishSourceHandover;
|
|
51
60
|
private handleHandover;
|
|
52
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handover-manager.d.ts","sourceRoot":"","sources":["../../src/uns/handover-manager.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"handover-manager.d.ts","sourceRoot":"","sources":["../../src/uns/handover-manager.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiD,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AACnH,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,SAAS,MAAM,2BAA2B,CAAC;AAElD,OAAO,YAAY,MAAM,+BAA+B,CAAC;AACzD,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAQhD;;;;GAIG;AACH,qBAAa,eAAe;IAqCxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IApC5B,KAAK,EAAE,2BAA2B,CAAC,qBAAqB,CAAC,CAA4D;IAC5H,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,kBAAkB,CAAkB;IAC5C,OAAO,CAAC,kBAAkB,CAAkB;IAC5C,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoC;IAClE,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAA6B;IACtC,sBAAsB,EAAE,OAAO,CAAS;IACxC,eAAe,EAAE,OAAO,CAAQ;IAChC,iBAAiB,EAAE,OAAO,CAAS;gBAGxC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,SAAS,EACpB,cAAc,EAAE,YAAY,EAAE,EAC9B,sBAAsB,EAAE,OAAO,EAC/B,eAAe,EAAE,OAAO,EACxB,iBAAiB,EAAE,OAAO,EAC1B,UAAU,CAAC,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,qBAAqB,EACpB,gBAAgB,UAAQ;IAkC3C,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,oBAAoB;IAU5B;;;OAGG;IACU,iBAAiB,CAAC,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqFxE;;OAEG;IACH,OAAO,CAAC,UAAU;YAIJ,qBAAqB;YA8ErB,iBAAiB;YAOjB,mBAAmB;YAkBnB,oBAAoB;YA2BpB,cAAc;CAwH7B"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mqttHandoverOfferSchema, parseMqttHandoverState, mqttHandoverStateDigest } from "../runtime/mqtt-handover-state.js";
|
|
1
2
|
import logger from "../logger.js";
|
|
2
3
|
import { runHandoverShutdown, validateHandoverShutdown } from "./handover-shutdown.js";
|
|
3
4
|
import { MqttTopicBuilder } from "../uns-mqtt/mqtt-topic-builder.js";
|
|
@@ -15,6 +16,7 @@ function normalizeHandoverId(value) {
|
|
|
15
16
|
* and processing handover responses.
|
|
16
17
|
*/
|
|
17
18
|
export class HandoverManager {
|
|
19
|
+
statefulHandover;
|
|
18
20
|
event = new HandoverManagerEventEmitter();
|
|
19
21
|
processName;
|
|
20
22
|
processId;
|
|
@@ -31,10 +33,17 @@ export class HandoverManager {
|
|
|
31
33
|
sourcePreparation;
|
|
32
34
|
sourceExit;
|
|
33
35
|
shutdownHooks;
|
|
36
|
+
expectedSourceId;
|
|
37
|
+
preparedState;
|
|
38
|
+
stateDigest;
|
|
39
|
+
acceptedStateDigest;
|
|
40
|
+
targetPreparation;
|
|
41
|
+
finRetry;
|
|
34
42
|
handoverRequestEnabled = false;
|
|
35
43
|
handoverEnabled = true;
|
|
36
44
|
forceStartEnabled = false;
|
|
37
|
-
constructor(processName, processId, mqttProxy, unsMqttProxies, handoverRequestEnabled, handoverEnabled, forceStartEnabled, handoverId, shutdownHooks) {
|
|
45
|
+
constructor(processName, processId, mqttProxy, unsMqttProxies, handoverRequestEnabled, handoverEnabled, forceStartEnabled, handoverId, shutdownHooks, statefulHandover = false) {
|
|
46
|
+
this.statefulHandover = statefulHandover;
|
|
38
47
|
validateHandoverShutdown(shutdownHooks);
|
|
39
48
|
this.shutdownHooks = shutdownHooks ? { ...shutdownHooks } : undefined;
|
|
40
49
|
this.processName = processName;
|
|
@@ -51,6 +60,8 @@ export class HandoverManager {
|
|
|
51
60
|
this.topicBuilder = new MqttTopicBuilder(`uns-infra/${MqttTopicBuilder.sanitizeTopicPart(packageName)}/${MqttTopicBuilder.sanitizeTopicPart(version)}/${MqttTopicBuilder.sanitizeTopicPart(this.processName)}/`);
|
|
52
61
|
// Set status as active after a timeout if no other active process are detected.
|
|
53
62
|
this.activeTimeout = setTimeout(() => {
|
|
63
|
+
if (this.statefulHandover && this.handoverId)
|
|
64
|
+
return; // A reviewed successor never self-promotes.
|
|
54
65
|
logger.info(`${this.processName} - No active message received within timeout. Assuming no other process is running.`);
|
|
55
66
|
this.active = true;
|
|
56
67
|
this.event.emit("handoverManager", { active: this.active });
|
|
@@ -75,6 +86,7 @@ export class HandoverManager {
|
|
|
75
86
|
const normalized = normalizeHandoverId(handoverId ?? this.handoverId);
|
|
76
87
|
return JSON.stringify({
|
|
77
88
|
type,
|
|
89
|
+
...(this.statefulHandover ? { protocol: 2 } : {}),
|
|
78
90
|
...fields,
|
|
79
91
|
...(normalized ? { handoverId: normalized } : {}),
|
|
80
92
|
});
|
|
@@ -161,6 +173,7 @@ export class HandoverManager {
|
|
|
161
173
|
this.activeTimeout = undefined;
|
|
162
174
|
this.event.emit("handoverManager", { active: this.active });
|
|
163
175
|
this.requestingHandover = true;
|
|
176
|
+
this.expectedSourceId = sourceProcessId;
|
|
164
177
|
const eventHandoverTopic = new MqttTopicBuilder(MqttTopicBuilder.extractBaseTopic(event.topic)).getHandoverTopic();
|
|
165
178
|
await this.mqttProxy.publish(eventHandoverTopic, this.handoverPayload("handover_intent"), {
|
|
166
179
|
retain: false,
|
|
@@ -172,7 +185,7 @@ export class HandoverManager {
|
|
|
172
185
|
setTimeout(async () => {
|
|
173
186
|
logger.info(`${this.processName} - Requesting handover ${eventHandoverTopic}.`);
|
|
174
187
|
this.handoverInProgress = true;
|
|
175
|
-
await this.mqttProxy.publish(eventHandoverTopic, this.handoverPayload("handover_request"), {
|
|
188
|
+
await this.mqttProxy.publish(eventHandoverTopic, this.handoverPayload("handover_request", undefined, this.statefulHandover ? { offer: this.unsMqttProxies.map(proxy => proxy.getHandoverCompatibility()) } : {}), {
|
|
176
189
|
retain: false,
|
|
177
190
|
properties: {
|
|
178
191
|
responseTopic: this.topicBuilder.getHandoverTopic(),
|
|
@@ -231,9 +244,29 @@ export class HandoverManager {
|
|
|
231
244
|
const mqttWorkerData = [];
|
|
232
245
|
for (let i = 0; i < this.unsMqttProxies.length; i++) {
|
|
233
246
|
const unsProxy = this.unsMqttProxies[i];
|
|
234
|
-
const workerData = await unsProxy.setSubscriberPassiveAndDrainQueue();
|
|
247
|
+
const workerData = await unsProxy.setSubscriberPassiveAndDrainQueue(this.statefulHandover);
|
|
235
248
|
mqttWorkerData.push(workerData);
|
|
236
249
|
}
|
|
250
|
+
if (this.statefulHandover) {
|
|
251
|
+
// Finish accepted application callbacks before taking the calculation-state boundary.
|
|
252
|
+
await this.shutdownHooks?.drain();
|
|
253
|
+
for (const proxy of this.unsMqttProxies)
|
|
254
|
+
await proxy.flush();
|
|
255
|
+
this.preparedState = parseMqttHandoverState({ protocol: 1, proxies: this.unsMqttProxies.map((proxy, i) => proxy.exportHandoverState({ batchSize: mqttWorkerData[i].batchSize, referenceHash: mqttWorkerData[i].referenceHash })) });
|
|
256
|
+
this.stateDigest = mqttHandoverStateDigest(this.preparedState);
|
|
257
|
+
await this.sendPreparedState(event, handoverId);
|
|
258
|
+
let retries = 0;
|
|
259
|
+
this.finRetry = setInterval(() => {
|
|
260
|
+
if (++retries > 30 || this.sourceExit) {
|
|
261
|
+
clearInterval(this.finRetry);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
void this.sendPreparedState(event, handoverId).catch(() => logger.warn("MQTT handover receipt retry unavailable"));
|
|
265
|
+
}, 1000);
|
|
266
|
+
this.finRetry.unref();
|
|
267
|
+
// Keep the old owner passive until the successor acknowledges this exact state.
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
237
270
|
logger.info(`${this.processName} - Handover request accepted. Sending handover_subscriber messages.`);
|
|
238
271
|
// Publish handover_subscriber messages for each instance that has processed some data.
|
|
239
272
|
for (let i = 0; i < mqttWorkerData.length; i++) {
|
|
@@ -265,10 +298,48 @@ export class HandoverManager {
|
|
|
265
298
|
logger.info(`${this.processName} - Handover fin message sent.`);
|
|
266
299
|
await Promise.all(this.unsMqttProxies.map((unsProxy) => unsProxy.stop()));
|
|
267
300
|
}
|
|
301
|
+
async sendPreparedState(event, handoverId) {
|
|
302
|
+
await this.mqttProxy.publish(event.packet.properties?.responseTopic ?? "", this.handoverPayload("handover_fin", handoverId, { state: this.preparedState, stateDigest: this.stateDigest }), {
|
|
303
|
+
retain: false, qos: 1, properties: { responseTopic: this.topicBuilder.getHandoverTopic(), userProperties: this.getUserProperties(handoverId) },
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async acceptPreparedState(response) {
|
|
307
|
+
const state = parseMqttHandoverState(response.state);
|
|
308
|
+
const digest = mqttHandoverStateDigest(state);
|
|
309
|
+
if (digest !== response.stateDigest)
|
|
310
|
+
throw Error("MQTT handover state digest mismatch");
|
|
311
|
+
const byName = new Map(this.unsMqttProxies.map(proxy => [proxy.instanceName, proxy]));
|
|
312
|
+
if (byName.size !== this.unsMqttProxies.length || state.proxies.length !== byName.size || state.proxies.some(p => !byName.has(p.instanceName)))
|
|
313
|
+
throw Error("MQTT handover proxy set differs");
|
|
314
|
+
for (const snapshot of state.proxies)
|
|
315
|
+
byName.get(snapshot.instanceName).validateHandoverState(snapshot);
|
|
316
|
+
// Validate every cursor before importing any state or activating any subscriber.
|
|
317
|
+
for (const snapshot of state.proxies)
|
|
318
|
+
await byName.get(snapshot.instanceName).prepareHandoverResume(snapshot.cursor);
|
|
319
|
+
for (const snapshot of state.proxies)
|
|
320
|
+
byName.get(snapshot.instanceName).importHandoverState(snapshot);
|
|
321
|
+
for (const proxy of this.unsMqttProxies)
|
|
322
|
+
proxy.setPublisherActive();
|
|
323
|
+
for (const proxy of this.unsMqttProxies)
|
|
324
|
+
await proxy.activateHandoverResume();
|
|
325
|
+
this.acceptedStateDigest = digest;
|
|
326
|
+
this.active = true;
|
|
327
|
+
this.event.emit("handoverManager", { active: true });
|
|
328
|
+
}
|
|
268
329
|
async finishSourceHandover() {
|
|
269
330
|
const result = await runHandoverShutdown(async () => {
|
|
270
331
|
await this.sourcePreparation;
|
|
271
332
|
await this.shutdownHooks?.drain();
|
|
333
|
+
if (this.statefulHandover) {
|
|
334
|
+
await Promise.all(this.unsMqttProxies.map(proxy => proxy.stop()));
|
|
335
|
+
// The host persists this receipt synchronously before the real process
|
|
336
|
+
// exit. No receipt is emitted for a partial/failed application drain.
|
|
337
|
+
process.emit("uns-kit:mqtt-handover-drained", {
|
|
338
|
+
protocol: 2, handoverId: this.acceptedHandoverId,
|
|
339
|
+
stateDigest: this.stateDigest, pendingRecords: 0,
|
|
340
|
+
outputs: this.unsMqttProxies.length,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
272
343
|
}, this.shutdownHooks?.timeoutMs ?? 30_000);
|
|
273
344
|
this.handoverInProgress = false;
|
|
274
345
|
this.requestingHandover = false;
|
|
@@ -284,14 +355,32 @@ export class HandoverManager {
|
|
|
284
355
|
}
|
|
285
356
|
async handleHandover(event) {
|
|
286
357
|
try {
|
|
358
|
+
if (event.message.length > 1100000)
|
|
359
|
+
return;
|
|
287
360
|
const response = JSON.parse(event.message.toString());
|
|
361
|
+
// No downgrade: stateful peers must agree before either relinquishes ownership.
|
|
362
|
+
if ((response.protocol === 2) !== this.statefulHandover)
|
|
363
|
+
return;
|
|
364
|
+
if (this.statefulHandover && (event.packet?.retain || !this.getHandoverId(response, event)))
|
|
365
|
+
return;
|
|
288
366
|
// Responder process
|
|
289
367
|
// Check if the message is a handover request and publish MULTIPLE handover_subscriber messages
|
|
290
368
|
if (response.type === "handover_request") {
|
|
291
369
|
const peerId = this.getSourceProcessId(event);
|
|
292
370
|
const replyTopic = event.packet?.properties?.responseTopic;
|
|
371
|
+
if (this.statefulHandover && this.sourcePeerId === peerId && this.acceptedHandoverId === this.getHandoverId(response, event) && this.preparedState) {
|
|
372
|
+
await this.sendPreparedState(event, this.acceptedHandoverId);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
293
375
|
if (this.sourcePeerId || !this.active || !peerId || typeof replyTopic !== "string" || !replyTopic)
|
|
294
376
|
return;
|
|
377
|
+
if (this.statefulHandover) {
|
|
378
|
+
// Reject incompatible upgrades before freezing the current owner.
|
|
379
|
+
const offer = mqttHandoverOfferSchema.parse(response.offer);
|
|
380
|
+
const own = this.unsMqttProxies.map(proxy => proxy.getHandoverCompatibility());
|
|
381
|
+
if (offer.length !== own.length || own.some(p => !offer.some(o => o.instanceName === p.instanceName && o.compatibility === p.compatibility)))
|
|
382
|
+
throw Error("MQTT source/destination or semantic contract differs; source remains active");
|
|
383
|
+
}
|
|
295
384
|
this.sourcePeerId = peerId;
|
|
296
385
|
this.acceptedHandoverId = this.getHandoverId(response, event);
|
|
297
386
|
this.handoverInProgress = true;
|
|
@@ -303,8 +392,8 @@ export class HandoverManager {
|
|
|
303
392
|
}
|
|
304
393
|
catch {
|
|
305
394
|
// Subscriber transfer or onRelease failed; never report successful source completion.
|
|
306
|
-
logger.error(`${this.processName} - Handover source preparation failed
|
|
307
|
-
if (!this.sourceExit)
|
|
395
|
+
logger.error(`${this.processName} - Handover source preparation failed; ownership requires review.`);
|
|
396
|
+
if (!this.sourceExit && !this.statefulHandover)
|
|
308
397
|
this.exitSource(1);
|
|
309
398
|
}
|
|
310
399
|
}
|
|
@@ -312,6 +401,8 @@ export class HandoverManager {
|
|
|
312
401
|
// Check if the message is one of the handover_subscriber message in response to handover_request
|
|
313
402
|
// and publish a handover_ack message
|
|
314
403
|
if (response.type === "handover_subscriber") {
|
|
404
|
+
if (this.statefulHandover)
|
|
405
|
+
return;
|
|
315
406
|
if (this.sourcePeerId)
|
|
316
407
|
return;
|
|
317
408
|
if (!this.acceptsHandoverResponse(this.getHandoverId(response, event)))
|
|
@@ -326,6 +417,20 @@ export class HandoverManager {
|
|
|
326
417
|
// Requestor process
|
|
327
418
|
// Check if the message is a handover_fin at the end of handover_subscriber messages
|
|
328
419
|
if (response.type === "handover_fin") {
|
|
420
|
+
if (this.statefulHandover) {
|
|
421
|
+
const id = this.getHandoverId(response, event);
|
|
422
|
+
if (this.sourcePeerId || !this.requestingHandover || id !== this.handoverId || this.getSourceProcessId(event) !== this.expectedSourceId)
|
|
423
|
+
return;
|
|
424
|
+
if (this.acceptedStateDigest && response.stateDigest !== this.acceptedStateDigest)
|
|
425
|
+
return;
|
|
426
|
+
this.targetPreparation ??= this.acceptPreparedState(response);
|
|
427
|
+
await this.targetPreparation;
|
|
428
|
+
// Duplicate fin only replays the receipt; it never reapplies a baseline.
|
|
429
|
+
if (response.stateDigest !== this.acceptedStateDigest)
|
|
430
|
+
return;
|
|
431
|
+
await this.mqttProxy.publish(event.packet.properties?.responseTopic ?? "", this.handoverPayload("handover_ack", id, { stateDigest: this.acceptedStateDigest }), { retain: false, qos: 1, properties: { userProperties: this.getUserProperties(id) } });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
329
434
|
if (this.sourcePeerId)
|
|
330
435
|
return;
|
|
331
436
|
const handoverId = this.getHandoverId(response, event);
|
|
@@ -363,6 +468,9 @@ export class HandoverManager {
|
|
|
363
468
|
logger.warn(`${this.processName} - Ignoring acknowledgement for another migration.`);
|
|
364
469
|
return;
|
|
365
470
|
}
|
|
471
|
+
if (this.statefulHandover && (handoverId !== this.acceptedHandoverId || response.stateDigest !== this.stateDigest || !this.stateDigest))
|
|
472
|
+
return;
|
|
473
|
+
clearInterval(this.finRetry);
|
|
366
474
|
logger.info(`${this.processName} - Received handover ack from ${event.packet?.properties?.userProperties?.processName}.`);
|
|
367
475
|
this.sourceExit ??= this.finishSourceHandover();
|
|
368
476
|
await this.sourceExit;
|