-
Notifications
You must be signed in to change notification settings - Fork 5
Resolve all kafka offsets #124
Changes from all commits
ee912bb
00c3478
d160964
1515702
bc7555f
12ba2c6
3fd51b8
b897fbd
5d6d1b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,14 +6,12 @@ import { PluginEvent } from '@posthog/plugin-scaffold' | |
| import { status } from '../status' | ||
| import { killGracefully } from '../utils' | ||
|
|
||
| export type BatchCallback = (messages: Message[]) => Promise<void> | ||
|
|
||
| export class KafkaQueue implements Queue { | ||
| private pluginsServer: PluginsServer | ||
| private kafka: Kafka | ||
| private consumer: Consumer | ||
| private wasConsumerRan: boolean | ||
| private processEventBatch: (batch: PluginEvent[]) => Promise<any> | ||
| private processEventBatch: (batch: PluginEvent[]) => Promise<PluginEvent[]> | ||
| private saveEvent: (event: PluginEvent) => Promise<void> | ||
|
|
||
| constructor( | ||
|
|
@@ -34,32 +32,33 @@ export class KafkaQueue implements Queue { | |
| resolveOffset, | ||
| heartbeat, | ||
| commitOffsetsIfNecessary, | ||
| uncommittedOffsets, | ||
| isRunning, | ||
| isStale, | ||
| }: EachBatchPayload): Promise<void> { | ||
| const batchProcessingTimer = new Date() | ||
| const rawEvents: RawEventMessage[] = batch.messages.map((message) => ({ | ||
| ...JSON.parse(message.value!.toString()), | ||
| kafka_offset: message.offset, | ||
| })) | ||
| const parsedEvents = rawEvents.map((rawEvent) => ({ | ||
| ...rawEvent, | ||
| data: JSON.parse(rawEvent.data), | ||
| })) | ||
| const pluginEvents: PluginEvent[] = rawEvents.map((rawEvent) => { | ||
| const { data: dataStr, ...restOfRawEvent } = rawEvent | ||
| const event = { ...restOfRawEvent, ...JSON.parse(dataStr) } | ||
|
|
||
| const uuidOrder = new Map<string, number>() | ||
| const uuidOffset = new Map<string, string>() | ||
| const pluginEvents: PluginEvent[] = batch.messages.map((message, index) => { | ||
| const { data: dataStr, ...rawEvent } = JSON.parse(message.value!.toString()) | ||
| const event = { ...rawEvent, ...JSON.parse(dataStr) } | ||
| uuidOrder.set(event.uuid, index) | ||
| uuidOffset.set(event.uuid, message.offset) | ||
| return { | ||
| ...event, | ||
| kafka_offset: restOfRawEvent.kafka_offset, | ||
| site_url: event.site_url || null, | ||
| ip: event.ip || null, | ||
| } | ||
| }) | ||
| const processedEvents: PluginEvent[] = ( | ||
| await this.processEventBatch(pluginEvents) | ||
| ).filter((event: PluginEvent[] | false | null | undefined) => Boolean(event)) | ||
|
|
||
| const processedEvents = await this.processEventBatch(pluginEvents) | ||
|
|
||
| // Sort in the original order that the events came in, putting any randomly added events to the end. | ||
| // This is so we would resolve the correct kafka offsets in order. | ||
| processedEvents.sort( | ||
| (a, b) => (uuidOrder.get(a.uuid!) || pluginEvents.length) - (uuidOrder.get(b.uuid!) || pluginEvents.length) | ||
| ) | ||
|
Comment on lines
+54
to
+60
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice that the tacked on events are last!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should always return this or crash: https://github.com/PostHog/plugin-server/pull/124/files#diff-5d37292cf21755714262793e7553ee317a12b157d0601e27a0ca26e096a60c00L267 Though, yeah, there's no real sanitization. The plugins could return just
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose the |
||
|
|
||
| for (const event of processedEvents) { | ||
| if (!isRunning()) { | ||
| status.info('😮', 'Consumer not running anymore, canceling batch processing!') | ||
|
|
@@ -71,12 +70,17 @@ export class KafkaQueue implements Queue { | |
| } | ||
| const singleIngestionTimer = new Date() | ||
| await this.saveEvent(event) | ||
| resolveOffset(event.kafka_offset!) | ||
| const offset = uuidOffset.get(event.uuid!) | ||
| if (offset) { | ||
| resolveOffset(offset) | ||
| } | ||
| await heartbeat() | ||
| await commitOffsetsIfNecessary() | ||
| this.pluginsServer.statsd?.timing('kafka_queue.single_ingestion', singleIngestionTimer) | ||
| } | ||
| this.pluginsServer.statsd?.timing('kafka_queue.each_batch', batchProcessingTimer) | ||
| resolveOffset(batch.lastOffset()) | ||
| await commitOffsetsIfNecessary() | ||
| } | ||
|
|
||
| async start(): Promise<void> { | ||
|
|
@@ -88,9 +92,7 @@ export class KafkaQueue implements Queue { | |
| await this.consumer.subscribe({ topic: KAFKA_EVENTS_INGESTION_HANDOFF }) | ||
| // KafkaJS batching: https://kafka.js.org/docs/consuming#a-name-each-batch-a-eachbatch | ||
| await this.consumer.run({ | ||
| // TODO: eachBatchAutoResolve: false, // don't autoresolve whole batch in case we exit it early | ||
| // The issue is right now we'd miss some messages and not resolve them as processEventBatch COMPETELY | ||
| // discards some events, leaving us with no kafka_offset to resolve when in fact it should be resolved. | ||
|
Comment on lines
-91
to
-93
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment was a bit more cryptic than it should have been, but the concern was with potentially resolving offsets for unsaved events in two cases:
In the end though, just to control this behavior without any doubt (and we should always prefer at-least-once over at-most-once), I'd rather uncomment this (setting Here's the relevant comment from KafkaJS source:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
| eachBatchAutoResolve: false, // we are resolving the last offset of the batch more deliberately | ||
| autoCommitInterval: 500, // autocommit every 500 ms… | ||
| autoCommitThreshold: 1000, // …or every 1000 messages, whichever is sooner | ||
| eachBatch: this.eachBatch.bind(this), | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so this still doesn't address cases like: we've got events
a,b,c;bandcgets discarded; we never resolveb's andc's offsets; in case of restartbandc, get processed again. But then also it's true that plugins can return whatever the hell they want in any order, so no method can be completely trusted here (though in most cases there won't be shenanigans). Even sorting by UUIDs (which, the ones generated by us, are time-sortable with UUIDT), theuuidproperty may be randomized, so… Alright, whatever. In any case it's better to process an event at-least-once than at-most-once.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, this can go deep...