@yarkivaev/scada 2.3.57 → 2.3.59
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 +2 -2
- package/src/domain/segment/dispatch.js +47 -9
- package/src/domain/segment/segmentCoalesce.js +164 -0
- package/src/infrastructure/ingest/pipelines/segmentPipeline.js +36 -14
- package/src/infrastructure/ingest/sinks/closeOrphanOpen.js +4 -3
- package/src/infrastructure/persistence/memory/operations.js +10 -1
- package/src/infrastructure/persistence/pg/operations.js +20 -1
- package/src/infrastructure/sync/operationSyncSink.js +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yarkivaev/scada",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.59",
|
|
4
4
|
"description": "SCADA domain objects, state persistence, and plant monitoring",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@clickhouse/client": "^1.0.0",
|
|
31
31
|
"@yarkivaev/simple-server": "1.0.1",
|
|
32
|
-
"@yarkivaev/source-to-sink": "1.3.
|
|
32
|
+
"@yarkivaev/source-to-sink": "1.3.4",
|
|
33
33
|
"amqplib": "^0.10.9",
|
|
34
34
|
"pg": "^8.0.0",
|
|
35
35
|
"stompit": "^1.0.0"
|
|
@@ -1,31 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persists a pending heartbeat, optionally inside one pool transaction.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} segmentSink - upsert sink
|
|
5
|
+
* @param {object} closer - orphan closer
|
|
6
|
+
* @param {object} [pool] - optional pg pool
|
|
7
|
+
* @param {object} record - normalized segment record
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
async function persistPending(segmentSink, closer, pool, record) {
|
|
11
|
+
const kind = record.kind || 'phase';
|
|
12
|
+
if (!pool || typeof pool.connect !== 'function') {
|
|
13
|
+
await closer.close(record.machine, record.start_time, kind);
|
|
14
|
+
await segmentSink.write([record]);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const client = await pool.connect();
|
|
18
|
+
try {
|
|
19
|
+
await client.query('BEGIN');
|
|
20
|
+
await closer.close(record.machine, record.start_time, kind, client);
|
|
21
|
+
await segmentSink.write([record], client);
|
|
22
|
+
await client.query('COMMIT');
|
|
23
|
+
} catch (error) {
|
|
24
|
+
await client.query('ROLLBACK').catch((ignored) => { return ignored; });
|
|
25
|
+
throw error;
|
|
26
|
+
} finally {
|
|
27
|
+
client.release();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
1
31
|
/**
|
|
2
32
|
* Routes normalized segment records to the correct persistence sink.
|
|
3
33
|
*
|
|
34
|
+
* When a pool is supplied, pending segment heartbeats (`duration === 0`)
|
|
35
|
+
* run orphan-close and upsert inside one transaction on a single client.
|
|
36
|
+
*
|
|
4
37
|
* @param {object} segmentSink - batch insert/update sink
|
|
5
38
|
* @param {object} retag - retag sink
|
|
6
39
|
* @param {object} splitSink - split update sink
|
|
7
40
|
* @param {object} closer - orphan open segment closer
|
|
41
|
+
* @param {object} [pool] - optional pg pool for transactional pending writes
|
|
8
42
|
* @returns {object} collector with accept(record)
|
|
9
43
|
*
|
|
10
44
|
* @example
|
|
11
|
-
* const route = segmentDispatch(segmentSink, retag, splitSink, closer);
|
|
45
|
+
* const route = segmentDispatch(segmentSink, retag, splitSink, closer, pool);
|
|
12
46
|
* await route.accept({ type: 'segment', machine: 'm1', start_time: '...', duration: 0 });
|
|
13
47
|
*/
|
|
14
|
-
export default function segmentDispatch(segmentSink, retag, splitSink, closer) {
|
|
48
|
+
export default function segmentDispatch(segmentSink, retag, splitSink, closer, pool) {
|
|
15
49
|
return {
|
|
16
50
|
async accept(record) {
|
|
17
51
|
if (record.type === 'retag') {
|
|
18
52
|
await retag.accept(record);
|
|
19
|
-
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (record.type === 'split') {
|
|
20
56
|
await splitSink.write([record]);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
await segmentSink.write([record]);
|
|
26
|
-
} else {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (record.type !== 'segment') {
|
|
27
60
|
throw new Error(`Segment command type ${record.type} is not defined`);
|
|
28
61
|
}
|
|
62
|
+
if (record.duration !== 0) {
|
|
63
|
+
await segmentSink.write([record]);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await persistPending(segmentSink, closer, pool, record);
|
|
29
67
|
}
|
|
30
68
|
};
|
|
31
69
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the per-machine lane state, creating it when missing.
|
|
3
|
+
*
|
|
4
|
+
* @param {Map} lanes - machine → lane map
|
|
5
|
+
* @param {string} machine - machine id
|
|
6
|
+
* @returns {object} lane state
|
|
7
|
+
*/
|
|
8
|
+
function laneState(lanes, machine) {
|
|
9
|
+
if (!lanes.has(machine)) {
|
|
10
|
+
lanes.set(machine, { queue: Promise.resolve(), pending: null, timer: null });
|
|
11
|
+
}
|
|
12
|
+
return lanes.get(machine);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Builds the coalesce key for a pending heartbeat.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} parsed - raw segment JSON
|
|
19
|
+
* @returns {string} coalesce key
|
|
20
|
+
*/
|
|
21
|
+
function heartbeatKey(parsed) {
|
|
22
|
+
const kind = typeof parsed.kind === 'string' && parsed.kind.length > 0 ? parsed.kind : 'phase';
|
|
23
|
+
return `${parsed.machine}\0${kind}\0${parsed.start}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether the payload is a pending open-segment heartbeat.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} parsed - raw segment JSON
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
function isHeartbeat(parsed) {
|
|
33
|
+
return parsed.type === 'segment' && parsed.duration === 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Flushes a buffered pending heartbeat batch through the codec.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} lane - lane state
|
|
40
|
+
* @param {object} codec - downstream codec
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
async function flushLane(lane, codec) {
|
|
44
|
+
const batch = lane.pending;
|
|
45
|
+
if (!batch) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
lane.pending = null;
|
|
49
|
+
if (lane.timer) {
|
|
50
|
+
clearTimeout(lane.timer);
|
|
51
|
+
lane.timer = null;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await codec.accept({ destination: batch.destination, payload: batch.payload });
|
|
55
|
+
for (const settle of batch.settles) {
|
|
56
|
+
settle.ack();
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
for (const settle of batch.settles) {
|
|
60
|
+
settle.nack();
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Buffers or flushes one pending heartbeat on a lane.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} ctx - lane context with api/size/intervalMs
|
|
70
|
+
* @param {object} envelope - STOMP envelope
|
|
71
|
+
* @param {object} parsed - parsed payload
|
|
72
|
+
* @returns {Promise<void>}
|
|
73
|
+
*/
|
|
74
|
+
async function bufferHeartbeat(ctx, envelope, parsed) {
|
|
75
|
+
const key = heartbeatKey(parsed);
|
|
76
|
+
const { api, lane, size, intervalMs } = ctx;
|
|
77
|
+
if (lane.pending && lane.pending.key === key) {
|
|
78
|
+
lane.pending.payload = envelope.payload;
|
|
79
|
+
lane.pending.settles.push(envelope.settle);
|
|
80
|
+
} else {
|
|
81
|
+
await api.flush(lane);
|
|
82
|
+
const pending = {
|
|
83
|
+
key,
|
|
84
|
+
destination: envelope.destination,
|
|
85
|
+
payload: envelope.payload,
|
|
86
|
+
settles: [envelope.settle]
|
|
87
|
+
};
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
lane.queue = lane.queue.then(() => {
|
|
90
|
+
return api.flush(lane);
|
|
91
|
+
});
|
|
92
|
+
}, intervalMs);
|
|
93
|
+
Object.assign(lane, { pending, timer });
|
|
94
|
+
}
|
|
95
|
+
if (lane.pending && lane.pending.settles.length >= size) {
|
|
96
|
+
await api.flush(lane);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Handles one envelope on a machine lane.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} ctx - lane context
|
|
104
|
+
* @param {object} envelope - STOMP envelope with settle
|
|
105
|
+
* @param {object} parsed - parsed payload
|
|
106
|
+
* @returns {Promise<void>}
|
|
107
|
+
*/
|
|
108
|
+
async function step(ctx, envelope, parsed) {
|
|
109
|
+
if (isHeartbeat(parsed)) {
|
|
110
|
+
await bufferHeartbeat(ctx, envelope, parsed);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
await ctx.api.flush(ctx.lane);
|
|
114
|
+
try {
|
|
115
|
+
await ctx.codec.accept({ destination: envelope.destination, payload: envelope.payload });
|
|
116
|
+
envelope.settle.ack();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
envelope.settle.nack();
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Coalesces pending segment heartbeats per machine before the codec.
|
|
125
|
+
*
|
|
126
|
+
* Expects STOMP envelopes with manual settle: `{ destination, payload, settle }`.
|
|
127
|
+
* Consecutive `type=segment` + `duration=0` messages that share
|
|
128
|
+
* `(machine, kind, start)` keep only the latest payload; all settles ack
|
|
129
|
+
* after one successful downstream accept. Retag, split, completed segments,
|
|
130
|
+
* and key changes flush immediately. Machines run on independent lanes.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} codec - Downstream collector with accept({destination, payload})
|
|
133
|
+
* @param {object} [options] - Coalesce tuning
|
|
134
|
+
* @param {number} [options.size=50] - Max pending heartbeats before forced flush
|
|
135
|
+
* @param {number} [options.interval=2] - Flush interval in seconds
|
|
136
|
+
* @returns {object} Collector with accept(envelope)
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* const coalesce = segmentCoalesce(segmentCodec(dispatch), { size: 50, interval: 2 });
|
|
140
|
+
* await coalesce.accept({ destination, payload, settle });
|
|
141
|
+
*/
|
|
142
|
+
export default function segmentCoalesce(codec, options = {}) {
|
|
143
|
+
if (!codec || typeof codec.accept !== 'function') {
|
|
144
|
+
throw new Error('Codec must have an accept() method');
|
|
145
|
+
}
|
|
146
|
+
const size = options.size || 50;
|
|
147
|
+
const intervalMs = (options.interval || 2) * 1000;
|
|
148
|
+
const lanes = new Map();
|
|
149
|
+
const api = {
|
|
150
|
+
accept(envelope) {
|
|
151
|
+
const parsed = JSON.parse(envelope.payload);
|
|
152
|
+
const lane = laneState(lanes, parsed.machine);
|
|
153
|
+
const ctx = { api, lane, codec, size, intervalMs };
|
|
154
|
+
lane.queue = lane.queue.then(() => {
|
|
155
|
+
return step(ctx, envelope, parsed);
|
|
156
|
+
});
|
|
157
|
+
return lane.queue;
|
|
158
|
+
},
|
|
159
|
+
async flush(lane) {
|
|
160
|
+
await flushLane(lane, codec);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
return api;
|
|
164
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
stompSource
|
|
4
4
|
} from '@yarkivaev/source-to-sink';
|
|
5
5
|
import segmentDispatch from '../../../domain/segment/dispatch.js';
|
|
6
|
+
import segmentCoalesce from '../../../domain/segment/segmentCoalesce.js';
|
|
6
7
|
import silenceBudget from '../../../domain/segment/silenceBudget.js';
|
|
7
8
|
import segmentCodec from '../codecs/segmentCodec.js';
|
|
8
9
|
import silentOpenWatch from '../silentOpenWatch.js';
|
|
@@ -19,9 +20,27 @@ export const segmentsIngestDestination = '/queue/scada.segments.ingest';
|
|
|
19
20
|
|
|
20
21
|
export { default as segmentDispatch } from '../../../domain/segment/dispatch.js';
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Builds segment and split postgres sinks that share one pool.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} postgres - PostgreSQL URL
|
|
27
|
+
* @param {object} pool - shared pg pool
|
|
28
|
+
* @returns {{ segmentSink: object, splitSink: object }}
|
|
29
|
+
*/
|
|
30
|
+
function segmentSinks(postgres, pool) {
|
|
31
|
+
const shared = { pool, conflict: segmentConflict };
|
|
32
|
+
return {
|
|
33
|
+
segmentSink: postgresSink(postgres, 'segments', segmentColumns,
|
|
34
|
+
{ ...shared, update: segmentUpdateColumns }),
|
|
35
|
+
splitSink: postgresSink(postgres, 'segments', segmentColumns,
|
|
36
|
+
{ ...shared, update: splitUpdateColumns })
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
/**
|
|
23
41
|
* Pipeline for streaming STOMP segment data to PostgreSQL segments table.
|
|
24
42
|
* Subscribes to the durable ingest queue so shovel traffic survives consumer gaps.
|
|
43
|
+
* Pending heartbeats are coalesced per machine; machines run in parallel lanes.
|
|
25
44
|
*
|
|
26
45
|
* @param {string} stomp - STOMP broker URL
|
|
27
46
|
* @param {string} postgres - PostgreSQL connection URL
|
|
@@ -30,21 +49,24 @@ export { default as segmentDispatch } from '../../../domain/segment/dispatch.js'
|
|
|
30
49
|
* @returns {object} Pipeline with start() and stop() methods
|
|
31
50
|
*/
|
|
32
51
|
export default function segmentPipeline(stomp, postgres, pool, config) {
|
|
33
|
-
const segmentSink =
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const dispatch = segmentDispatch(segmentSink, retag, splitSink, closer);
|
|
40
|
-
const codec = segmentCodec(dispatch);
|
|
52
|
+
const { segmentSink, splitSink } = segmentSinks(postgres, pool);
|
|
53
|
+
const dispatch = segmentDispatch(segmentSink, retagSink(pool), splitSink, closeOrphanOpen(pool), pool);
|
|
54
|
+
const coalesce = segmentCoalesce(segmentCodec(dispatch), {
|
|
55
|
+
size: config.size,
|
|
56
|
+
interval: config.interval
|
|
57
|
+
});
|
|
41
58
|
const destination = config.segmentsDestination || segmentsIngestDestination;
|
|
42
|
-
const source = stompSource(stomp, destination,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
const source = stompSource(stomp, destination, coalesce, {
|
|
60
|
+
login: config.login,
|
|
61
|
+
passcode: config.passcode,
|
|
62
|
+
host: config.host,
|
|
63
|
+
serial: false,
|
|
64
|
+
manualAck: true
|
|
65
|
+
});
|
|
66
|
+
const budget = silenceBudget(config.segmentWindow || 15);
|
|
67
|
+
const silence = silentOpenWatch(closeSilentOpen(pool), budget, {
|
|
68
|
+
intervalMs: (config.poll || 5) * 1000
|
|
69
|
+
});
|
|
48
70
|
return {
|
|
49
71
|
destination,
|
|
50
72
|
start() {
|
|
@@ -4,7 +4,7 @@ import processingErrorLog from '../processingErrorLog.js';
|
|
|
4
4
|
* Closes stale open segment rows for one machine before a new open segment lands.
|
|
5
5
|
*
|
|
6
6
|
* @param {object} pool - pg Pool with query() method
|
|
7
|
-
* @returns {object} closer with close(machine, startTime, kind)
|
|
7
|
+
* @returns {object} closer with close(machine, startTime, kind, client?)
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* const closer = closeOrphanOpen(pool);
|
|
@@ -15,10 +15,11 @@ export default function closeOrphanOpen(pool) {
|
|
|
15
15
|
throw new Error('Pool must have a query() method');
|
|
16
16
|
}
|
|
17
17
|
return {
|
|
18
|
-
async close(machine, startTime, kind) {
|
|
18
|
+
async close(machine, startTime, kind, client) {
|
|
19
19
|
const track = kind || 'phase';
|
|
20
|
+
const runner = client && typeof client.query === 'function' ? client : pool;
|
|
20
21
|
try {
|
|
21
|
-
await
|
|
22
|
+
await runner.query(
|
|
22
23
|
`UPDATE segments
|
|
23
24
|
SET end_time = start_time + interval '1 second', duration = 1
|
|
24
25
|
WHERE machine = $1 AND kind = $3 AND duration = 0 AND start_time <> $2`,
|
|
@@ -99,7 +99,7 @@ function filterLatest(store, machineId, kind, bound) {
|
|
|
99
99
|
* In-memory operations state port for tests and local runs.
|
|
100
100
|
*
|
|
101
101
|
* @param {object} store - shared mutable store with operations array
|
|
102
|
-
* @returns {object} operations port matching operationStatePg shape
|
|
102
|
+
* @returns {object} operations port matching operationStatePg shape including drop
|
|
103
103
|
*/
|
|
104
104
|
export default function operationStateMemory(store) {
|
|
105
105
|
if (!store.operations) {
|
|
@@ -126,6 +126,15 @@ export default function operationStateMemory(store) {
|
|
|
126
126
|
const [row] = store.operations.splice(index, 1);
|
|
127
127
|
return Promise.resolve(row);
|
|
128
128
|
},
|
|
129
|
+
drop(machineId, key) {
|
|
130
|
+
const index = store.operations.findIndex((row) => {
|
|
131
|
+
return row.machine === machineId && row.key === key;
|
|
132
|
+
});
|
|
133
|
+
if (index >= 0) {
|
|
134
|
+
store.operations.splice(index, 1);
|
|
135
|
+
}
|
|
136
|
+
return Promise.resolve();
|
|
137
|
+
},
|
|
129
138
|
listForMachine(machineId, kind, range) {
|
|
130
139
|
return Promise.resolve(filterList(store, machineId, kind, range));
|
|
131
140
|
},
|
|
@@ -120,17 +120,33 @@ async function removeOperation(pool, machineId, key) {
|
|
|
120
120
|
return result.rows[0];
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Deletes an operation when present. Absence is success.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} pool - pg pool
|
|
127
|
+
* @param {string} machineId - machine identifier
|
|
128
|
+
* @param {string} key - operation storage key
|
|
129
|
+
* @returns {Promise<void>}
|
|
130
|
+
*/
|
|
131
|
+
function dropOperation(pool, machineId, key) {
|
|
132
|
+
return pool.query(
|
|
133
|
+
'DELETE FROM operations WHERE machine = $1 AND key = $2',
|
|
134
|
+
[machineId, key]
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
123
138
|
/**
|
|
124
139
|
* PostgreSQL operations persistence port for generic machine operations.
|
|
125
140
|
*
|
|
126
141
|
* @param {object} pool - pg pool
|
|
127
|
-
* @returns {object} operations port with upsert, get, remove, listForMachine, latestForMachine
|
|
142
|
+
* @returns {object} operations port with upsert, get, remove, drop, listForMachine, latestForMachine
|
|
128
143
|
*
|
|
129
144
|
* @example
|
|
130
145
|
* const store = operationStatePg(pool);
|
|
131
146
|
* await store.upsert({ machine: 'm1', key: 'nb-1', kind: 'sample', ... });
|
|
132
147
|
* await store.latestForMachine('m1', 'sample', { to: new Date() });
|
|
133
148
|
* await store.remove('m1', 'nb-1');
|
|
149
|
+
* await store.drop('m1', 'nb-1');
|
|
134
150
|
*/
|
|
135
151
|
export default function operationStatePg(pool) {
|
|
136
152
|
return {
|
|
@@ -143,6 +159,9 @@ export default function operationStatePg(pool) {
|
|
|
143
159
|
remove(machineId, key) {
|
|
144
160
|
return removeOperation(pool, machineId, key);
|
|
145
161
|
},
|
|
162
|
+
drop(machineId, key) {
|
|
163
|
+
return dropOperation(pool, machineId, key);
|
|
164
|
+
},
|
|
146
165
|
listForMachine(machineId, kind, range) {
|
|
147
166
|
return listForMachine(pool, machineId, kind, range);
|
|
148
167
|
},
|
|
@@ -3,7 +3,9 @@ import processingErrorLog from '../ingest/processingErrorLog.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* PostgreSQL sink for generic operation sync records.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Federated deletes call operations.drop, which is idempotent when the row is absent.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} operations - Operations port with upsert(item) and drop(machineId, key)
|
|
7
9
|
* @returns {object} Sink with accept() and remove() methods
|
|
8
10
|
*
|
|
9
11
|
* @example
|
|
@@ -26,7 +28,7 @@ export default function operationSyncSink(operations) {
|
|
|
26
28
|
},
|
|
27
29
|
async remove(record) {
|
|
28
30
|
try {
|
|
29
|
-
await operations.
|
|
31
|
+
await operations.drop(record.machine, record.key);
|
|
30
32
|
} catch (error) {
|
|
31
33
|
processingErrorLog('operation_sync_sink', error, {
|
|
32
34
|
machine: record.machine,
|