feat: Publish committed records to Kafka document topic#208
Conversation
08eac38 to
97e86bf
Compare
|
pushed an update because I realized the package I had used wasn't compatible with the latest versions of note: had some issues with the package in the build pipeline because of the way we're blocking the post install scripts. the dockerfile acrobatics are an isolated solution for it without compromising the rest of the image |
6780df7 to
09c8ddb
Compare
| # all packages in subdirs of packages/ | ||
| - 'packages/*' | ||
| allowBuilds: | ||
| '@confluentinc/kafka-javascript': true |
There was a problem hiding this comment.
is it really needed to be true?
There was a problem hiding this comment.
Yes, because of the way our build pipeline works: @confluentinc/kafka-JavaScript bundles a native add-on built on librdkafka (a C library). It uses node-pre-gyp in its install lifecycle script to download a pre-built platform binary (the .node file). Without that binary, the package fails at runtime e.g. once deployed, with "Could not locate the bindings file" as a critical error.
The cause is that pnpm workspaces block all package lifecycle scripts by default, even on a plain pnpm install with no flags (which is a security feature we want). Now, to state the obvious, to benefit those who don't know, allowBuilds is the workspace-level opt-in list for which packages are permitted to run those scripts.... this should be used sparingly, and it works as an exception to that security feature, which is why @leoraba is asking to confirm.
Without '@confluentinc/kafka-javascript': true, pnpm silently skips its install script and the binary never lands, breaking both local dev and even the CI build stage (which does a full install before compilation).
The Dockerfile prod-deps stage is a separate concern: it uses --no-scripts explicitly as an OWASP requirement, which overrides allowBuilds entirely; so there we have to invoke node-pre-gyp manually after the fact.
i.e. allowBuilds only matters for installs that don't pass --no-scripts.
Hope that clarifies why this line is here.
leoraba
left a comment
There was a problem hiding this comment.
Approved, just left a comment regarding the message format, which might need an improvement later
| topic: string; | ||
| }; | ||
|
|
||
| const toMessage = (record: SubmittedDataResponse, action: 'delete' | 'insert' | 'update') => ({ |
There was a problem hiding this comment.
Surprised we don't have an existing type that enumerates these options. Would prefer these be defined as constants with a corresponding type. This is reflective of the type options that already exist, so it might exist somewhere
There was a problem hiding this comment.
Adding a new type for this, because the only ones that already exist are:
SUBMISSION_ACTION_TYPE: ['INSERTS', 'UPDATES', 'DELETES'] which are plural, uppercase, and represent submission categories, not individual record events.
AUDIT_ACTION: ['UPDATE', 'DELETE', 'MIGRATION'] with no insert, includes MIGRATION (?), and "audit" seems like it would diverge over time.
|
|
||
| if (configData.onFinishCommit && resultCommit) { | ||
| configData.onFinishCommit(resultCommit); | ||
| await configData.onFinishCommit(resultCommit); |
There was a problem hiding this comment.
@leoraba is this a case where we intended the onFinishCommit to occur asynchronously and not be waited for? or is this a missed await? if the former, needs a comment to clarify why its not being waited for.
There was a problem hiding this comment.
submissionService.ts:99 calls workerPool.commitSubmission(commitData) with no await: as the worker runs fully async and the HTTP 200 goes back immediately with PROCESSING status... so the await here is meant to just control the sequencing within the worker's own flow.
may be worth just adding a comment to make the intent obvious 🤷 (adding one now, for good measure)
There was a problem hiding this comment.
I've reviewed the context - onFinishCommit likely does not need to be awaited, but this is happening in a worker already so we're not blocking responses to a user so its safer to await so that our error handling can handle anything unexpected in this function call.
tldr - await is good ✅
| logger.error('[kafka] Failed to connect to broker(s) after retries, shutting down', err); | ||
| throw err; | ||
| } | ||
| logger.info(`[kafka] Connected (clientId: ${clientId}). Publishing commits to topic: ${topic}`); |
There was a problem hiding this comment.
👍 was confirming that we log the clientId, since its important that the logs say it if we end up using the default.
| allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || [], | ||
| corsEnabled: getBoolean(process.env.CORS_ENABLED, false), | ||
| logLevel: process.env.LOG_LEVEL || 'info', | ||
| port: process.env.PORT || 3030, |
There was a problem hiding this comment.
alphabetics are bae 👍
Summary
Add Kafka publishing to Lyric: after each commit, affected records are pushed as individual JSON messages to a configurable Kafka topic. A
published_attimestamp on thesubmissionstable now tracks successful publication without touching submission status or the existing state machine.Issues
No ticket. needed for overture-dev setup.
Description of Changes
This PR adds opt-in Kafka publishing. Setting
KAFKA_BROKERSenables it; omitting it leaves Lyric behaviour unchanged.NOTE:
onFinishCommit's type signature changed, and may require adjustment when integrating.packages/data-modelpublished_attimestamp column to thesubmissionstableALTER TABLE submissions ADD COLUMN published_at timestamp; backfills the Unix epoch (1970-01-01) for all existingCOMMITTEDrows as a "pre-Kafka" sentinel (distinguishes "committed before Kafka existed" from "publish failed")packages/data-provideronFinishCommitcallback signature changed from() => voidto(resultOnCommit: ResultOnCommit) => Promise<void>- coordinated with PCGL/iMicroSeq (@leoraba)src/external/kafkaPublisher.ts:createKafkaPublisherfactory implementing the document topic pattern - one message per affected record (inserts, updates, deletes), batched in a singleproducer.sendcall; deleted records haveisValidforced tofalsesrc/external/kafkaPublishTracker.ts:createPublishTrackerwritespublished_attimestamp on successful send; publish and tracking errors are isolated in separate try/catch blocksprovider()accepts an optionaldbparameter so the server can share one DB pool between the provider and the publish trackercreateKafkaPublisher,createPublishTracker,KafkaProducer,KafkaPublisherConfig,connect,getLogger,LoggercreateKafkaPublisherintest/unit/external/kafkaPublisher.spec.tsdocs/kafka-publishing.md: integration guide for library consumers (message format, wiring pattern, config reference, error handling, publish tracking semantics)apps/serverkafkajsdependencysrc/config/kafka.ts:setupKafkareads env vars, connects producer, checks/creates the topic via Admin client on startup, wirescreateKafkaPublisher+createPublishTracker; kafkajs retry config (5 retries, 300ms-30s exponential backoff); graceful disconnectsrc/config/app.ts: refactored from a staticappConfigexport tobuildAppConfig(overrides)+getDbConfig()for composable setup that accepts the Kafka callbacksrc/server.ts: creates a shared logger and DB pool before the provider; passes both tosetupKafkaandprovider; Kafka disconnect included in graceful shutdown sequence before worker pool drainapps/server/.env.schema: addedKAFKA_BROKERS,KAFKA_TOPIC,KAFKA_CLIENT_ID(all commented - opt-in)Special Instructions
pnpm installto installkafkajspnpm run migrate-dev)KAFKA_BROKERSto enable; omit it to run unchangedKAFKA_BROKERS,KAFKA_TOPIC,KAFKA_CLIENT_ID- seeDEVELOPMENT.mdReadiness Checklist
createKafkaPublishercovering the publish path,onSuccesscallback,onErrorcallback, and empty result short-circuitapps/server/.env.schemaand documented inDEVELOPMENT.md