Skip to content

Commit 7ffd959

Browse files
committed
feat(balance-worker): add partition track writer
1 parent ca99739 commit 7ffd959

5 files changed

Lines changed: 1341 additions & 84 deletions

File tree

apps/balance-worker/src/state/sqliteBalanceStateStore.ts

Lines changed: 112 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ export type KafkaRecordPosition = {
4141
offset: bigint;
4242
};
4343

44+
export type DurableTrackOutcomeRecord = {
45+
position: KafkaRecordPosition;
46+
outcome: TrackOutcome;
47+
};
48+
4449
export type DurableTrackOutcomeApplyResult =
4550
| {
4651
kind: "applied" | "duplicate";
@@ -174,98 +179,121 @@ export class SqliteBalanceStateStore {
174179
applyDurableTrackOutcome({
175180
position,
176181
outcome,
182+
}: DurableTrackOutcomeRecord): DurableTrackOutcomeApplyResult {
183+
const [result] = this.applyDurableTrackOutcomes({
184+
records: [{ position, outcome }],
185+
});
186+
if (!result) throw new Error("Expected a durable track outcome result");
187+
return result;
188+
}
189+
190+
applyDurableTrackOutcomes({
191+
records,
177192
}: {
178-
position: KafkaRecordPosition;
179-
outcome: TrackOutcome;
180-
}): DurableTrackOutcomeApplyResult {
181-
assertTopic({ topic: position.topic });
182-
assertPartition({ partition: position.partition });
183-
assertOffset({ offset: position.offset });
184-
const parsedOutcome = parseTrackOutcome({ input: outcome });
193+
records: readonly DurableTrackOutcomeRecord[];
194+
}): DurableTrackOutcomeApplyResult[] {
195+
const parsedRecords = records.map(({ position, outcome }) => {
196+
assertTopic({ topic: position.topic });
197+
assertPartition({ partition: position.partition });
198+
assertOffset({ offset: position.offset });
199+
return {
200+
position,
201+
outcome: parseTrackOutcome({ input: outcome }),
202+
};
203+
});
204+
if (parsedRecords.length === 0) return [];
185205

186206
return this.database
187-
.transaction(() => {
188-
const expectedOffset = readNextOffset({
189-
database: this.database,
190-
topic: position.topic,
191-
partition: position.partition,
192-
});
193-
if (expectedOffset === null) {
194-
throw new PartitionProgressNotFoundError({
195-
topic: position.topic,
196-
partition: position.partition,
197-
});
198-
}
199-
if (position.offset < expectedOffset) {
200-
return {
201-
kind: "position_already_applied",
202-
nextOffset: expectedOffset,
203-
} as const;
204-
}
205-
const partitionKey = meteringPartitionKeyOf({
206-
identity: parsedOutcome.identity,
207-
});
208-
const state = readState({
209-
database: this.database,
210-
identity: parsedOutcome.identity,
211-
});
212-
if (!state) throw new MeteringStateNotFoundError({ partitionKey });
207+
.transaction(() =>
208+
parsedRecords.map(({ position, outcome }) =>
209+
this.applyParsedDurableTrackOutcome({ position, outcome }),
210+
),
211+
)
212+
.immediate();
213+
}
213214

214-
const existingReceipt = readTrackReceipt({
215-
database: this.database,
216-
identity: parsedOutcome.identity,
217-
commandId: parsedOutcome.commandId,
218-
});
219-
const executed = executeEngineTrack({
220-
state,
221-
outcome: parsedOutcome,
222-
existingReceipt,
223-
});
224-
const nextOffset = position.offset + 1n;
215+
private applyParsedDurableTrackOutcome({
216+
position,
217+
outcome,
218+
}: DurableTrackOutcomeRecord): DurableTrackOutcomeApplyResult {
219+
const expectedOffset = readNextOffset({
220+
database: this.database,
221+
topic: position.topic,
222+
partition: position.partition,
223+
});
224+
if (expectedOffset === null) {
225+
throw new PartitionProgressNotFoundError({
226+
topic: position.topic,
227+
partition: position.partition,
228+
});
229+
}
230+
if (position.offset < expectedOffset) {
231+
return {
232+
kind: "position_already_applied",
233+
nextOffset: expectedOffset,
234+
};
235+
}
225236

226-
if (executed.kind === "applied") {
227-
const stateUpdate = updateState({
228-
database: this.database,
229-
partitionKey,
230-
revisionBefore: parsedOutcome.revisionBefore,
231-
state: executed.state,
232-
});
233-
if (stateUpdate.changes !== 1) {
234-
throw new CorruptBalanceStateError({ partitionKey });
235-
}
237+
const partitionKey = meteringPartitionKeyOf({ identity: outcome.identity });
238+
const state = readState({
239+
database: this.database,
240+
identity: outcome.identity,
241+
});
242+
if (!state) throw new MeteringStateNotFoundError({ partitionKey });
236243

237-
insertTrackReceipt({
238-
database: this.database,
239-
partitionKey,
240-
position,
241-
receipt: executed.receipt,
242-
});
243-
}
244+
const existingReceipt = readTrackReceipt({
245+
database: this.database,
246+
identity: outcome.identity,
247+
commandId: outcome.commandId,
248+
});
249+
const executed = executeEngineTrack({
250+
state,
251+
outcome,
252+
existingReceipt,
253+
});
254+
const nextOffset = position.offset + 1n;
244255

245-
const progressUpdate = advancePartitionProgress({
246-
database: this.database,
247-
topic: position.topic,
248-
partition: position.partition,
249-
expectedOffset,
250-
nextOffset,
251-
});
252-
if (progressUpdate.changes !== 1) {
253-
throw new UnexpectedKafkaOffsetError({
254-
topic: position.topic,
255-
partition: position.partition,
256-
expectedOffset,
257-
receivedOffset: position.offset,
258-
});
259-
}
256+
if (executed.kind === "applied") {
257+
const stateUpdate = updateState({
258+
database: this.database,
259+
partitionKey,
260+
revisionBefore: outcome.revisionBefore,
261+
state: executed.state,
262+
});
263+
if (stateUpdate.changes !== 1) {
264+
throw new CorruptBalanceStateError({ partitionKey });
265+
}
260266

261-
return {
262-
kind: executed.kind,
263-
state: executed.state,
264-
receipt: executed.receipt,
265-
nextOffset,
266-
} as const;
267-
})
268-
.immediate();
267+
insertTrackReceipt({
268+
database: this.database,
269+
partitionKey,
270+
position,
271+
receipt: executed.receipt,
272+
});
273+
}
274+
275+
const progressUpdate = advancePartitionProgress({
276+
database: this.database,
277+
topic: position.topic,
278+
partition: position.partition,
279+
expectedOffset,
280+
nextOffset,
281+
});
282+
if (progressUpdate.changes !== 1) {
283+
throw new UnexpectedKafkaOffsetError({
284+
topic: position.topic,
285+
partition: position.partition,
286+
expectedOffset,
287+
receivedOffset: position.offset,
288+
});
289+
}
290+
291+
return {
292+
kind: executed.kind,
293+
state: executed.state,
294+
receipt: executed.receipt,
295+
nextOffset,
296+
};
269297
}
270298

271299
close(): void {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { TrackOutcome } from "@autumn/balance-engine";
2+
3+
export class TrackOutcomeBatchNotCommittedError extends Error {
4+
constructor({ cause }: { cause: unknown }) {
5+
super("Track outcome batch was not committed", { cause });
6+
this.name = "TrackOutcomeBatchNotCommittedError";
7+
}
8+
}
9+
10+
export type CommittedTrackOutcomeAppender = {
11+
appendCommitted({
12+
topic,
13+
partition,
14+
outcomes,
15+
}: {
16+
topic: string;
17+
partition: number;
18+
outcomes: readonly TrackOutcome[];
19+
}): Promise<{ baseOffset: bigint }>;
20+
};

0 commit comments

Comments
 (0)