Проблема
Affiliate networks fire several deposit postbacks per click — concurrently and out of order. Running totals, FTD and redeposit events could double-count or vanish. On top of that, one partner killed requests after 5 s while the handler waited 1–6 s on the tracker.
Рішення
Per-click Redis lock (SET NX EX, 10 s TTL, bounded retries) around a Zod-validated click state. If state is missing it is rebuilt from ClickHouse deposit logs. State and log are committed first, the tracker forward runs asynchronously.
Нюанс
A redeposit arriving before any known deposit still flags the click as newly qualified, so the qualification event is never lost. Replaced execFile(curl) with native fetch — another ~100 ms of process-fork overhead gone.
Результат
Deposit postback latency 1–6 s → under 200 ms; lead postbacks 1–5 s → under 10 ms. Partner timeouts disappeared.
// ілюстрація, переписано з нуля
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
async handleDeposit(pb: DepositPostback) {
return this.locks.withLock(`s2s-lock:${pb.clickId}`, { ttl: 10, retries: 5 }, async () => {
const state = (await this.state.get(pb.clickId))
?? (await this.rebuildFromClickHouse(pb.clickId))
const usd = await this.fx.toUsd(pb.amount, pb.currency, pb.occurredAt)
const next = ClickState.parse(applyDeposit(state, usd))
await Promise.all([
this.state.set(pb.clickId, next),
this.ch.insert('deposit_logs', [toLogRow(pb, next, usd)]),
])
setImmediate(() => {
this.tracker.forward(pb, next).catch(err => this.log.warn({ err, clickId: pb.clickId }, 'tracker forward failed'))
})
return { ok: true, ftd: next.isNewlyQualified, n: next.depositCount }
})
}- NestJS
- Fastify
- Redis
- ClickHouse
- Zod