@hotmeshio/long-tail 0.28.1 → 0.28.2
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/build/lib/events/publish.js +3 -2
- package/build/lib/machines/transport/nats-bus.d.ts +2 -0
- package/build/lib/machines/transport/stream-bus.d.ts +2 -0
- package/build/lib/machines/transport/stream-bus.js +8 -2
- package/build/lib/machines/transport/streams.d.ts +2 -1
- package/build/lib/machines/transport/streams.js +3 -2
- package/build/lib/messaging/memory.js +7 -3
- package/build/lib/messaging/nats/permissions.js +1 -0
- package/build/lib/messaging/nats/streams.d.ts +18 -4
- package/build/lib/messaging/nats/streams.js +55 -10
- package/build/services/machines/gateway/log.d.ts +1 -0
- package/build/services/machines/gateway/log.js +1 -0
- package/build/services/machines/leadership.d.ts +2 -0
- package/build/services/machines/leadership.js +7 -1
- package/build/services/machines/select.d.ts +1 -1
- package/build/services/machines/select.js +2 -2
- package/build/services/machines/surface.js +8 -2
- package/build/types/messaging.d.ts +5 -1
- package/docs/machines.md +2 -2
- package/package.json +1 -1
|
@@ -18,12 +18,13 @@ const index_1 = require("./index");
|
|
|
18
18
|
const matching_1 = require("./matching");
|
|
19
19
|
const logger_1 = require("../logger");
|
|
20
20
|
/**
|
|
21
|
-
* Fire-and-forget publish helper. Swallows errors (best-effort).
|
|
21
|
+
* Fire-and-forget publish helper. Swallows errors (best-effort). The publish
|
|
22
|
+
* is noted at debug: a line's wire can carry many events a second.
|
|
22
23
|
*/
|
|
23
24
|
function fireAndForget(event) {
|
|
24
25
|
if (!index_1.eventRegistry.hasAdapters)
|
|
25
26
|
return Promise.resolve();
|
|
26
|
-
logger_1.loggerRegistry.
|
|
27
|
+
logger_1.loggerRegistry.debug(`[lt-pub] ${event.type} ${event.workflowId || ''} ${event.escalationId || event.taskId || ''}`);
|
|
27
28
|
return index_1.eventRegistry.publish(event).catch(() => { });
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
@@ -20,6 +20,8 @@ export interface NatsBusOptions {
|
|
|
20
20
|
/** The codecs of every line on this site, so the streams keep all their subjects. */
|
|
21
21
|
siteCodecs?: StreamCodec[];
|
|
22
22
|
nakDelayMs?: number;
|
|
23
|
+
/** Told once when the consumer can deliver nothing more, so the line can give its lead up and contend again. */
|
|
24
|
+
onLost?: (reason: string) => void;
|
|
23
25
|
}
|
|
24
26
|
export declare class NatsMachineBus implements MachineBus {
|
|
25
27
|
private readonly opts;
|
|
@@ -24,6 +24,8 @@ export interface StreamBusOptions {
|
|
|
24
24
|
/** The codecs of every line on this site; the streams keep all their subjects, whichever line starts last. */
|
|
25
25
|
siteCodecs?: StreamCodec[];
|
|
26
26
|
nakDelayMs?: number;
|
|
27
|
+
/** Told once when the consumer can deliver nothing more, so the line can give its lead up and contend again. */
|
|
28
|
+
onLost?: (reason: string) => void;
|
|
27
29
|
}
|
|
28
30
|
export declare class StreamMachineBus implements MachineBus {
|
|
29
31
|
private readonly opts;
|
|
@@ -30,6 +30,12 @@ class StreamMachineBus {
|
|
|
30
30
|
await streams.ensureStream(spec);
|
|
31
31
|
const consumer = (0, streams_1.lineEventConsumerSpec)(codec, siteId, lineId, this.durableName);
|
|
32
32
|
await streams.ensureConsumer(streams_1.LINE_STREAM.EVENTS, consumer);
|
|
33
|
+
const held = await streams.describeConsumer(streams_1.LINE_STREAM.EVENTS, consumer.name);
|
|
34
|
+
const reads = [...(held?.filterSubjects ?? [])].sort().join(', ');
|
|
35
|
+
const wants = [...consumer.filterSubjects].sort().join(', ');
|
|
36
|
+
if (reads !== wants) {
|
|
37
|
+
throw new Error(`${consumer.name} reads ${reads || 'nothing'} and not ${wants}; the consumer did not follow the line`);
|
|
38
|
+
}
|
|
33
39
|
const events = await streams.describeStream(streams_1.LINE_STREAM.EVENTS);
|
|
34
40
|
const missing = events ? (0, streams_1.uncoveredSubjects)(events, consumer.filterSubjects) : consumer.filterSubjects;
|
|
35
41
|
if (missing.length) {
|
|
@@ -38,8 +44,8 @@ class StreamMachineBus {
|
|
|
38
44
|
this.session = await mqtt_session_1.MqttSession.open(this.opts.mqtt, controllerId);
|
|
39
45
|
}
|
|
40
46
|
async consume(handler) {
|
|
41
|
-
const { streams, pubsub, codec, siteId, lineId, nakDelayMs = 1_000 } = this.opts;
|
|
42
|
-
this.subs.push(await streams.consume(streams_1.LINE_STREAM.EVENTS, this.durableName, (m) => this.queue.run(() => handler({ topic: (0, protocol_1.toTopic)(m.subject), payload: m.payload, deliveries: m.deliveries })), { nakDelayMs }));
|
|
47
|
+
const { streams, pubsub, codec, siteId, lineId, nakDelayMs = 1_000, onLost } = this.opts;
|
|
48
|
+
this.subs.push(await streams.consume(streams_1.LINE_STREAM.EVENTS, this.durableName, (m) => this.queue.run(() => handler({ topic: (0, protocol_1.toTopic)(m.subject), payload: m.payload, deliveries: m.deliveries })), { nakDelayMs, onLost }));
|
|
43
49
|
for (const subject of (0, streams_1.telemetrySubjects)(codec, siteId, lineId)) {
|
|
44
50
|
this.subs.push(pubsub.subscribe(subject, (m) => void this.queue.run(() => handler({ topic: (0, protocol_1.toTopic)(m.subject), payload: m.payload, deliveries: 1 }))));
|
|
45
51
|
}
|
|
@@ -16,7 +16,8 @@ export declare function filterToSubject(filter: string): string;
|
|
|
16
16
|
/**
|
|
17
17
|
* The site's streams: every line under each codec's root, since lines on
|
|
18
18
|
* different protocols share one site NATS. A root alone means the generic
|
|
19
|
-
* layout; several codecs give the union of their subjects,
|
|
19
|
+
* layout; several codecs give the union of their subjects, sorted, so every
|
|
20
|
+
* line asks the site for the same stream whichever codec it lists first.
|
|
20
21
|
*/
|
|
21
22
|
export declare function lineStreamSpecs(codecs: StreamCodec | string | (StreamCodec | string)[]): StreamSpec[];
|
|
22
23
|
/** Whether a stream subject pattern keeps everything a consumer filter would read. */
|
|
@@ -31,11 +31,12 @@ function subjectsFor(codec, channels, siteId, lineId) {
|
|
|
31
31
|
/**
|
|
32
32
|
* The site's streams: every line under each codec's root, since lines on
|
|
33
33
|
* different protocols share one site NATS. A root alone means the generic
|
|
34
|
-
* layout; several codecs give the union of their subjects,
|
|
34
|
+
* layout; several codecs give the union of their subjects, sorted, so every
|
|
35
|
+
* line asks the site for the same stream whichever codec it lists first.
|
|
35
36
|
*/
|
|
36
37
|
function lineStreamSpecs(codecs) {
|
|
37
38
|
const list = (Array.isArray(codecs) ? codecs : [codecs]).map(codecOf);
|
|
38
|
-
const union = (channel) => [...new Set(list.flatMap((c) => subjectsFor(c, c.channels[channel])))];
|
|
39
|
+
const union = (channel) => [...new Set(list.flatMap((c) => subjectsFor(c, c.channels[channel])))].sort();
|
|
39
40
|
return [
|
|
40
41
|
{ name: exports.LINE_STREAM.EVENTS, subjects: union('events'), maxAgeMs: 30 * DAY_MS },
|
|
41
42
|
{ name: exports.LINE_STREAM.TELEMETRY, subjects: union('telemetry'), maxAgeMs: 3 * DAY_MS },
|
|
@@ -39,16 +39,20 @@ class MemoryStreams {
|
|
|
39
39
|
async describeStream(name) {
|
|
40
40
|
return this.streams.get(name)?.spec ?? null;
|
|
41
41
|
}
|
|
42
|
+
/** An existing consumer keeps its place while its subjects match; one whose subjects changed starts again. */
|
|
42
43
|
async ensureConsumer(stream, spec) {
|
|
43
44
|
const s = this.stream(stream);
|
|
44
45
|
const existing = s.consumers.get(spec.name);
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
const same = (a, b) => [...a].sort().join('|') === [...b].sort().join('|');
|
|
47
|
+
if (existing && same(existing.spec.filterSubjects, spec.filterSubjects))
|
|
47
48
|
return;
|
|
48
|
-
}
|
|
49
49
|
const floor = (spec.startAt ?? constants_1.START_AT.NEW) === constants_1.START_AT.ALL ? 0 : s.seq;
|
|
50
50
|
s.consumers.set(spec.name, { spec, floor, settled: new Set(), deliveries: new Map(), retryAt: new Map() });
|
|
51
51
|
}
|
|
52
|
+
async describeConsumer(stream, name) {
|
|
53
|
+
const spec = this.streams.get(stream)?.consumers.get(name)?.spec;
|
|
54
|
+
return spec ? { ...spec, filterSubjects: [...spec.filterSubjects] } : null;
|
|
55
|
+
}
|
|
52
56
|
async append(subject, payload) {
|
|
53
57
|
const now = Date.now();
|
|
54
58
|
for (const s of this.streams.values()) {
|
|
@@ -15,6 +15,7 @@ function jsPermissions({ streams, durable }) {
|
|
|
15
15
|
`$JS.API.CONSUMER.CREATE.${s}.${durable}`,
|
|
16
16
|
`$JS.API.CONSUMER.CREATE.${s}.${durable}.>`,
|
|
17
17
|
`$JS.API.CONSUMER.DURABLE.CREATE.${s}.${durable}`,
|
|
18
|
+
`$JS.API.CONSUMER.DELETE.${s}.${durable}`,
|
|
18
19
|
`$JS.API.CONSUMER.MSG.NEXT.${s}.${durable}`,
|
|
19
20
|
`$JS.ACK.${s}.${durable}.>`,
|
|
20
21
|
]);
|
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Durable streams on JetStream. A spec becomes a file-backed stream or an
|
|
3
|
-
* explicit-ack durable consumer; ensuring either is safe on every boot
|
|
4
|
-
* existing consumer keeps its position while
|
|
3
|
+
* explicit-ack durable consumer; ensuring either is safe on every boot and
|
|
4
|
+
* from several lines at once. An existing consumer keeps its position while
|
|
5
|
+
* its subjects match the spec; one whose subjects changed is made again, since
|
|
6
|
+
* a position in another protocol's messages means nothing to this one.
|
|
5
7
|
*/
|
|
6
8
|
import { type ConsumerConfig, type JetStreamClient, type JetStreamManager, type StreamConfig } from 'nats';
|
|
7
9
|
import type { ConsumerSpec, Delivery, LTConsumeOptions, LTStreamMessage, LTStreams, LTSubscription, StreamSpec, Subject } from '../../../types/messaging';
|
|
8
10
|
export declare function streamConfig(spec: StreamSpec): Partial<StreamConfig>;
|
|
9
11
|
export declare function consumerConfig(spec: ConsumerSpec): Partial<ConsumerConfig>;
|
|
10
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Create the stream, or update an existing one to the spec. Two lines may
|
|
14
|
+
* ensure the same stream at once: the create that loses the race finds the
|
|
15
|
+
* stream in use and updates it instead.
|
|
16
|
+
*/
|
|
11
17
|
export declare function ensureStream(jsm: JetStreamManager, spec: StreamSpec): Promise<void>;
|
|
12
18
|
/** The stream as JetStream holds it, or null when there is none. */
|
|
13
19
|
export declare function describeStream(jsm: JetStreamManager, name: string): Promise<StreamSpec | null>;
|
|
14
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Create the consumer, or keep an existing one whose subjects match the spec,
|
|
22
|
+
* position and all. One whose subjects differ is deleted and made again at
|
|
23
|
+
* the stream's newest message, since its old position was in another
|
|
24
|
+
* protocol's messages.
|
|
25
|
+
*/
|
|
15
26
|
export declare function ensureConsumer(jsm: JetStreamManager, stream: string, spec: ConsumerSpec): Promise<void>;
|
|
27
|
+
/** The consumer as JetStream holds it, or null when there is none. */
|
|
28
|
+
export declare function describeConsumer(jsm: JetStreamManager, stream: string, name: string): Promise<ConsumerSpec | null>;
|
|
16
29
|
export declare function payloadOf(m: {
|
|
17
30
|
data: Uint8Array;
|
|
18
31
|
}): string;
|
|
@@ -23,6 +36,7 @@ export declare class NatsStreams implements LTStreams {
|
|
|
23
36
|
ensureStream(spec: StreamSpec): Promise<void>;
|
|
24
37
|
ensureConsumer(stream: string, spec: ConsumerSpec): Promise<void>;
|
|
25
38
|
describeStream(name: string): Promise<StreamSpec | null>;
|
|
39
|
+
describeConsumer(stream: string, name: string): Promise<ConsumerSpec | null>;
|
|
26
40
|
append(subject: Subject, payload: string): Promise<void>;
|
|
27
41
|
/** One message at a time: the next is not taken until the handler settles this one. */
|
|
28
42
|
consume(stream: string, consumer: string, handler: (m: LTStreamMessage) => Promise<Delivery>, opts?: LTConsumeOptions): Promise<LTSubscription>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Durable streams on JetStream. A spec becomes a file-backed stream or an
|
|
4
|
-
* explicit-ack durable consumer; ensuring either is safe on every boot
|
|
5
|
-
* existing consumer keeps its position while
|
|
4
|
+
* explicit-ack durable consumer; ensuring either is safe on every boot and
|
|
5
|
+
* from several lines at once. An existing consumer keeps its position while
|
|
6
|
+
* its subjects match the spec; one whose subjects changed is made again, since
|
|
7
|
+
* a position in another protocol's messages means nothing to this one.
|
|
6
8
|
*/
|
|
7
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
10
|
exports.NatsStreams = void 0;
|
|
@@ -11,11 +13,15 @@ exports.consumerConfig = consumerConfig;
|
|
|
11
13
|
exports.ensureStream = ensureStream;
|
|
12
14
|
exports.describeStream = describeStream;
|
|
13
15
|
exports.ensureConsumer = ensureConsumer;
|
|
16
|
+
exports.describeConsumer = describeConsumer;
|
|
14
17
|
exports.payloadOf = payloadOf;
|
|
15
18
|
const nats_1 = require("nats");
|
|
16
19
|
const constants_1 = require("../constants");
|
|
17
20
|
const DEFAULT_ACK_WAIT_MS = 30_000;
|
|
18
21
|
const DEFAULT_NAK_DELAY_MS = 1_000;
|
|
22
|
+
/** Consumer events after which the consumer delivers nothing more: its stream or itself is gone, or the server fell silent. */
|
|
23
|
+
const CONSUMER_LOST = new Set(['consumer_deleted', 'consumer_not_found', 'stream_not_found', 'heartbeats_missed']);
|
|
24
|
+
const sameSubjects = (a = [], b = []) => [...a].sort().join('|') === [...b].sort().join('|');
|
|
19
25
|
function streamConfig(spec) {
|
|
20
26
|
return { name: spec.name, subjects: spec.subjects, max_age: (0, nats_1.nanos)(spec.maxAgeMs), storage: nats_1.StorageType.File };
|
|
21
27
|
}
|
|
@@ -28,7 +34,11 @@ function consumerConfig(spec) {
|
|
|
28
34
|
filter_subjects: spec.filterSubjects,
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Create the stream, or update an existing one to the spec. Two lines may
|
|
39
|
+
* ensure the same stream at once: the create that loses the race finds the
|
|
40
|
+
* stream in use and updates it instead.
|
|
41
|
+
*/
|
|
32
42
|
async function ensureStream(jsm, spec) {
|
|
33
43
|
const config = streamConfig(spec);
|
|
34
44
|
try {
|
|
@@ -37,8 +47,14 @@ async function ensureStream(jsm, spec) {
|
|
|
37
47
|
catch (err) {
|
|
38
48
|
if (!/stream not found/i.test(err?.message ?? ''))
|
|
39
49
|
throw err;
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
try {
|
|
51
|
+
await jsm.streams.add(config);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
catch (added) {
|
|
55
|
+
if (!/already in use/i.test(added?.message ?? ''))
|
|
56
|
+
throw added;
|
|
57
|
+
}
|
|
42
58
|
}
|
|
43
59
|
await jsm.streams.update(spec.name, config);
|
|
44
60
|
}
|
|
@@ -54,7 +70,12 @@ async function describeStream(jsm, name) {
|
|
|
54
70
|
throw err;
|
|
55
71
|
}
|
|
56
72
|
}
|
|
57
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Create the consumer, or keep an existing one whose subjects match the spec,
|
|
75
|
+
* position and all. One whose subjects differ is deleted and made again at
|
|
76
|
+
* the stream's newest message, since its old position was in another
|
|
77
|
+
* protocol's messages.
|
|
78
|
+
*/
|
|
58
79
|
async function ensureConsumer(jsm, stream, spec) {
|
|
59
80
|
let existing;
|
|
60
81
|
try {
|
|
@@ -66,10 +87,22 @@ async function ensureConsumer(jsm, stream, spec) {
|
|
|
66
87
|
await jsm.consumers.add(stream, consumerConfig(spec));
|
|
67
88
|
return;
|
|
68
89
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
if (sameSubjects(spec.filterSubjects, existing.config.filter_subjects))
|
|
91
|
+
return;
|
|
92
|
+
await jsm.consumers.delete(stream, spec.name);
|
|
93
|
+
await jsm.consumers.add(stream, consumerConfig(spec));
|
|
94
|
+
}
|
|
95
|
+
/** The consumer as JetStream holds it, or null when there is none. */
|
|
96
|
+
async function describeConsumer(jsm, stream, name) {
|
|
97
|
+
try {
|
|
98
|
+
const info = await jsm.consumers.info(stream, name);
|
|
99
|
+
return { name, filterSubjects: info.config.filter_subjects ?? (info.config.filter_subject ? [info.config.filter_subject] : []) };
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (/consumer not found|stream not found/i.test(err?.message ?? ''))
|
|
103
|
+
return null;
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
73
106
|
}
|
|
74
107
|
function payloadOf(m) {
|
|
75
108
|
return Buffer.from(m.data).toString('utf8');
|
|
@@ -88,6 +121,9 @@ class NatsStreams {
|
|
|
88
121
|
describeStream(name) {
|
|
89
122
|
return describeStream(this.jsm, name);
|
|
90
123
|
}
|
|
124
|
+
describeConsumer(stream, name) {
|
|
125
|
+
return describeConsumer(this.jsm, stream, name);
|
|
126
|
+
}
|
|
91
127
|
async append(subject, payload) {
|
|
92
128
|
await this.js.publish(subject, payload);
|
|
93
129
|
}
|
|
@@ -95,6 +131,15 @@ class NatsStreams {
|
|
|
95
131
|
async consume(stream, consumer, handler, opts = {}) {
|
|
96
132
|
const nakDelayMs = opts.nakDelayMs ?? DEFAULT_NAK_DELAY_MS;
|
|
97
133
|
const messages = await (await this.js.consumers.get(stream, consumer)).consume();
|
|
134
|
+
// The consumer's own status: once its stream or itself is gone, or the server fell silent, the reader is told.
|
|
135
|
+
void (async () => {
|
|
136
|
+
for await (const status of await messages.status()) {
|
|
137
|
+
if (!CONSUMER_LOST.has(String(status.type)))
|
|
138
|
+
continue;
|
|
139
|
+
opts.onLost?.(String(status.type));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
})().catch(() => { });
|
|
98
143
|
void (async () => {
|
|
99
144
|
for await (const m of messages) {
|
|
100
145
|
const delivery = await handler({ subject: m.subject, payload: payloadOf(m), deliveries: m.info.deliveryCount })
|
|
@@ -32,6 +32,7 @@ export declare const GATEWAY_LOG: {
|
|
|
32
32
|
readonly LEADING: "machine_gateway.line.leading";
|
|
33
33
|
readonly STANDBY: "machine_gateway.line.standby";
|
|
34
34
|
readonly LEADERSHIP_LOST: "machine_gateway.line.leadership_lost";
|
|
35
|
+
readonly WIRE_LOST: "machine_gateway.line.wire_lost";
|
|
35
36
|
readonly START_FAILED: "machine_gateway.line.start_failed";
|
|
36
37
|
};
|
|
37
38
|
export type GatewayLogEvent = typeof GATEWAY_LOG[keyof typeof GATEWAY_LOG];
|
|
@@ -37,6 +37,7 @@ exports.GATEWAY_LOG = {
|
|
|
37
37
|
LEADING: 'machine_gateway.line.leading',
|
|
38
38
|
STANDBY: 'machine_gateway.line.standby',
|
|
39
39
|
LEADERSHIP_LOST: 'machine_gateway.line.leadership_lost',
|
|
40
|
+
WIRE_LOST: 'machine_gateway.line.wire_lost',
|
|
40
41
|
START_FAILED: 'machine_gateway.line.start_failed',
|
|
41
42
|
};
|
|
42
43
|
exports.consoleLog = {
|
|
@@ -30,6 +30,8 @@ export declare class LineLeadership {
|
|
|
30
30
|
private leadOnce;
|
|
31
31
|
/** Lead while the lock is held; the error that ended it. */
|
|
32
32
|
private holdLock;
|
|
33
|
+
/** The leader gives its turn up without stopping: `lost` runs, the lock goes, and the line contends again after the pause. */
|
|
34
|
+
relinquish(reason: string): void;
|
|
33
35
|
/** A leader unlocks; a replica still waiting drops its connection, which ends the wait. */
|
|
34
36
|
stop(): Promise<void>;
|
|
35
37
|
}
|
|
@@ -62,7 +62,7 @@ class LineLeadership {
|
|
|
62
62
|
async holdLock(client, lead) {
|
|
63
63
|
const ended = new Promise((resolve) => {
|
|
64
64
|
client.once('error', resolve);
|
|
65
|
-
this.endTurn = () => resolve(new Error('leadership stopped'));
|
|
65
|
+
this.endTurn = (err) => resolve(err ?? new Error('leadership stopped'));
|
|
66
66
|
});
|
|
67
67
|
try {
|
|
68
68
|
await client.query(LOCK_SQL, [this.key]);
|
|
@@ -76,6 +76,12 @@ class LineLeadership {
|
|
|
76
76
|
return err;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
/** The leader gives its turn up without stopping: `lost` runs, the lock goes, and the line contends again after the pause. */
|
|
80
|
+
relinquish(reason) {
|
|
81
|
+
if (!this.leading)
|
|
82
|
+
return;
|
|
83
|
+
this.endTurn?.(new Error(reason));
|
|
84
|
+
}
|
|
79
85
|
/** A leader unlocks; a replica still waiting drops its connection, which ends the wait. */
|
|
80
86
|
async stop() {
|
|
81
87
|
this.stopped = true;
|
|
@@ -19,7 +19,7 @@ export type LineMessaging = Pick<typeof messagingRegistry, 'streams' | 'pubsub'>
|
|
|
19
19
|
* server of the site's own, which the bus connects to itself. `siteCodecs` are
|
|
20
20
|
* the codecs of every line on the site, so the streams keep all their subjects.
|
|
21
21
|
*/
|
|
22
|
-
export declare function createMachineBus(config: MachineLineConfig, line: LineDef, codec?: LineCodec, messaging?: LineMessaging, siteCodecs?: LineCodec[]): MachineBus;
|
|
22
|
+
export declare function createMachineBus(config: MachineLineConfig, line: LineDef, codec?: LineCodec, messaging?: LineMessaging, siteCodecs?: LineCodec[], onLost?: (reason: string) => void): MachineBus;
|
|
23
23
|
/** Long Tail's escalation events for the gateway; `in-process` needs the running instance's callback adapter. */
|
|
24
24
|
export declare function createEscalationFeed(config: MachineLineConfig, line: LineDef, callbacks?: CallbackEventAdapter): EscalationFeed;
|
|
25
25
|
/** One MQTT link per simulated machine; machines speak MQTT in every networked mode. */
|
|
@@ -36,10 +36,10 @@ function defaultCodec(config, line) {
|
|
|
36
36
|
* server of the site's own, which the bus connects to itself. `siteCodecs` are
|
|
37
37
|
* the codecs of every line on the site, so the streams keep all their subjects.
|
|
38
38
|
*/
|
|
39
|
-
function createMachineBus(config, line, codec = defaultCodec(config, line), messaging = messaging_1.messagingRegistry, siteCodecs = []) {
|
|
39
|
+
function createMachineBus(config, line, codec = defaultCodec(config, line), messaging = messaging_1.messagingRegistry, siteCodecs = [], onLost) {
|
|
40
40
|
switch (config.transport) {
|
|
41
41
|
case config_1.MACHINE_TRANSPORT.NATS: {
|
|
42
|
-
const where = { mqtt: mqttOf(config), controllerId: (0, line_1.controllerId)(line), siteId: line.siteId, lineId: line.lineId, codec, siteCodecs };
|
|
42
|
+
const where = { mqtt: mqttOf(config), controllerId: (0, line_1.controllerId)(line), siteId: line.siteId, lineId: line.lineId, codec, siteCodecs, onLost };
|
|
43
43
|
if (config.siteNatsUrl) {
|
|
44
44
|
return new nats_bus_1.NatsMachineBus({ natsUrl: config.siteNatsUrl, user: config.siteNatsUser, password: config.siteNatsPassword, ...where });
|
|
45
45
|
}
|
|
@@ -103,8 +103,14 @@ class LineRun {
|
|
|
103
103
|
return id;
|
|
104
104
|
};
|
|
105
105
|
const { config, line, codec } = this;
|
|
106
|
+
const where = { site: line.siteId, line: line.lineId };
|
|
107
|
+
// A wire that stops delivering ends this turn: the gateway stops, the line contends again and ensures its streams anew.
|
|
108
|
+
const onLost = (reason) => {
|
|
109
|
+
this.log.warn(log_1.GATEWAY_LOG.WIRE_LOST, { ...where, reason });
|
|
110
|
+
this.leadership.relinquish(`wire lost: ${reason}`);
|
|
111
|
+
};
|
|
106
112
|
this.gateway = new gateway_1.MachineGateway({
|
|
107
|
-
bus: (0, select_1.createMachineBus)(config, line, codec, undefined, this.siteCodecs()),
|
|
113
|
+
bus: (0, select_1.createMachineBus)(config, line, codec, undefined, this.siteCodecs(), onLost),
|
|
108
114
|
codec,
|
|
109
115
|
rows: this.opts.rows,
|
|
110
116
|
feed: (0, select_1.createEscalationFeed)(config, line, this.opts.callbacks),
|
|
@@ -121,7 +127,7 @@ class LineRun {
|
|
|
121
127
|
});
|
|
122
128
|
await this.gateway.start();
|
|
123
129
|
this.ready = true;
|
|
124
|
-
this.log.info(log_1.GATEWAY_LOG.LEADING,
|
|
130
|
+
this.log.info(log_1.GATEWAY_LOG.LEADING, where);
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
133
|
class MachineSurface {
|
|
@@ -47,15 +47,19 @@ export interface LTStreamMessage extends LTMessage {
|
|
|
47
47
|
export interface LTConsumeOptions {
|
|
48
48
|
/** Delay before a nak'd message is delivered again. */
|
|
49
49
|
nakDelayMs?: number;
|
|
50
|
+
/** Told once when the consumer can deliver nothing more: its stream or itself is gone, or the server fell silent. */
|
|
51
|
+
onLost?: (reason: string) => void;
|
|
50
52
|
}
|
|
51
53
|
/** Durable streams: messages kept, delivered to named consumers in order, one at a time, until settled. */
|
|
52
54
|
export interface LTStreams {
|
|
53
55
|
/** Create the stream, or bring an existing one's subjects and age up to the spec. */
|
|
54
56
|
ensureStream(spec: StreamSpec): Promise<void>;
|
|
55
|
-
/** Create the consumer, or
|
|
57
|
+
/** Create the consumer, or keep an existing one whose subjects match the spec; one whose subjects changed is made again. */
|
|
56
58
|
ensureConsumer(stream: string, spec: ConsumerSpec): Promise<void>;
|
|
57
59
|
/** The stream as it is now, or null when there is none. */
|
|
58
60
|
describeStream(name: string): Promise<StreamSpec | null>;
|
|
61
|
+
/** The consumer as it is now, or null when there is none. */
|
|
62
|
+
describeConsumer(stream: string, name: string): Promise<ConsumerSpec | null>;
|
|
59
63
|
/** Append a message; resolves once the stream holds it. */
|
|
60
64
|
append(subject: Subject, payload: string): Promise<void>;
|
|
61
65
|
/** Deliver the consumer's messages to `handler` one at a time; a thrown handler counts as a nak. */
|
package/docs/machines.md
CHANGED
|
@@ -327,8 +327,8 @@ The built-in `long-tail-machines` server offers the same surface to agents, gate
|
|
|
327
327
|
```
|
|
328
328
|
|
|
329
329
|
- **Machines:** each machine logs in over MQTT 3.1.1 or 5 with its own certificate; the CN (`{site}-{line}-{address}`) is its username. The broker ACL lets it write only its own channels and read only its own commands. The line controller may command every machine on the line.
|
|
330
|
-
- **Durable streams:** the bridge carries the line's topics one way into NATS, where only the bridge may publish them. JetStream keeps `LINE_EVENTS` (30 days) and `LINE_TELEMETRY` (3 days). The gateway reads through a durable consumer, `<controller>-events`, and may publish only JetStream requests and acks for its own streams and consumer; it cannot delete or purge
|
|
331
|
-
- **One site, several codecs:** the streams are ensured with the subjects of every codec the surface runs on the site, so lines on different roots share `LINE_EVENTS` and `LINE_TELEMETRY` whichever line starts last; a gateway whose consumer the stream would feed nothing refuses to start and says which subjects are missing.
|
|
330
|
+
- **Durable streams:** the bridge carries the line's topics one way into NATS, where only the bridge may publish them. JetStream keeps `LINE_EVENTS` (30 days) and `LINE_TELEMETRY` (3 days). The gateway reads through a durable consumer, `<controller>-events`, and may publish only JetStream requests and acks for its own streams and consumer, and delete that one consumer; it cannot delete or purge a stream. A consumer whose subjects no longer match the line (the line changed protocol) is made again at the stream's newest message rather than moved, so a position in another protocol's messages is never carried over. A consumer that can deliver nothing more (its stream or itself gone, the server silent) ends the leader's turn: the gateway stops, the line contends again and ensures its streams anew, so no line leads a wire it cannot read.
|
|
331
|
+
- **One site, several codecs:** the streams are ensured with the subjects of every codec the surface runs on the site, sorted, so lines on different roots share `LINE_EVENTS` and `LINE_TELEMETRY` whichever line starts first or last, and two lines starting together on an empty JetStream agree on the stream (the create that loses the race updates instead); a gateway whose consumer the stream would feed nothing, or whose consumer did not follow its line, refuses to start and says which subjects are missing.
|
|
332
332
|
- **Where the streams live:** by default in Long Tail's messaging ([events](events.md#messaging)), whose NATS server must run JetStream. The bridge then logs in to that server's edge account for the line (`Machines.edgeAccountFor`), which may publish only the line's subjects, so the edge reaches nothing of Long Tail's. With `MACHINE_SITE_NATS_URL`, the streams live instead in a NATS server of the site's own (`Machines.renderSiteConf`), and Long Tail's NATS stays inside Long Tail. Either way the gateway reads the same streams under the same consumer name.
|
|
333
333
|
- **Escalation events:** the gateway inside Long Tail reads them in process. A standalone gateway subscribes to them over Long Tail's messaging.
|
|
334
334
|
- **Commands:** the gateway publishes commands to the machine broker at QoS 1, so they queue for a machine that is away.
|
package/package.json
CHANGED