@winkjs/composer 0.6.0 → 0.8.0
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 +2 -2
- package/package.json +2 -2
- package/src/composer.js +1 -1
- package/src/core/emitter-manager/mqtt/connection.js +209 -0
- package/src/core/emitter-manager/mqtt/constants.js +43 -29
- package/src/core/emitter-manager/mqtt/emitter.js +103 -658
- package/src/core/emitter-manager/mqtt/health.js +151 -0
- package/src/core/emitter-manager/mqtt/index.js +68 -4
- package/src/core/emitter-manager/mqtt/publish.js +292 -0
- package/src/core/emitter-manager/mqtt/resolve-config.js +312 -0
- package/src/core/emitter-manager/mqtt/shutdown-drain.js +247 -0
- package/src/core/emitter-manager/terminal/index.js +16 -1
- package/src/core/env-vars.js +92 -24
- package/src/core/logger/index.js +1 -1
- package/src/core/source-manager/mqtt/client.js +197 -63
- package/src/core/source-manager/mqtt/dedup.js +16 -11
- package/src/core/source-manager/mqtt/index.js +32 -5
- package/src/core/source-manager/mqtt/status.js +168 -56
- package/src/core/storage-manager/questdb/address-policy.js +264 -0
- package/src/core/storage-manager/questdb/delivery-gate.js +218 -0
- package/src/core/storage-manager/questdb/flush-engine.js +852 -0
- package/src/core/storage-manager/questdb/flush-tracker.js +331 -0
- package/src/core/storage-manager/questdb/index.js +542 -862
- package/src/core/storage-manager/questdb/persist-plan.js +128 -167
- package/src/core/storage-manager/questdb/resolve-options.js +266 -0
- package/src/core/storage-manager/questdb/shutdown-drain.js +181 -0
- package/src/core/storage-manager/questdb/skip-warnings.js +180 -0
- package/src/core/test-utils/tcp-proxy.js +194 -1
- package/src/core/utils/address/index.js +362 -0
- package/src/core/utils/address/probe.js +210 -0
- package/src/core/utils/callback-guard/index.js +23 -2
- package/src/core/utils/clock/index.js +34 -0
- package/src/core/utils/jitter/index.js +51 -0
- package/src/core/utils/line-rate/index.js +104 -0
- package/src/core/utils/quantize/index.js +43 -19
- package/src/core/wiring/wire-emitters.js +3 -1
- package/src/core/wiring/wire-storages.js +10 -2
- package/src/flow/flow.js +24 -3
- package/src/flow/run.js +24 -2
|
@@ -38,19 +38,28 @@
|
|
|
38
38
|
* strict mode (an `onWarning` that throws) rejects a bad row with the sender
|
|
39
39
|
* untouched.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
* - @questdb/nodejs-client v4 declares sender.at() as async
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
41
|
+
* Row append rejections — the no-silent-failures contract:
|
|
42
|
+
* - @questdb/nodejs-client v4 declares sender.at() as async. The append
|
|
43
|
+
* runs first and synchronously; the trailing `await this.tryFlush()`
|
|
44
|
+
* does nothing, because the adapter turns the client's own flush
|
|
45
|
+
* trigger off (ADR-029). So the promise rejects only when the append
|
|
46
|
+
* itself threw, which for a validated row means the client's byte
|
|
47
|
+
* ceiling (`max_buf_size`). The row never completed. Per composer's
|
|
48
|
+
* "no silent failures" contract, that loss MUST surface.
|
|
47
49
|
*
|
|
48
|
-
*
|
|
49
|
-
* a
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
* The rejection is routed through the `onDeliveryFailure` callback,
|
|
51
|
+
* with a context of the same shape as a flush report:
|
|
52
|
+
* `{ trigger: 'append', rowsLost: 1, abandoned: false, probe: null,
|
|
53
|
+
* tableName }`. The context is one object per insight type, shared
|
|
54
|
+
* by every rejection, so a handler must not keep it.
|
|
55
|
+
* When a caller provides one, they own the response (log, alert,
|
|
56
|
+
* stop the flow). When none is provided, one classified
|
|
57
|
+
* `DELIVERY_FAILED` console line reports it and the process keeps
|
|
58
|
+
* running: an unattended deployment must report a lost row, not
|
|
59
|
+
* stop on it. One handler exists per insight type and is attached
|
|
60
|
+
* to each row's promise. The derived promise that `.catch()` creates
|
|
61
|
+
* is the one per-row allocation this file cannot avoid while `at()`
|
|
62
|
+
* is async.
|
|
54
63
|
*
|
|
55
64
|
* This separates two concerns:
|
|
56
65
|
* - `onWarning` — soft, per-row data quality (NaN in float column,
|
|
@@ -62,8 +71,13 @@
|
|
|
62
71
|
* configuration, not in the data, so every row would repeat it.
|
|
63
72
|
* The field is ignored either way; the column stores the
|
|
64
73
|
* partition id.
|
|
65
|
-
*
|
|
66
|
-
*
|
|
74
|
+
* The wording of the lines, the default handler, and the bound
|
|
75
|
+
* on that default live in `skip-warnings.js`. The default is
|
|
76
|
+
* bounded per column (ADR-029): two full lines per episode,
|
|
77
|
+
* then one summary a minute. A user `onWarning` hears every
|
|
78
|
+
* skip, and a throwing one is strict mode (ADR-027).
|
|
79
|
+
* - `onDeliveryFailure` — hard data loss: the row the client
|
|
80
|
+
* refused. Default behaviour: one DELIVERY_FAILED console line.
|
|
67
81
|
*
|
|
68
82
|
* `err.code` (setup-time throws per ADR-018 fail-fast setup):
|
|
69
83
|
* - `INVALID_CONFIG` — buildPersistPlans called with non-function
|
|
@@ -72,9 +86,9 @@
|
|
|
72
86
|
* name through a throwaway client buffer — a name is not a value, so the
|
|
73
87
|
* per-message phase 1 below cannot catch it).
|
|
74
88
|
*
|
|
75
|
-
*
|
|
76
|
-
* - `DELIVERY_FAILED` — sender.at()
|
|
77
|
-
*
|
|
89
|
+
* Runtime console classification:
|
|
90
|
+
* - `DELIVERY_FAILED` — sender.at() rejected and no `onDeliveryFailure`
|
|
91
|
+
* callback was provided. One `logger.error` line naming the table.
|
|
78
92
|
*
|
|
79
93
|
* @see docs/architecture/storage-layer.md
|
|
80
94
|
* @see ADR-018
|
|
@@ -83,6 +97,7 @@
|
|
|
83
97
|
import { SenderBufferV1 } from '@questdb/nodejs-client';
|
|
84
98
|
|
|
85
99
|
import { QUEST_WRITERS, writeAsString, createFloat64Writer } from './writers.js';
|
|
100
|
+
import { defaultOnWarning, makeColumnWarning, makeRowWarning } from './skip-warnings.js';
|
|
86
101
|
import { wrapCallback } from '../../utils/callback-guard/index.js';
|
|
87
102
|
import { logger } from '../../logger/index.js';
|
|
88
103
|
|
|
@@ -97,33 +112,6 @@ const reportCallbackFault = function ( severity, name, detail ) {
|
|
|
97
112
|
);
|
|
98
113
|
}; // reportCallbackFault()
|
|
99
114
|
|
|
100
|
-
// ============================================================================
|
|
101
|
-
// DEFAULT WARNING HANDLER
|
|
102
|
-
// ============================================================================
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Default warning handler for invalid column values.
|
|
106
|
-
* Logs to console in winkComposer format.
|
|
107
|
-
*
|
|
108
|
-
* Validation behavior:
|
|
109
|
-
* - null/undefined columns: skip column only (QuestDB stores NULL)
|
|
110
|
-
* - NaN/Infinity in numeric columns: skip column only (QuestDB stores NULL)
|
|
111
|
-
* - Wrong-typed column values (e.g. a number where the column is string-typed):
|
|
112
|
-
* skip column only, never coerce — the warning names the expected and
|
|
113
|
-
* received types
|
|
114
|
-
* - Invalid designatedTimestamp: skip entire row
|
|
115
|
-
*
|
|
116
|
-
* For strict mode (throw on any invalid), provide:
|
|
117
|
-
* { onWarning: (msg) => { throw new Error(msg); } }
|
|
118
|
-
*
|
|
119
|
-
* Future extension: return value may control skip-row vs skip-column behavior.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} message - Warning message describing the issue
|
|
122
|
-
*/
|
|
123
|
-
const defaultOnWarning = function ( message ) {
|
|
124
|
-
logger.warn( `winkComposer/questdb: ${message}` );
|
|
125
|
-
};
|
|
126
|
-
|
|
127
115
|
/**
|
|
128
116
|
* Check if column type requires numeric validation (NaN/Infinity check).
|
|
129
117
|
*
|
|
@@ -166,29 +154,6 @@ ACCEPTS.bool = ( value ) => typeof value === 'boolean';
|
|
|
166
154
|
*/
|
|
167
155
|
const acceptAny = () => true;
|
|
168
156
|
|
|
169
|
-
/**
|
|
170
|
-
* Builds the reason text for a value that failed its phase-1 acceptance
|
|
171
|
-
* check. Called only on the rare skip path, so its allocations are
|
|
172
|
-
* acceptable (same budget as the error returns in index.js).
|
|
173
|
-
*
|
|
174
|
-
* @param {*} rawValue - the rejected value
|
|
175
|
-
* @param {boolean} isNumeric - whether the column type is numeric
|
|
176
|
-
* @param {string} expectedType - declared column type, named in the message
|
|
177
|
-
* @returns {string} plain reason text for the skip warning
|
|
178
|
-
*/
|
|
179
|
-
const skipReason = function ( rawValue, isNumeric, expectedType ) {
|
|
180
|
-
if ( rawValue === null ) return 'null';
|
|
181
|
-
if ( rawValue === undefined ) return 'undefined';
|
|
182
|
-
if ( isNumeric && typeof rawValue === 'number' ) {
|
|
183
|
-
if ( Number.isNaN( rawValue ) ) return 'NaN';
|
|
184
|
-
// A finite number can only be rejected by the integer-required
|
|
185
|
-
// types (int64/timestamp/designated timestamp) — float64 accepts
|
|
186
|
-
// every finite number.
|
|
187
|
-
return Number.isFinite( rawValue ) ? 'non-integer' : 'non-finite';
|
|
188
|
-
}
|
|
189
|
-
return `wrong-typed (expected ${expectedType}, received ${typeof rawValue})`;
|
|
190
|
-
}; // skipReason()
|
|
191
|
-
|
|
192
157
|
/**
|
|
193
158
|
* Asserts every ILP name a plan will write — the table name and each column
|
|
194
159
|
* name — by driving them once through a throwaway client buffer, so the
|
|
@@ -264,11 +229,11 @@ const assertIlpNames = function ( tableName, columnNames ) {
|
|
|
264
229
|
* @param {string} tablePrefix - Prefix for table names (typically assetClass.name)
|
|
265
230
|
* @param {Object} [options] - Optional configuration
|
|
266
231
|
* @param {function} [options.onWarning] - Soft per-row warning callback for
|
|
267
|
-
* invalid values (NaN, null, invalid timestamp). Default:
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
232
|
+
* invalid values (NaN, null, invalid timestamp). Default: one
|
|
233
|
+
* `logger.warn` line per skip, bounded per column (see the file header).
|
|
234
|
+
* @param {function} [options.onDeliveryFailure] - Called as
|
|
235
|
+
* `( err, { tableName } )` when `sender.at()` rejects (the client's
|
|
236
|
+
* byte ceiling). Default: one classified DELIVERY_FAILED console line.
|
|
272
237
|
* @returns {Object.<string, function>} Map of insightType to persistRow function
|
|
273
238
|
*/
|
|
274
239
|
const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
@@ -287,16 +252,19 @@ const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
|
287
252
|
throw err;
|
|
288
253
|
}
|
|
289
254
|
|
|
290
|
-
const onWarning = providedOnWarning || defaultOnWarning;
|
|
291
255
|
// onWarning stays raw on purpose. A strict-mode onWarning throws,
|
|
292
256
|
// and that throw is the instruction that rejects the row. ADR-027
|
|
293
257
|
// keeps such callbacks — ones whose throw or return the adapter
|
|
294
|
-
// acts on — out of the guard's scope.
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
//
|
|
299
|
-
//
|
|
258
|
+
// acts on — out of the guard's scope. The skip sites reach it
|
|
259
|
+
// through the pre-built warnings (`skip-warnings.js`); this raw
|
|
260
|
+
// handler serves the once-per-type mismatch warning below.
|
|
261
|
+
const onWarning = providedOnWarning || defaultOnWarning;
|
|
262
|
+
//
|
|
263
|
+
// onDeliveryFailure only notifies, so the shared guard arms it. It
|
|
264
|
+
// was validated raw above and is wrapped once here (ADR-018: a
|
|
265
|
+
// broken handler costs its own output, never the flush chain).
|
|
266
|
+
// Absent stays null, so the no-handler DELIVERY_FAILED escape
|
|
267
|
+
// hatch below keeps its exact meaning.
|
|
300
268
|
const onDeliveryFailure = wrapCallback( providedOnDeliveryFailure || null, {
|
|
301
269
|
name: 'onDeliveryFailure', severity: 'red', report: reportCallbackFault
|
|
302
270
|
} );
|
|
@@ -316,10 +284,12 @@ const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
|
316
284
|
// column itself, from the partition id. A persisted column with
|
|
317
285
|
// the same name would make the CREATE TABLE ask for 'assetId'
|
|
318
286
|
// twice. That happens whether the name appears in the columns
|
|
319
|
-
// list or as the designated timestamp.
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
287
|
+
// list or as the designated timestamp.
|
|
288
|
+
//
|
|
289
|
+
// Fail fast here and name the fix. Otherwise QuestDB answers at
|
|
290
|
+
// table creation with its raw "Duplicate column" error. A
|
|
291
|
+
// dictionary column named 'assetId' that no insightType
|
|
292
|
+
// persists stays legal.
|
|
323
293
|
if ( persistedColumnNames.includes( 'assetId' ) || designatedTimestamp === 'assetId' ) {
|
|
324
294
|
const err = new Error(
|
|
325
295
|
`winkComposer/questdb: insightType '${insightTypeName}' uses reserved column name 'assetId' — ` +
|
|
@@ -330,31 +300,34 @@ const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
|
330
300
|
throw err;
|
|
331
301
|
}
|
|
332
302
|
|
|
333
|
-
// Pre-compile writer steps
|
|
334
|
-
//
|
|
335
|
-
//
|
|
303
|
+
// Pre-compile the writer steps. The designated timestamp is
|
|
304
|
+
// handled separately, via sender.at(). Every other column uses
|
|
305
|
+
// its type-specific writer.
|
|
336
306
|
const stepNames = [];
|
|
337
307
|
const stepWriters = [];
|
|
338
|
-
const stepIsNumeric = [];
|
|
339
|
-
const stepTypes = [];
|
|
340
308
|
const stepAccepts = [];
|
|
309
|
+
// One skip warning per column, pre-built here so the per-row
|
|
310
|
+
// path only calls it (see the file header for the bound).
|
|
311
|
+
const stepWarnings = [];
|
|
341
312
|
|
|
342
313
|
for ( let j = 0; j < persistedColumnNames.length; j += 1 ) {
|
|
343
314
|
const columnName = persistedColumnNames[ j ];
|
|
344
315
|
|
|
345
|
-
// Skip
|
|
316
|
+
// Skip the designated timestamp. It is handled separately, via
|
|
317
|
+
// sender.at().
|
|
346
318
|
if ( columnName !== designatedTimestamp ) {
|
|
347
319
|
const columnSpec = columns[ columnName ];
|
|
348
320
|
const columnType = columnSpec ? columnSpec.type : 'string';
|
|
349
321
|
|
|
350
322
|
stepNames.push( columnName );
|
|
351
|
-
stepIsNumeric.push( isNumericType( columnType ) );
|
|
352
|
-
stepTypes.push( columnType );
|
|
353
323
|
stepAccepts.push( ACCEPTS[ columnType ] || acceptAny );
|
|
324
|
+
stepWarnings.push( makeColumnWarning(
|
|
325
|
+
providedOnWarning, insightTypeName, columnName, isNumericType( columnType ), columnType
|
|
326
|
+
) );
|
|
354
327
|
|
|
355
|
-
// float64 columns use resolution-aware writer factory
|
|
356
|
-
//
|
|
357
|
-
//
|
|
328
|
+
// float64 columns use the resolution-aware writer factory.
|
|
329
|
+
// columnSpec exists here: columnType is 'float64' only when
|
|
330
|
+
// columnSpec.type is 'float64'.
|
|
358
331
|
if ( columnType === 'float64' ) {
|
|
359
332
|
stepWriters.push( createFloat64Writer( columnSpec.resolution ) );
|
|
360
333
|
} else {
|
|
@@ -365,6 +338,28 @@ const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
|
365
338
|
|
|
366
339
|
const stepCount = stepNames.length;
|
|
367
340
|
const tableName = tablePrefix + '_' + insightTypeName;
|
|
341
|
+
const warnRowSkipped = makeRowWarning( providedOnWarning, insightTypeName, designatedTimestamp );
|
|
342
|
+
|
|
343
|
+
// One rejection handler and one shared context per insight
|
|
344
|
+
// type, built here so the per-row path allocates nothing. See
|
|
345
|
+
// the file header for the context's shape and what a rejection
|
|
346
|
+
// means.
|
|
347
|
+
const appendContext = {
|
|
348
|
+
trigger: 'append',
|
|
349
|
+
rowsLost: 1,
|
|
350
|
+
abandoned: false,
|
|
351
|
+
probe: null,
|
|
352
|
+
tableName
|
|
353
|
+
};
|
|
354
|
+
const onAppendRejected = function ( err ) {
|
|
355
|
+
if ( onDeliveryFailure ) {
|
|
356
|
+
onDeliveryFailure( err, appendContext );
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
logger.error(
|
|
360
|
+
`winkComposer/questdb: row append failed for table '${tableName}' [DELIVERY_FAILED]: ${err.message}`
|
|
361
|
+
);
|
|
362
|
+
}; // onAppendRejected()
|
|
368
363
|
// Fail-fast at startup: a bad table or column name would otherwise
|
|
369
364
|
// throw inside the client mid-row at write time.
|
|
370
365
|
assertIlpNames( tableName, stepNames );
|
|
@@ -378,36 +373,31 @@ const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
|
378
373
|
// One report carries all the information.
|
|
379
374
|
let assetIdMismatchWarned = false;
|
|
380
375
|
|
|
381
|
-
// Create closure that captures pre-resolved references
|
|
382
|
-
// Interface: persistRow(sender, message, partitionId) -> boolean
|
|
383
|
-
//
|
|
384
|
-
// false when phase 1 skipped the whole row
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
376
|
+
// Create the closure that captures the pre-resolved references.
|
|
377
|
+
// Interface: persistRow(sender, message, partitionId) -> boolean.
|
|
378
|
+
// It returns true when a row was opened and completed on the
|
|
379
|
+
// sender. It returns false when phase 1 skipped the whole row,
|
|
380
|
+
// so the sender was never touched.
|
|
381
|
+
//
|
|
382
|
+
// The caller's buffered-row accounting keys off that return. A
|
|
383
|
+
// skipped row must not count as buffered. Note that partitionId
|
|
384
|
+
// is the internal name. It is written as the 'assetId' column
|
|
385
|
+
// in QuestDB.
|
|
388
386
|
plansByInsightType[ insightTypeName ] = function ( sender, message, partitionId ) {
|
|
389
387
|
// ---- Phase 1: validate. No sender calls — nothing irreversible
|
|
390
388
|
// happens until every value has been checked (see file header).
|
|
391
389
|
|
|
392
|
-
//
|
|
390
|
+
// The designated timestamp first. When it is missing or not
|
|
391
|
+
// an integer, skip the entire row. Integer or bigint matches
|
|
392
|
+
// the client's own .at() validation. Probe-verified: at(
|
|
393
|
+
// ...000.5, 'ms' ) throws "Designated timestamp must be an
|
|
394
|
+
// integer or BigInt". It throws AFTER the whole row is
|
|
395
|
+
// written, so catching it here is the only place the row
|
|
396
|
+
// survives intact. A null or undefined value fails the same
|
|
397
|
+
// check, and the warning names which it was.
|
|
393
398
|
const tsValue = message[ designatedTimestamp ];
|
|
394
|
-
if ( tsValue === undefined || tsValue === null ) {
|
|
395
|
-
onWarning(
|
|
396
|
-
`designatedTimestamp '${designatedTimestamp}' is ${tsValue === null ? 'null' : 'undefined'} ` +
|
|
397
|
-
`in insightType '${insightTypeName}' (asset: ${partitionId}) - row skipped`
|
|
398
|
-
);
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
|
-
// Integer-or-bigint, matching the client's own .at() validation
|
|
402
|
-
// (probe-verified: at( ...000.5, 'ms' ) throws "Designated
|
|
403
|
-
// timestamp must be an integer or BigInt" — and it throws AFTER
|
|
404
|
-
// the whole row is written, so catching it here is the only
|
|
405
|
-
// place the row survives intact).
|
|
406
399
|
if ( !Number.isInteger( tsValue ) && typeof tsValue !== 'bigint' ) {
|
|
407
|
-
|
|
408
|
-
`designatedTimestamp '${designatedTimestamp}' is ${skipReason( tsValue, true, 'timestamp' )} ` +
|
|
409
|
-
`in insightType '${insightTypeName}' (asset: ${partitionId}) - row skipped`
|
|
410
|
-
);
|
|
400
|
+
warnRowSkipped( tsValue, partitionId );
|
|
411
401
|
return false;
|
|
412
402
|
}
|
|
413
403
|
|
|
@@ -415,9 +405,10 @@ const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
|
415
405
|
// That column always stores the partition id. A record value
|
|
416
406
|
// that differs is almost always one mistake: trying to relabel
|
|
417
407
|
// identity in the record. It is reported once per insightType,
|
|
418
|
-
// never silently dropped (ADR-018 no-silent-failures).
|
|
419
|
-
//
|
|
420
|
-
//
|
|
408
|
+
// never silently dropped (ADR-018 no-silent-failures).
|
|
409
|
+
//
|
|
410
|
+
// The flag advances only after onWarning returns. So under
|
|
411
|
+
// strict mode, where onWarning throws, every mismatched row is
|
|
421
412
|
// rejected here with the sender untouched.
|
|
422
413
|
if ( !assetIdMismatchWarned && message.assetId !== undefined && message.assetId !== partitionId ) {
|
|
423
414
|
onWarning(
|
|
@@ -429,22 +420,20 @@ const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
|
429
420
|
}
|
|
430
421
|
|
|
431
422
|
// Check every column value against its declared type. A failed
|
|
432
|
-
// check marks the column for a silent skip in phase 2
|
|
433
|
-
// stores NULL
|
|
434
|
-
//
|
|
435
|
-
// (documented strict mode) rejects the row with the
|
|
436
|
-
// untouched instead of wedging it mid-row.
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
423
|
+
// check marks the column for a silent skip in phase 2, where
|
|
424
|
+
// QuestDB stores NULL. That is the treatment null already gets.
|
|
425
|
+
// The warning fires HERE, before the row opens. So an onWarning
|
|
426
|
+
// that throws (documented strict mode) rejects the row with the
|
|
427
|
+
// sender untouched instead of wedging it mid-row.
|
|
428
|
+
//
|
|
429
|
+
// Composer's NaN propagation ends here exactly as before. A
|
|
430
|
+
// NaN in a numeric column fails its acceptance check and lands
|
|
431
|
+
// as a NULL column while the row survives.
|
|
440
432
|
for ( let k = 0; k < stepCount; k += 1 ) {
|
|
441
433
|
const rawValue = message[ stepNames[ k ] ];
|
|
442
434
|
stepValueOk[ k ] = ( rawValue !== null ) && ( rawValue !== undefined ) && stepAccepts[ k ]( rawValue );
|
|
443
435
|
if ( !stepValueOk[ k ] ) {
|
|
444
|
-
|
|
445
|
-
`column '${stepNames[ k ]}' is ${skipReason( rawValue, stepIsNumeric[ k ], stepTypes[ k ] )} ` +
|
|
446
|
-
`in insightType '${insightTypeName}' (asset: ${partitionId}) — column skipped`
|
|
447
|
-
);
|
|
436
|
+
stepWarnings[ k ]( rawValue, partitionId );
|
|
448
437
|
}
|
|
449
438
|
}
|
|
450
439
|
|
|
@@ -461,41 +450,13 @@ const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
|
461
450
|
// about in phase 1 — before the row opened.
|
|
462
451
|
}
|
|
463
452
|
|
|
464
|
-
// Designated timestamp (ends the row in ILP).
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// sender.at() is async — the buffer mutation is sync but the
|
|
469
|
-
// trailing `await this.tryFlush()` may fire a network flush.
|
|
470
|
-
// When that flush fails (HTTP timeout, buffer overflow at the
|
|
471
|
-
// client's max_buf_size, QDB unreachable), the rows in that
|
|
472
|
-
// batch are dropped. Composer must NOT silently lose data.
|
|
473
|
-
//
|
|
474
|
-
// Routing rules:
|
|
475
|
-
// - If a caller provided `onDeliveryFailure`, route the
|
|
476
|
-
// failure to it. The caller owns the response (log,
|
|
477
|
-
// retry-budget, alert, stop the flow).
|
|
478
|
-
// - Otherwise, throw a classified `DELIVERY_FAILED` error.
|
|
479
|
-
// Inside a .catch() handler this surfaces as an
|
|
480
|
-
// unhandled rejection — Node logs it loudly and (15+)
|
|
481
|
-
// terminates the process. Loud failure beats silent
|
|
482
|
-
// loss every time.
|
|
453
|
+
// Designated timestamp (ends the row in ILP). sender.at() is
|
|
454
|
+
// async; its promise rejects when the append itself threw
|
|
455
|
+
// (see the file header). The prebuilt handler routes the
|
|
456
|
+
// rejection; a mock at() that returns no promise is skipped.
|
|
483
457
|
const atResult = sender.at( tsValue, 'ms' );
|
|
484
458
|
if ( atResult && typeof atResult.catch === 'function' ) {
|
|
485
|
-
atResult.catch(
|
|
486
|
-
if ( onDeliveryFailure ) {
|
|
487
|
-
onDeliveryFailure( err, { tableName } );
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
const failure = new Error(
|
|
491
|
-
`winkComposer/questdb: silent data loss — sender.at() async flush failed for table '${tableName}': ${err.message}. ` +
|
|
492
|
-
'Rows in this batch were dropped. Per the no-silent-failures contract this is a hard failure. ' +
|
|
493
|
-
'Provide an `onDeliveryFailure` callback in the storage config to handle these explicitly.'
|
|
494
|
-
);
|
|
495
|
-
failure.code = 'DELIVERY_FAILED';
|
|
496
|
-
failure.cause = err;
|
|
497
|
-
throw failure;
|
|
498
|
-
} );
|
|
459
|
+
atResult.catch( onAppendRejected );
|
|
499
460
|
}
|
|
500
461
|
|
|
501
462
|
return true;
|
|
@@ -509,4 +470,4 @@ const buildPersistPlans = function ( assetClass, tablePrefix, options ) {
|
|
|
509
470
|
// EXPORTS
|
|
510
471
|
// ============================================================================
|
|
511
472
|
|
|
512
|
-
export { buildPersistPlans
|
|
473
|
+
export { buildPersistPlans };
|