Class: Rdkafka::Producer::PartitionsCountCache
- Inherits:
-
Object
- Object
- Rdkafka::Producer::PartitionsCountCache
- Includes:
- Helpers::Time
- Defined in:
- lib/rdkafka/producer/partitions_count_cache.rb
Overview
Design considerations:
Caching mechanism for Kafka topic partition counts to avoid frequent cluster queries
This cache is designed to optimize the process of obtaining partition counts for topics. It uses several strategies to minimize Kafka cluster queries:
-
Statistics-based updates When statistics callbacks are enabled (via
statistics.interval.ms), we leverage this data to proactively update the partition counts cache. This approach costs approximately 0.02ms of processing time during each statistics interval (typically every 5 seconds) but eliminates the need for explicit blocking metadata queries. -
Edge case handling If a user configures
statistics.interval.msmuch higher than the default cache TTL (Defaults::PARTITIONS_COUNT_CACHE_TTL_MSms), the cache will still function correctly. When statistics updates don't occur frequently enough, the cache entries will expire naturally, triggering a blocking refresh when needed. -
User configuration awareness The cache respects user-defined settings. If
topic.metadata.refresh.interval.msis set very high, the responsibility for potentially stale data falls on the user. This is an explicit design choice to honor user configuration preferences and align with librdkafka settings. -
Process-wide efficiency Since this cache is shared across all Rdkafka producers and consumers within a process, having multiple clients improves overall efficiency. Each client contributes to keeping the cache updated, benefiting all other clients.
-
Thread-safety approach The implementation uses fine-grained locking with per-topic mutexes to minimize contention in multi-threaded environments while ensuring data consistency.
-
Topic recreation handling If a topic is deleted and recreated with fewer partitions, the cache keeps reporting the higher count only until the entry's TTL expires. The first refresh after expiry performs an authoritative metadata read and adopts the lower count. Within the TTL window a lower value is still ignored, so a transient or racy lower read cannot clobber a correct higher count.
Instance Method Summary collapse
-
#get(topic) { ... } ⇒ Integer
Reads partition count for a topic with automatic refresh when expired.
-
#initialize(ttl = :not_provided, ttl_ms: :not_provided) ⇒ PartitionsCountCache
constructor
Creates a new partition count cache.
-
#set(topic, new_count) ⇒ Object
Update partition count for a topic when needed.
-
#to_h ⇒ Hash
Hash with ttls and partitions counts array.
Methods included from Helpers::Time
#monotonic_now, #monotonic_now_ms
Constructor Details
#initialize(ttl = :not_provided, ttl_ms: :not_provided) ⇒ PartitionsCountCache
Creates a new partition count cache
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/rdkafka/producer/partitions_count_cache.rb', line 54 def initialize(ttl = :not_provided, ttl_ms: :not_provided) @counts = {} @mutex_hash = {} # Used only for @mutex_hash access to ensure thread-safety when creating new mutexes @mutex_for_hash = Mutex.new # Determine which TTL value to use if ttl != :not_provided && ttl_ms != :not_provided warn "DEPRECATION WARNING: Both ttl and ttl_ms were provided to PartitionsCountCache. " \ "Using ttl_ms. The ttl parameter is deprecated and will be removed in v1.0.0." @ttl_ms = ttl_ms elsif ttl != :not_provided warn "DEPRECATION WARNING: ttl (seconds) parameter for PartitionsCountCache is deprecated. " \ "Use ttl_ms (milliseconds) instead. This parameter will be removed in v1.0.0." @ttl_ms = (ttl * 1000).to_i elsif ttl_ms == :not_provided @ttl_ms = Defaults::PARTITIONS_COUNT_CACHE_TTL_MS else @ttl_ms = ttl_ms end end |
Instance Method Details
#get(topic) { ... } ⇒ Integer
The implementation prioritizes read performance over write consistency since partition counts typically only increase during normal operation.
Reads partition count for a topic with automatic refresh when expired
This method will return the cached partition count if available and not expired. If the value is expired or not available, it will execute the provided block to fetch the current value from Kafka.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/rdkafka/producer/partitions_count_cache.rb', line 89 def get(topic) current_info = @counts[topic] if current_info.nil? || expired?(current_info[0]) # The cached entry is missing or expired, so the block performs an authoritative metadata # read. We hand it to `set`, which adopts a higher count always and a lower count once the # entry has expired (e.g. the topic was recreated with fewer partitions). We then return # whatever `set` settled on so a concurrent refresh that wrote a higher value still wins. new_count = yield set(topic, new_count) return @counts[topic][1] end current_info[1] end |
#set(topic, new_count) ⇒ Object
We prioritize higher partition counts and only accept them when using a mutex to ensure consistency. This design decision is based on the fact that partition counts in Kafka only increase during normal operation.
Update partition count for a topic when needed
This method updates the partition count for a topic in the cache. It uses a mutex to ensure thread-safety during updates.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/rdkafka/producer/partitions_count_cache.rb', line 117 def set(topic, new_count) # First check outside mutex to avoid unnecessary locking current_info = @counts[topic] # Within the TTL window a lower value is treated as a stale/racy read and ignored, since # partition counts only grow during normal operation. Once the entry has expired a lower # value is an authoritative refresh (e.g. the topic was recreated with fewer partitions), # so we fall through and adopt it below. if current_info && new_count < current_info[1] && !expired?(current_info[0]) (topic) return end # Only lock the specific topic mutex mutex_for(topic).synchronize do # Check again inside the lock as another thread might have updated current_info = @counts[topic] if current_info.nil? # Create new entry @counts[topic] = [monotonic_now_ms, new_count] elsif new_count > current_info[1] || expired?(current_info[0]) # A higher count always wins; a lower count is accepted only when the existing entry # has expired, so a concurrent fresh higher value (which reset the timestamp) is never # clobbered by a stale lower one. current_info[0] = monotonic_now_ms current_info[1] = new_count else # Same or lower count within the TTL window: refresh the timestamp only current_info[0] = monotonic_now_ms end end end |
#to_h ⇒ Hash
Returns hash with ttls and partitions counts array.
153 154 155 |
# File 'lib/rdkafka/producer/partitions_count_cache.rb', line 153 def to_h @counts end |