Repository navigation
Add ReloadingKafkaProducer - #573
Conversation
| } | ||
|
|
||
| impl ArroyoProducer<KafkaPayload> for ReloadingKafkaProducer { | ||
| fn produce( |
There was a problem hiding this comment.
joris: this method is now blocking, we need to make it non-blocking (but not async) again. wait for drain in bg too
| /// means rollout started, not that the config is live; watch | ||
| /// [`ReloadingKafkaProducer::generation`]. Broker work happens on the worker. | ||
| pub fn push_config(&self, blob: &[u8]) -> Result<ReloadOutcome, ReloadError> { | ||
| let config = ConfigBlob::parse(blob)?.producer_config(&self.inner.selector)?; |
There was a problem hiding this comment.
we need to add metrics here for failing to parse configs, tagged too
evanh
left a comment
There was a problem hiding this comment.
Conceptually all makes sense to me!
| /// Client construction does no I/O; this prevents swapping in an | ||
| /// unreachable producer. | ||
| fn probe(&self, candidate: &KafkaProducer) -> Result<(), KafkaError> { | ||
| let Some(timeout) = self.settings.probe_timeout else { |
There was a problem hiding this comment.
Should we skip the probe if the timeout isn't set? Or block indefinitely?
There was a problem hiding this comment.
i hate both, i made it non-optional
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9267fba. Configure here.
| } | ||
|
|
||
| struct Inner { | ||
| /// Read on every produce, taken for writing only to start buffering and |
There was a problem hiding this comment.
Pending gauge stuck after drop
Low Severity
ShutdownOnDrop sets shutdown and wakes the worker but never updates config_reload_pending. The worker also returns immediately on shutdown without calling record_pending. Dropping a producer that still has a desired config leaves the gauge at 1.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 9267fba. Configure here.
There was a problem hiding this comment.
yes i considered handling this but thought it was too much work. we'll have to deal with it at query time anyway since processes can die by e.g. OOM too. in the end we will be able to see an absence of datapoints.
| let producer = producer(&servers); | ||
|
|
||
| let outcome = producer.push_config(&blob(&servers, "all")).unwrap(); | ||
|
|
There was a problem hiding this comment.
Bug: A race condition can cause a re-pushed configuration to be silently dropped after a failed rollout attempt, even though push_config returns Accepted.
Severity: HIGH
Suggested Fix
Add a sequence or version number to the desired config. This will allow the worker to distinguish a new push of the same configuration from the one it just failed to apply, preventing incorrect deduplication.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: rust-arroyo/src/backends/kafka/reloading_producer.rs#L1000
Potential issue: A race condition in the `run_worker` method can cause a configuration
update to be silently dropped. If a config rollout fails (e.g., due to a failed probe)
and the same configuration is pushed again before the worker re-acquires its lock, the
worker will incorrectly deduplicate the request. It clears the `desired` config,
believing it has already processed it. However, the `push_config` call will have
returned `Accepted` to the caller, leading to a state mismatch where the caller believes
the config is queued for application, but the producer remains on the old configuration.
This is explicitly acknowledged as a "known gap" in the code comments.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
there are more bugs like this, adding a seq counter only fixes one of them. i want to punt on that rabbithole for now
evanh
left a comment
There was a problem hiding this comment.
Seems good to me, minus the couple followup TODOs. Nice work!
| if let Err(error) = producer.produce(&destination, payload) { | ||
| metrics::counter!( | ||
| "arroyo.producer.config_reload_replay_failed", | ||
| "topic" => self.selector.topic().to_owned(), | ||
| "producer_name" => self.producer_name.clone(), | ||
| ) | ||
| .increment(1); | ||
| tracing::error!( | ||
| topic = %self.selector.topic(), | ||
| %error, | ||
| "could not replay a buffered message onto the new client" |
There was a problem hiding this comment.
Does that mean we lose the message during replay if produce returns an error? Should we retry here?
There was a problem hiding this comment.
yeah unfortunately (note that this is only for semantic partitioning, if you set ignore_key_ordering, the buffer is disabled entirely). rdkafka retries internally already though.
|
|
||
| if purged > 0 { | ||
| metrics::counter!( | ||
| "arroyo.producer.config_reload_purged_messages", |
There was a problem hiding this comment.
Should we drop the messages outright when drain times out? Could we retain the old client and retry draining, though that would further delay the reload.
There was a problem hiding this comment.
same as in the other comment, rdkafka already ought to retry on network errors, so i think the draining retry won't do much


POC of a kafka producer that can change config at runtime. It is built entirely outside of KafkaProducer so the POC is easier to review can be safely rolled out, but I'm not sure that's a good idea (after all, relay will probably be tied to KafkaProducer).
The basic idea of hot-reloading is that whenever a new config is provided, the producer is swapped out for a new one and the old one is drained in a bg thread. Bursts of config updates are debounced because only one swapping+draining operation can happen at a time. Newer config updates overwrite old ones.
produce()waits for the old producer to finish draining iff the produced message has a partition key. A partition key implies strict ordering of messages per-partition and in order to preserve these semantics, it's technically more correct to wait for the old producer to drain before using the new producer. In practice Relay sets partition keys for every message regardless of ordering requirements, so there's also a producer setting to opt out of blocking drain again.Config is fully hidden in Arroyo, users are expected to pass opaque config blobs from options-automator. These config blobs are ultimately managed by the streaming team in a central location.
ref STREAM-1804