@yarkivaev/scada 2.3.112 → 2.3.114
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/README.md +12 -0
- package/charts/scada/Chart.yaml +2 -2
- package/charts/scada/templates/_helpers.tpl +25 -0
- package/charts/scada/templates/clickhouse.yaml +14 -9
- package/charts/scada/templates/collector.yaml +2 -3
- package/charts/scada/templates/postgres.yaml +6 -9
- package/charts/scada/templates/rabbitmq.yaml +40 -10
- package/charts/scada/templates/site-server.yaml +4 -7
- package/charts/scada/templates/supervisor-sink.yaml +7 -5
- package/charts/scada/values.yaml +11 -0
- package/package.json +2 -2
- package/src/application/deploy/releaseValues.js +2 -1
- package/src/application/supervisorSink.js +6 -0
- package/src/domain/segment/segmentCoalesce.js +26 -10
- package/src/infrastructure/ingest/codecs/segmentCodec.js +10 -3
- package/src/infrastructure/ingest/pipelines/segmentPipeline.js +3 -1
package/README.md
CHANGED
|
@@ -274,3 +274,15 @@ Depend on a pinned version (`"@yarkivaev/scada": "…#v2.3.46"`), not a local pa
|
|
|
274
274
|
## CI/CD
|
|
275
275
|
|
|
276
276
|
GitHub Actions via `yarkivaev/npm-workflows` (lint, test, version check). This repo publishes an npm package only; container images are built in downstream deploy repos.
|
|
277
|
+
|
|
278
|
+
### Ingest delivery and quarantine
|
|
279
|
+
|
|
280
|
+
`STOMP_PREFETCH` sets a positive, bounded segment delivery window (default 128).
|
|
281
|
+
`SEGMENTS_DEAD_LETTER_ENABLED=true` opts into rejecting malformed segment records
|
|
282
|
+
without requeue; transient persistence failures are retried. Enable this only with
|
|
283
|
+
a durable DLQ and routing policy. In the deployment DSL, configure
|
|
284
|
+
`profiles.<profile>.modules.message_bus.dead_letter.queue: scada.ingest.dlq`.
|
|
285
|
+
The scada chart declares the queue and applies an ingest-only dead-letter policy,
|
|
286
|
+
without changing existing queue arguments. The profile `env` is passed to both
|
|
287
|
+
site-server and supervisor-sink. DLQ records retain the original body and RabbitMQ
|
|
288
|
+
`x-death` metadata for inspection and deliberate replay; they are not auto-expired.
|
package/charts/scada/Chart.yaml
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
app.kubernetes.io/name: scada
|
|
3
3
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
4
4
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
5
|
+
{{- include "scada.extraLabels" . }}
|
|
6
|
+
{{- end }}
|
|
7
|
+
|
|
8
|
+
{{- define "scada.extraLabels" -}}
|
|
9
|
+
{{- range $key, $val := (.Values.extraLabels | default dict) }}
|
|
10
|
+
{{ $key }}: {{ $val | quote }}
|
|
11
|
+
{{- end }}
|
|
12
|
+
{{- end }}
|
|
13
|
+
|
|
14
|
+
{{- define "scada.match" -}}
|
|
15
|
+
app: {{ .app }}
|
|
16
|
+
{{- range $key, $val := (.ctx.Values.extraLabels | default dict) }}
|
|
17
|
+
{{ $key }}: {{ $val | quote }}
|
|
18
|
+
{{- end }}
|
|
19
|
+
{{- end }}
|
|
20
|
+
|
|
21
|
+
{{- define "scada.claim" -}}
|
|
22
|
+
accessModes: ["ReadWriteOnce"]
|
|
23
|
+
{{- if .class }}
|
|
24
|
+
storageClassName: {{ .class | quote }}
|
|
25
|
+
{{- end }}
|
|
26
|
+
resources:
|
|
27
|
+
requests:
|
|
28
|
+
storage: {{ .size | quote }}
|
|
29
|
+
volumeMode: Filesystem
|
|
5
30
|
{{- end }}
|
|
6
31
|
|
|
7
32
|
{{- define "scada.device" -}}
|
|
@@ -12,9 +12,14 @@ spec:
|
|
|
12
12
|
- name: http
|
|
13
13
|
port: 8123
|
|
14
14
|
targetPort: http
|
|
15
|
+
- name: native
|
|
16
|
+
port: 9000
|
|
17
|
+
targetPort: native
|
|
18
|
+
- name: metrics
|
|
19
|
+
port: 9363
|
|
20
|
+
targetPort: metrics
|
|
15
21
|
selector:
|
|
16
|
-
app
|
|
17
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
22
|
+
{{- include "scada.match" (dict "app" "clickhouse" "ctx" .) | nindent 4 }}
|
|
18
23
|
---
|
|
19
24
|
apiVersion: apps/v1
|
|
20
25
|
kind: StatefulSet
|
|
@@ -29,13 +34,12 @@ spec:
|
|
|
29
34
|
replicas: 1
|
|
30
35
|
selector:
|
|
31
36
|
matchLabels:
|
|
32
|
-
app
|
|
33
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
37
|
+
{{- include "scada.match" (dict "app" "clickhouse" "ctx" .) | nindent 6 }}
|
|
34
38
|
template:
|
|
35
39
|
metadata:
|
|
36
40
|
labels:
|
|
37
41
|
{{- include "scada.labels" . | nindent 8 }}
|
|
38
|
-
app
|
|
42
|
+
{{- include "scada.match" (dict "app" "clickhouse" "ctx" .) | nindent 8 }}
|
|
39
43
|
spec:
|
|
40
44
|
containers:
|
|
41
45
|
- name: clickhouse
|
|
@@ -43,6 +47,10 @@ spec:
|
|
|
43
47
|
ports:
|
|
44
48
|
- name: http
|
|
45
49
|
containerPort: 8123
|
|
50
|
+
- name: native
|
|
51
|
+
containerPort: 9000
|
|
52
|
+
- name: metrics
|
|
53
|
+
containerPort: 9363
|
|
46
54
|
volumeMounts:
|
|
47
55
|
- name: data
|
|
48
56
|
mountPath: /var/lib/clickhouse
|
|
@@ -50,8 +58,5 @@ spec:
|
|
|
50
58
|
- metadata:
|
|
51
59
|
name: data
|
|
52
60
|
spec:
|
|
53
|
-
|
|
54
|
-
resources:
|
|
55
|
-
requests:
|
|
56
|
-
storage: 20Gi
|
|
61
|
+
{{- include "scada.claim" (dict "size" .Values.clickhouse.storage "class" .Values.clickhouse.storageClass) | nindent 8 }}
|
|
57
62
|
{{- end }}
|
|
@@ -62,14 +62,13 @@ spec:
|
|
|
62
62
|
replicas: 1
|
|
63
63
|
selector:
|
|
64
64
|
matchLabels:
|
|
65
|
-
app
|
|
66
|
-
app.kubernetes.io/instance: {{ $.Release.Name }}
|
|
65
|
+
{{- include "scada.match" (dict "app" "collector" "ctx" $) | nindent 6 }}
|
|
67
66
|
scada/command: {{ $slug }}
|
|
68
67
|
template:
|
|
69
68
|
metadata:
|
|
70
69
|
labels:
|
|
71
70
|
{{- include "scada.labels" $ | nindent 8 }}
|
|
72
|
-
app
|
|
71
|
+
{{- include "scada.match" (dict "app" "collector" "ctx" $) | nindent 8 }}
|
|
73
72
|
scada/command: {{ $slug }}
|
|
74
73
|
spec:
|
|
75
74
|
{{- with $.Values.pullSecret }}
|
|
@@ -13,8 +13,7 @@ spec:
|
|
|
13
13
|
port: 5432
|
|
14
14
|
targetPort: postgres
|
|
15
15
|
selector:
|
|
16
|
-
app
|
|
17
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
16
|
+
{{- include "scada.match" (dict "app" "postgres" "ctx" .) | nindent 4 }}
|
|
18
17
|
---
|
|
19
18
|
apiVersion: apps/v1
|
|
20
19
|
kind: StatefulSet
|
|
@@ -29,13 +28,12 @@ spec:
|
|
|
29
28
|
replicas: 1
|
|
30
29
|
selector:
|
|
31
30
|
matchLabels:
|
|
32
|
-
app
|
|
33
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
31
|
+
{{- include "scada.match" (dict "app" "postgres" "ctx" .) | nindent 6 }}
|
|
34
32
|
template:
|
|
35
33
|
metadata:
|
|
36
34
|
labels:
|
|
37
35
|
{{- include "scada.labels" . | nindent 8 }}
|
|
38
|
-
app
|
|
36
|
+
{{- include "scada.match" (dict "app" "postgres" "ctx" .) | nindent 8 }}
|
|
39
37
|
spec:
|
|
40
38
|
containers:
|
|
41
39
|
- name: postgres
|
|
@@ -56,6 +54,8 @@ spec:
|
|
|
56
54
|
secretKeyRef:
|
|
57
55
|
name: {{ .Values.postgres.secret }}
|
|
58
56
|
key: password
|
|
57
|
+
- name: PGDATA
|
|
58
|
+
value: {{ .Values.postgres.data | quote }}
|
|
59
59
|
volumeMounts:
|
|
60
60
|
- name: data
|
|
61
61
|
mountPath: /var/lib/postgresql/data
|
|
@@ -63,8 +63,5 @@ spec:
|
|
|
63
63
|
- metadata:
|
|
64
64
|
name: data
|
|
65
65
|
spec:
|
|
66
|
-
|
|
67
|
-
resources:
|
|
68
|
-
requests:
|
|
69
|
-
storage: 10Gi
|
|
66
|
+
{{- include "scada.claim" (dict "size" .Values.postgres.storage "class" .Values.postgres.storageClass) | nindent 8 }}
|
|
70
67
|
{{- end }}
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
{{- if $outgoing }}
|
|
36
36
|
{{- $queue = append $queue (dict "name" "scada.telemetry.outbox" "vhost" "/" "durable" true "auto_delete" false) -}}
|
|
37
37
|
{{- end }}
|
|
38
|
+
{{- $policies := list -}}
|
|
39
|
+
{{- with .Values.rabbitmq.deadLetter }}
|
|
40
|
+
{{- $queue = append $queue (dict "name" .queue "vhost" "/" "durable" true "auto_delete" false) -}}
|
|
41
|
+
{{- $policies = append $policies (dict "name" "ingest-dlq" "vhost" "/" "pattern" "^scada\\.(segments|operations|telemetry)\\.ingest$" "apply-to" "queues" "priority" 10 "definition" (dict "dead-letter-exchange" "" "dead-letter-routing-key" .queue)) -}}
|
|
42
|
+
{{- end }}
|
|
38
43
|
{{- $binding := list -}}
|
|
39
44
|
{{- range $key := $ingest }}
|
|
40
45
|
{{- $binding = append $binding (dict "source" "scada.telemetry" "vhost" "/" "destination" "scada.telemetry.ingest" "destination_type" "queue" "routing_key" $key) -}}
|
|
@@ -54,6 +59,7 @@
|
|
|
54
59
|
"permissions" (list (dict "user" "guest" "vhost" "/" "configure" ".*" "write" ".*" "read" ".*"))
|
|
55
60
|
"exchanges" $exchange
|
|
56
61
|
"queues" $queue
|
|
62
|
+
"policies" $policies
|
|
57
63
|
"bindings" $binding
|
|
58
64
|
-}}
|
|
59
65
|
apiVersion: v1
|
|
@@ -85,19 +91,38 @@ metadata:
|
|
|
85
91
|
{{- include "scada.labels" . | nindent 4 }}
|
|
86
92
|
app: rabbitmq
|
|
87
93
|
spec:
|
|
94
|
+
{{- $svc := .Values.rabbitmq.service | default dict }}
|
|
95
|
+
type: {{ $svc.type | default "ClusterIP" }}
|
|
88
96
|
ports:
|
|
89
97
|
- name: amqp
|
|
90
98
|
port: 5672
|
|
91
99
|
targetPort: amqp
|
|
100
|
+
{{- with $svc.nodePorts }}
|
|
101
|
+
{{- if .amqp }}
|
|
102
|
+
nodePort: {{ .amqp }}
|
|
103
|
+
{{- end }}
|
|
104
|
+
{{- end }}
|
|
92
105
|
- name: mqtt
|
|
93
106
|
port: 1883
|
|
94
107
|
targetPort: mqtt
|
|
95
108
|
- name: stomp
|
|
96
109
|
port: 61613
|
|
97
110
|
targetPort: stomp
|
|
111
|
+
{{- with $svc.nodePorts }}
|
|
112
|
+
{{- if .stomp }}
|
|
113
|
+
nodePort: {{ .stomp }}
|
|
114
|
+
{{- end }}
|
|
115
|
+
{{- end }}
|
|
116
|
+
- name: management
|
|
117
|
+
port: 15672
|
|
118
|
+
targetPort: management
|
|
119
|
+
{{- with $svc.nodePorts }}
|
|
120
|
+
{{- if .management }}
|
|
121
|
+
nodePort: {{ .management }}
|
|
122
|
+
{{- end }}
|
|
123
|
+
{{- end }}
|
|
98
124
|
selector:
|
|
99
|
-
app
|
|
100
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
125
|
+
{{- include "scada.match" (dict "app" "rabbitmq" "ctx" .) | nindent 4 }}
|
|
101
126
|
---
|
|
102
127
|
apiVersion: apps/v1
|
|
103
128
|
kind: StatefulSet
|
|
@@ -112,13 +137,12 @@ spec:
|
|
|
112
137
|
replicas: 1
|
|
113
138
|
selector:
|
|
114
139
|
matchLabels:
|
|
115
|
-
app
|
|
116
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
140
|
+
{{- include "scada.match" (dict "app" "rabbitmq" "ctx" .) | nindent 6 }}
|
|
117
141
|
template:
|
|
118
142
|
metadata:
|
|
119
143
|
labels:
|
|
120
144
|
{{- include "scada.labels" . | nindent 8 }}
|
|
121
|
-
app
|
|
145
|
+
{{- include "scada.match" (dict "app" "rabbitmq" "ctx" .) | nindent 8 }}
|
|
122
146
|
spec:
|
|
123
147
|
containers:
|
|
124
148
|
- name: rabbitmq
|
|
@@ -130,9 +154,18 @@ spec:
|
|
|
130
154
|
containerPort: 1883
|
|
131
155
|
- name: stomp
|
|
132
156
|
containerPort: 61613
|
|
157
|
+
- name: management
|
|
158
|
+
containerPort: 15672
|
|
133
159
|
volumeMounts:
|
|
134
160
|
- name: config
|
|
135
|
-
mountPath: /etc/rabbitmq
|
|
161
|
+
mountPath: /etc/rabbitmq/enabled_plugins
|
|
162
|
+
subPath: enabled_plugins
|
|
163
|
+
- name: config
|
|
164
|
+
mountPath: /etc/rabbitmq/rabbitmq.conf
|
|
165
|
+
subPath: rabbitmq.conf
|
|
166
|
+
- name: config
|
|
167
|
+
mountPath: /etc/rabbitmq/definitions.json
|
|
168
|
+
subPath: definitions.json
|
|
136
169
|
- name: data
|
|
137
170
|
mountPath: /var/lib/rabbitmq
|
|
138
171
|
volumes:
|
|
@@ -143,8 +176,5 @@ spec:
|
|
|
143
176
|
- metadata:
|
|
144
177
|
name: data
|
|
145
178
|
spec:
|
|
146
|
-
|
|
147
|
-
resources:
|
|
148
|
-
requests:
|
|
149
|
-
storage: 5Gi
|
|
179
|
+
{{- include "scada.claim" (dict "size" .Values.rabbitmq.storage "class" .Values.rabbitmq.storageClass) | nindent 8 }}
|
|
150
180
|
{{- end }}
|
|
@@ -10,13 +10,12 @@ spec:
|
|
|
10
10
|
replicas: 1
|
|
11
11
|
selector:
|
|
12
12
|
matchLabels:
|
|
13
|
-
app
|
|
14
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
13
|
+
{{- include "scada.match" (dict "app" "site-server" "ctx" .) | nindent 6 }}
|
|
15
14
|
template:
|
|
16
15
|
metadata:
|
|
17
16
|
labels:
|
|
18
17
|
{{- include "scada.labels" . | nindent 8 }}
|
|
19
|
-
app
|
|
18
|
+
{{- include "scada.match" (dict "app" "site-server" "ctx" .) | nindent 8 }}
|
|
20
19
|
spec:
|
|
21
20
|
{{- with .Values.pullSecret }}
|
|
22
21
|
imagePullSecrets:
|
|
@@ -85,8 +84,7 @@ spec:
|
|
|
85
84
|
port: {{ .Values.service.internalPort }}
|
|
86
85
|
targetPort: internal-api
|
|
87
86
|
selector:
|
|
88
|
-
app
|
|
89
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
87
|
+
{{- include "scada.match" (dict "app" "site-server" "ctx" .) | nindent 4 }}
|
|
90
88
|
{{- if gt (int .Values.service.nodePort) 0 }}
|
|
91
89
|
---
|
|
92
90
|
apiVersion: v1
|
|
@@ -105,6 +103,5 @@ spec:
|
|
|
105
103
|
targetPort: http
|
|
106
104
|
nodePort: {{ .Values.service.nodePort }}
|
|
107
105
|
selector:
|
|
108
|
-
app
|
|
109
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
106
|
+
{{- include "scada.match" (dict "app" "site-server" "ctx" .) | nindent 4 }}
|
|
110
107
|
{{- end }}
|
|
@@ -10,13 +10,12 @@ spec:
|
|
|
10
10
|
replicas: 1
|
|
11
11
|
selector:
|
|
12
12
|
matchLabels:
|
|
13
|
-
app
|
|
14
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
13
|
+
{{- include "scada.match" (dict "app" "supervisor-sink" "ctx" .) | nindent 6 }}
|
|
15
14
|
template:
|
|
16
15
|
metadata:
|
|
17
16
|
labels:
|
|
18
17
|
{{- include "scada.labels" . | nindent 8 }}
|
|
19
|
-
app
|
|
18
|
+
{{- include "scada.match" (dict "app" "supervisor-sink" "ctx" .) | nindent 8 }}
|
|
20
19
|
spec:
|
|
21
20
|
{{- with .Values.pullSecret }}
|
|
22
21
|
imagePullSecrets:
|
|
@@ -33,6 +32,10 @@ spec:
|
|
|
33
32
|
- name: STATE_HTTP_PORT
|
|
34
33
|
value: {{ .Values.service.internalPort | quote }}
|
|
35
34
|
{{- include "scada.linkEnv" . | nindent 12 }}
|
|
35
|
+
{{- range .Values.env }}
|
|
36
|
+
- name: {{ .name }}
|
|
37
|
+
value: {{ .value | quote }}
|
|
38
|
+
{{- end }}
|
|
36
39
|
readinessProbe:
|
|
37
40
|
tcpSocket:
|
|
38
41
|
port: state
|
|
@@ -60,5 +63,4 @@ spec:
|
|
|
60
63
|
port: {{ .Values.service.internalPort }}
|
|
61
64
|
targetPort: state
|
|
62
65
|
selector:
|
|
63
|
-
app
|
|
64
|
-
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
66
|
+
{{- include "scada.match" (dict "app" "supervisor-sink" "ctx" .) | nindent 4 }}
|
package/charts/scada/values.yaml
CHANGED
|
@@ -6,6 +6,7 @@ machines: []
|
|
|
6
6
|
sites: []
|
|
7
7
|
collectors: []
|
|
8
8
|
env: []
|
|
9
|
+
extraLabels: {}
|
|
9
10
|
service:
|
|
10
11
|
port: 3000
|
|
11
12
|
internalPort: 8081
|
|
@@ -15,12 +16,22 @@ postgres:
|
|
|
15
16
|
image: postgres:16
|
|
16
17
|
secret: scada-postgres
|
|
17
18
|
database: scada
|
|
19
|
+
storage: 10Gi
|
|
20
|
+
storageClass: ""
|
|
21
|
+
data: /var/lib/postgresql/data/pgdata
|
|
18
22
|
rabbitmq:
|
|
19
23
|
enabled: true
|
|
24
|
+
deadLetter: null
|
|
20
25
|
image: rabbitmq:3.13-management
|
|
26
|
+
storage: 5Gi
|
|
27
|
+
storageClass: ""
|
|
28
|
+
service:
|
|
29
|
+
type: ClusterIP
|
|
21
30
|
clickhouse:
|
|
22
31
|
enabled: false
|
|
23
32
|
image: clickhouse/clickhouse-server:24.8
|
|
33
|
+
storage: 20Gi
|
|
34
|
+
storageClass: ""
|
|
24
35
|
resources:
|
|
25
36
|
requests:
|
|
26
37
|
cpu: 100m
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yarkivaev/scada",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.114",
|
|
4
4
|
"description": "SCADA domain objects, state persistence, and plant monitoring",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@clickhouse/client": "^1.0.0",
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
33
33
|
"@yarkivaev/simple-server": "1.0.2",
|
|
34
|
-
"@yarkivaev/source-to-sink": "1.3.
|
|
34
|
+
"@yarkivaev/source-to-sink": "1.3.10",
|
|
35
35
|
"amqplib": "^0.10.9",
|
|
36
36
|
"pg": "^8.0.0",
|
|
37
37
|
"stompit": "^1.0.0",
|
|
@@ -16,7 +16,8 @@ function bindOne(name, spec, secret) {
|
|
|
16
16
|
return ['postgres', { enabled: spec.enabled !== false, secret: secret || 'scada-postgres' }];
|
|
17
17
|
}
|
|
18
18
|
if (name === 'message_bus') {
|
|
19
|
-
return ['rabbitmq', { enabled: spec.enabled !== false
|
|
19
|
+
return ['rabbitmq', { enabled: spec.enabled !== false,
|
|
20
|
+
...(spec.dead_letter ? { deadLetter: spec.dead_letter } : {}) }];
|
|
20
21
|
}
|
|
21
22
|
if (name === 'telemetry_store') {
|
|
22
23
|
return ['clickhouse', { enabled: spec.enabled !== false }];
|
|
@@ -23,6 +23,10 @@ const appRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
|
23
23
|
*/
|
|
24
24
|
export function readDeploymentConfig(env) {
|
|
25
25
|
const root = path.join(appRoot, '../..');
|
|
26
|
+
const prefetch = Number(env.STOMP_PREFETCH || '128');
|
|
27
|
+
if (!Number.isSafeInteger(prefetch) || prefetch < 1) {
|
|
28
|
+
throw new Error('STOMP_PREFETCH must be a positive safe integer');
|
|
29
|
+
}
|
|
26
30
|
return {
|
|
27
31
|
stomp: env.STOMP_URL,
|
|
28
32
|
postgres: env.POSTGRES_URL,
|
|
@@ -39,6 +43,8 @@ export function readDeploymentConfig(env) {
|
|
|
39
43
|
requestTimeoutMs: parseRequestTimeoutMs(env.REQUEST_TIMEOUT_MS),
|
|
40
44
|
pipelines: env.SINK_PIPELINES !== 'false',
|
|
41
45
|
pipeline: {
|
|
46
|
+
prefetch,
|
|
47
|
+
deadLetter: env.SEGMENTS_DEAD_LETTER_ENABLED === 'true',
|
|
42
48
|
size: parseInt(env.BATCH_SIZE || '50', 10),
|
|
43
49
|
interval: parseInt(env.FLUSH_INTERVAL || '2', 10),
|
|
44
50
|
threshold: parseInt(env.CIRCUIT_THRESHOLD || '5', 10),
|
|
@@ -40,7 +40,7 @@ function isHeartbeat(parsed) {
|
|
|
40
40
|
* @param {object} codec - downstream codec
|
|
41
41
|
* @returns {Promise<void>}
|
|
42
42
|
*/
|
|
43
|
-
async function flushLane(lane, codec) {
|
|
43
|
+
async function flushLane(lane, codec, deadLetter) {
|
|
44
44
|
const batch = lane.pending;
|
|
45
45
|
if (!batch) {
|
|
46
46
|
return;
|
|
@@ -57,9 +57,11 @@ async function flushLane(lane, codec) {
|
|
|
57
57
|
}
|
|
58
58
|
} catch (error) {
|
|
59
59
|
for (const settle of batch.settles) {
|
|
60
|
-
settle.nack();
|
|
60
|
+
settle.nack(!(deadLetter && error.permanent));
|
|
61
|
+
}
|
|
62
|
+
if (!error.permanent) {
|
|
63
|
+
throw error;
|
|
61
64
|
}
|
|
62
|
-
throw error;
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -78,7 +80,12 @@ async function bufferHeartbeat(ctx, envelope, parsed) {
|
|
|
78
80
|
lane.pending.payload = envelope.payload;
|
|
79
81
|
lane.pending.settles.push(envelope.settle);
|
|
80
82
|
} else {
|
|
81
|
-
|
|
83
|
+
try {
|
|
84
|
+
await api.flush(lane);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
envelope.settle.nack(true);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
82
89
|
const pending = {
|
|
83
90
|
key,
|
|
84
91
|
destination: envelope.destination,
|
|
@@ -88,7 +95,7 @@ async function bufferHeartbeat(ctx, envelope, parsed) {
|
|
|
88
95
|
const timer = setTimeout(() => {
|
|
89
96
|
lane.queue = lane.queue.then(() => {
|
|
90
97
|
return api.flush(lane);
|
|
91
|
-
});
|
|
98
|
+
}).catch(() => { return undefined; });
|
|
92
99
|
}, intervalMs);
|
|
93
100
|
Object.assign(lane, { pending, timer });
|
|
94
101
|
}
|
|
@@ -110,12 +117,12 @@ async function step(ctx, envelope, parsed) {
|
|
|
110
117
|
await bufferHeartbeat(ctx, envelope, parsed);
|
|
111
118
|
return;
|
|
112
119
|
}
|
|
113
|
-
await ctx.api.flush(ctx.lane);
|
|
114
120
|
try {
|
|
121
|
+
await ctx.api.flush(ctx.lane);
|
|
115
122
|
await ctx.codec.accept({ destination: envelope.destination, payload: envelope.payload });
|
|
116
123
|
envelope.settle.ack();
|
|
117
124
|
} catch (error) {
|
|
118
|
-
envelope.settle.nack();
|
|
125
|
+
envelope.settle.nack(!(ctx.deadLetter && error.permanent));
|
|
119
126
|
throw error;
|
|
120
127
|
}
|
|
121
128
|
}
|
|
@@ -148,9 +155,18 @@ export default function segmentCoalesce(codec, options = {}) {
|
|
|
148
155
|
const lanes = new Map();
|
|
149
156
|
const api = {
|
|
150
157
|
accept(envelope) {
|
|
151
|
-
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(envelope.payload);
|
|
161
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
162
|
+
throw new Error('Segment must be an object');
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
envelope.settle.nack(!options.deadLetter);
|
|
166
|
+
return Promise.resolve();
|
|
167
|
+
}
|
|
152
168
|
const lane = laneState(lanes, parsed.machine);
|
|
153
|
-
const ctx = { api, lane, codec, size, intervalMs };
|
|
169
|
+
const ctx = { api, lane, codec, size, intervalMs, deadLetter: options.deadLetter };
|
|
154
170
|
lane.queue = lane.queue.then(() => {
|
|
155
171
|
return step(ctx, envelope, parsed);
|
|
156
172
|
}).catch(() => {
|
|
@@ -159,7 +175,7 @@ export default function segmentCoalesce(codec, options = {}) {
|
|
|
159
175
|
return lane.queue;
|
|
160
176
|
},
|
|
161
177
|
async flush(lane) {
|
|
162
|
-
await flushLane(lane, codec);
|
|
178
|
+
await flushLane(lane, codec, options.deadLetter);
|
|
163
179
|
}
|
|
164
180
|
};
|
|
165
181
|
return api;
|
|
@@ -32,12 +32,19 @@ export default function segmentCodec(collector) {
|
|
|
32
32
|
return {
|
|
33
33
|
async accept(raw) {
|
|
34
34
|
try {
|
|
35
|
-
|
|
35
|
+
let parsed;
|
|
36
|
+
let record;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw.payload);
|
|
39
|
+
record = parsed.type === 'split-family' ? familyRecord(parsed) : segmentNormalize(parsed);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
error.permanent = true;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
36
44
|
if (parsed.type === 'split-family') {
|
|
37
|
-
await collector.accept(
|
|
45
|
+
await collector.accept(record);
|
|
38
46
|
return;
|
|
39
47
|
}
|
|
40
|
-
const record = segmentNormalize(parsed);
|
|
41
48
|
await collector.accept(record);
|
|
42
49
|
} catch (error) {
|
|
43
50
|
processingErrorLog('segment_codec', error, { destination: raw.destination, payload: raw.payload });
|
|
@@ -60,6 +60,7 @@ export default function segmentPipeline(stomp, postgres, pool, config) {
|
|
|
60
60
|
const dispatch = segmentDispatch(continued, retagSink(pool), splitSink, closeOrphanOpen(pool), pool);
|
|
61
61
|
const coalesce = segmentCoalesce(segmentCodec(dispatch), {
|
|
62
62
|
size: config.size,
|
|
63
|
+
deadLetter: config.deadLetter,
|
|
63
64
|
interval: config.interval
|
|
64
65
|
});
|
|
65
66
|
const destination = config.segmentsDestination || segmentsIngestDestination;
|
|
@@ -68,7 +69,8 @@ export default function segmentPipeline(stomp, postgres, pool, config) {
|
|
|
68
69
|
passcode: config.passcode,
|
|
69
70
|
host: config.host,
|
|
70
71
|
serial: false,
|
|
71
|
-
manualAck: true
|
|
72
|
+
manualAck: true,
|
|
73
|
+
prefetch: config.prefetch
|
|
72
74
|
});
|
|
73
75
|
const silence = silentOpenWatch(closeSilentOpen(pool), budget, {
|
|
74
76
|
intervalMs: (config.poll || 5) * 1000
|