@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yarkivaev/scada",
3
- "version": "2.3.75",
3
+ "version": "2.3.76",
4
4
  "description": "SCADA domain objects, state persistence, and plant monitoring",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- async start() {
66
- if (session) {
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
- const active = session;
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 amqp from 'amqplib';
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 session;
49
+ let opened = false;
50
+ const session = durableConsume(
51
+ amqpUrl,
52
+ subscribeRelay(queue, prefetch, sink),
53
+ options
54
+ );
40
55
  return {
41
- async start() {
42
- if (session) {
43
- return;
56
+ start() {
57
+ if (!opened) {
58
+ opened = true;
59
+ sink.start();
44
60
  }
45
- const conn = await amqp.connect(amqpUrl);
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
- const active = session;
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 amqp from 'amqplib';
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
- let session;
60
+ const session = durableConsume(
61
+ amqpUrl,
62
+ subscribeOperations(queue, prefetch, codec),
63
+ options
64
+ );
51
65
  return {
52
- async start() {
53
- if (session) {
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
- async stop() {
72
- const active = session;
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
  }