Bound raw quote retention drain
All checks were successful
deploy / deploy (push) Successful in 1m13s

Proof: Raw NEAR Intents quote retention now keeps only a 30 minute raw firehose window and drains up to 10M stale unlinked raw rows per pass. Targeted raw retention tests, full npm test, and operator dashboard bundle build pass.

Assumptions: raw quote firehose rows are debug evidence, while normalized quote demand, decisions, commands, executor results, outcomes, inventory, pricing, and DB config remain the durable trading evidence.

Still fake: venue-native terminal fill ids and fee-complete realized PnL remain unavailable; raw quote firehose rows truncated during emergency recovery are intentionally no longer readable.
This commit is contained in:
philipp 2026-05-24 14:59:58 +02:00
parent a00b5dffad
commit d9e7d570f4
4 changed files with 84 additions and 37 deletions

View file

@ -121,9 +121,10 @@ const topics = [
const rawQuoteTopics = [ const rawQuoteTopics = [
config.kafkaTopicRawNearIntentsQuote, config.kafkaTopicRawNearIntentsQuote,
]; ];
const rawQuoteHistoryPruneIntervalMs = 15 * 60 * 1000; const rawQuoteHistoryPruneIntervalMs = 60 * 1000;
const rawQuoteHistoryRetainRecentMs = 6 * 60 * 60 * 1000; const rawQuoteHistoryRetainRecentMs = 30 * 60 * 1000;
const rawQuoteHistoryPruneBatchSize = 100_000; const rawQuoteHistoryPruneBatchSize = 500_000;
const rawQuoteHistoryPruneMaxBatches = 20;
const liveEvidenceTopics = [ const liveEvidenceTopics = [
config.kafkaTopicNormSwapDemand, config.kafkaTopicNormSwapDemand,
config.kafkaTopicDecisionTradeDecision, config.kafkaTopicDecisionTradeDecision,
@ -312,6 +313,7 @@ async function maybePruneRawQuoteHistory({ force = false } = {}) {
now: new Date(nowMs).toISOString(), now: new Date(nowMs).toISOString(),
retainRecentMs: rawQuoteHistoryRetainRecentMs, retainRecentMs: rawQuoteHistoryRetainRecentMs,
batchSize: rawQuoteHistoryPruneBatchSize, batchSize: rawQuoteHistoryPruneBatchSize,
maxBatches: rawQuoteHistoryPruneMaxBatches,
}); });
state.last_raw_quote_prune_at = new Date(nowMs).toISOString(); state.last_raw_quote_prune_at = new Date(nowMs).toISOString();
state.raw_quote_prune_deleted_count += result.deletedCount; state.raw_quote_prune_deleted_count += result.deletedCount;

View file

