@sentio/runtime 4.3.5-rc.1 → 4.3.5-rc.3
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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{plugin-7YBR3bUA.d.ts → plugin-0w1rBRGy.d.ts} +34 -2
- package/dist/plugin-0w1rBRGy.d.ts.map +1 -0
- package/dist/processor-runner.js +1 -1
- package/dist/{service-v3-D6oVpUbL.js → service-v3-hRmfCtqr.js} +98 -1
- package/dist/{service-v3-D6oVpUbL.js.map → service-v3-hRmfCtqr.js.map} +1 -1
- package/package.json +1 -1
- package/src/db-context.ts +139 -1
- package/src/service-v3.ts +6 -0
- package/dist/plugin-7YBR3bUA.d.ts.map +0 -1
package/package.json
CHANGED
package/src/db-context.ts
CHANGED
|
@@ -36,8 +36,69 @@ type ProcessStreamResponseV3Init = MessageInitShape<typeof ProcessStreamResponse
|
|
|
36
36
|
type Request = NonNullable<MessageInitShape<typeof DBRequestSchema>['op']>
|
|
37
37
|
type RequestType = NonNullable<Request['case']>
|
|
38
38
|
|
|
39
|
+
// Both ends of a stack carry the useful information, so a deep one is trimmed in the
|
|
40
|
+
// middle rather than at the tail: the top frames are where the late call was actually
|
|
41
|
+
// made (runtime frames are already trimmed off by captureStackTrace), and the bottom
|
|
42
|
+
// ones are the entry point that led there — which is the closest thing to naming the
|
|
43
|
+
// handler that the stack can offer, on the occasions it has not been lost.
|
|
44
|
+
const MAX_HEAD_FRAMES = 8
|
|
45
|
+
const MAX_TAIL_FRAMES = 4
|
|
46
|
+
|
|
47
|
+
// V8 keeps only the innermost `Error.stackTraceLimit` frames — 10 by default — so on a
|
|
48
|
+
// deep stack the entry point is discarded before we ever see it. Raised just for this
|
|
49
|
+
// capture, since keeping both ends is the entire point, and restored immediately after.
|
|
50
|
+
const STACK_CAPTURE_DEPTH = 50
|
|
51
|
+
|
|
39
52
|
export const timeoutError = new Error('timeout')
|
|
40
53
|
|
|
54
|
+
// Name the entity and id, not just the op kind: "get BinanceAlphaPriceEntity 56-0x…"
|
|
55
|
+
// usually points straight at the call site, while a bare "get" rarely does.
|
|
56
|
+
function describeRequest(request: Request): string {
|
|
57
|
+
switch (request.case) {
|
|
58
|
+
case 'get':
|
|
59
|
+
return `get ${request.value.entity} ${request.value.id}`
|
|
60
|
+
case 'list':
|
|
61
|
+
return `list ${request.value.entity}`
|
|
62
|
+
case 'upsert':
|
|
63
|
+
case 'update':
|
|
64
|
+
case 'delete': {
|
|
65
|
+
const entities = [...new Set(request.value.entity ?? [])].join(',')
|
|
66
|
+
return `${request.case} ${entities} (${request.value.id?.length ?? 0} row(s))`
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
// The oneof can be unset, in which case `case` is undefined.
|
|
70
|
+
return request.case ?? 'op'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Keep both ends of a stack, dropping the middle when it is too long to inline.
|
|
76
|
+
*
|
|
77
|
+
* A diagnostic must not grow with the depth of what it describes, but truncating from one
|
|
78
|
+
* end throws away half of what makes it useful: the top frames are where the late call was
|
|
79
|
+
* made, the bottom ones the entry point that led there.
|
|
80
|
+
*
|
|
81
|
+
* Node's own internals are dropped first so the budget goes to frames the reader can act
|
|
82
|
+
* on — otherwise a deep call inside the test runner or the processor host fills the tail
|
|
83
|
+
* with `node:internal/...` and pushes the user's entry point into the omitted middle.
|
|
84
|
+
* V8-synthesised markers like `async Promise.all (index 3)` are not `node:` frames and are
|
|
85
|
+
* kept; they are often the single most informative line.
|
|
86
|
+
*/
|
|
87
|
+
function boundedFrames(frames: string[]): string[] {
|
|
88
|
+
const ownCode = frames.filter((frame) => !frame.includes('node:'))
|
|
89
|
+
// If everything was internal there is nothing better to show.
|
|
90
|
+
const usable = ownCode.length ? ownCode : frames
|
|
91
|
+
if (usable.length <= MAX_HEAD_FRAMES + MAX_TAIL_FRAMES) {
|
|
92
|
+
return usable
|
|
93
|
+
}
|
|
94
|
+
const omitted = usable.length - MAX_HEAD_FRAMES - MAX_TAIL_FRAMES
|
|
95
|
+
return [
|
|
96
|
+
...usable.slice(0, MAX_HEAD_FRAMES),
|
|
97
|
+
` ... ${omitted} frame(s) omitted ...`,
|
|
98
|
+
...usable.slice(-MAX_TAIL_FRAMES)
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
|
|
41
102
|
export interface IStoreContext {
|
|
42
103
|
sendRequest(request: Request, timeoutSecs?: number): Promise<DBResponse>
|
|
43
104
|
|
|
@@ -64,7 +125,7 @@ export abstract class AbstractStoreContext implements IStoreContext {
|
|
|
64
125
|
// Set once the process has finished and the context was closed. Guards
|
|
65
126
|
// against a lingering batch timer emitting after the final result (which
|
|
66
127
|
// would both lose the write and desync the reused processor stream).
|
|
67
|
-
|
|
128
|
+
protected closed = false
|
|
68
129
|
|
|
69
130
|
constructor(readonly processId: number) {}
|
|
70
131
|
|
|
@@ -78,6 +139,10 @@ export abstract class AbstractStoreContext implements IStoreContext {
|
|
|
78
139
|
abstract doSend(resp: ProcessStreamResponseInit | ProcessStreamResponseV3Init): void
|
|
79
140
|
|
|
80
141
|
sendRequest(request: Request, timeoutSecs?: number): Promise<DBResponse> {
|
|
142
|
+
if (this.closed) {
|
|
143
|
+
return Promise.reject(this.reportLateMessage(`store ${describeRequest(request)}`, this.sendRequest))
|
|
144
|
+
}
|
|
145
|
+
|
|
81
146
|
if (STORE_BATCH_IDLE > 0 && STORE_BATCH_SIZE > 1 && request.case === 'upsert') {
|
|
82
147
|
// batch upsert if possible
|
|
83
148
|
return this.sendUpsertInBatch(request.value as DBRequest_DBUpsert)
|
|
@@ -169,6 +234,21 @@ export abstract class AbstractStoreContext implements IStoreContext {
|
|
|
169
234
|
this.doSend({ value: { case: 'result', value: errorResult }, processId })
|
|
170
235
|
}
|
|
171
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Mark the process finished. Must be called synchronously *immediately before*
|
|
239
|
+
* the final result is emitted — not from a later `.finally()`.
|
|
240
|
+
*
|
|
241
|
+
* Once the result is on the stream the driver may hand that stream to another
|
|
242
|
+
* process, so nothing may follow it. The gap between emitting the result and
|
|
243
|
+
* close() running is several microtasks wide, which is more than enough for a
|
|
244
|
+
* detached continuation to wake up and slip a request through: without this the
|
|
245
|
+
* observed outbound order is ["result", "dbRequest"], exactly what the guards
|
|
246
|
+
* exist to prevent. close() still does the teardown afterwards.
|
|
247
|
+
*/
|
|
248
|
+
finish() {
|
|
249
|
+
this.closed = true
|
|
250
|
+
}
|
|
251
|
+
|
|
172
252
|
close() {
|
|
173
253
|
this.closed = true
|
|
174
254
|
// Drop any un-flushed batch and cancel its timer so it can never emit
|
|
@@ -266,6 +346,53 @@ export abstract class AbstractStoreContext implements IStoreContext {
|
|
|
266
346
|
}
|
|
267
347
|
}
|
|
268
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Report a message the process tried to emit after it had already finished, and
|
|
351
|
+
* return the Error describing it.
|
|
352
|
+
*
|
|
353
|
+
* Such a message cannot be delivered: the final result was already sent, and the
|
|
354
|
+
* stream it would go out on has been handed back to the pool and may now belong to
|
|
355
|
+
* another process — writing to it makes the driver fail *that* process with ERR200
|
|
356
|
+
* "unexpected ProcessID". Dropping it is the only safe option.
|
|
357
|
+
*
|
|
358
|
+
* This always logs rather than relying solely on the rejection, because the late
|
|
359
|
+
* call is nearly always on a path that already swallows errors — the classic case
|
|
360
|
+
* being one `Promise.all` branch rejecting while its siblings keep running — so the
|
|
361
|
+
* rejection alone would be invisible. The Error is built here, at the call site, so
|
|
362
|
+
* its stack points into the handler code that failed to await.
|
|
363
|
+
*
|
|
364
|
+
* `boundary` is the runtime entry point the caller came through; its frame and
|
|
365
|
+
* everything below it is trimmed from the stack so the very first line the user
|
|
366
|
+
* reads is their own code rather than two frames of ours.
|
|
367
|
+
*/
|
|
368
|
+
protected reportLateMessage(what: string, boundary?: (...args: never[]) => unknown): Error {
|
|
369
|
+
const summary =
|
|
370
|
+
`[sentio] ${what} was issued after process ${this.processId} had already finished, so it was ` +
|
|
371
|
+
`dropped. Something in the handler was still running after the handler returned — every store ` +
|
|
372
|
+
`write, store read and metric must be awaited before the handler completes. A common cause is ` +
|
|
373
|
+
`one Promise.all() branch rejecting while its siblings keep running: Promise.all rejects ` +
|
|
374
|
+
`immediately but does NOT cancel the others, so give each branch its own .catch() (or use ` +
|
|
375
|
+
`Promise.allSettled) to keep them inside the handler.`
|
|
376
|
+
|
|
377
|
+
const previousLimit = Error.stackTraceLimit
|
|
378
|
+
Error.stackTraceLimit = Math.max(previousLimit, STACK_CAPTURE_DEPTH)
|
|
379
|
+
const err = new Error(summary)
|
|
380
|
+
Error.captureStackTrace?.(err, boundary ?? this.reportLateMessage)
|
|
381
|
+
// Read `stack` before restoring: the frames are captured at construction, but forcing
|
|
382
|
+
// the (lazy) formatting here keeps this independent of when anyone reads it.
|
|
383
|
+
const captured = err.stack ?? ''
|
|
384
|
+
Error.stackTraceLimit = previousLimit
|
|
385
|
+
|
|
386
|
+
// Inline the frames into `message`. The datasource log view renders only the message,
|
|
387
|
+
// so a stack left in `err.stack` alone is invisible to the user who has to act on it.
|
|
388
|
+
const frames = boundedFrames(captured.split('\n').slice(1))
|
|
389
|
+
if (frames.length) {
|
|
390
|
+
err.message = `${summary}\nIssued at:\n${frames.join('\n')}`
|
|
391
|
+
}
|
|
392
|
+
console.error(err)
|
|
393
|
+
return err
|
|
394
|
+
}
|
|
395
|
+
|
|
269
396
|
async awaitPendings() {
|
|
270
397
|
// Flush any buffered upsert batch and wait for its ack before returning.
|
|
271
398
|
// Callers use this to guarantee every write has been sent AND applied by
|
|
@@ -308,6 +435,13 @@ export class DataBindingContext extends AbstractStoreContext implements IDataBin
|
|
|
308
435
|
}
|
|
309
436
|
|
|
310
437
|
sendTemplateRequest(templates: Array<TemplateInstance>, unbind: boolean) {
|
|
438
|
+
if (this.closed) {
|
|
439
|
+
this.reportLateMessage(
|
|
440
|
+
`${unbind ? 'unbind' : 'bind'} of ${templates.length} template instance(s)`,
|
|
441
|
+
this.sendTemplateRequest
|
|
442
|
+
)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
311
445
|
this.subject.next({
|
|
312
446
|
processId: this.processId,
|
|
313
447
|
value: {
|
|
@@ -320,6 +454,10 @@ export class DataBindingContext extends AbstractStoreContext implements IDataBin
|
|
|
320
454
|
})
|
|
321
455
|
}
|
|
322
456
|
sendTimeseriesRequest(timeseries: Array<TimeseriesResult>) {
|
|
457
|
+
if (this.closed) {
|
|
458
|
+
this.reportLateMessage(`${timeseries.length} timeseries record(s)`, this.sendTimeseriesRequest)
|
|
459
|
+
return
|
|
460
|
+
}
|
|
323
461
|
this.subject.next({
|
|
324
462
|
processId: this.processId,
|
|
325
463
|
value: {
|
package/src/service-v3.ts
CHANGED
|
@@ -181,6 +181,11 @@ export class ProcessorServiceImplV3 implements ServiceImpl<typeof ProcessorV3> {
|
|
|
181
181
|
const otherResults = clone(ProcessResultSchema, result)
|
|
182
182
|
otherResults.timeseriesResult = []
|
|
183
183
|
|
|
184
|
+
// Close the context to further messages in the same synchronous step that
|
|
185
|
+
// emits the result: after this point the stream may be recycled to another
|
|
186
|
+
// process, and waiting for the .finally() below would leave a microtask
|
|
187
|
+
// window in which a detached continuation could still append to it.
|
|
188
|
+
context.finish()
|
|
184
189
|
subject.next({
|
|
185
190
|
processId,
|
|
186
191
|
value: {
|
|
@@ -191,6 +196,7 @@ export class ProcessorServiceImplV3 implements ServiceImpl<typeof ProcessorV3> {
|
|
|
191
196
|
})
|
|
192
197
|
.catch((e) => {
|
|
193
198
|
console.error(e, e.stack)
|
|
199
|
+
context.finish() // same reasoning as the success path — error() emits a result too
|
|
194
200
|
context.error(processId, e)
|
|
195
201
|
process_binding_error.add(1)
|
|
196
202
|
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-7YBR3bUA.d.ts","names":[],"sources":["../src/db-context.ts","../src/plugin.ts"],"mappings":"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"}
|