Releases: karafka/waterdrop
Releases · karafka/waterdrop
Release list
v2.10.2
- [Feature] Expose
Producer#current_variantas a public method. It returns the variant active for the current dispatch on the current fiber - the custom variant while inside a#with/#variant-wrapped call, otherwise the producer's default variant - so middleware and instrumentation listeners running synchronously within a dispatch can read the effective per-dispatch settings (topic_config,max_wait_timeout,default?). The lookup is fiber-local and dispatch-scoped: outside a variant-wrapped call (or from an asynchronous delivery callback) it returns the default variant. - [Enhancement] Stop allocating one interpolated string per message in
LoggerListenerbatch produce handlers. The quoted topic strings were only ever counted (quoting is a 1:1 mapping), never displayed, so counting the raw topic values yields the identical number with zero string allocations - relevant for largeproduce_many_*batches with the default logger listener attached. - [Enhancement] Use
Array#concatinProducer#buffer_manyinstead of appending messages one by one. - [Enhancement] Skip building the
message.acknowledgedinstrumentation payload in the delivery callback when nothing is subscribed to that event. The notifications bus already short-circuits on empty listeners, but only after the payload hash was allocated - once per delivered message on the polling thread. Mirrors the listener guard already used by the statistics callback. Late subscribers keep working as the check happens on each emission. - [Enhancement] Resolve the fiber-local variant once per
#producecall and once per#produce_many_syncwait phase instead of re-resolving it for every usage and for every waited delivery handle. For a 1,000-message sync batch this removes ~2,000 redundant fiber-local lookups. - [Enhancement] Do not allocate the fiber-local variants hash on the
Producer#current_variantread path. Previously every fiber that produced messages got a Hash pinned to it for the fiber's lifetime (per producer use), even when variants were never used - wasteful under fiber-per-request servers (Falcon, async). The hash is now only created by variant wrapper methods that actually need to write to it. - [Enhancement] Cache the variant validation contract in a constant instead of instantiating a new
Contracts::Varianton everyProducer#with/Producer#variantcall (mirrors the existingTransactions::CONTRACTpattern). - [Enhancement] Cache the tombstone validation contract in a constant instead of instantiating a new
Contracts::Tombstoneper tombstone message, removing per-message allocations in thetombstone_*APIs (mirrors the existingTransactions::CONTRACTpattern). - [Enhancement] Replace explicit
Warning[:performance]opt-in with a dynamic approach usingWarning.categories(available since Ruby 3.4) to automatically enable all stable opt-in warning categories in the test suite, including:strict_unused_blockintroduced in Ruby 4.0. - [Fix] Prevent a deadlock between a transactional single-message dispatch and
#close. A singleproduce_sync/produce_asyncon a transactional producer incremented the operations counter (which#closedrains while holding@transaction_mutex) before acquiring@transaction_mutexfor its per-message transaction - an inverted lock order. A dispatch that had counted itself but not yet taken@transaction_mutexcould deadlock a concurrent#closepermanently (the close wait loop has no timeout). Transactional dispatches now take@transaction_mutexbefore the operation is counted, matching#close's lock order (@transaction_mutex->@operating_mutex-> operations counter). - [Fix] Prevent a deadlock (
ThreadError: deadlock; recursive locking) when closing an idempotent producer (withreload_on_idempotent_fatal_errorenabled) that has buffered messages whose final flush surfaces a fatal librdkafka error.#closeperforms the final flush while already holding@operating_mutex, and the idempotent fatal-error reload tried to re-acquire that same mutex, leaving the producer stuck in:closingwith the native client leaked. The idempotent reload is now skipped on the closing path, and the final buffer flush is best-effort so client teardown always completes. - [Fix] Make concurrent idempotent fatal-error reload thread-safe. When several threads shared an idempotent producer (with
reload_on_idempotent_fatal_errorenabled), a single fatal librdkafka condition failed all their in-flight produces at once and each entered the reload path; the second reload ranreload!after the first had already reset@clienttonil, raisingNoMethodError. The idempotent reload now bails out if another thread already reloaded (mirroring the transactional path'sreturn if @status.configured?guard). Additionally,Status#active?now classifies the lifecycle from a single atomic read andProducer#ensure_active!branches on one snapshot, so a concurrentconfigured -> connectedtransition during a reload can no longer makeensure_active!raiseStatusInvalidErrorfor a valid, active producer. - [Fix] Stop
#flush_async/#flush_syncfrom silently dropping valid buffered messages when the dispatch fails.#flushremoves the batch from the internal buffer before dispatching it, and a failure (a single invalid message failing validation before anything is sent, or a mid-batch inline error such as queue full) previously discarded the entire taken batch - the removed messages were never restored. A failed flush now re-buffers the messages that never reached librdkafka (the whole batch on validation failure or on a transactional rollback, the unsent remainder otherwise) so they can be retried instead of being lost. - [Fix] Make
Producer#closefork-safe so the GC finalizer inherited by a forked child can no longer close the parent's client.#clientregisters anObjectSpacefinalizer that calls#close; that finalizer is inherited acrossfork, and a child that inherited a used producer, never touched it, and exited normally would run#closein the child - flushing and closing (with the real rdkafka client,rd_kafka_destroyon a fork-inherited handle, i.e. undefined behavior) a client owned by the parent.#closenow detects when it runs in a process other than the one that built the client, drops the inherited references and finalizer, and returns without touching the native client (matching the existing fork guard on the#clientpath). - [Fix] Guard the internal buffer appends in
Producer#bufferandProducer#buffer_manywith@buffer_mutex. The appends mutated the shared@messagesbuffer without the lock thatflush/purge/closehold while swapping it for a fresh array, so a concurrent swap landing between reading@messagesand appending could drop the message into an orphaned array that is never dispatched - silently losing buffered messages in the documented "buffer in one thread, flush in another" pattern. - [Fix] Stop a nested same-producer variant call from clobbering the outer variant inside a variant
transactionblock.transactionis the only variant-wrapped method that yields user code, so a variant call nested inside it (anothervariant.produce_*, or a raw producer dispatch in the same scope) used to delete the sharedFiber.current.waterdrop_clientsentry on return, making the rest of the block silently fall back to the default variant and dispatch with defaulttopic_config(timeouts, compression, partitioner) instead of the altered one. The wrapper now saves and restores the previous entry instead of unconditionally deleting it (still deleting when there was none, so the fiber-local hash does not accumulate stale keys). - [Fix] Stop
ConnectionPool#shutdownand#reloadfrom silently dropping in-flight messages. Both closed every pooled producer withclose!(force), which flushes for the max wait timeout and then purges whatever has not drained - so on a slow or unreachable broker, queuedproduce_asyncmessages were cancelled and lost with no delivery report. They now close producers gracefully by default (#reloadalways;#shutdownunless called with the newforce: true), letting messages flush instead of being purged. Passpool.shutdown(force: true)to keep the old force-and-purge behavior. - [Fix] Close a race in the FD poller where a producer registered while the last one was being torn down could be left permanently unpolled (sync produces hang until timeout, async deliveries are never acknowledged). The poller thread decided to exit (last producer unregistered) and cleared its thread reference in two separate, unsynchronized steps, so a
registerlanding in that gap saw the still-alive exiting thread, skipped starting a fresh one, and then had its producer's state closed by the exiting thread's cleanup. The thread now decides to stop and clears its reference in a single mutex section, so a racingregistereither keeps it running or starts a fresh thread; and the exit cleanup runs only on an abnormal exit, since a normal exit always leaves an empty registry and so can never close a producer registered in the gap.
v2.10.1
- [Fix] Prevent
Producer#closefrom raisingThreadError: can't be called from trap contextwhen invoked from a Ruby signal trap context (e.g. Puma'safter_stoppedDSL hook in single mode).closenow detects this case and delegates to a background thread, joining it so the caller blocks until the producer is fully closed (#866).
v2.10.0
- [Fix] Clean up native rdkafka client, global instrumentation callbacks, and poller registration when
init_transactionsfails during producer client construction. Previously, each failed attempt permanently leaked native threads, pipe file descriptors, and callback registry entries because the startedrd_kafka_thandle was abandoned without being destroyed. - [Breaking] Skip emitting librdkafka statistics when nothing is subscribed to
statistics.emittedat the time the underlying rdkafka client is constructed. When no listener is present at build time,statistics.interval.msis forced to0regardless of user configuration and the statistics callback is not registered, saving substantial allocations in the hot path (no JSON parsing, no statistics hash materialization, no decoration work). To use statistics, subscribe a listener tostatistics.emittedBEFORE the first producer use (before the underlying client is lazily initialized). - [Breaking] Raise
WaterDrop::Errors::StatisticsNotEnabledErrorwhen attempting to subscribe tostatistics.emitted(either via block or via a listener that responds toon_statistics_emitted) on a monitor where librdkafka statistics have been disabled at client build time. This replaces the "silent nothing" failure mode with an immediate, actionable error that pinpoints the timing mistake. - [Feature] Add tombstone API (
#tombstone_sync,#tombstone_async,#tombstone_many_sync,#tombstone_many_async) for producing tombstone records (nil-payload messages) with required key and partition validation. Works with variants. - [Fix] Add
ensure_same_process!toPoller#unregisterfor fork safety. Without this, a child process that inherited a pre-fork producer would deadlock onproducer.closebecauseunregisterwaited on a latch the dead parent poller thread would never release.
v2.9.0
- [Fix] Use
deletein the variant ensure block to avoid leaving stale nil entries inFiber.current.waterdrop_clientsand prevent memory leaks in long-running processes (#836). - [Fix] Exclude test files,
.github/, andlog/directories from gem releases to reduce package size. - [Breaking] Switch default polling mode from
:threadto:fd. If you experience any issues, you can revert to the previous behavior by settingconfig.polling.mode = :thread. The:threadmode will be deprecated in 2.10 and removed in 2.11. - [Breaking] Statistics decorator now only decorates keys used by the built-in Datadog metrics listener (
tx,txretries,txerrs,rxerrs) and skips unused subtrees (topics, broker window stats). This reduces decoration cost by ~425x on large clusters (10 brokers, 20 topics, 2000 partitions). Users who rely on other_dor_fdkeys in custom instrumentation should provide a custom decorator viaconfig.statistics_decorator. - [Feature] Add
config.statistics_decoratorsetting to allow full control over theStatisticsDecoratorinstance used for statistics decoration. Users can provide a custom decorator with differentonly_keysandexcluded_keysto match their instrumentation needs. - [Change] Upscale default timeout values 3x closer to librdkafka defaults to prevent intermediate timeouts during node recovery (
message.timeout.ms: 50s → 150s,transaction.timeout.ms: 55s → 165s,max_wait_timeout: 60s → 180s).
Upgrade Notes: https://karafka.io/docs/Upgrades-WaterDrop-2.9/
v2.8.16
- [Feature] Add FD-based polling mode (
config.polling.mode = :fd) as an alternative to the default thread-based polling. FD mode uses a single Ruby thread with IO.select for efficient multiplexing, providing 39-54% higher throughput, lower memory usage, and fewer threads compared to the default thread mode. - [Feature] Add
config.polling.fd.max_timesetting (default: 100ms) to control maximum polling time per producer per cycle. This enables per-producer priority differentiation. - [Feature] Add
Producer#queue_size(alias:#queue_length) to return the count of messages pending in the librdkafka queue. This counts messages that have been dispatched to librdkafka but not yet transmitted to the Kafka broker. - [Enhancement] Add
poll_nbandpoll_drainmethods to rdkafka Producer for efficient non-blocking polling without GVL release overhead. - [Enhancement] Add
poller.producer_registeredandpoller.producer_unregisteredinstrumentation events for FD mode. - [Change] Update to use
max_wait_timeout_msparameter in rdkafkaAbstractHandle#waitfor consistency with librdkafka conventions. - [Change] Require
karafka-rdkafka>=0.24.0due tomax_wait_timeout_msAPI change and FD polling support.
v2.8.15
- [Enhancement] Skip statistics decoration and emission when no listeners are subscribed to
statistics.emittedevents to reduce overhead. - [Enhancement] Support late subscription to
statistics.emittedby checking for listeners on each emission (every 5 seconds).
v2.8.14
- [Fix] Fatal error replacement not working without exact error unwind from the fatal envelope
- [Testing] Add
WaterDrop::Producer::Testingmodule for injecting and querying librdkafka fatal errors in tests. - [Testing] Add
#trigger_test_fatal_error(error_code, reason)method to simulate fatal errors without requiring actual broker-side conditions. - [Testing] Add
#fatal_errormethod to query current fatal error state for validation in tests. - [Testing] Add comprehensive test coverage for fatal error injection, reload behavior, and event instrumentation with real librdkafka errors.
- [Change] Require
karafka-rdkafka>=0.23.1due to error handling fixes.
v2.8.13
- [Enhancement] Make
fencederror skip-reload behavior configurable via newnon_reloadable_errorssetting (defaults to[:fenced]for backward compatibility). - [Enhancement] Add
producer.reloadevent allowing config modification before reload to escape fencing loops (#706). - [Enhancement] Do not early initialize the new instance on reload.
v2.8.12
- [Enhancement] Introduce
reload_on_idempotent_fatal_errorto automatically reload librdkafka producer after fatal errors on idempotent (non-transactional) producers. - [Enhancement] Add configurable backoff and retry limits for fatal error recovery to prevent infinite reload loops:
wait_backoff_on_idempotent_fatal_error(default: 5000ms) - backoff before retrying after idempotent fatal error reloadmax_attempts_on_idempotent_fatal_error(default: 5) - max reload attempts for idempotent fatal errorswait_backoff_on_transaction_fatal_error(default: 1000ms) - backoff after transactional fatal error reloadmax_attempts_on_transaction_fatal_error(default: 10) - max reload attempts for transactional fatal errors
- [Enhancement] Ensure
error.occurredis instrumented before idempotent fatal error reload for visibility. - [Enhancement] Automatically reset fatal error reload attempts counter on successful produce/transaction to allow recovery.
- [Refactor] Extract idempotence-related logic into separate
WaterDrop::Producer::Idempotencemodule. - [Refactor] Initialize
@idempotentand@transactionalinstance variables in Producer#initialize for consistent Ruby object shapes optimization. - [Refactor] Add
idempotent_reloadable?andidempotent_retryable?methods to encapsulate idempotent fatal error reload checks. - [Refactor] Add
transactional_retryable?method to encapsulate transactional fatal error reload retry checks. - [Fix] Waterdrop
config.kafkaerrors on frozen hash. - [Fix]
Producer#transactional?method now correctly computes transactional status when@transactionalis initialized to nil.
v2.8.11
- [Enhancement] Provide fast-track for middleware-less flows (20% faster) for single message, 5000x faster for batches.
- [Enhancement] Optimize middlewares application by around 20%.
- [Change] Remove Ruby
3.1according to the EOL schedule. - [Fix] Connection pool timeout parameter now accepts milliseconds instead of seconds for consistency with other WaterDrop timeouts. The default timeout has been changed from
5seconds to5000milliseconds (equivalent value).