@usherlabs/cex-broker 0.2.43 → 0.2.45
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 +1 -1
- package/dist/commands/cli.js +140 -30
- package/dist/helpers/broker-execution-archive/index.d.ts +1 -0
- package/dist/helpers/broker-execution-archive/journal-export.d.ts +20 -0
- package/dist/helpers/broker-execution-archive/loss-journal.d.ts +36 -0
- package/dist/index.js +141 -31
- package/dist/index.js.map +6 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -735,7 +735,7 @@ Every successful `CreateOrder` response, successful `GetOrderDetails` response,
|
|
|
735
735
|
- Execution values: requested quantity/notional, executed base quantity, executed quote quantity/cost, average execution price, filled amount, remaining amount, fee amount, fee currency, fee rate
|
|
736
736
|
- Timing: exchange timestamp when present and broker observed timestamp
|
|
737
737
|
|
|
738
|
-
Use metrics for aggregations and alerts. For the durable execution audit trail, the broker archives every order lifecycle event to `broker_execution.order_events` (and pre-order top-of-book to `broker_execution.market_metadata_snapshots`) through the **archive forwarder** — the same HTTP `/archive` → ClickHouse path used for `market_data.*`. Set `CEX_BROKER_ARCHIVE_ENABLED=true`, an explicit HTTP(S) `CEX_BROKER_ARCHIVE_FORWARDER_URL`, and a writable durable JSONL path in `CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH`; startup fails if either required sink configuration is missing or invalid. In production, that path must be on persistent writable storage or a mounted volume rather than the container's ephemeral filesystem. Queue shedding
|
|
738
|
+
Use metrics for aggregations and alerts. For the durable execution audit trail, the broker archives every order lifecycle event to `broker_execution.order_events` (and pre-order top-of-book to `broker_execution.market_metadata_snapshots`) through the **archive forwarder** — the same HTTP `/archive` → ClickHouse path used for `market_data.*`. Set `CEX_BROKER_ARCHIVE_ENABLED=true`, an explicit HTTP(S) `CEX_BROKER_ARCHIVE_FORWARDER_URL`, and a writable durable JSONL path in `CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH`; startup fails if either required sink configuration is missing or invalid. In production, that path must be on persistent writable storage or a mounted volume rather than the container's ephemeral filesystem. Queue shedding, rows that remain undeliverable during shutdown, and batches the forwarder rejected on every attempt are written to that loss journal with their original `{table,row}` payload before being discarded. Replay them with `bun run archive:loss:replay --journal <path>`, which re-posts through the forwarder and reports a per-entry verdict; it is idempotent across reruns, never modifies the journal, and only archives it once every entry is terminal and the operator asserts `--broker-stopped`. When the journal lives on an encrypted enclave mount (SGX deployments), its host-side bytes are sealed ciphertext the replay tool cannot read; set `CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH` to a path on a plain (untrusted) mount and the broker exports a boot-time plaintext snapshot of the journal there, with an adjacent `.sha256` receipt whose digest is also logged from inside the enclave for integrity comparison. The export is one-shot — an existing target file is left untouched, so remove it to request a fresh snapshot — and a failed export fails startup. Setting `CEX_BROKER_ARCHIVE_OTEL_LOGS_ENABLED=true` additionally mirrors execution rows to OTel logs for observability, but OTel is never the archive sink of record. Analysts join Maker action rows to `broker_execution.order_events` using `maker_action_id`, `idempotency_id`, `client_order_id`, or the exchange `order_id`, then compare Maker propAMM execution price against `average_execution_price` and fees. Failed CreateOrder rows keep bounded exchange error detail in `error_message`; their telemetry-shaped `payload_json`, metrics, and ordinary telemetry logs remain redacted. The broker does not emit raw exchange payloads, API keys, secrets, or credentials in telemetry fields.
|
|
739
739
|
|
|
740
740
|
### User-stream health archive contract
|
|
741
741
|
|
package/dist/commands/cli.js
CHANGED
|
@@ -315811,11 +315811,118 @@ function compactUndefined2(record) {
|
|
|
315811
315811
|
// src/helpers/broker-execution-archive/writer.ts
|
|
315812
315812
|
var import_api_logs2 = __toESM(require_src7(), 1);
|
|
315813
315813
|
import { randomUUID } from "node:crypto";
|
|
315814
|
-
import { closeSync, fsyncSync, openSync, writeSync } from "node:fs";
|
|
315814
|
+
import { closeSync as closeSync2, fsyncSync as fsyncSync2, openSync as openSync2, writeSync as writeSync2 } from "node:fs";
|
|
315815
315815
|
import {
|
|
315816
315816
|
request as httpRequest2
|
|
315817
315817
|
} from "node:http";
|
|
315818
315818
|
import { request as httpsRequest2 } from "node:https";
|
|
315819
|
+
|
|
315820
|
+
// src/helpers/broker-execution-archive/journal-export.ts
|
|
315821
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
315822
|
+
import {
|
|
315823
|
+
closeSync,
|
|
315824
|
+
existsSync,
|
|
315825
|
+
fsyncSync,
|
|
315826
|
+
openSync,
|
|
315827
|
+
readSync,
|
|
315828
|
+
renameSync,
|
|
315829
|
+
writeSync
|
|
315830
|
+
} from "node:fs";
|
|
315831
|
+
import { basename } from "node:path";
|
|
315832
|
+
var EXPORT_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
315833
|
+
|
|
315834
|
+
class DeadLetterJournalExportError extends Error {
|
|
315835
|
+
constructor(message, options) {
|
|
315836
|
+
super(message, options);
|
|
315837
|
+
this.name = "DeadLetterJournalExportError";
|
|
315838
|
+
}
|
|
315839
|
+
}
|
|
315840
|
+
function exportDeadLetterJournalFromEnv() {
|
|
315841
|
+
const exportPath = process.env.CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH?.trim();
|
|
315842
|
+
if (!exportPath) {
|
|
315843
|
+
return { status: "disabled" };
|
|
315844
|
+
}
|
|
315845
|
+
const journalPath = process.env.CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH?.trim();
|
|
315846
|
+
if (!journalPath) {
|
|
315847
|
+
throw new DeadLetterJournalExportError("CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH is set but CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH is missing");
|
|
315848
|
+
}
|
|
315849
|
+
return exportDeadLetterJournal(journalPath, exportPath);
|
|
315850
|
+
}
|
|
315851
|
+
function exportDeadLetterJournal(journalPath, exportPath) {
|
|
315852
|
+
if (existsSync(exportPath)) {
|
|
315853
|
+
log.info("Dead-letter journal export target already exists, skipping", {
|
|
315854
|
+
export_path: exportPath
|
|
315855
|
+
});
|
|
315856
|
+
return { status: "skipped_export_exists", exportPath };
|
|
315857
|
+
}
|
|
315858
|
+
if (!existsSync(journalPath)) {
|
|
315859
|
+
log.warn("Dead-letter journal export requested but no journal exists, skipping", { journal_path: journalPath });
|
|
315860
|
+
return { status: "skipped_missing_journal", journalPath };
|
|
315861
|
+
}
|
|
315862
|
+
const partialPath = `${exportPath}.partial`;
|
|
315863
|
+
let bytes2 = 0;
|
|
315864
|
+
let digest;
|
|
315865
|
+
try {
|
|
315866
|
+
const hash4 = createHash3("sha256");
|
|
315867
|
+
const sourceFd = openSync(journalPath, "r");
|
|
315868
|
+
try {
|
|
315869
|
+
const targetFd = openSync(partialPath, "w", 384);
|
|
315870
|
+
try {
|
|
315871
|
+
const chunk = Buffer.alloc(EXPORT_CHUNK_BYTES);
|
|
315872
|
+
for (;; ) {
|
|
315873
|
+
const read = readSync(sourceFd, chunk, 0, chunk.length, null);
|
|
315874
|
+
if (read === 0) {
|
|
315875
|
+
break;
|
|
315876
|
+
}
|
|
315877
|
+
const view = chunk.subarray(0, read);
|
|
315878
|
+
hash4.update(view);
|
|
315879
|
+
writeSync(targetFd, view);
|
|
315880
|
+
bytes2 += read;
|
|
315881
|
+
}
|
|
315882
|
+
fsyncSync(targetFd);
|
|
315883
|
+
} finally {
|
|
315884
|
+
closeSync(targetFd);
|
|
315885
|
+
}
|
|
315886
|
+
} finally {
|
|
315887
|
+
closeSync(sourceFd);
|
|
315888
|
+
}
|
|
315889
|
+
digest = hash4.digest("hex");
|
|
315890
|
+
renameSync(partialPath, exportPath);
|
|
315891
|
+
const receiptFd = openSync(`${exportPath}.sha256`, "w", 384);
|
|
315892
|
+
try {
|
|
315893
|
+
writeSync(receiptFd, `${digest} ${basename(exportPath)}
|
|
315894
|
+
`);
|
|
315895
|
+
fsyncSync(receiptFd);
|
|
315896
|
+
} finally {
|
|
315897
|
+
closeSync(receiptFd);
|
|
315898
|
+
}
|
|
315899
|
+
} catch (error) {
|
|
315900
|
+
throw new DeadLetterJournalExportError("Dead-letter journal export failed", { cause: error });
|
|
315901
|
+
}
|
|
315902
|
+
log.info("Dead-letter journal exported", {
|
|
315903
|
+
journal_path: journalPath,
|
|
315904
|
+
export_path: exportPath,
|
|
315905
|
+
bytes: bytes2,
|
|
315906
|
+
sha256: digest
|
|
315907
|
+
});
|
|
315908
|
+
return {
|
|
315909
|
+
status: "exported",
|
|
315910
|
+
journalPath,
|
|
315911
|
+
exportPath,
|
|
315912
|
+
bytes: bytes2,
|
|
315913
|
+
sha256: digest
|
|
315914
|
+
};
|
|
315915
|
+
}
|
|
315916
|
+
|
|
315917
|
+
// src/helpers/broker-execution-archive/loss-journal.ts
|
|
315918
|
+
var LOSS_JOURNAL_RECORD_VERSION = 1;
|
|
315919
|
+
var LOSS_REASONS = new Set([
|
|
315920
|
+
"queue_shed",
|
|
315921
|
+
"shutdown_forwarder_failure",
|
|
315922
|
+
"retry_exhausted"
|
|
315923
|
+
]);
|
|
315924
|
+
|
|
315925
|
+
// src/helpers/broker-execution-archive/writer.ts
|
|
315819
315926
|
var BROKER_EXECUTION_ARCHIVE_TABLES = new Set([
|
|
315820
315927
|
"broker_execution.order_events",
|
|
315821
315928
|
"broker_execution.market_metadata_snapshots",
|
|
@@ -315935,7 +316042,7 @@ class BrokerExecutionArchiver {
|
|
|
315935
316042
|
throw new Error("Broker execution archive is enabled but CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH is missing");
|
|
315936
316043
|
}
|
|
315937
316044
|
try {
|
|
315938
|
-
this.deadLetterFd =
|
|
316045
|
+
this.deadLetterFd = openSync2(this.deadLetterPath, "a", 384);
|
|
315939
316046
|
} catch {
|
|
315940
316047
|
throw new Error("Broker execution archive cannot open CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH for append");
|
|
315941
316048
|
}
|
|
@@ -316065,11 +316172,9 @@ class BrokerExecutionArchiver {
|
|
|
316065
316172
|
continue;
|
|
316066
316173
|
}
|
|
316067
316174
|
if (!await this.flushBatch()) {
|
|
316068
|
-
const
|
|
316069
|
-
|
|
316070
|
-
|
|
316071
|
-
];
|
|
316072
|
-
this.appendLossRecords(undelivered, "shutdown_forwarder_failure");
|
|
316175
|
+
const pinned = this.pendingRetry;
|
|
316176
|
+
this.appendLossRecords(pinned?.rows ?? [], "shutdown_forwarder_failure", pinned?.batchId ?? null);
|
|
316177
|
+
this.appendLossRecords(this.queue, "shutdown_forwarder_failure");
|
|
316073
316178
|
this.pendingRetry = null;
|
|
316074
316179
|
this.queue.length = 0;
|
|
316075
316180
|
break;
|
|
@@ -316182,14 +316287,14 @@ class BrokerExecutionArchiver {
|
|
|
316182
316287
|
}
|
|
316183
316288
|
const fd2 = this.deadLetterFd;
|
|
316184
316289
|
try {
|
|
316185
|
-
|
|
316290
|
+
closeSync2(fd2);
|
|
316186
316291
|
} catch (error) {
|
|
316187
316292
|
throw new BrokerExecutionArchiveDurabilityError("Broker execution archive failed to close the configured CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH loss journal", { cause: error });
|
|
316188
316293
|
} finally {
|
|
316189
316294
|
this.deadLetterFd = undefined;
|
|
316190
316295
|
}
|
|
316191
316296
|
}
|
|
316192
|
-
appendLossRecords(rows, reason) {
|
|
316297
|
+
appendLossRecords(rows, reason, batchId = null) {
|
|
316193
316298
|
if (rows.length === 0) {
|
|
316194
316299
|
return;
|
|
316195
316300
|
}
|
|
@@ -316197,22 +316302,26 @@ class BrokerExecutionArchiver {
|
|
|
316197
316302
|
throw new BrokerExecutionArchiveDurabilityError(`Broker execution archive cannot record ${reason}: dead-letter file is not open`);
|
|
316198
316303
|
}
|
|
316199
316304
|
const timestamp = new Date().toISOString();
|
|
316200
|
-
const records = rows.map((payload) => ({
|
|
316305
|
+
const records = rows.map((payload, index2) => ({
|
|
316201
316306
|
timestamp,
|
|
316202
316307
|
source: this.source,
|
|
316203
316308
|
deployment_id: this.deploymentId,
|
|
316204
316309
|
reason,
|
|
316205
|
-
payload
|
|
316310
|
+
payload,
|
|
316311
|
+
record_version: LOSS_JOURNAL_RECORD_VERSION,
|
|
316312
|
+
batch_id: batchId,
|
|
316313
|
+
batch_row_index: index2,
|
|
316314
|
+
batch_row_count: rows.length
|
|
316206
316315
|
}));
|
|
316207
316316
|
try {
|
|
316208
316317
|
const bytes2 = Buffer.from(records.map((record) => JSON.stringify(record)).join(`
|
|
316209
316318
|
`) + `
|
|
316210
316319
|
`);
|
|
316211
|
-
const written =
|
|
316320
|
+
const written = writeSync2(this.deadLetterFd, bytes2);
|
|
316212
316321
|
if (written !== bytes2.length) {
|
|
316213
316322
|
throw new Error(`wrote ${written} of ${bytes2.length} bytes`);
|
|
316214
316323
|
}
|
|
316215
|
-
|
|
316324
|
+
fsyncSync2(this.deadLetterFd);
|
|
316216
316325
|
const byFeedAndTable = new Map;
|
|
316217
316326
|
for (const row of rows) {
|
|
316218
316327
|
const key = `${row.table}\x00${archiveFeed(row)}`;
|
|
@@ -316257,7 +316366,7 @@ class BrokerExecutionArchiver {
|
|
|
316257
316366
|
try {
|
|
316258
316367
|
if (attempts >= MAX_PINNED_BATCH_ATTEMPTS) {
|
|
316259
316368
|
this.pendingRetry = null;
|
|
316260
|
-
this.appendLossRecords(batch, "retry_exhausted");
|
|
316369
|
+
this.appendLossRecords(batch, "retry_exhausted", batchId);
|
|
316261
316370
|
log.warn("Broker execution archive gave up on a batch", {
|
|
316262
316371
|
attempts,
|
|
316263
316372
|
rows: batch.length
|
|
@@ -316519,6 +316628,7 @@ function flattenArchiveAttributes(entry) {
|
|
|
316519
316628
|
return attributes;
|
|
316520
316629
|
}
|
|
316521
316630
|
function createBrokerExecutionArchiverFromEnv(otelLogs, otelMetrics) {
|
|
316631
|
+
exportDeadLetterJournalFromEnv();
|
|
316522
316632
|
if (process.env.CEX_BROKER_ARCHIVE_ENABLED !== "true") {
|
|
316523
316633
|
return BrokerExecutionArchiver.disabled();
|
|
316524
316634
|
}
|
|
@@ -317432,7 +317542,7 @@ class FillArchivePoller {
|
|
|
317432
317542
|
}
|
|
317433
317543
|
|
|
317434
317544
|
// src/helpers/market-data-archive/capture-contract.ts
|
|
317435
|
-
import { createHash as
|
|
317545
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
317436
317546
|
var MARKET_CAPTURE_SCHEMA_VERSION = "1.0.0";
|
|
317437
317547
|
var CHECKSUM_ALGORITHM = "sha256-canonical-json-v1";
|
|
317438
317548
|
var ARCHIVE_SOURCES = ["broker_read", "broker_write"];
|
|
@@ -317537,7 +317647,7 @@ function omitChecksumFields(value) {
|
|
|
317537
317647
|
return value;
|
|
317538
317648
|
}
|
|
317539
317649
|
function sha256Canonical(value) {
|
|
317540
|
-
return
|
|
317650
|
+
return createHash4("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
|
|
317541
317651
|
}
|
|
317542
317652
|
function normalizeTimestampMs(value, field) {
|
|
317543
317653
|
let timestamp;
|
|
@@ -318118,13 +318228,13 @@ function createOtelLogsFromEnv() {
|
|
|
318118
318228
|
}
|
|
318119
318229
|
|
|
318120
318230
|
// src/helpers/stream-health-publisher.ts
|
|
318121
|
-
import { createHash as
|
|
318231
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
|
|
318122
318232
|
import {
|
|
318123
|
-
closeSync as
|
|
318124
|
-
fsyncSync as
|
|
318125
|
-
openSync as
|
|
318233
|
+
closeSync as closeSync3,
|
|
318234
|
+
fsyncSync as fsyncSync3,
|
|
318235
|
+
openSync as openSync3,
|
|
318126
318236
|
readFileSync,
|
|
318127
|
-
renameSync,
|
|
318237
|
+
renameSync as renameSync2,
|
|
318128
318238
|
statSync,
|
|
318129
318239
|
unlinkSync,
|
|
318130
318240
|
writeFileSync
|
|
@@ -318167,7 +318277,7 @@ function registryRevision(snapshots) {
|
|
|
318167
318277
|
account_scope: snapshot.accountScope,
|
|
318168
318278
|
registry_status: snapshot.registryStatus
|
|
318169
318279
|
})).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
318170
|
-
return
|
|
318280
|
+
return createHash5("sha256").update(JSON.stringify(rows)).digest("hex");
|
|
318171
318281
|
}
|
|
318172
318282
|
function validState(value) {
|
|
318173
318283
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -318431,21 +318541,21 @@ class StreamHealthPublisher {
|
|
|
318431
318541
|
const temporary = `${this.#statePath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
318432
318542
|
let fd2;
|
|
318433
318543
|
try {
|
|
318434
|
-
fd2 =
|
|
318544
|
+
fd2 = openSync3(temporary, "wx", 384);
|
|
318435
318545
|
writeFileSync(fd2, JSON.stringify(this.#state));
|
|
318436
|
-
|
|
318437
|
-
|
|
318546
|
+
fsyncSync3(fd2);
|
|
318547
|
+
closeSync3(fd2);
|
|
318438
318548
|
fd2 = undefined;
|
|
318439
|
-
|
|
318440
|
-
const parentFd =
|
|
318549
|
+
renameSync2(temporary, this.#statePath);
|
|
318550
|
+
const parentFd = openSync3(parent, "r");
|
|
318441
318551
|
try {
|
|
318442
|
-
|
|
318552
|
+
fsyncSync3(parentFd);
|
|
318443
318553
|
} finally {
|
|
318444
|
-
|
|
318554
|
+
closeSync3(parentFd);
|
|
318445
318555
|
}
|
|
318446
318556
|
} catch (error) {
|
|
318447
318557
|
if (fd2 !== undefined)
|
|
318448
|
-
|
|
318558
|
+
closeSync3(fd2);
|
|
318449
318559
|
try {
|
|
318450
318560
|
unlinkSync(temporary);
|
|
318451
318561
|
} catch {}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { archiveOrderExecutionInBackground, archiveSubscribeStreamInBackground, archiveTransferEventInBackground, archiveWithdrawalObservationsInBackground, captureMarketMetadataSnapshot, captureMarketMetadataSnapshotInBackground, } from "./capture";
|
|
2
|
+
export { DeadLetterJournalExportError, type DeadLetterJournalExportResult, exportDeadLetterJournal, exportDeadLetterJournalFromEnv, } from "./journal-export";
|
|
2
3
|
export { hashMarketMetadata, redactErrorForArchive, redactSecretLiterals, redactStreamPayload, } from "./redact";
|
|
3
4
|
export { buildAccountBalanceSnapshotRow, buildCommonArchiveTags, buildFillEventArchiveRow, buildMarketMetadataSnapshotRow, buildOrderEventArchiveRow, buildSubscribeStreamArchiveRow, buildTransferEventArchiveRow, buildUserAssetSnapshotRow, extractBinanceInternalTransferId, type FillArchiveFields, type NormalizedCcxtBalance, type NormalizedCcxtTransfer, type NormalizedUserAssets, normalizeBinanceUserAssetsForArchive, normalizeCcxtBalanceForArchive, normalizeCcxtTradeForArchive, normalizeCcxtTransactionForArchive, normalizeTimestamp, type TransferArchiveFields, } from "./rows";
|
|
4
5
|
export { ACCOUNT_BALANCE_PRECISION_BASIS, ACCOUNT_BALANCE_SCOPE, ARCHIVE_SCHEMA_VERSION, BROKER_READ_SOURCE, BROKER_WRITE_SOURCE, type BrokerArchiveCommonTags, type BrokerArchiveRow, type BrokerArchiveSource, type BrokerArchiveTable, type OrderArchiveAction, type SubscribeArchiveType, type TransferEventKind, type TransferLifecycleAction, USER_ASSET_BALANCE_SCOPE, USER_ASSET_PRECISION_BASIS, } from "./types";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class DeadLetterJournalExportError extends Error {
|
|
2
|
+
constructor(message: string, options?: ErrorOptions);
|
|
3
|
+
}
|
|
4
|
+
export type DeadLetterJournalExportResult = {
|
|
5
|
+
status: "disabled";
|
|
6
|
+
} | {
|
|
7
|
+
status: "skipped_export_exists";
|
|
8
|
+
exportPath: string;
|
|
9
|
+
} | {
|
|
10
|
+
status: "skipped_missing_journal";
|
|
11
|
+
journalPath: string;
|
|
12
|
+
} | {
|
|
13
|
+
status: "exported";
|
|
14
|
+
journalPath: string;
|
|
15
|
+
exportPath: string;
|
|
16
|
+
bytes: number;
|
|
17
|
+
sha256: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function exportDeadLetterJournalFromEnv(): DeadLetterJournalExportResult;
|
|
20
|
+
export declare function exportDeadLetterJournal(journalPath: string, exportPath: string): DeadLetterJournalExportResult;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BrokerArchiveRow, BrokerArchiveSource } from "./types";
|
|
2
|
+
export declare const LOSS_JOURNAL_RECORD_VERSION: 1;
|
|
3
|
+
export type ArchiveLossReason = "queue_shed" | "shutdown_forwarder_failure" | "retry_exhausted";
|
|
4
|
+
export type ArchiveLossRecord = {
|
|
5
|
+
timestamp: string;
|
|
6
|
+
source: BrokerArchiveSource;
|
|
7
|
+
deployment_id: string;
|
|
8
|
+
reason: ArchiveLossReason;
|
|
9
|
+
payload: BrokerArchiveRow;
|
|
10
|
+
record_version: typeof LOSS_JOURNAL_RECORD_VERSION;
|
|
11
|
+
batch_id: string | null;
|
|
12
|
+
batch_row_index: number;
|
|
13
|
+
batch_row_count: number;
|
|
14
|
+
};
|
|
15
|
+
type ParsedArchiveLossRecordBase = Omit<ArchiveLossRecord, "payload" | "record_version" | "batch_id" | "batch_row_index" | "batch_row_count"> & {
|
|
16
|
+
payload: {
|
|
17
|
+
table: string;
|
|
18
|
+
row: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export type ParsedArchiveLossRecord = ParsedArchiveLossRecordBase & (Pick<ArchiveLossRecord, "record_version" | "batch_id" | "batch_row_index" | "batch_row_count"> | {
|
|
22
|
+
record_version?: never;
|
|
23
|
+
batch_id?: never;
|
|
24
|
+
batch_row_index?: never;
|
|
25
|
+
batch_row_count?: never;
|
|
26
|
+
});
|
|
27
|
+
export type ParsedLossJournalLine = {
|
|
28
|
+
ok: true;
|
|
29
|
+
record: ParsedArchiveLossRecord;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
34
|
+
export declare function parseLossJournalLine(line: string): ParsedLossJournalLine;
|
|
35
|
+
export declare function lossJournalLineDigest(rawLine: string): string;
|
|
36
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -291325,11 +291325,118 @@ function compactUndefined2(record) {
|
|
|
291325
291325
|
// src/helpers/broker-execution-archive/writer.ts
|
|
291326
291326
|
var import_api_logs2 = __toESM(require_src4(), 1);
|
|
291327
291327
|
import { randomUUID } from "node:crypto";
|
|
291328
|
-
import { closeSync, fsyncSync, openSync, writeSync } from "node:fs";
|
|
291328
|
+
import { closeSync as closeSync2, fsyncSync as fsyncSync2, openSync as openSync2, writeSync as writeSync2 } from "node:fs";
|
|
291329
291329
|
import {
|
|
291330
291330
|
request as httpRequest2
|
|
291331
291331
|
} from "node:http";
|
|
291332
291332
|
import { request as httpsRequest2 } from "node:https";
|
|
291333
|
+
|
|
291334
|
+
// src/helpers/broker-execution-archive/journal-export.ts
|
|
291335
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
291336
|
+
import {
|
|
291337
|
+
closeSync,
|
|
291338
|
+
existsSync,
|
|
291339
|
+
fsyncSync,
|
|
291340
|
+
openSync,
|
|
291341
|
+
readSync,
|
|
291342
|
+
renameSync,
|
|
291343
|
+
writeSync
|
|
291344
|
+
} from "node:fs";
|
|
291345
|
+
import { basename } from "node:path";
|
|
291346
|
+
var EXPORT_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
291347
|
+
|
|
291348
|
+
class DeadLetterJournalExportError extends Error {
|
|
291349
|
+
constructor(message, options) {
|
|
291350
|
+
super(message, options);
|
|
291351
|
+
this.name = "DeadLetterJournalExportError";
|
|
291352
|
+
}
|
|
291353
|
+
}
|
|
291354
|
+
function exportDeadLetterJournalFromEnv() {
|
|
291355
|
+
const exportPath = process.env.CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH?.trim();
|
|
291356
|
+
if (!exportPath) {
|
|
291357
|
+
return { status: "disabled" };
|
|
291358
|
+
}
|
|
291359
|
+
const journalPath = process.env.CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH?.trim();
|
|
291360
|
+
if (!journalPath) {
|
|
291361
|
+
throw new DeadLetterJournalExportError("CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH is set but CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH is missing");
|
|
291362
|
+
}
|
|
291363
|
+
return exportDeadLetterJournal(journalPath, exportPath);
|
|
291364
|
+
}
|
|
291365
|
+
function exportDeadLetterJournal(journalPath, exportPath) {
|
|
291366
|
+
if (existsSync(exportPath)) {
|
|
291367
|
+
log.info("Dead-letter journal export target already exists, skipping", {
|
|
291368
|
+
export_path: exportPath
|
|
291369
|
+
});
|
|
291370
|
+
return { status: "skipped_export_exists", exportPath };
|
|
291371
|
+
}
|
|
291372
|
+
if (!existsSync(journalPath)) {
|
|
291373
|
+
log.warn("Dead-letter journal export requested but no journal exists, skipping", { journal_path: journalPath });
|
|
291374
|
+
return { status: "skipped_missing_journal", journalPath };
|
|
291375
|
+
}
|
|
291376
|
+
const partialPath = `${exportPath}.partial`;
|
|
291377
|
+
let bytes2 = 0;
|
|
291378
|
+
let digest;
|
|
291379
|
+
try {
|
|
291380
|
+
const hash4 = createHash3("sha256");
|
|
291381
|
+
const sourceFd = openSync(journalPath, "r");
|
|
291382
|
+
try {
|
|
291383
|
+
const targetFd = openSync(partialPath, "w", 384);
|
|
291384
|
+
try {
|
|
291385
|
+
const chunk = Buffer.alloc(EXPORT_CHUNK_BYTES);
|
|
291386
|
+
for (;; ) {
|
|
291387
|
+
const read = readSync(sourceFd, chunk, 0, chunk.length, null);
|
|
291388
|
+
if (read === 0) {
|
|
291389
|
+
break;
|
|
291390
|
+
}
|
|
291391
|
+
const view = chunk.subarray(0, read);
|
|
291392
|
+
hash4.update(view);
|
|
291393
|
+
writeSync(targetFd, view);
|
|
291394
|
+
bytes2 += read;
|
|
291395
|
+
}
|
|
291396
|
+
fsyncSync(targetFd);
|
|
291397
|
+
} finally {
|
|
291398
|
+
closeSync(targetFd);
|
|
291399
|
+
}
|
|
291400
|
+
} finally {
|
|
291401
|
+
closeSync(sourceFd);
|
|
291402
|
+
}
|
|
291403
|
+
digest = hash4.digest("hex");
|
|
291404
|
+
renameSync(partialPath, exportPath);
|
|
291405
|
+
const receiptFd = openSync(`${exportPath}.sha256`, "w", 384);
|
|
291406
|
+
try {
|
|
291407
|
+
writeSync(receiptFd, `${digest} ${basename(exportPath)}
|
|
291408
|
+
`);
|
|
291409
|
+
fsyncSync(receiptFd);
|
|
291410
|
+
} finally {
|
|
291411
|
+
closeSync(receiptFd);
|
|
291412
|
+
}
|
|
291413
|
+
} catch (error) {
|
|
291414
|
+
throw new DeadLetterJournalExportError("Dead-letter journal export failed", { cause: error });
|
|
291415
|
+
}
|
|
291416
|
+
log.info("Dead-letter journal exported", {
|
|
291417
|
+
journal_path: journalPath,
|
|
291418
|
+
export_path: exportPath,
|
|
291419
|
+
bytes: bytes2,
|
|
291420
|
+
sha256: digest
|
|
291421
|
+
});
|
|
291422
|
+
return {
|
|
291423
|
+
status: "exported",
|
|
291424
|
+
journalPath,
|
|
291425
|
+
exportPath,
|
|
291426
|
+
bytes: bytes2,
|
|
291427
|
+
sha256: digest
|
|
291428
|
+
};
|
|
291429
|
+
}
|
|
291430
|
+
|
|
291431
|
+
// src/helpers/broker-execution-archive/loss-journal.ts
|
|
291432
|
+
var LOSS_JOURNAL_RECORD_VERSION = 1;
|
|
291433
|
+
var LOSS_REASONS = new Set([
|
|
291434
|
+
"queue_shed",
|
|
291435
|
+
"shutdown_forwarder_failure",
|
|
291436
|
+
"retry_exhausted"
|
|
291437
|
+
]);
|
|
291438
|
+
|
|
291439
|
+
// src/helpers/broker-execution-archive/writer.ts
|
|
291333
291440
|
var BROKER_EXECUTION_ARCHIVE_TABLES = new Set([
|
|
291334
291441
|
"broker_execution.order_events",
|
|
291335
291442
|
"broker_execution.market_metadata_snapshots",
|
|
@@ -291449,7 +291556,7 @@ class BrokerExecutionArchiver {
|
|
|
291449
291556
|
throw new Error("Broker execution archive is enabled but CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH is missing");
|
|
291450
291557
|
}
|
|
291451
291558
|
try {
|
|
291452
|
-
this.deadLetterFd =
|
|
291559
|
+
this.deadLetterFd = openSync2(this.deadLetterPath, "a", 384);
|
|
291453
291560
|
} catch {
|
|
291454
291561
|
throw new Error("Broker execution archive cannot open CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH for append");
|
|
291455
291562
|
}
|
|
@@ -291579,11 +291686,9 @@ class BrokerExecutionArchiver {
|
|
|
291579
291686
|
continue;
|
|
291580
291687
|
}
|
|
291581
291688
|
if (!await this.flushBatch()) {
|
|
291582
|
-
const
|
|
291583
|
-
|
|
291584
|
-
|
|
291585
|
-
];
|
|
291586
|
-
this.appendLossRecords(undelivered, "shutdown_forwarder_failure");
|
|
291689
|
+
const pinned = this.pendingRetry;
|
|
291690
|
+
this.appendLossRecords(pinned?.rows ?? [], "shutdown_forwarder_failure", pinned?.batchId ?? null);
|
|
291691
|
+
this.appendLossRecords(this.queue, "shutdown_forwarder_failure");
|
|
291587
291692
|
this.pendingRetry = null;
|
|
291588
291693
|
this.queue.length = 0;
|
|
291589
291694
|
break;
|
|
@@ -291696,14 +291801,14 @@ class BrokerExecutionArchiver {
|
|
|
291696
291801
|
}
|
|
291697
291802
|
const fd2 = this.deadLetterFd;
|
|
291698
291803
|
try {
|
|
291699
|
-
|
|
291804
|
+
closeSync2(fd2);
|
|
291700
291805
|
} catch (error) {
|
|
291701
291806
|
throw new BrokerExecutionArchiveDurabilityError("Broker execution archive failed to close the configured CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH loss journal", { cause: error });
|
|
291702
291807
|
} finally {
|
|
291703
291808
|
this.deadLetterFd = undefined;
|
|
291704
291809
|
}
|
|
291705
291810
|
}
|
|
291706
|
-
appendLossRecords(rows, reason) {
|
|
291811
|
+
appendLossRecords(rows, reason, batchId = null) {
|
|
291707
291812
|
if (rows.length === 0) {
|
|
291708
291813
|
return;
|
|
291709
291814
|
}
|
|
@@ -291711,22 +291816,26 @@ class BrokerExecutionArchiver {
|
|
|
291711
291816
|
throw new BrokerExecutionArchiveDurabilityError(`Broker execution archive cannot record ${reason}: dead-letter file is not open`);
|
|
291712
291817
|
}
|
|
291713
291818
|
const timestamp = new Date().toISOString();
|
|
291714
|
-
const records = rows.map((payload) => ({
|
|
291819
|
+
const records = rows.map((payload, index2) => ({
|
|
291715
291820
|
timestamp,
|
|
291716
291821
|
source: this.source,
|
|
291717
291822
|
deployment_id: this.deploymentId,
|
|
291718
291823
|
reason,
|
|
291719
|
-
payload
|
|
291824
|
+
payload,
|
|
291825
|
+
record_version: LOSS_JOURNAL_RECORD_VERSION,
|
|
291826
|
+
batch_id: batchId,
|
|
291827
|
+
batch_row_index: index2,
|
|
291828
|
+
batch_row_count: rows.length
|
|
291720
291829
|
}));
|
|
291721
291830
|
try {
|
|
291722
291831
|
const bytes2 = Buffer.from(records.map((record) => JSON.stringify(record)).join(`
|
|
291723
291832
|
`) + `
|
|
291724
291833
|
`);
|
|
291725
|
-
const written =
|
|
291834
|
+
const written = writeSync2(this.deadLetterFd, bytes2);
|
|
291726
291835
|
if (written !== bytes2.length) {
|
|
291727
291836
|
throw new Error(`wrote ${written} of ${bytes2.length} bytes`);
|
|
291728
291837
|
}
|
|
291729
|
-
|
|
291838
|
+
fsyncSync2(this.deadLetterFd);
|
|
291730
291839
|
const byFeedAndTable = new Map;
|
|
291731
291840
|
for (const row of rows) {
|
|
291732
291841
|
const key = `${row.table}\x00${archiveFeed(row)}`;
|
|
@@ -291771,7 +291880,7 @@ class BrokerExecutionArchiver {
|
|
|
291771
291880
|
try {
|
|
291772
291881
|
if (attempts >= MAX_PINNED_BATCH_ATTEMPTS) {
|
|
291773
291882
|
this.pendingRetry = null;
|
|
291774
|
-
this.appendLossRecords(batch, "retry_exhausted");
|
|
291883
|
+
this.appendLossRecords(batch, "retry_exhausted", batchId);
|
|
291775
291884
|
log.warn("Broker execution archive gave up on a batch", {
|
|
291776
291885
|
attempts,
|
|
291777
291886
|
rows: batch.length
|
|
@@ -292033,6 +292142,7 @@ function flattenArchiveAttributes(entry) {
|
|
|
292033
292142
|
return attributes;
|
|
292034
292143
|
}
|
|
292035
292144
|
function createBrokerExecutionArchiverFromEnv(otelLogs, otelMetrics) {
|
|
292145
|
+
exportDeadLetterJournalFromEnv();
|
|
292036
292146
|
if (process.env.CEX_BROKER_ARCHIVE_ENABLED !== "true") {
|
|
292037
292147
|
return BrokerExecutionArchiver.disabled();
|
|
292038
292148
|
}
|
|
@@ -292946,7 +293056,7 @@ class FillArchivePoller {
|
|
|
292946
293056
|
}
|
|
292947
293057
|
|
|
292948
293058
|
// src/helpers/market-data-archive/capture-contract.ts
|
|
292949
|
-
import { createHash as
|
|
293059
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
292950
293060
|
var MARKET_CAPTURE_SCHEMA_VERSION = "1.0.0";
|
|
292951
293061
|
var CHECKSUM_ALGORITHM = "sha256-canonical-json-v1";
|
|
292952
293062
|
var ARCHIVE_SOURCES = ["broker_read", "broker_write"];
|
|
@@ -293051,7 +293161,7 @@ function omitChecksumFields(value) {
|
|
|
293051
293161
|
return value;
|
|
293052
293162
|
}
|
|
293053
293163
|
function sha256Canonical(value) {
|
|
293054
|
-
return
|
|
293164
|
+
return createHash4("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
|
|
293055
293165
|
}
|
|
293056
293166
|
function normalizeTimestampMs(value, field) {
|
|
293057
293167
|
let timestamp;
|
|
@@ -293632,13 +293742,13 @@ function createOtelLogsFromEnv() {
|
|
|
293632
293742
|
}
|
|
293633
293743
|
|
|
293634
293744
|
// src/helpers/stream-health-publisher.ts
|
|
293635
|
-
import { createHash as
|
|
293745
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
|
|
293636
293746
|
import {
|
|
293637
|
-
closeSync as
|
|
293638
|
-
fsyncSync as
|
|
293639
|
-
openSync as
|
|
293747
|
+
closeSync as closeSync3,
|
|
293748
|
+
fsyncSync as fsyncSync3,
|
|
293749
|
+
openSync as openSync3,
|
|
293640
293750
|
readFileSync,
|
|
293641
|
-
renameSync,
|
|
293751
|
+
renameSync as renameSync2,
|
|
293642
293752
|
statSync,
|
|
293643
293753
|
unlinkSync,
|
|
293644
293754
|
writeFileSync
|
|
@@ -293681,7 +293791,7 @@ function registryRevision(snapshots) {
|
|
|
293681
293791
|
account_scope: snapshot.accountScope,
|
|
293682
293792
|
registry_status: snapshot.registryStatus
|
|
293683
293793
|
})).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
293684
|
-
return
|
|
293794
|
+
return createHash5("sha256").update(JSON.stringify(rows)).digest("hex");
|
|
293685
293795
|
}
|
|
293686
293796
|
function validState(value) {
|
|
293687
293797
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -293945,21 +294055,21 @@ class StreamHealthPublisher {
|
|
|
293945
294055
|
const temporary = `${this.#statePath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
293946
294056
|
let fd2;
|
|
293947
294057
|
try {
|
|
293948
|
-
fd2 =
|
|
294058
|
+
fd2 = openSync3(temporary, "wx", 384);
|
|
293949
294059
|
writeFileSync(fd2, JSON.stringify(this.#state));
|
|
293950
|
-
|
|
293951
|
-
|
|
294060
|
+
fsyncSync3(fd2);
|
|
294061
|
+
closeSync3(fd2);
|
|
293952
294062
|
fd2 = undefined;
|
|
293953
|
-
|
|
293954
|
-
const parentFd =
|
|
294063
|
+
renameSync2(temporary, this.#statePath);
|
|
294064
|
+
const parentFd = openSync3(parent, "r");
|
|
293955
294065
|
try {
|
|
293956
|
-
|
|
294066
|
+
fsyncSync3(parentFd);
|
|
293957
294067
|
} finally {
|
|
293958
|
-
|
|
294068
|
+
closeSync3(parentFd);
|
|
293959
294069
|
}
|
|
293960
294070
|
} catch (error) {
|
|
293961
294071
|
if (fd2 !== undefined)
|
|
293962
|
-
|
|
294072
|
+
closeSync3(fd2);
|
|
293963
294073
|
try {
|
|
293964
294074
|
unlinkSync(temporary);
|
|
293965
294075
|
} catch {}
|
|
@@ -312456,4 +312566,4 @@ export {
|
|
|
312456
312566
|
CEXBroker as default
|
|
312457
312567
|
};
|
|
312458
312568
|
|
|
312459
|
-
//# debugId=
|
|
312569
|
+
//# debugId=B0758AED2799A60464756E2164756E21
|