@kronos-ts/axon-server 0.4.0 → 0.6.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/dist/axon-server-event-store.d.ts +0 -16
- package/dist/axon-server-event-store.d.ts.map +1 -1
- package/dist/axon-server-event-store.js +73 -45
- package/dist/axon-server-event-store.js.map +1 -1
- package/dist/axon-server-snapshotting-event-store.d.ts +30 -0
- package/dist/axon-server-snapshotting-event-store.d.ts.map +1 -0
- package/dist/axon-server-snapshotting-event-store.js +154 -0
- package/dist/axon-server-snapshotting-event-store.js.map +1 -0
- package/dist/axon-server.d.ts +46 -65
- package/dist/axon-server.d.ts.map +1 -1
- package/dist/axon-server.js +490 -317
- package/dist/axon-server.js.map +1 -1
- package/dist/bounded-read.d.ts +19 -0
- package/dist/bounded-read.d.ts.map +1 -0
- package/dist/bounded-read.js +39 -0
- package/dist/bounded-read.js.map +1 -0
- package/dist/connection-manager.d.ts +2 -2
- package/dist/connection-manager.d.ts.map +1 -1
- package/dist/connection.d.ts +36 -21
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +84 -45
- package/dist/connection.js.map +1 -1
- package/dist/context-view.d.ts +2 -2
- package/dist/context-view.d.ts.map +1 -1
- package/dist/control-plane.d.ts +14 -24
- package/dist/control-plane.d.ts.map +1 -1
- package/dist/control-plane.js +6 -37
- package/dist/control-plane.js.map +1 -1
- package/dist/event-processor-info.d.ts +7 -25
- package/dist/event-processor-info.d.ts.map +1 -1
- package/dist/event-processor-info.js +16 -20
- package/dist/event-processor-info.js.map +1 -1
- package/dist/flow-controlled-sender.d.ts +2 -2
- package/dist/flow-controlled-sender.d.ts.map +1 -1
- package/dist/flow-controlled-sender.js +37 -17
- package/dist/flow-controlled-sender.js.map +1 -1
- package/dist/generated/command.d.ts +20 -20
- package/dist/generated/command.d.ts.map +1 -1
- package/dist/generated/common.d.ts +20 -20
- package/dist/generated/common.d.ts.map +1 -1
- package/dist/generated/control.d.ts +36 -40
- package/dist/generated/control.d.ts.map +1 -1
- package/dist/generated/dcb.d.ts +88 -94
- package/dist/generated/dcb.d.ts.map +1 -1
- package/dist/generated/event.d.ts +81 -84
- package/dist/generated/event.d.ts.map +1 -1
- package/dist/generated/google/protobuf/empty.d.ts +3 -4
- package/dist/generated/google/protobuf/empty.d.ts.map +1 -1
- package/dist/generated/query.d.ts +40 -40
- package/dist/generated/query.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/message-size.d.ts +2 -2
- package/dist/message-size.d.ts.map +1 -1
- package/dist/outbound-stream.d.ts +7 -11
- package/dist/outbound-stream.d.ts.map +1 -1
- package/dist/outbound-stream.js +65 -16
- package/dist/outbound-stream.js.map +1 -1
- package/dist/platform-service.d.ts +14 -5
- package/dist/platform-service.d.ts.map +1 -1
- package/dist/platform-service.js +70 -10
- package/dist/platform-service.js.map +1 -1
- package/dist/resilience.d.ts +62 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +103 -0
- package/dist/resilience.js.map +1 -0
- package/dist/shutdown-latch.d.ts +5 -4
- package/dist/shutdown-latch.d.ts.map +1 -1
- package/dist/shutdown-latch.js +20 -1
- package/dist/shutdown-latch.js.map +1 -1
- package/dist/stream-recovery.d.ts +9 -0
- package/dist/stream-recovery.d.ts.map +1 -0
- package/dist/stream-recovery.js +70 -0
- package/dist/stream-recovery.js.map +1 -0
- package/package.json +3 -3
- package/src/axon-server-event-store.ts +77 -47
- package/src/axon-server-snapshotting-event-store.ts +194 -0
- package/src/axon-server.ts +472 -374
- package/src/bounded-read.ts +43 -0
- package/src/connection-manager.ts +1 -1
- package/src/connection.ts +93 -59
- package/src/context-view.ts +1 -1
- package/src/control-plane.ts +22 -66
- package/src/event-processor-info.ts +23 -44
- package/src/flow-controlled-sender.ts +34 -15
- package/src/generated/command.ts +10 -10
- package/src/generated/common.ts +10 -10
- package/src/generated/control.ts +20 -20
- package/src/generated/dcb.ts +47 -47
- package/src/generated/event.ts +42 -42
- package/src/generated/google/protobuf/empty.ts +2 -2
- package/src/generated/query.ts +20 -20
- package/src/index.ts +1 -2
- package/src/message-size.ts +1 -1
- package/src/outbound-stream.ts +58 -28
- package/src/platform-service.ts +77 -13
- package/src/resilience.ts +149 -0
- package/src/shutdown-latch.ts +16 -3
- package/src/stream-recovery.ts +72 -0
- package/dist/axon-server-snapshot-store.d.ts +0 -12
- package/dist/axon-server-snapshot-store.d.ts.map +0 -1
- package/dist/axon-server-snapshot-store.js +0 -82
- package/dist/axon-server-snapshot-store.js.map +0 -1
- package/src/axon-server-snapshot-store.ts +0 -110
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A read that never answers is cancelled and asked once more.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. Under Bun, a gRPC call whose response has fully arrived —
|
|
5
|
+
* headers, the message, trailers carrying an OK status, the HTTP/2 stream
|
|
6
|
+
* closed cleanly — can still never complete on the client. grpc-js releases
|
|
7
|
+
* an OK status only once the stream's `end` event has fired, and Bun's http2
|
|
8
|
+
* client intermittently never emits it (Node does). The Axon integration
|
|
9
|
+
* suites hit it about one run in four on a loaded machine, on `source` and on
|
|
10
|
+
* the snapshot store's `getLast`; the command handler above the read then
|
|
11
|
+
* never replies, and Axon Server cancels the command at its own 300 s timeout.
|
|
12
|
+
*
|
|
13
|
+
* A deadline is the one lever this side of grpc-js: a cancelled call DOES
|
|
14
|
+
* surface, because a non-OK status needs no `end`. The reads this guards are
|
|
15
|
+
* idempotent, so the answer to a lost one is simply to ask again. A second
|
|
16
|
+
* loss is reported rather than retried forever.
|
|
17
|
+
*/
|
|
18
|
+
export async function boundedRead<T>(
|
|
19
|
+
deadlineMs: number,
|
|
20
|
+
read: (signal: AbortSignal) => Promise<T>,
|
|
21
|
+
): Promise<T> {
|
|
22
|
+
for (let attempt = 1; ; attempt++) {
|
|
23
|
+
const cancel = new AbortController()
|
|
24
|
+
const timer = setTimeout(() => cancel.abort(), deadlineMs)
|
|
25
|
+
try {
|
|
26
|
+
return await read(cancel.signal)
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (!cancel.signal.aborted) throw err
|
|
29
|
+
if (attempt === 2) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Axon Server read did not complete within ${deadlineMs} ms, twice in a row`,
|
|
32
|
+
{ cause: err },
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
console.warn(
|
|
36
|
+
`Axon Server read did not complete within ${deadlineMs} ms; cancelled it and asking again ` +
|
|
37
|
+
"(a known Bun http2 client fault: the stream's end event is lost, see bounded-read.ts)",
|
|
38
|
+
)
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timer)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -22,7 +22,7 @@ import { connectToAxonServer } from "./connection.js"
|
|
|
22
22
|
* await manager.disconnectAll()
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
export
|
|
25
|
+
export type AxonServerConnectionManager = {
|
|
26
26
|
/**
|
|
27
27
|
* Get or create a connection for the given context.
|
|
28
28
|
* Connections are created lazily and cached.
|
package/src/connection.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { withMessagingTimeout } from "@kronos-ts/core"
|
|
1
2
|
import {
|
|
2
3
|
createChannel,
|
|
3
|
-
createClient,
|
|
4
|
+
waitForChannelReady, createClient,
|
|
4
5
|
type Channel,
|
|
5
6
|
type Client,
|
|
6
7
|
type ChannelCredentials,
|
|
7
8
|
} from "nice-grpc"
|
|
8
9
|
import { ChannelCredentials as GrpcChannelCredentials } from "@grpc/grpc-js"
|
|
9
10
|
import { readFileSync } from "node:fs"
|
|
10
|
-
import
|
|
11
|
+
import type { Serializer } from "@kronos-ts/core"
|
|
12
|
+
import { withRetry, healthCheck, type ResilienceConfig } from "./resilience.js"
|
|
11
13
|
import { PlatformServiceDefinition } from "./generated/control.js"
|
|
12
14
|
import { CommandServiceDefinition } from "./generated/command.js"
|
|
13
15
|
import { QueryServiceDefinition } from "./generated/query.js"
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
/**
|
|
23
25
|
* Configuration for connecting to Axon Server.
|
|
24
26
|
*/
|
|
25
|
-
export
|
|
27
|
+
export type AxonServerConnectionConfig = {
|
|
26
28
|
/**
|
|
27
29
|
* Host of the Axon Server. Defaults to "localhost".
|
|
28
30
|
* For single-server setups.
|
|
@@ -70,6 +72,17 @@ export interface AxonServerConnectionConfig {
|
|
|
70
72
|
* Default: true.
|
|
71
73
|
*/
|
|
72
74
|
keepAlivePermitWithoutCalls?: boolean
|
|
75
|
+
/**
|
|
76
|
+
* How long a read (`source`, the snapshot store's `getLast`, `getHead`) may
|
|
77
|
+
* take before it is cancelled and asked once more. A read whose answer has
|
|
78
|
+
* fully arrived can still never complete under Bun — its http2 client
|
|
79
|
+
* intermittently loses the stream's `end` event, and grpc-js releases an OK
|
|
80
|
+
* status only after it — so without a bound a command handler parks on the
|
|
81
|
+
* read until Axon Server cancels the command (300 s). Reads are idempotent;
|
|
82
|
+
* a lost one is simply repeated. Raise this if a single sourcing read of
|
|
83
|
+
* yours legitimately takes longer. Default: 15000.
|
|
84
|
+
*/
|
|
85
|
+
readTimeoutMs?: number
|
|
73
86
|
|
|
74
87
|
/**
|
|
75
88
|
* TLS/SSL configuration. When enabled, the connection uses a secure gRPC channel.
|
|
@@ -107,7 +120,7 @@ export type ConnectionState =
|
|
|
107
120
|
* An active connection to Axon Server, providing typed gRPC clients
|
|
108
121
|
* for all services. Supports reconnection on failure.
|
|
109
122
|
*/
|
|
110
|
-
export
|
|
123
|
+
export type AxonServerConnection = {
|
|
111
124
|
/** The underlying gRPC channel. */
|
|
112
125
|
readonly channel: Channel
|
|
113
126
|
/** Platform service — connection management, topology. */
|
|
@@ -164,6 +177,7 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
164
177
|
keepAliveTimeMs: config.keepAliveTimeMs ?? 30000,
|
|
165
178
|
keepAliveTimeoutMs: config.keepAliveTimeoutMs ?? 10000,
|
|
166
179
|
keepAlivePermitWithoutCalls: config.keepAlivePermitWithoutCalls ?? true,
|
|
180
|
+
readTimeoutMs: config.readTimeoutMs ?? 15000,
|
|
167
181
|
servers: config.servers,
|
|
168
182
|
ssl: config.ssl,
|
|
169
183
|
}
|
|
@@ -188,6 +202,12 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
188
202
|
"grpc.keepalive_time_ms": config.keepAliveTimeMs ?? 30000,
|
|
189
203
|
"grpc.keepalive_timeout_ms": config.keepAliveTimeoutMs ?? 10000,
|
|
190
204
|
"grpc.keepalive_permit_without_calls": (config.keepAlivePermitWithoutCalls ?? true) ? 1 : 0,
|
|
205
|
+
// Own subchannel pool per channel. grpc-js pools subchannels PROCESS-WIDE by
|
|
206
|
+
// target address by default, so a later connection to the same host:port —
|
|
207
|
+
// a fresh container on a reused mapped port, in a test process that already
|
|
208
|
+
// talked to another one — can be handed the previous server's HTTP/2
|
|
209
|
+
// session, and its unary calls hang until the server-side timeout.
|
|
210
|
+
"grpc.use_local_subchannel_pool": 1,
|
|
191
211
|
}
|
|
192
212
|
|
|
193
213
|
// Build server address list for failover
|
|
@@ -207,6 +227,9 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
207
227
|
|
|
208
228
|
let channel = createGrpcChannel()
|
|
209
229
|
let state: ConnectionState = "connected"
|
|
230
|
+
let reconnectPromise: Promise<void> | undefined
|
|
231
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined
|
|
232
|
+
let finishRetryDelay: (() => void) | undefined
|
|
210
233
|
|
|
211
234
|
const reconnectCallbacks: Array<() => void> = []
|
|
212
235
|
const disconnectCallbacks: Array<(error?: Error) => void> = []
|
|
@@ -257,52 +280,59 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
257
280
|
},
|
|
258
281
|
|
|
259
282
|
close() {
|
|
283
|
+
if (state === "closed") return
|
|
260
284
|
state = "closed"
|
|
285
|
+
for (const callback of disconnectCallbacks) {
|
|
286
|
+
try { callback() } catch { /* Listener failures cannot prevent teardown. */ }
|
|
287
|
+
}
|
|
288
|
+
clearTimeout(retryTimer)
|
|
289
|
+
finishRetryDelay?.()
|
|
261
290
|
channel.close()
|
|
262
291
|
},
|
|
263
292
|
|
|
264
|
-
|
|
265
|
-
if (state === "closed")
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} catch {
|
|
288
|
-
/* ignore listener errors */
|
|
293
|
+
reconnect() {
|
|
294
|
+
if (state === "closed") return Promise.reject(new Error("Connection is permanently closed"))
|
|
295
|
+
if (reconnectPromise) return reconnectPromise
|
|
296
|
+
reconnectPromise = (async () => {
|
|
297
|
+
state = "reconnecting"
|
|
298
|
+
channel.close()
|
|
299
|
+
for (const callback of disconnectCallbacks) {
|
|
300
|
+
try { callback() } catch { /* Listener failures cannot prevent recovery. */ }
|
|
301
|
+
}
|
|
302
|
+
let attempt = 0
|
|
303
|
+
while (state === "reconnecting") {
|
|
304
|
+
attempt++
|
|
305
|
+
try {
|
|
306
|
+
currentServerIndex++
|
|
307
|
+
channel = createGrpcChannel()
|
|
308
|
+
// Channel construction is lazy. Only report recovery once the new
|
|
309
|
+
// transport is ready, otherwise a dead server falsely "succeeds".
|
|
310
|
+
await waitForChannelReady(channel, new Date(Date.now() + resolvedConfig.keepAliveTimeoutMs))
|
|
311
|
+
if ((state as ConnectionState) === "closed") return
|
|
312
|
+
clients = createClients()
|
|
313
|
+
state = "connected"
|
|
314
|
+
for (const callback of reconnectCallbacks) {
|
|
315
|
+
try { callback() } catch { /* Listener failures are isolated. */ }
|
|
289
316
|
}
|
|
317
|
+
return
|
|
318
|
+
} catch (error) {
|
|
319
|
+
channel.close()
|
|
320
|
+
if ((state as ConnectionState) === "closed") return
|
|
321
|
+
if (resolvedConfig.maxReconnectAttempts > 0 && attempt >= resolvedConfig.maxReconnectAttempts) {
|
|
322
|
+
state = "disconnected"
|
|
323
|
+
throw new Error(`Failed to reconnect after ${attempt} attempts: ${error}`)
|
|
324
|
+
}
|
|
325
|
+
const backoff = Math.min(resolvedConfig.reconnectIntervalMs * 2 ** (attempt - 1), 30000)
|
|
326
|
+
await new Promise<void>((resolve) => {
|
|
327
|
+
finishRetryDelay = resolve
|
|
328
|
+
retryTimer = setTimeout(resolve, backoff * (0.75 + Math.random() * 0.5))
|
|
329
|
+
})
|
|
330
|
+
finishRetryDelay = undefined
|
|
290
331
|
}
|
|
291
|
-
return
|
|
292
|
-
} catch (err) {
|
|
293
|
-
if (maxAttempts > 0 && attempt >= maxAttempts) {
|
|
294
|
-
state = "disconnected"
|
|
295
|
-
throw new Error(`Failed to reconnect after ${attempt} attempts: ${err}`)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Exponential backoff: base interval * 2^attempt, capped at 30s
|
|
299
|
-
const delay = Math.min(
|
|
300
|
-
resolvedConfig.reconnectIntervalMs * Math.pow(2, attempt - 1),
|
|
301
|
-
30000,
|
|
302
|
-
)
|
|
303
|
-
await new Promise((r) => setTimeout(r, delay))
|
|
304
332
|
}
|
|
305
|
-
}
|
|
333
|
+
})().finally(() => { reconnectPromise = undefined })
|
|
334
|
+
return reconnectPromise
|
|
335
|
+
|
|
306
336
|
},
|
|
307
337
|
}
|
|
308
338
|
|
|
@@ -321,10 +351,12 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
321
351
|
* of a bus. Every event, snapshot, command payload and query result this
|
|
322
352
|
* process exchanges with Axon Server goes through the same codec, so it is
|
|
323
353
|
* named once, here — which is also what leaves
|
|
324
|
-
* `axonServerEventStore(conn, context)` and `axonServerCommandBus(
|
|
354
|
+
* `axonServerEventStore(conn, context)` and `axonServerCommandBus(local, conn)`
|
|
325
355
|
* their honest two-argument shapes.
|
|
326
356
|
*/
|
|
327
|
-
export
|
|
357
|
+
export type AxonServerConnectionOptions = AxonServerConnectionConfig & {
|
|
358
|
+
/** Maximum graceful drain time; transport closes even when this expires. Default: 30000ms. */
|
|
359
|
+
shutdownTimeoutMs?: number
|
|
328
360
|
/** Payload codec for every message this client exchanges with Axon Server. */
|
|
329
361
|
serializer: Serializer
|
|
330
362
|
/** Retry / health-check policy for the initial connect and stream re-establishment. */
|
|
@@ -350,13 +382,13 @@ export interface AxonServerConnectionOptions extends AxonServerConnectionConfig
|
|
|
350
382
|
* the codec. Narrower than the handle on purpose — a test can drive a store
|
|
351
383
|
* with a fake `connection` and nothing else.
|
|
352
384
|
*/
|
|
353
|
-
export
|
|
385
|
+
export type AxonServerStoreSource = {
|
|
354
386
|
readonly connection: AxonServerConnection
|
|
355
387
|
readonly serializer: Serializer
|
|
356
388
|
}
|
|
357
389
|
|
|
358
390
|
/** What a BUS borrows: a store's two, plus the drain latch and the retry policy. */
|
|
359
|
-
export
|
|
391
|
+
export type AxonServerBusSource = AxonServerStoreSource & {
|
|
360
392
|
/**
|
|
361
393
|
* The connection-wide drain latch. In-flight dispatches register on it and
|
|
362
394
|
* `close()` waits for them, so a bus never has the transport pulled out from
|
|
@@ -367,7 +399,7 @@ export interface AxonServerBusSource extends AxonServerStoreSource {
|
|
|
367
399
|
}
|
|
368
400
|
|
|
369
401
|
/** What the CONTROL PLANE borrows: the platform stream, and only that. */
|
|
370
|
-
export
|
|
402
|
+
export type AxonServerPlatformSource = {
|
|
371
403
|
readonly platform: PlatformConnection
|
|
372
404
|
}
|
|
373
405
|
|
|
@@ -376,12 +408,12 @@ export interface AxonServerPlatformSource {
|
|
|
376
408
|
* stream riding on it, and the lifecycle pair.
|
|
377
409
|
*
|
|
378
410
|
* The stores and buses are NOT on it — `axonServerEventStore(conn, context)`,
|
|
379
|
-
* `axonServerCommandBus(
|
|
411
|
+
* `axonServerCommandBus(local, conn)` and friends are plain functions over
|
|
380
412
|
* this, and a caller who wants only commands builds only that one. Multiple
|
|
381
413
|
* contexts share this ONE channel: the per-call `AxonIQ-Context` header is what
|
|
382
414
|
* separates them.
|
|
383
415
|
*/
|
|
384
|
-
export
|
|
416
|
+
export type AxonServerConnectionHandle = AxonServerBusSource & AxonServerPlatformSource & {
|
|
385
417
|
/** Config after defaults — the resolved host, context, client id. */
|
|
386
418
|
readonly config: AxonServerConnection["config"]
|
|
387
419
|
/**
|
|
@@ -410,13 +442,14 @@ export interface AxonServerConnectionHandle extends AxonServerBusSource, AxonSer
|
|
|
410
442
|
* serializer: jsonSerializer(),
|
|
411
443
|
* })
|
|
412
444
|
* const eventStore = axonServerEventStore(axon, "default")
|
|
413
|
-
* const
|
|
445
|
+
* const eventStore = axonServerSnapshottingEventStore(
|
|
446
|
+
* axonServerEventStore(axon, "default"), axon, "default")
|
|
414
447
|
* const commandBus = interceptingCommandBus(
|
|
415
|
-
* axonServerCommandBus(
|
|
448
|
+
* axonServerCommandBus(localCommandBus(unitOfWork), axon), correlation)
|
|
416
449
|
* const queryBus = interceptingQueryBus(
|
|
417
|
-
* axonServerQueryBus(
|
|
450
|
+
* axonServerQueryBus(localQueryBus(unitOfWork), axon), correlation)
|
|
418
451
|
*
|
|
419
|
-
* const app = kronos({
|
|
452
|
+
* const app = kronos({ commandHandlers, queryHandlers })
|
|
420
453
|
* await axon.start() // readiness barrier: the server can route to us
|
|
421
454
|
* // opt in to remote administration
|
|
422
455
|
* const control = await axonServerControlPlane(axon, app.processors.values())
|
|
@@ -514,11 +547,12 @@ export async function axonServerConnection(
|
|
|
514
547
|
},
|
|
515
548
|
|
|
516
549
|
async close() {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
550
|
+
try {
|
|
551
|
+
await withMessagingTimeout(shutdown.initiateShutdown(), options.shutdownTimeoutMs ?? 30000, "Messaging shutdown")
|
|
552
|
+
} finally {
|
|
553
|
+
platform.stop()
|
|
554
|
+
connection.close()
|
|
555
|
+
}
|
|
522
556
|
},
|
|
523
557
|
}
|
|
524
558
|
}
|
package/src/context-view.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { AxonServerStoreSource } from "./connection.js"
|
|
|
12
12
|
* channel, the codec or the drain latch differs between them; only this header
|
|
13
13
|
* does, which is why the whole difference fits in one small record.
|
|
14
14
|
*/
|
|
15
|
-
export
|
|
15
|
+
export type AxonServerContextView = AxonServerStoreSource & {
|
|
16
16
|
/** The Axon Server context every call through this view addresses. */
|
|
17
17
|
readonly context: string
|
|
18
18
|
/**
|
package/src/control-plane.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* Axon Server's admin surface needs from a client:
|
|
7
7
|
*
|
|
8
8
|
* 1. inbound — Axon Server pushes processor instructions (pause-processor,
|
|
9
|
-
* start-processor
|
|
10
|
-
*
|
|
9
|
+
* start-processor) which have to be routed to the live processor of that
|
|
10
|
+
* name; segment instructions (split, merge, release) are ignored, a kronos
|
|
11
|
+
* processor has one lane;
|
|
11
12
|
* 2. outbound — the client periodically reports each processor's status so
|
|
12
13
|
* the Axon Dashboard can render it.
|
|
13
14
|
*
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
*
|
|
20
21
|
* ```ts
|
|
21
22
|
* const axon = await axonServerConnection({ ... })
|
|
22
|
-
* const app = kronos({
|
|
23
|
+
* const app = kronos({ commandHandlers, queryHandlers, eventHandlers })
|
|
23
24
|
* await axon.start() // data path only
|
|
24
25
|
*
|
|
25
26
|
* // opt in to remote administration
|
|
@@ -39,47 +40,33 @@
|
|
|
39
40
|
* caller to get wrong.
|
|
40
41
|
*/
|
|
41
42
|
import type { AxonServerPlatformSource } from "./connection.js"
|
|
42
|
-
import type { ProcessorStatus
|
|
43
|
+
import type { ProcessorStatus } from "./event-processor-info.js"
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
|
-
* A processor Axon Server is allowed to observe and control.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `any` before poking at `start` / `stop` / `releaseSegment` / … — this is that
|
|
49
|
-
* cast, written down. Both `TrackingEventProcessor` and
|
|
50
|
-
* `StreamingEventProcessor` satisfy it structurally; anything else that can
|
|
51
|
-
* name itself and answer some of these calls does too. Every member past the
|
|
52
|
-
* name is optional because Axon Server asks for things a given processor kind
|
|
53
|
-
* may not implement (a subscribing processor has no segments), and the
|
|
54
|
-
* instruction handler simply skips what is absent.
|
|
46
|
+
* A processor Axon Server is allowed to observe and control. `RunningProcessor`
|
|
47
|
+
* satisfies it structurally; everything past `name` is optional so a foreign
|
|
48
|
+
* processor-shaped object is skipped rather than crashed on.
|
|
55
49
|
*/
|
|
56
|
-
export
|
|
50
|
+
export type ManagedEventProcessor = {
|
|
57
51
|
readonly name: string
|
|
58
52
|
readonly running?: boolean
|
|
59
53
|
readonly replaying?: boolean
|
|
60
54
|
readonly position?: bigint
|
|
61
55
|
start?(): Promise<void> | void
|
|
62
56
|
stop?(): void
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
readonly replaying?: boolean
|
|
70
|
-
readonly error?: Error
|
|
71
|
-
}
|
|
72
|
-
>
|
|
73
|
-
releaseSegment?(segmentId: number): Promise<unknown> | unknown
|
|
74
|
-
splitSegment?(segmentId: number): Promise<unknown> | unknown
|
|
75
|
-
mergeSegment?(segmentId: number): Promise<unknown> | unknown
|
|
57
|
+
status?(): {
|
|
58
|
+
readonly caughtUp?: boolean
|
|
59
|
+
readonly replaying?: boolean
|
|
60
|
+
readonly position?: bigint
|
|
61
|
+
readonly error?: Error
|
|
62
|
+
}
|
|
76
63
|
}
|
|
77
64
|
|
|
78
65
|
/**
|
|
79
66
|
* A running control plane. The platform stream is live; instructions are being
|
|
80
67
|
* routed and status is being reported until `close()`.
|
|
81
68
|
*/
|
|
82
|
-
export
|
|
69
|
+
export type AxonServerControlPlane = {
|
|
83
70
|
/**
|
|
84
71
|
* The processors this control plane addresses, keyed by name — the snapshot
|
|
85
72
|
* taken at construction. Exposed for introspection and tests.
|
|
@@ -92,37 +79,14 @@ export interface AxonServerControlPlane {
|
|
|
92
79
|
/** Map the managed processors into the status shape the platform stream reports. */
|
|
93
80
|
function processorStatuses(processors: Iterable<ManagedEventProcessor>): ProcessorStatus[] {
|
|
94
81
|
return Array.from(processors, (proc) => {
|
|
95
|
-
const
|
|
96
|
-
const perSegment = proc.processingStatus?.()
|
|
97
|
-
const segments: SegmentStatus[] = perSegment
|
|
98
|
-
? Array.from(perSegment.entries()).map(([segmentId, status]) => ({
|
|
99
|
-
segmentId,
|
|
100
|
-
caughtUp: status.caughtUp ?? false,
|
|
101
|
-
replaying: status.replaying ?? false,
|
|
102
|
-
onePartOf: 1,
|
|
103
|
-
tokenPosition: status.position ?? 0n,
|
|
104
|
-
errorState: status.error?.message ?? "",
|
|
105
|
-
}))
|
|
106
|
-
: [
|
|
107
|
-
{
|
|
108
|
-
segmentId: 0,
|
|
109
|
-
caughtUp: true,
|
|
110
|
-
replaying: proc.replaying ?? false,
|
|
111
|
-
onePartOf: 1,
|
|
112
|
-
tokenPosition: proc.position ?? 0n,
|
|
113
|
-
errorState: "",
|
|
114
|
-
},
|
|
115
|
-
]
|
|
82
|
+
const status = proc.status?.()
|
|
116
83
|
return {
|
|
117
84
|
name: proc.name,
|
|
118
85
|
running: proc.running ?? false,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
error: false,
|
|
124
|
-
tokenStoreIdentifier: "",
|
|
125
|
-
segments,
|
|
86
|
+
caughtUp: status?.caughtUp ?? true,
|
|
87
|
+
replaying: status?.replaying ?? proc.replaying ?? false,
|
|
88
|
+
position: status?.position ?? proc.position ?? 0n,
|
|
89
|
+
error: status?.error?.message,
|
|
126
90
|
}
|
|
127
91
|
})
|
|
128
92
|
}
|
|
@@ -167,15 +131,7 @@ export async function axonServerControlPlane(
|
|
|
167
131
|
case "start-processor":
|
|
168
132
|
await byName.get(instruction.processorName)?.start?.()
|
|
169
133
|
break
|
|
170
|
-
|
|
171
|
-
await byName.get(instruction.processorName)?.releaseSegment?.(instruction.segmentId)
|
|
172
|
-
break
|
|
173
|
-
case "split-segment":
|
|
174
|
-
await byName.get(instruction.processorName)?.splitSegment?.(instruction.segmentId)
|
|
175
|
-
break
|
|
176
|
-
case "merge-segment":
|
|
177
|
-
await byName.get(instruction.processorName)?.mergeSegment?.(instruction.segmentId)
|
|
178
|
-
break
|
|
134
|
+
// release/split/merge-segment: a kronos processor has one lane. Ignored.
|
|
179
135
|
}
|
|
180
136
|
})
|
|
181
137
|
|
|
@@ -1,62 +1,41 @@
|
|
|
1
|
-
import type { EventProcessorInfo
|
|
1
|
+
import type { EventProcessorInfo } from "./generated/control.js"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* What a processor reports about itself. SIX FIELDS — see the kronosdb
|
|
5
|
+
* package's twin for why the wire's thread counts, token-store identifier and
|
|
6
|
+
* per-segment list are filled with constants here rather than carried.
|
|
6
7
|
*/
|
|
7
|
-
export
|
|
8
|
+
export type ProcessorStatus = {
|
|
8
9
|
readonly name: string
|
|
9
10
|
readonly running: boolean
|
|
10
|
-
readonly mode: "Tracking" | "Subscribing"
|
|
11
|
-
readonly isStreamingProcessor: boolean
|
|
12
|
-
readonly activeThreads: number
|
|
13
|
-
readonly availableThreads: number
|
|
14
|
-
readonly error: boolean
|
|
15
|
-
readonly errorMessage?: string
|
|
16
|
-
readonly tokenStoreIdentifier: string
|
|
17
|
-
readonly segments: SegmentStatus[]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface SegmentStatus {
|
|
21
|
-
readonly segmentId: number
|
|
22
11
|
readonly caughtUp: boolean
|
|
23
12
|
readonly replaying: boolean
|
|
24
|
-
readonly
|
|
25
|
-
readonly
|
|
26
|
-
readonly errorState: string
|
|
13
|
+
readonly position: bigint
|
|
14
|
+
readonly error?: string
|
|
27
15
|
}
|
|
28
16
|
|
|
29
|
-
/**
|
|
30
|
-
* Converts a ProcessorStatus to the proto EventProcessorInfo format.
|
|
31
|
-
*/
|
|
32
17
|
export function toEventProcessorInfo(status: ProcessorStatus): EventProcessorInfo {
|
|
33
18
|
return {
|
|
34
19
|
processorName: status.name,
|
|
35
|
-
mode:
|
|
36
|
-
activeThreads: status.
|
|
20
|
+
mode: "Tracking",
|
|
21
|
+
activeThreads: status.running ? 1 : 0,
|
|
37
22
|
running: status.running,
|
|
38
|
-
error: status.error,
|
|
39
|
-
segmentStatus:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
23
|
+
error: status.error !== undefined,
|
|
24
|
+
segmentStatus: [
|
|
25
|
+
{
|
|
26
|
+
segmentId: 0,
|
|
27
|
+
caughtUp: status.caughtUp,
|
|
28
|
+
replaying: status.replaying,
|
|
29
|
+
onePartOf: 1,
|
|
30
|
+
tokenPosition: status.position,
|
|
31
|
+
errorState: status.error ?? "",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
availableThreads: 0,
|
|
35
|
+
tokenStoreIdentifier: "",
|
|
36
|
+
isStreamingProcessor: true,
|
|
43
37
|
loadBalancingStrategyName: "",
|
|
44
38
|
}
|
|
45
39
|
}
|
|
46
40
|
|
|
47
|
-
function toSegmentStatus(seg: SegmentStatus): EventProcessorInfo_SegmentStatus {
|
|
48
|
-
return {
|
|
49
|
-
segmentId: seg.segmentId,
|
|
50
|
-
caughtUp: seg.caughtUp,
|
|
51
|
-
replaying: seg.replaying,
|
|
52
|
-
onePartOf: seg.onePartOf,
|
|
53
|
-
tokenPosition: seg.tokenPosition,
|
|
54
|
-
errorState: seg.errorState,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Supplier function that returns the current status of all event processors.
|
|
60
|
-
* Registered with the platform connection for periodic reporting.
|
|
61
|
-
*/
|
|
62
41
|
export type ProcessorStatusSupplier = () => ProcessorStatus[]
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Aligned with Java's `FlowControlledResponseSender`.
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
10
|
+
export type FlowControlledSender<T> = {
|
|
11
11
|
/** Send an update. Buffers if no permits available. */
|
|
12
12
|
offer(value: T): boolean
|
|
13
13
|
/** Grant additional permits to the sender. */
|
|
@@ -33,29 +33,49 @@ export function flowControlledSender<T>(
|
|
|
33
33
|
onError?: (error: Error) => void,
|
|
34
34
|
maxBufferSize: number = 256,
|
|
35
35
|
): FlowControlledSender<T> {
|
|
36
|
+
if (!Number.isSafeInteger(maxBufferSize) || maxBufferSize <= 0) throw new RangeError("maxBufferSize must be a positive integer")
|
|
36
37
|
const buffer: T[] = []
|
|
37
38
|
let permits = 0
|
|
38
39
|
let isActive = true
|
|
40
|
+
let completionRequested = false
|
|
41
|
+
|
|
42
|
+
function fail(error: Error) {
|
|
43
|
+
if (!isActive) return
|
|
44
|
+
isActive = false
|
|
45
|
+
buffer.length = 0
|
|
46
|
+
onError?.(error)
|
|
47
|
+
}
|
|
48
|
+
function sendOne(value: T) {
|
|
49
|
+
try { send(value) }
|
|
50
|
+
catch (error) {
|
|
51
|
+
fail(error instanceof Error ? error : new Error(String(error)))
|
|
52
|
+
throw error
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function finishIfDrained() {
|
|
56
|
+
if (isActive && completionRequested && !buffer.length) {
|
|
57
|
+
isActive = false
|
|
58
|
+
onComplete?.()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
|
|
40
63
|
function drain() {
|
|
41
64
|
while (permits > 0 && buffer.length > 0 && isActive) {
|
|
42
65
|
const value = buffer.shift()!
|
|
43
66
|
permits--
|
|
44
|
-
|
|
45
|
-
send(value)
|
|
46
|
-
} catch (err) {
|
|
47
|
-
console.warn("FlowControlledSender: send error", err)
|
|
48
|
-
}
|
|
67
|
+
sendOne(value)
|
|
49
68
|
}
|
|
69
|
+
finishIfDrained()
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
return {
|
|
53
73
|
offer(value: T): boolean {
|
|
54
|
-
if (!isActive) return false
|
|
74
|
+
if (!isActive || completionRequested) return false
|
|
55
75
|
|
|
56
76
|
if (permits > 0) {
|
|
57
77
|
permits--
|
|
58
|
-
|
|
78
|
+
sendOne(value)
|
|
59
79
|
return true
|
|
60
80
|
}
|
|
61
81
|
|
|
@@ -68,24 +88,23 @@ export function flowControlledSender<T>(
|
|
|
68
88
|
},
|
|
69
89
|
|
|
70
90
|
addPermits(count: number) {
|
|
91
|
+
if (!Number.isSafeInteger(count) || count <= 0 || !Number.isSafeInteger(permits + count)) throw new RangeError("Permits must be positive safe integers")
|
|
92
|
+
if (!isActive) return
|
|
71
93
|
permits += count
|
|
72
94
|
drain()
|
|
73
95
|
},
|
|
74
96
|
|
|
75
97
|
complete() {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (onComplete) onComplete()
|
|
98
|
+
completionRequested = true
|
|
99
|
+
finishIfDrained()
|
|
79
100
|
},
|
|
80
101
|
|
|
81
102
|
completeExceptionally(error: Error) {
|
|
82
|
-
|
|
83
|
-
buffer.length = 0
|
|
84
|
-
if (onError) onError(error)
|
|
103
|
+
fail(error)
|
|
85
104
|
},
|
|
86
105
|
|
|
87
106
|
get active() {
|
|
88
|
-
return isActive
|
|
107
|
+
return isActive && !completionRequested
|
|
89
108
|
},
|
|
90
109
|
}
|
|
91
110
|
}
|