@yarkivaev/scada 2.3.75 → 2.3.76
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/package.json
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import amqp from 'amqplib';
|
|
2
|
+
|
|
3
|
+
function connectBroker(url) {
|
|
4
|
+
return amqp.connect(url, { heartbeat: 10 });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function scheduleLater(fn, pause) {
|
|
8
|
+
return setTimeout(fn, pause);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function clearLater(handle) {
|
|
12
|
+
clearTimeout(handle);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function reportFailure(error) {
|
|
16
|
+
process.stderr.write(`amqp consume failed ${error.message}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function nextDelay(delay) {
|
|
20
|
+
return Math.min(delay * 2, 30000);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function blankState() {
|
|
24
|
+
return {
|
|
25
|
+
generation: 0,
|
|
26
|
+
timer: undefined,
|
|
27
|
+
stopping: false,
|
|
28
|
+
delay: 1000,
|
|
29
|
+
session: undefined
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function running(state) {
|
|
34
|
+
return Boolean(state.session) || (state.generation > 0 && !state.stopping);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scheduleRetry(ctx, generation) {
|
|
38
|
+
if (ctx.state.stopping || generation !== ctx.state.generation) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const pause = ctx.state.delay;
|
|
42
|
+
ctx.state.delay = nextDelay(ctx.state.delay);
|
|
43
|
+
ctx.state.timer = ctx.schedule(() => {
|
|
44
|
+
ctx.state.timer = undefined;
|
|
45
|
+
return ctx.open(generation);
|
|
46
|
+
}, pause);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bindClose(ctx, generation, live) {
|
|
50
|
+
let dropped = false;
|
|
51
|
+
function lose() {
|
|
52
|
+
if (dropped || ctx.state.stopping || generation !== ctx.state.generation) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
dropped = true;
|
|
56
|
+
if (ctx.state.session === live) {
|
|
57
|
+
ctx.state.session = undefined;
|
|
58
|
+
}
|
|
59
|
+
scheduleRetry(ctx, generation);
|
|
60
|
+
}
|
|
61
|
+
live.connection.on('error', lose);
|
|
62
|
+
live.connection.on('close', lose);
|
|
63
|
+
live.channel.on('error', lose);
|
|
64
|
+
live.channel.on('close', lose);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function accept(ctx, generation, connection) {
|
|
68
|
+
if (ctx.state.stopping || generation !== ctx.state.generation) {
|
|
69
|
+
await connection.close();
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return connection.createChannel();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function keep(ctx, generation, connection, channel, tag) {
|
|
76
|
+
if (ctx.state.stopping || generation !== ctx.state.generation) {
|
|
77
|
+
await channel.close();
|
|
78
|
+
await connection.close();
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const live = { connection, channel, tag };
|
|
82
|
+
ctx.state.session = live;
|
|
83
|
+
ctx.state.delay = 1000;
|
|
84
|
+
bindClose(ctx, generation, live);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function open(ctx, generation) {
|
|
89
|
+
if (ctx.state.stopping || generation !== ctx.state.generation) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const connection = await ctx.connect(ctx.url);
|
|
94
|
+
const channel = await accept(ctx, generation, connection);
|
|
95
|
+
if (!channel) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const tag = await ctx.subscribe(channel);
|
|
99
|
+
await keep(ctx, generation, connection, channel, tag);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
ctx.report(error);
|
|
102
|
+
scheduleRetry(ctx, generation);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function begin(ctx) {
|
|
107
|
+
if (running(ctx.state)) {
|
|
108
|
+
return Promise.resolve();
|
|
109
|
+
}
|
|
110
|
+
ctx.state.stopping = false;
|
|
111
|
+
ctx.state.generation += 1;
|
|
112
|
+
ctx.state.delay = 1000;
|
|
113
|
+
return open(ctx, ctx.state.generation);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function end(ctx) {
|
|
117
|
+
ctx.state.stopping = true;
|
|
118
|
+
ctx.state.generation += 1;
|
|
119
|
+
if (ctx.state.timer) {
|
|
120
|
+
ctx.clear(ctx.state.timer);
|
|
121
|
+
ctx.state.timer = undefined;
|
|
122
|
+
}
|
|
123
|
+
const live = ctx.state.session;
|
|
124
|
+
ctx.state.session = undefined;
|
|
125
|
+
if (!live) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await live.channel.cancel(live.tag);
|
|
129
|
+
await live.channel.close();
|
|
130
|
+
await live.connection.close();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Keeps one AMQP consumer subscribed across broker restarts.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} url - RabbitMQ AMQP URL
|
|
137
|
+
* @param {function} subscribe - async (channel) => consumerTag
|
|
138
|
+
* @param {object} [options] - connect, schedule, clear, report overrides
|
|
139
|
+
* @returns {object} session with start and stop
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* const session = durableConsume('amqp://localhost', async (channel) => {
|
|
143
|
+
* const tag = await channel.consume('scada.telemetry.ingest', () => {});
|
|
144
|
+
* return tag.consumerTag;
|
|
145
|
+
* });
|
|
146
|
+
* await session.start();
|
|
147
|
+
*/
|
|
148
|
+
export default function durableConsume(url, subscribe, options = {}) {
|
|
149
|
+
const ctx = {
|
|
150
|
+
url,
|
|
151
|
+
subscribe,
|
|
152
|
+
connect: options.connect || connectBroker,
|
|
153
|
+
schedule: options.schedule || scheduleLater,
|
|
154
|
+
clear: options.clear || clearLater,
|
|
155
|
+
report: options.report || reportFailure,
|
|
156
|
+
state: blankState(),
|
|
157
|
+
open(generation) {
|
|
158
|
+
return open(ctx, generation);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
start() {
|
|
163
|
+
return begin(ctx);
|
|
164
|
+
},
|
|
165
|
+
stop() {
|
|
166
|
+
return end(ctx);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import amqp from 'amqplib';
|
|
2
1
|
import { batch, circuit, clock, timedBatch } from '@yarkivaev/source-to-sink';
|
|
2
|
+
import durableConsume from '../durableConsume.js';
|
|
3
3
|
import deliverToMqttRecord from './deliverToMqttRecord.js';
|
|
4
4
|
import metricsCodec from '../mqtt/metricsTransformer.js';
|
|
5
5
|
import streamNameFromTopic from './streamNameFromTopic.js';
|
|
@@ -50,44 +50,37 @@ export function telemetryConsumer(codec, channel, onSeen) {
|
|
|
50
50
|
* @param {object} [options] - batch and prefetch options
|
|
51
51
|
* @returns {object} ingest with start and stop
|
|
52
52
|
*/
|
|
53
|
+
function subscribeMetrics(queue, prefetch, codec, onSeen) {
|
|
54
|
+
return async (channel) => {
|
|
55
|
+
await channel.assertQueue(queue, { durable: true });
|
|
56
|
+
channel.prefetch(prefetch);
|
|
57
|
+
const onMessage = telemetryConsumer(codec, channel, onSeen);
|
|
58
|
+
const tag = await channel.consume(queue, onMessage, { noAck: false });
|
|
59
|
+
return tag.consumerTag;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
export default function amqpMetricsIngest(amqpUrl, queue, sink, options = {}) {
|
|
54
64
|
const prefetch = options.prefetch || 32;
|
|
55
65
|
const size = options.size || 100;
|
|
56
66
|
const interval = options.interval || 5;
|
|
57
67
|
const threshold = options.threshold || 5;
|
|
58
68
|
const timeout = options.timeout || 60;
|
|
59
|
-
let session;
|
|
60
69
|
const clk = clock();
|
|
61
70
|
const breaker = circuit(threshold, timeout, clk);
|
|
62
71
|
const collector = timedBatch(batch(sink, size, breaker), interval);
|
|
63
72
|
const codec = metricsCodec(collector);
|
|
73
|
+
const session = durableConsume(
|
|
74
|
+
amqpUrl,
|
|
75
|
+
subscribeMetrics(queue, prefetch, codec, options.onSeen),
|
|
76
|
+
options
|
|
77
|
+
);
|
|
64
78
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const conn = await amqp.connect(amqpUrl);
|
|
70
|
-
const ch = await conn.createChannel();
|
|
71
|
-
if (session) {
|
|
72
|
-
await ch.close();
|
|
73
|
-
await conn.close();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
await ch.assertQueue(queue, { durable: true });
|
|
77
|
-
ch.prefetch(prefetch);
|
|
78
|
-
const onMessage = telemetryConsumer(codec, ch, options.onSeen);
|
|
79
|
-
const tag = await ch.consume(queue, onMessage, { noAck: false });
|
|
80
|
-
session = { conn, channel: ch, tag: tag.consumerTag };
|
|
79
|
+
start() {
|
|
80
|
+
return session.start();
|
|
81
81
|
},
|
|
82
82
|
async stop() {
|
|
83
|
-
|
|
84
|
-
if (!active) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
session = undefined;
|
|
88
|
-
await active.channel.cancel(active.tag);
|
|
89
|
-
await active.channel.close();
|
|
90
|
-
await active.conn.close();
|
|
83
|
+
await session.stop();
|
|
91
84
|
collector.stop();
|
|
92
85
|
}
|
|
93
86
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import durableConsume from '../durableConsume.js';
|
|
2
2
|
import deliverToMqttRecord from './deliverToMqttRecord.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -34,37 +34,34 @@ export function relayConsumer(sink, channel) {
|
|
|
34
34
|
* @param {object} [options] - prefetch count
|
|
35
35
|
* @returns {object} Relay with start and stop
|
|
36
36
|
*/
|
|
37
|
+
function subscribeRelay(queue, prefetch, sink) {
|
|
38
|
+
return async (channel) => {
|
|
39
|
+
await channel.assertQueue(queue, { durable: true });
|
|
40
|
+
channel.prefetch(prefetch);
|
|
41
|
+
const onMessage = relayConsumer(sink, channel);
|
|
42
|
+
const tag = await channel.consume(queue, onMessage, { noAck: false });
|
|
43
|
+
return tag.consumerTag;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
export default function amqpMqttRelay(amqpUrl, queue, sink, options = {}) {
|
|
38
48
|
const prefetch = options.prefetch || 32;
|
|
39
|
-
let
|
|
49
|
+
let opened = false;
|
|
50
|
+
const session = durableConsume(
|
|
51
|
+
amqpUrl,
|
|
52
|
+
subscribeRelay(queue, prefetch, sink),
|
|
53
|
+
options
|
|
54
|
+
);
|
|
40
55
|
return {
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
56
|
+
start() {
|
|
57
|
+
if (!opened) {
|
|
58
|
+
opened = true;
|
|
59
|
+
sink.start();
|
|
44
60
|
}
|
|
45
|
-
|
|
46
|
-
const ch = await conn.createChannel();
|
|
47
|
-
if (session) {
|
|
48
|
-
await ch.close();
|
|
49
|
-
await conn.close();
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
await ch.assertQueue(queue, { durable: true });
|
|
53
|
-
ch.prefetch(prefetch);
|
|
54
|
-
sink.start();
|
|
55
|
-
const onMessage = relayConsumer(sink, ch);
|
|
56
|
-
const tag = await ch.consume(queue, onMessage, { noAck: false });
|
|
57
|
-
session = { conn, channel: ch, tag: tag.consumerTag };
|
|
61
|
+
return session.start();
|
|
58
62
|
},
|
|
59
63
|
async stop() {
|
|
60
|
-
|
|
61
|
-
if (!active) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
session = undefined;
|
|
65
|
-
await active.channel.cancel(active.tag);
|
|
66
|
-
await active.channel.close();
|
|
67
|
-
await active.conn.close();
|
|
64
|
+
await session.stop();
|
|
68
65
|
sink.stop();
|
|
69
66
|
}
|
|
70
67
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import durableConsume from '../ingest/durableConsume.js';
|
|
2
2
|
import operationCodec from './operationCodec.js';
|
|
3
3
|
import operationSyncSink from './operationSyncSink.js';
|
|
4
4
|
|
|
@@ -43,40 +43,31 @@ export function operationConsumer(codec, channel) {
|
|
|
43
43
|
* const ingest = operationSyncIngest(amqpUrl, 'scada.operations.ingest', dataAccess.operations);
|
|
44
44
|
* await ingest.start();
|
|
45
45
|
*/
|
|
46
|
+
function subscribeOperations(queue, prefetch, codec) {
|
|
47
|
+
return async (channel) => {
|
|
48
|
+
await channel.assertQueue(queue, { durable: true });
|
|
49
|
+
channel.prefetch(prefetch);
|
|
50
|
+
const onMessage = operationConsumer(codec, channel);
|
|
51
|
+
const tag = await channel.consume(queue, onMessage, { noAck: false });
|
|
52
|
+
return tag.consumerTag;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
export default function operationSyncIngest(amqpUrl, queue, operations, options = {}) {
|
|
47
57
|
const prefetch = options.prefetch || 32;
|
|
48
58
|
const sink = operationSyncSink(operations);
|
|
49
59
|
const codec = operationCodec(sink);
|
|
50
|
-
|
|
60
|
+
const session = durableConsume(
|
|
61
|
+
amqpUrl,
|
|
62
|
+
subscribeOperations(queue, prefetch, codec),
|
|
63
|
+
options
|
|
64
|
+
);
|
|
51
65
|
return {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const conn = await amqp.connect(amqpUrl);
|
|
57
|
-
const ch = await conn.createChannel();
|
|
58
|
-
await ch.assertQueue(queue, { durable: true });
|
|
59
|
-
ch.prefetch(prefetch);
|
|
60
|
-
const onMessage = operationConsumer(codec, ch);
|
|
61
|
-
const tag = await ch.consume(queue, onMessage, { noAck: false });
|
|
62
|
-
const started = { conn, channel: ch, tag: tag.consumerTag };
|
|
63
|
-
if (session) {
|
|
64
|
-
await ch.cancel(tag.consumerTag);
|
|
65
|
-
await ch.close();
|
|
66
|
-
await conn.close();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
session = started;
|
|
66
|
+
start() {
|
|
67
|
+
return session.start();
|
|
70
68
|
},
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!active) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
session = undefined;
|
|
77
|
-
await active.channel.cancel(active.tag);
|
|
78
|
-
await active.channel.close();
|
|
79
|
-
await active.conn.close();
|
|
69
|
+
stop() {
|
|
70
|
+
return session.stop();
|
|
80
71
|
}
|
|
81
72
|
};
|
|
82
73
|
}
|