@ -2127,8 +2127,9 @@ export async function insertHistoryEvents(pool, entries = []) {
export async function pruneRawNearIntentsQuoteHistory(pool, { export async function pruneRawNearIntentsQuoteHistory(pool, {
now = new Date().toISOString(), now = new Date().toISOString(),
retainRecentMs = 6 * 60 * 60 * 1000, retainRecentMs = 30 * 60 * 1000,
batchSize = 100_000, batchSize = 100_000,
maxBatches = 1,
} = {}) { } = {}) {
const nowMs = Date.parse(now); const nowMs = Date.parse(now);
if (!Number.isFinite(nowMs)) throw new Error('now must be a valid timestamp'); if (!Number.isFinite(nowMs)) throw new Error('now must be a valid timestamp');
@ -2136,8 +2137,12 @@ export async function pruneRawNearIntentsQuoteHistory(pool, {
throw new Error('retain_recent_ms must be a positive integer'); throw new Error('retain_recent_ms must be a positive integer');
} }
const boundedBatchSize = Math.max(1, Math.min(500_000, Math.floor(Number(batchSize) || 0))); const boundedBatchSize = Math.max(1, Math.min(500_000, Math.floor(Number(batchSize) || 0)));
const boundedMaxBatches = Math.max(1, Math.min(100, Math.floor(Number(maxBatches) || 0)));
const cutoff = new Date(nowMs - retainRecentMs).toISOString(); const cutoff = new Date(nowMs - retainRecentMs).toISOString();
let deletedCount = 0;
let batches = 0;
for (let batch = 0; batch < boundedMaxBatches; batch += 1) {
const result = await pool.query( const result = await pool.query(
` `
WITH stale_raw_quotes AS ( WITH stale_raw_quotes AS (
@ -2169,12 +2174,19 @@ export async function pruneRawNearIntentsQuoteHistory(pool, {
`, `,
[cutoff, boundedBatchSize], [cutoff, boundedBatchSize],
); );
const batchDeleted = Number(result.rowCount || 0);
deletedCount += batchDeleted;
batches += 1;
if (batchDeleted < boundedBatchSize) break;
}
return { return {
deletedCount: Number(result.rowCount || 0), deletedCount,
cutoff, cutoff,
retainRecentMs, retainRecentMs,
batchSize: boundedBatchSize, batchSize: boundedBatchSize,
maxBatches: boundedMaxBatches,
batches,
}; };
} }

View file

@ -20,8 +20,12 @@ test('history writer replays durable topics but joins the raw quote firehose liv
assert.match(source, /runHistoryConsumer\(rawQuoteConsumer\)/); assert.match(source, /runHistoryConsumer\(rawQuoteConsumer\)/);
assert.match(source, /eachBatch/); assert.match(source, /eachBatch/);
assert.match(source, /insertHistoryEvents/); assert.match(source, /insertHistoryEvents/);
assert.match(source, /rawQuoteHistoryRetainRecentMs\s*=\s*6 \* 60 \* 60 \* 1000/); assert.match(source, /rawQuoteHistoryPruneIntervalMs\s*=\s*60 \* 1000/);
assert.match(source, /rawQuoteHistoryRetainRecentMs\s*=\s*30 \* 60 \* 1000/);
assert.match(source, /rawQuoteHistoryPruneBatchSize\s*=\s*500_000/);
assert.match(source, /rawQuoteHistoryPruneMaxBatches\s*=\s*20/);
assert.match(source, /pruneRawNearIntentsQuoteHistory/); assert.match(source, /pruneRawNearIntentsQuoteHistory/);
assert.match(source, /maxBatches:\s*rawQuoteHistoryPruneMaxBatches/);
assert.match(source, /batch\.topic === config\.kafkaTopicRawNearIntentsQuote/); assert.match(source, /batch\.topic === config\.kafkaTopicRawNearIntentsQuote/);
}); });

View file

@ -128,6 +128,35 @@ test('raw quote retention preserves rows linked to maker lifecycle evidence', as
assert.deepEqual(queries[0].params, ['2026-05-21T06:00:00.000Z', 123]); assert.deepEqual(queries[0].params, ['2026-05-21T06:00:00.000Z', 123]);
}); });
test('raw quote retention drains multiple bounded batches when firehose backlog exceeds one batch', async () => {
const rowCounts = [500_000, 500_000, 12];
const queries = [];
const pool = {
async query(sql, params) {
queries.push({ sql, params });
return { rows: [], rowCount: rowCounts.shift() ?? 0 };
},
};
const result = await pruneRawNearIntentsQuoteHistory(pool, {
now: '2026-05-21T12:00:00.000Z',
retainRecentMs: 30 * 60 * 1000,
batchSize: 500_000,
maxBatches: 20,
});
assert.equal(result.deletedCount, 1_000_012);
assert.equal(result.cutoff, '2026-05-21T11:30:00.000Z');
assert.equal(result.batchSize, 500_000);
assert.equal(result.maxBatches, 20);
assert.equal(result.batches, 3);
assert.equal(queries.length, 3);
for (const query of queries) {
assert.match(query.sql, /DELETE FROM raw_near_intents_quotes/);
assert.deepEqual(query.params, ['2026-05-21T11:30:00.000Z', 500_000]);
}
});
function historyEvent(eventId, payload) { function historyEvent(eventId, payload) {
return { return {
event_id: eventId, event_id: eventId,