karafka 2.6.0.beta1 → 2.6.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -27
- data/certs/expired.txt +2 -0
- data/config/locales/errors.yml +5 -0
- data/karafka.gemspec +1 -1
- data/lib/active_job/queue_adapters/karafka_adapter.rb +1 -1
- data/lib/karafka/active_job/current_attributes/loading.rb +1 -1
- data/lib/karafka/active_job/current_attributes.rb +4 -2
- data/lib/karafka/active_job/dispatcher.rb +1 -2
- data/lib/karafka/admin/consumer_groups.rb +8 -2
- data/lib/karafka/admin/replication.rb +6 -3
- data/lib/karafka/admin/topics.rb +32 -12
- data/lib/karafka/admin.rb +155 -59
- data/lib/karafka/app.rb +1 -1
- data/lib/karafka/base_consumer.rb +33 -5
- data/lib/karafka/cli/console.rb +0 -1
- data/lib/karafka/cli/help.rb +0 -1
- data/lib/karafka/cli/info.rb +0 -1
- data/lib/karafka/cli/install.rb +0 -1
- data/lib/karafka/cli/server.rb +0 -1
- data/lib/karafka/cli/swarm.rb +0 -1
- data/lib/karafka/cli.rb +2 -2
- data/lib/karafka/connection/client.rb +44 -7
- data/lib/karafka/connection/listener.rb +3 -3
- data/lib/karafka/connection/manager.rb +0 -1
- data/lib/karafka/connection/pauses_manager.rb +17 -0
- data/lib/karafka/connection/proxy.rb +45 -0
- data/lib/karafka/connection/status.rb +0 -1
- data/lib/karafka/instrumentation/assignments_tracker.rb +34 -0
- data/lib/karafka/instrumentation/callbacks/consumer_groups/decorator.rb +19 -0
- data/lib/karafka/instrumentation/callbacks/consumer_groups/error.rb +0 -1
- data/lib/karafka/instrumentation/callbacks/consumer_groups/statistics.rb +3 -2
- data/lib/karafka/instrumentation/critical_errors_listener.rb +85 -0
- data/lib/karafka/instrumentation/logger_listener.rb +2 -0
- data/lib/karafka/instrumentation/monitor.rb +2 -3
- data/lib/karafka/instrumentation/vendors/appsignal/errors_listener.rb +0 -1
- data/lib/karafka/instrumentation/vendors/appsignal/metrics_listener.rb +0 -1
- data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +4 -3
- data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +0 -2
- data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +181 -18
- data/lib/karafka/pro/admin/recovery/errors.rb +0 -1
- data/lib/karafka/pro/admin/recovery.rb +6 -4
- data/lib/karafka/pro/base_consumer.rb +5 -5
- data/lib/karafka/pro/cleaner/messages/metadata.rb +0 -1
- data/lib/karafka/pro/cli/parallel_segments/base.rb +1 -2
- data/lib/karafka/pro/connection/manager.rb +7 -4
- data/lib/karafka/pro/encryption/messages/middleware.rb +5 -0
- data/lib/karafka/pro/encryption/messages/parser.rb +4 -0
- data/lib/karafka/pro/instrumentation/callbacks/consumer_groups/decorator.rb +63 -0
- data/lib/karafka/pro/instrumentation/consumer_groups/lag_compensation/compensator.rb +120 -0
- data/lib/karafka/pro/instrumentation/consumer_groups/lag_compensation/fetcher.rb +88 -0
- data/lib/karafka/pro/instrumentation/consumer_groups/lag_compensation/refresher.rb +185 -0
- data/lib/karafka/pro/instrumentation/consumer_groups/lag_compensation/registry.rb +102 -0
- data/lib/karafka/pro/instrumentation/performance_tracker.rb +40 -6
- data/lib/karafka/pro/iterator/tpl_builder.rb +27 -7
- data/lib/karafka/pro/iterator.rb +22 -7
- data/lib/karafka/pro/loader.rb +12 -0
- data/lib/karafka/pro/processing/consumer_groups/adaptive_iterator/consumer.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/adaptive_iterator/tracker.rb +2 -4
- data/lib/karafka/pro/processing/consumer_groups/collapser.rb +6 -9
- data/lib/karafka/pro/processing/consumer_groups/coordinator.rb +1 -4
- data/lib/karafka/pro/processing/consumer_groups/coordinators/errors_tracker.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/coordinators/filters_applier.rb +21 -10
- data/lib/karafka/pro/processing/consumer_groups/coordinators/virtual_offset_manager.rb +4 -4
- data/lib/karafka/pro/processing/consumer_groups/expansions_selector.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/filters/actions.rb +84 -0
- data/lib/karafka/pro/processing/consumer_groups/filters/base.rb +5 -6
- data/lib/karafka/pro/processing/consumer_groups/filters/delayer.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/filters/expirer.rb +1 -4
- data/lib/karafka/pro/processing/consumer_groups/filters/inline_insights_delayer.rb +1 -4
- data/lib/karafka/pro/processing/consumer_groups/filters/throttler.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/filters/virtual_limiter.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/jobs/consume_non_blocking.rb +2 -3
- data/lib/karafka/pro/processing/consumer_groups/jobs/eofed_non_blocking.rb +0 -2
- data/lib/karafka/pro/processing/consumer_groups/jobs/periodic.rb +2 -3
- data/lib/karafka/pro/processing/consumer_groups/jobs/periodic_non_blocking.rb +0 -2
- data/lib/karafka/pro/processing/consumer_groups/jobs/revoked_non_blocking.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/jobs_builder.rb +5 -4
- data/lib/karafka/pro/processing/consumer_groups/offset_metadata/consumer.rb +3 -7
- data/lib/karafka/pro/processing/consumer_groups/offset_metadata/fetcher.rb +8 -10
- data/lib/karafka/pro/processing/consumer_groups/offset_metadata/listener.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/parallel_segments/filters/base.rb +3 -5
- data/lib/karafka/pro/processing/consumer_groups/parallel_segments/filters/default.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/parallel_segments/filters/mom.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/partitioner.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/periodic_job/consumer.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/piping/consumer.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_ftr_lrj_mom.rb +10 -9
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_ftr_lrj_mom_vp.rb +11 -10
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_ftr_mom.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_ftr_mom_vp.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_lrj_mom.rb +8 -7
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_lrj_mom_vp.rb +9 -8
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_mom.rb +5 -8
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_mom_vp.rb +3 -6
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/ftr_lrj_mom.rb +4 -7
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/ftr_lrj_mom_vp.rb +6 -9
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/ftr_mom.rb +3 -7
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/ftr_mom_vp.rb +4 -8
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/lrj_mom.rb +3 -6
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/lrj_mom_vp.rb +4 -7
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/mom.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/strategies/aj/mom_vp.rb +3 -6
- data/lib/karafka/pro/processing/consumer_groups/strategies/base.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/default.rb +56 -33
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/default.rb +27 -14
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_lrj.rb +4 -7
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_lrj_mom.rb +5 -8
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_lrj_mom_vp.rb +5 -8
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_lrj_vp.rb +3 -7
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_mom.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_mom_vp.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_vp.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/lrj.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/lrj_mom.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/lrj_mom_vp.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/lrj_vp.rb +3 -6
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/mom.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/mom_vp.rb +3 -6
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/vp.rb +65 -8
- data/lib/karafka/pro/processing/consumer_groups/strategies/ftr/default.rb +0 -4
- data/lib/karafka/pro/processing/consumer_groups/strategies/ftr/vp.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/default.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/ftr.rb +3 -6
- data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/ftr_mom.rb +4 -7
- data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/ftr_mom_vp.rb +4 -8
- data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/ftr_vp.rb +3 -6
- data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/mom.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/mom_vp.rb +3 -7
- data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/vp.rb +2 -5
- data/lib/karafka/pro/processing/consumer_groups/strategies/mom/default.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/mom/ftr.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/mom/ftr_vp.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/mom/vp.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/strategies/vp/default.rb +11 -6
- data/lib/karafka/pro/processing/consumer_groups/strategy_selector.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/subscription_groups_coordinator.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/virtual_partitions/distributors/balanced.rb +0 -3
- data/lib/karafka/pro/processing/consumer_groups/virtual_partitions/distributors/base.rb +2 -4
- data/lib/karafka/pro/processing/consumer_groups/virtual_partitions/distributors/consistent.rb +0 -3
- data/lib/karafka/pro/processing/jobs_queue.rb +4 -1
- data/lib/karafka/pro/processing/schedulers/base.rb +4 -2
- data/lib/karafka/pro/processing/schedulers/default.rb +2 -2
- data/lib/karafka/pro/recurring_tasks/consumer.rb +9 -2
- data/lib/karafka/pro/recurring_tasks/contracts/config.rb +0 -1
- data/lib/karafka/pro/recurring_tasks/executor.rb +25 -13
- data/lib/karafka/pro/routing/features/consumer_groups/direct_assignments/subscription_group.rb +0 -2
- data/lib/karafka/pro/routing/features/consumer_groups/long_running_job.rb +0 -2
- data/lib/karafka/pro/routing/features/consumer_groups/multiplexing/proxy.rb +2 -2
- data/lib/karafka/pro/routing/features/consumer_groups/non_blocking_job.rb +0 -2
- data/lib/karafka/pro/routing/features/consumer_groups/offset_metadata/topic.rb +0 -6
- data/lib/karafka/pro/routing/features/consumer_groups/parallel_segments/consumer_group.rb +5 -4
- data/lib/karafka/pro/routing/features/consumer_groups/patterns/config.rb +0 -6
- data/lib/karafka/pro/routing/features/consumer_groups/patterns/consumer_group.rb +2 -1
- data/lib/karafka/pro/routing/features/consumer_groups/patterns/contracts/pattern.rb +0 -1
- data/lib/karafka/pro/routing/features/consumer_groups/patterns/detector.rb +2 -1
- data/lib/karafka/pro/routing/features/consumer_groups/patterns/pattern.rb +2 -1
- data/lib/karafka/pro/routing/features/consumer_groups/patterns/topics.rb +8 -8
- data/lib/karafka/pro/routing/features/consumer_groups/periodic_job/topic.rb +5 -5
- data/lib/karafka/pro/routing/features/consumer_groups/recurring_tasks/builder.rb +33 -13
- data/lib/karafka/pro/routing/features/consumer_groups/scheduled_messages/builder.rb +52 -28
- data/lib/karafka/pro/routing/features/consumer_groups/scheduled_messages/topic.rb +2 -1
- data/lib/karafka/pro/routing/features/consumer_groups/virtual_partitions/topic.rb +6 -5
- data/lib/karafka/pro/scheduled_messages/consumer.rb +7 -1
- data/lib/karafka/pro/scheduled_messages/schema_validator.rb +6 -1
- data/lib/karafka/pro/swarm/liveness_listener.rb +118 -13
- data/lib/karafka/pro.rb +0 -6
- data/lib/karafka/processing/consumer_groups/coordinator.rb +4 -5
- data/lib/karafka/processing/consumer_groups/coordinators_buffer.rb +6 -2
- data/lib/karafka/processing/consumer_groups/executor.rb +4 -4
- data/lib/karafka/processing/consumer_groups/executors_buffer.rb +2 -4
- data/lib/karafka/processing/consumer_groups/expansions_selector.rb +0 -2
- data/lib/karafka/processing/consumer_groups/inline_insights/consumer.rb +0 -2
- data/lib/karafka/processing/consumer_groups/inline_insights/listener.rb +0 -2
- data/lib/karafka/processing/consumer_groups/inline_insights/tracker.rb +14 -16
- data/lib/karafka/processing/consumer_groups/jobs/consume.rb +2 -5
- data/lib/karafka/processing/consumer_groups/jobs/eofed.rb +2 -3
- data/lib/karafka/processing/consumer_groups/jobs/idle.rb +2 -2
- data/lib/karafka/processing/consumer_groups/jobs/revoked.rb +2 -4
- data/lib/karafka/processing/consumer_groups/jobs/shutdown.rb +2 -2
- data/lib/karafka/processing/consumer_groups/partitioner.rb +3 -6
- data/lib/karafka/processing/consumer_groups/strategies/aj_dlq_mom.rb +5 -2
- data/lib/karafka/processing/consumer_groups/strategies/aj_mom.rb +2 -4
- data/lib/karafka/processing/consumer_groups/strategies/base.rb +3 -5
- data/lib/karafka/processing/consumer_groups/strategies/default.rb +43 -13
- data/lib/karafka/processing/consumer_groups/strategies/dlq.rb +5 -2
- data/lib/karafka/processing/consumer_groups/strategies/dlq_mom.rb +5 -2
- data/lib/karafka/processing/consumer_groups/strategies/mom.rb +2 -3
- data/lib/karafka/processing/consumer_groups/strategy_selector.rb +0 -2
- data/lib/karafka/processing/coordinator.rb +0 -2
- data/lib/karafka/processing/coordinators_buffer.rb +0 -2
- data/lib/karafka/processing/partitioner.rb +0 -2
- data/lib/karafka/processing/strategy_selector.rb +0 -2
- data/lib/karafka/processing/worker.rb +14 -6
- data/lib/karafka/processing/workers_pool.rb +21 -4
- data/lib/karafka/railtie.rb +0 -1
- data/lib/karafka/routing/contracts/routing.rb +2 -2
- data/lib/karafka/routing/features/consumer_groups/active_job/topic.rb +6 -5
- data/lib/karafka/routing/features/consumer_groups/manual_offset_management/topic.rb +2 -2
- data/lib/karafka/routing/features/deserializers/topic.rb +0 -1
- data/lib/karafka/routing/subscription_group.rb +1 -1
- data/lib/karafka/routing/topic.rb +4 -4
- data/lib/karafka/routing/topics.rb +21 -1
- data/lib/karafka/server.rb +9 -0
- data/lib/karafka/setup/config.rb +54 -10
- data/lib/karafka/setup/config_proxy.rb +2 -1
- data/lib/karafka/setup/contracts/config.rb +14 -0
- data/lib/karafka/swarm/node.rb +2 -2
- data/lib/karafka/version.rb +1 -2
- data/lib/karafka.rb +2 -2
- metadata +12 -4
|
@@ -65,6 +65,51 @@ module Karafka
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# Resolves offsets of many partitions with one batched `ListOffsets` request instead of one
|
|
69
|
+
# roundtrip per partition. This is the same broker query as `#query_watermark_offsets`,
|
|
70
|
+
# only batched, so it is sensitive to the same latencies and leader-election hiccups and we
|
|
71
|
+
# recover from them the same way and with the same settings.
|
|
72
|
+
#
|
|
73
|
+
# Named after `Admin::Topics#read_partition_offsets` (same specs in, same offsets out) and
|
|
74
|
+
# **not** after the underlying `#list_offsets`, so we do not shadow the rdkafka method of
|
|
75
|
+
# that name: it returns a handle to wait on, while we return the already resolved offsets.
|
|
76
|
+
#
|
|
77
|
+
# @param topic_partition_offsets [Hash{String => Array<Hash>}] topics with arrays of
|
|
78
|
+
# partition offset specs, each with a `:partition` and an `:offset` (`:earliest`,
|
|
79
|
+
# `:latest`, `:max_timestamp` or an integer timestamp in ms)
|
|
80
|
+
# @param isolation_level [Integer, nil] isolation level with which the offsets should be
|
|
81
|
+
# resolved or nil for the librdkafka default (read_uncommitted)
|
|
82
|
+
# @return [Array<Hash>] resolved offsets, each with `:topic`, `:partition` and `:offset`
|
|
83
|
+
# @note The `ListOffsets` admin API resolves `:latest` to the high watermark even with
|
|
84
|
+
# `isolation_level` set to read_committed, unlike `#query_watermark_offsets`, which returns
|
|
85
|
+
# the last stable offset for a read_committed consumer. On a topic with an in-flight
|
|
86
|
+
# transaction `:latest` therefore includes the uncommitted messages. Non-transactional
|
|
87
|
+
# topics are unaffected.
|
|
88
|
+
# @note Specs given as a literal hash need explicit curly braces: a brace-less trailing
|
|
89
|
+
# hash is parsed by Ruby as keyword arguments, which this method (having the
|
|
90
|
+
# `isolation_level:` keyword) would reject as a missing positional argument
|
|
91
|
+
def read_partition_offsets(topic_partition_offsets, isolation_level: nil)
|
|
92
|
+
l_config = proxy_config.query_watermark_offsets
|
|
93
|
+
|
|
94
|
+
with_broker_errors_retry(
|
|
95
|
+
# required to be in seconds, not ms
|
|
96
|
+
wait_time: l_config.wait_time / 1_000.to_f,
|
|
97
|
+
max_attempts: l_config.max_attempts
|
|
98
|
+
) do
|
|
99
|
+
@wrapped
|
|
100
|
+
.list_offsets(topic_partition_offsets, isolation_level: isolation_level)
|
|
101
|
+
.wait(max_wait_timeout_ms: l_config.timeout)
|
|
102
|
+
.offsets
|
|
103
|
+
rescue Rdkafka::AbstractHandle::WaitTimeoutError
|
|
104
|
+
# Align with the error contract of the per-partition watermark query, so a slow broker
|
|
105
|
+
# is retried here instead of escaping as a handle wait timeout
|
|
106
|
+
raise Rdkafka::RdkafkaError.new(
|
|
107
|
+
Rdkafka::Bindings::RD_KAFKA_RESP_ERR__TIMED_OUT,
|
|
108
|
+
"Error querying offsets of '#{topic_partition_offsets.keys.join(", ")}'"
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
68
113
|
# Similar to `#query_watermark_offsets`, this method can be sensitive to latency. We handle
|
|
69
114
|
# this the same way
|
|
70
115
|
#
|
|
@@ -14,6 +14,16 @@ module Karafka
|
|
|
14
14
|
class AssignmentsTracker
|
|
15
15
|
include Singleton
|
|
16
16
|
|
|
17
|
+
# Upper bound on the number of distinct topics tracked in `@generations`. Generations are
|
|
18
|
+
# intentionally retained after revocation (so they can keep incrementing across
|
|
19
|
+
# reassignments), which means the map only ever grows. With static topologies that is
|
|
20
|
+
# bounded by the routing topic count, but pattern subscriptions inject a fresh, permanent
|
|
21
|
+
# `Routing::Topic` per discovered name, so without a bound the map (and the frozen deep copy
|
|
22
|
+
# built in `#generations`) would grow for the whole process lifetime. When the bound is
|
|
23
|
+
# exceeded we evict the least-recently-assigned topics. It is high enough never to be reached
|
|
24
|
+
# by realistic static or even large dynamic topologies.
|
|
25
|
+
GENERATIONS_TOPICS_LIMIT = 100_000
|
|
26
|
+
|
|
17
27
|
class << self
|
|
18
28
|
# @return [Hash{Karafka::Routing::Topic => Array<Integer>}]
|
|
19
29
|
# @see #current
|
|
@@ -74,6 +84,10 @@ module Karafka
|
|
|
74
84
|
# reassignment. Revoked partitions remain in the hash with their last generation value.
|
|
75
85
|
#
|
|
76
86
|
# @note Returns a frozen deep copy to prevent external mutation
|
|
87
|
+
#
|
|
88
|
+
# @note The map is bounded to `GENERATIONS_TOPICS_LIMIT` topics; once exceeded, the
|
|
89
|
+
# least-recently-assigned topics are evicted. This only ever matters for very large dynamic
|
|
90
|
+
# (pattern-subscribed) topologies - static ones never reach the bound.
|
|
77
91
|
def generations
|
|
78
92
|
result = {}
|
|
79
93
|
|
|
@@ -188,11 +202,31 @@ module Karafka
|
|
|
188
202
|
@generations[topic][partition_id] += 1
|
|
189
203
|
end
|
|
190
204
|
|
|
205
|
+
# Move this topic to the end of the insertion-ordered map so it counts as
|
|
206
|
+
# most-recently-assigned for the bounded eviction below (keeps actively reassigned
|
|
207
|
+
# topics, drops genuinely stale ones first).
|
|
208
|
+
@generations[topic] = @generations.delete(topic)
|
|
209
|
+
|
|
191
210
|
@assignments[topic] += partition_ids
|
|
192
211
|
@assignments[topic].sort!
|
|
193
212
|
end
|
|
213
|
+
|
|
214
|
+
evict_stale_generations
|
|
194
215
|
end
|
|
195
216
|
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
# Keeps `@generations` bounded by evicting the least-recently-assigned topics once the limit
|
|
221
|
+
# is exceeded. Without this, pattern subscriptions (which keep injecting new permanent topics)
|
|
222
|
+
# would grow the map without bound for the whole process lifetime.
|
|
223
|
+
def evict_stale_generations
|
|
224
|
+
excess = @generations.size - GENERATIONS_TOPICS_LIMIT
|
|
225
|
+
|
|
226
|
+
return unless excess.positive?
|
|
227
|
+
|
|
228
|
+
@generations.keys.first(excess).each { |topic| @generations.delete(topic) }
|
|
229
|
+
end
|
|
196
230
|
end
|
|
197
231
|
end
|
|
198
232
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module Callbacks
|
|
6
|
+
module ConsumerGroups
|
|
7
|
+
# Default statistics decorator used for consumer group statistics decoration.
|
|
8
|
+
#
|
|
9
|
+
# This is a thin subclass (rather than referencing
|
|
10
|
+
# `Karafka::Core::Monitoring::StatisticsDecorator` directly) so it can be swapped out via
|
|
11
|
+
# `config.internal.statistics.consumer_groups.decorator_class` for a custom decorator (for
|
|
12
|
+
# example one that also enriches or corrects specific statistics values) without having to
|
|
13
|
+
# alter the callback that uses it.
|
|
14
|
+
class Decorator < Karafka::Core::Monitoring::StatisticsDecorator
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -4,7 +4,6 @@ module Karafka
|
|
|
4
4
|
module Instrumentation
|
|
5
5
|
# Callbacks used to transport things from rdkafka
|
|
6
6
|
module Callbacks
|
|
7
|
-
# Consumer-group-specific librdkafka callbacks
|
|
8
7
|
module ConsumerGroups
|
|
9
8
|
# Callback that kicks in when consumer error occurs and is published in a background thread
|
|
10
9
|
class Error
|
|
@@ -9,7 +9,8 @@ module Karafka
|
|
|
9
9
|
# those statistics
|
|
10
10
|
class Statistics
|
|
11
11
|
include Helpers::ConfigImporter.new(
|
|
12
|
-
monitor: %i[monitor]
|
|
12
|
+
monitor: %i[monitor],
|
|
13
|
+
decorator_class: %i[internal statistics consumer_groups decorator_class]
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
# @param subscription_group_id [String]
|
|
@@ -19,7 +20,7 @@ module Karafka
|
|
|
19
20
|
@subscription_group_id = subscription_group_id
|
|
20
21
|
@group_id = group_id
|
|
21
22
|
@client_name = client_name
|
|
22
|
-
@statistics_decorator =
|
|
23
|
+
@statistics_decorator = decorator_class.new
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
# Emits decorated statistics to the monitor
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Instrumentation
|
|
5
|
+
# Listener that watches the `error.occurred` bus for process-critical errors and initiates a
|
|
6
|
+
# graceful shutdown when one appears, regardless of where in the framework it surfaced
|
|
7
|
+
# (consumption flow, after-consume flow, worker, etc.).
|
|
8
|
+
#
|
|
9
|
+
# Process-critical errors (see the `internal.processing.critical_errors` setting) are not
|
|
10
|
+
# something a retry can handle: SystemExit means termination was explicitly requested and
|
|
11
|
+
# NoMemoryError means the VM cannot be trusted to continue. The error containment in the
|
|
12
|
+
# processing flow keeps the affected partition paused and its offsets uncommitted, so after
|
|
13
|
+
# the restart the failed work is redelivered - the stop initiated here is what converts that
|
|
14
|
+
# containment into the actual process replacement.
|
|
15
|
+
#
|
|
16
|
+
# It is auto-subscribed as part of Karafka itself.
|
|
17
|
+
class CriticalErrorsListener
|
|
18
|
+
include Singleton
|
|
19
|
+
include Helpers::ConfigImporter.new(
|
|
20
|
+
critical_errors: %i[internal processing critical_errors]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Sets up the one-shot initiation state
|
|
24
|
+
def initialize
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
@initiated = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Re-arms the listener when the app (re)starts so in-process restarts (embedded mode,
|
|
30
|
+
# integration flows) can escalate again
|
|
31
|
+
# @param _event [Karafka::Core::Monitoring::Event]
|
|
32
|
+
def on_app_running(_event)
|
|
33
|
+
@mutex.synchronize { @initiated = false }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param event [Karafka::Core::Monitoring::Event] error.occurred event. Its `:error` key
|
|
37
|
+
# carries the error that was reported.
|
|
38
|
+
def on_error_occurred(event)
|
|
39
|
+
error = event[:error]
|
|
40
|
+
|
|
41
|
+
return unless critical_errors.any? { |type| error.is_a?(type) }
|
|
42
|
+
|
|
43
|
+
# The one-shot flag makes the check-and-spawn atomic: critical errors reported by many
|
|
44
|
+
# consumers at the same moment (or a duplicated bus subscription) spawn exactly one
|
|
45
|
+
# supervising stop thread
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
return if @initiated
|
|
48
|
+
# No need to initiate the stop if one is already in motion (shutdown requested before
|
|
49
|
+
# the error surfaced). We deliberately do not use `App.done?` here: it includes the
|
|
50
|
+
# quieting/quiet states, in which the process is parked but alive - a critical error
|
|
51
|
+
# raised by in-flight work during quiet must still escalate to a full stop
|
|
52
|
+
return if Karafka::App.stopping?
|
|
53
|
+
return if Karafka::App.stopped?
|
|
54
|
+
return if Karafka::App.terminated?
|
|
55
|
+
|
|
56
|
+
@initiated = true
|
|
57
|
+
|
|
58
|
+
# `Karafka::Server.stop` supervises the whole shutdown (including the forceful timeout
|
|
59
|
+
# path), so it runs from a dedicated thread: the reporting thread must return so the
|
|
60
|
+
# processing flow can finish and engage the pause that protects the affected partition
|
|
61
|
+
# during the shutdown window
|
|
62
|
+
Thread.new do
|
|
63
|
+
Thread.current.name = "karafka.critical_shutdown"
|
|
64
|
+
|
|
65
|
+
Karafka::Server.stop
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
# The spawn itself can fail exactly under the conditions this listener handles (e.g.
|
|
69
|
+
# NoMemoryError, ThreadError at the thread limit). It must never escape: an error raised
|
|
70
|
+
# from an `error.occurred` subscriber would propagate into the reporting flow and could
|
|
71
|
+
# bypass the containment that protects the failed batch. We leave a best-effort fatal
|
|
72
|
+
# trace because in this degenerate state the process will NOT shut down despite the
|
|
73
|
+
# critical error - without it the why would be undiagnosable
|
|
74
|
+
rescue Exception
|
|
75
|
+
begin
|
|
76
|
+
Karafka.logger.fatal(
|
|
77
|
+
"Failed to initiate the critical error shutdown - process continues without it"
|
|
78
|
+
)
|
|
79
|
+
rescue Exception
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -467,6 +467,8 @@ module Karafka
|
|
|
467
467
|
when "virtual_partitions.partitioner.error"
|
|
468
468
|
error "Virtual partitions partitioner error occurred: #{details}"
|
|
469
469
|
error backtrace
|
|
470
|
+
when "lag_compensation.refresher.error"
|
|
471
|
+
warn "Lag compensation refresher error occurred: #{details}"
|
|
470
472
|
# This handles any custom errors coming from places like Web-UI, etc
|
|
471
473
|
else
|
|
472
474
|
error "#{type} error occurred: #{error.class} - #{details}"
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Karafka
|
|
4
4
|
module Instrumentation
|
|
5
|
-
# Karafka instrumentation monitor that we use to publish events
|
|
6
|
-
#
|
|
7
|
-
# `ActiveSupport::Notifications` as well
|
|
5
|
+
# Karafka instrumentation monitor that we use to publish events. By default uses our internal
|
|
6
|
+
# notifications bus but can be used with `ActiveSupport::Notifications` as well
|
|
8
7
|
class Monitor < Karafka::Core::Monitoring::Monitor
|
|
9
8
|
attr_reader :notifications_bus
|
|
10
9
|
|
|
@@ -5,7 +5,6 @@ require_relative "base"
|
|
|
5
5
|
module Karafka
|
|
6
6
|
module Instrumentation
|
|
7
7
|
module Vendors
|
|
8
|
-
# Namespace for Appsignal instrumentation
|
|
9
8
|
module Appsignal
|
|
10
9
|
# Listener for reporting errors from both consumers and producers
|
|
11
10
|
# Since we have the same API for WaterDrop and Karafka, we can use one listener with
|
|
@@ -6,8 +6,7 @@ module Karafka
|
|
|
6
6
|
module Vendors
|
|
7
7
|
# Datadog specific instrumentation
|
|
8
8
|
module Datadog
|
|
9
|
-
# A karafka's logger listener for Datadog
|
|
10
|
-
# It depends on the 'ddtrace' gem
|
|
9
|
+
# A karafka's logger listener for Datadog. It depends on the 'ddtrace' gem
|
|
11
10
|
class LoggerListener
|
|
12
11
|
include Karafka::Core::Configurable
|
|
13
12
|
extend Forwardable
|
|
@@ -17,7 +16,7 @@ module Karafka
|
|
|
17
16
|
# `Datadog::Tracing` client that we should use to trace stuff
|
|
18
17
|
setting :client
|
|
19
18
|
|
|
20
|
-
#
|
|
19
|
+
# See https://docs.datadoghq.com/tracing/trace_collection/dd_libraries/ruby
|
|
21
20
|
setting :service_name, default: nil
|
|
22
21
|
|
|
23
22
|
configure
|
|
@@ -145,6 +144,8 @@ module Karafka
|
|
|
145
144
|
error "Parallel segments partitioner error occurred: #{error}"
|
|
146
145
|
when "virtual_partitions.partitioner.error"
|
|
147
146
|
error "Virtual partitions partitioner error occurred: #{error}"
|
|
147
|
+
when "lag_compensation.refresher.error"
|
|
148
|
+
warn "Lag compensation refresher error occurred: #{error}"
|
|
148
149
|
else
|
|
149
150
|
error "#{event[:type]} error occurred: #{error}"
|
|
150
151
|
end
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Karafka
|
|
4
4
|
module Instrumentation
|
|
5
|
-
# Namespace for vendor specific instrumentation
|
|
6
5
|
module Vendors
|
|
7
|
-
# Datadog specific instrumentation
|
|
8
6
|
module Datadog
|
|
9
7
|
# Listener that can be used to subscribe to Karafka to receive stats via StatsD
|
|
10
8
|
# and/or Datadog
|
|
@@ -5,7 +5,6 @@ require "karafka/instrumentation/vendors/kubernetes/base_listener"
|
|
|
5
5
|
module Karafka
|
|
6
6
|
module Instrumentation
|
|
7
7
|
module Vendors
|
|
8
|
-
# Namespace for instrumentation related with Kubernetes
|
|
9
8
|
module Kubernetes
|
|
10
9
|
# Kubernetes HTTP listener that does not only reply when process is not fully hanging, but
|
|
11
10
|
# also allows to define max time of processing and looping.
|
|
@@ -26,6 +25,21 @@ module Karafka
|
|
|
26
25
|
#
|
|
27
26
|
# @note Please use `Kubernetes::SwarmLivenessListener` when operating in the swarm mode
|
|
28
27
|
class LivenessListener < BaseListener
|
|
28
|
+
# Default time in ms after which we consider consumption hanging (5 minutes)
|
|
29
|
+
DEFAULT_CONSUMING_TTL = 5 * 60 * 1_000
|
|
30
|
+
# Default max time in ms between polls before the process is considered dead (5 minutes)
|
|
31
|
+
DEFAULT_POLLING_TTL = 5 * 60 * 1_000
|
|
32
|
+
# Multiplier applied to max.poll.interval.ms when deriving the default stability_ttl.
|
|
33
|
+
# 2x gives headroom above the slowest legitimate rebalance phase (cooperative
|
|
34
|
+
# `wait-unassign-to-complete` can approach max.poll.interval.ms).
|
|
35
|
+
STABILITY_TTL_MAX_POLL_MULTIPLIER = 2
|
|
36
|
+
|
|
37
|
+
private_constant(
|
|
38
|
+
:DEFAULT_CONSUMING_TTL,
|
|
39
|
+
:DEFAULT_POLLING_TTL,
|
|
40
|
+
:STABILITY_TTL_MAX_POLL_MULTIPLIER
|
|
41
|
+
)
|
|
42
|
+
|
|
29
43
|
# @param hostname [String, nil] hostname or nil to bind on all
|
|
30
44
|
# @param port [Integer] TCP port on which we want to run our HTTP status server
|
|
31
45
|
# @param consuming_ttl [Integer] time in ms after which we consider consumption hanging.
|
|
@@ -33,12 +47,36 @@ module Karafka
|
|
|
33
47
|
# process as hanging
|
|
34
48
|
# @param polling_ttl [Integer] max time in ms for polling. If polling (any) does not
|
|
35
49
|
# happen that often, process should be considered dead.
|
|
36
|
-
# @
|
|
50
|
+
# @param stability_ttl [Integer, nil] max time in ms a subscription group can remain in
|
|
51
|
+
# the same tracked non-"steady" librdkafka `cgrp.join_state` (e.g. `wait-join`,
|
|
52
|
+
# `wait-assn`, `wait-sync`) before the process is considered unhealthy. When nil
|
|
53
|
+
# (default), it is derived lazily on first health evaluation as the maximum
|
|
54
|
+
# `max.poll.interval.ms` across all active subscription groups, multiplied by 2,
|
|
55
|
+
# which keeps the threshold safe relative to the user's actual poll intervals rather
|
|
56
|
+
# than a fixed constant. Per subscription group overrides of `max.poll.interval.ms`
|
|
57
|
+
# set via the routing DSL are respected - the largest value wins. When no subscription
|
|
58
|
+
# groups are available (routes not drawn), the root `Karafka::App.config.kafka` value
|
|
59
|
+
# is used instead (with librdkafka's default applied). `wait-metadata`
|
|
60
|
+
# and `init` are
|
|
61
|
+
# excluded from tracking and additionally clear any stale prior tracking for the
|
|
62
|
+
# subscription group: `wait-metadata` appears in normal scenarios (auto-create waits,
|
|
63
|
+
# unmatched pattern subscriptions, and as the group leader's post-JoinGroup state while
|
|
64
|
+
# fetching cluster metadata to compute the partition assignment); `init` indicates a
|
|
65
|
+
# client reset. Both states are covered by `polling_ttl` for genuine freezes.
|
|
66
|
+
# The timer resets on every join_state transition between tracked states because any
|
|
67
|
+
# change indicates the join protocol is still progressing; only a consumer frozen in
|
|
68
|
+
# the same non-steady state continuously triggers the alarm. A consumer cycling
|
|
69
|
+
# rapidly between non-steady states (e.g. wait-join -> wait-assn -> wait-join) will
|
|
70
|
+
# not trip this check - it only catches consumers frozen in a single join state.
|
|
71
|
+
# Requires `statistics.interval.ms` to be configured in the Kafka client settings;
|
|
72
|
+
# without it `statistics.emitted` never fires, tracking hashes remain empty, and this
|
|
73
|
+
# check has no effect (no misreporting).
|
|
37
74
|
def initialize(
|
|
38
75
|
hostname: nil,
|
|
39
76
|
port: 3000,
|
|
40
|
-
consuming_ttl:
|
|
41
|
-
polling_ttl:
|
|
77
|
+
consuming_ttl: DEFAULT_CONSUMING_TTL,
|
|
78
|
+
polling_ttl: DEFAULT_POLLING_TTL,
|
|
79
|
+
stability_ttl: nil
|
|
42
80
|
)
|
|
43
81
|
# If this is set to a symbol, it indicates unrecoverable error like fencing
|
|
44
82
|
# While fencing can be partial (for one of the SGs), we still should consider this
|
|
@@ -47,9 +85,21 @@ module Karafka
|
|
|
47
85
|
@unrecoverable = false
|
|
48
86
|
@polling_ttl = polling_ttl
|
|
49
87
|
@consuming_ttl = consuming_ttl
|
|
88
|
+
# When nil, resolved lazily via #stability_ttl on first health evaluation because
|
|
89
|
+
# the default derives from the routing subscription groups, which may not be drawn
|
|
90
|
+
# yet when the listener is constructed in karafka.rb
|
|
91
|
+
@stability_ttl = stability_ttl
|
|
50
92
|
@mutex = Mutex.new
|
|
51
93
|
@pollings = {}
|
|
52
94
|
@consumptions = {}
|
|
95
|
+
# Maps subscription_group_id => last observed cgrp.join_state string.
|
|
96
|
+
# Used to detect state changes so the stuck timer resets on any transition.
|
|
97
|
+
# Both hashes are only populated by on_statistics_emitted, which requires
|
|
98
|
+
# statistics.interval.ms to be set. Without it they remain empty and the
|
|
99
|
+
# stability flag in evaluate_ttl_flags is always false - no misreporting.
|
|
100
|
+
@join_states = {}
|
|
101
|
+
# Maps subscription_group_id => monotonic_now when the current non-steady state began.
|
|
102
|
+
@instabilities = {}
|
|
53
103
|
super(hostname: hostname, port: port)
|
|
54
104
|
end
|
|
55
105
|
|
|
@@ -70,6 +120,43 @@ module Karafka
|
|
|
70
120
|
mark_polling_tick
|
|
71
121
|
end
|
|
72
122
|
|
|
123
|
+
# Track when each subscription group's consumer enters or leaves a non-steady join state.
|
|
124
|
+
# A prolonged non-steady state (e.g., wait-join, wait-assn caused by a broker stuck in
|
|
125
|
+
# CompletingRebalance) is reported as unhealthy.
|
|
126
|
+
#
|
|
127
|
+
# `steady`, `init`, and `wait-metadata` all clear any stale tracking for the subscription
|
|
128
|
+
# group rather than skip the event. Clearing on `wait-metadata` matters because the
|
|
129
|
+
# state legitimately appears mid-join (leader fetching cluster metadata to compute
|
|
130
|
+
# assignment), during auto-create waits, and on unmatched pattern subscriptions - if we
|
|
131
|
+
# only `return` from these the previous non-steady timer keeps running and eventually
|
|
132
|
+
# trips a false positive. Clearing on `init` covers client resets / subscription-group
|
|
133
|
+
# removal. Genuine freezes in these states are covered by `polling_ttl`.
|
|
134
|
+
#
|
|
135
|
+
# The stuck timer resets on every transition between tracked states because any change
|
|
136
|
+
# indicates the join protocol is still progressing. Only a consumer frozen in the same
|
|
137
|
+
# non-steady state continuously for stability_ttl is flagged. A consumer cycling
|
|
138
|
+
# rapidly between non-steady states (e.g. wait-join -> wait-assn -> wait-join) will not
|
|
139
|
+
# trip this alarm - only a frozen-in-one-state consumer is caught.
|
|
140
|
+
# @param event [Karafka::Core::Monitoring::Event]
|
|
141
|
+
def on_statistics_emitted(event)
|
|
142
|
+
cgrp = event[:statistics]&.dig("cgrp")
|
|
143
|
+
return unless cgrp
|
|
144
|
+
|
|
145
|
+
sg_id = event[:subscription_group_id]
|
|
146
|
+
join_state = cgrp["join_state"]
|
|
147
|
+
return unless join_state
|
|
148
|
+
|
|
149
|
+
synchronize do
|
|
150
|
+
if join_state == "steady" || join_state == "init" || join_state == "wait-metadata"
|
|
151
|
+
@instabilities.delete(sg_id)
|
|
152
|
+
@join_states.delete(sg_id)
|
|
153
|
+
elsif @join_states[sg_id] != join_state
|
|
154
|
+
@join_states[sg_id] = join_state
|
|
155
|
+
@instabilities[sg_id] = monotonic_now
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
73
160
|
{
|
|
74
161
|
consume: :consumed,
|
|
75
162
|
revoke: :revoked,
|
|
@@ -127,30 +214,95 @@ module Karafka
|
|
|
127
214
|
clear_polling_tick
|
|
128
215
|
end
|
|
129
216
|
|
|
217
|
+
# Clear instability tracking for a subscription group whose listener finished its fetch
|
|
218
|
+
# loop (dynamic downscale or shutdown). This event fires after the listener's client has
|
|
219
|
+
# already been stopped, so no further statistics can move the join_state back to steady
|
|
220
|
+
# and no late statistics callback can re-create the entry. Without this cleanup a
|
|
221
|
+
# lingering non-steady entry (e.g. a downscale mid-rebalance) would guarantee a false
|
|
222
|
+
# positive once stability_ttl elapses. Genuine shutdown freezes remain covered by
|
|
223
|
+
# polling_ttl.
|
|
224
|
+
# @param event [Karafka::Core::Monitoring::Event]
|
|
225
|
+
def on_connection_listener_after_fetch_loop(event)
|
|
226
|
+
clear_instability_tracking(event[:subscription_group])
|
|
227
|
+
end
|
|
228
|
+
|
|
130
229
|
# Did we exceed any of the ttls
|
|
131
|
-
# @return [
|
|
230
|
+
# @return [Boolean] true if all TTLs are within bounds and no unrecoverable error
|
|
231
|
+
# @note When called from inside `status_body`, reuses the cached snapshot so the
|
|
232
|
+
# per-request HTTP response stays internally consistent. When called standalone,
|
|
233
|
+
# takes its own snapshot.
|
|
132
234
|
def healthy?
|
|
133
235
|
return false if @unrecoverable
|
|
134
|
-
return false if polling_ttl_exceeded?
|
|
135
|
-
return false if consuming_ttl_exceeded?
|
|
136
236
|
|
|
137
|
-
|
|
237
|
+
flags = @evaluation_snapshot || evaluate_ttl_flags
|
|
238
|
+
!flags[:polling] && !flags[:consuming] && !flags[:stability]
|
|
138
239
|
end
|
|
139
240
|
|
|
140
241
|
private
|
|
141
242
|
|
|
142
|
-
#
|
|
143
|
-
|
|
243
|
+
# Single-snapshot evaluation of all three TTL flags under one lock. Used by both
|
|
244
|
+
# `healthy?` and `status_body` so the HTTP status code (derived from healthy?) and the
|
|
245
|
+
# error details in the response body are always derived from the same atomic snapshot.
|
|
246
|
+
# @return [Hash{Symbol => Boolean}] flags for polling, consuming and stability
|
|
247
|
+
def evaluate_ttl_flags
|
|
144
248
|
time = monotonic_now
|
|
249
|
+
synchronize do
|
|
250
|
+
{
|
|
251
|
+
polling: @pollings.values.any? { |tick| (time - tick) > @polling_ttl },
|
|
252
|
+
consuming: @consumptions.values.any? { |tick| (time - tick) > @consuming_ttl },
|
|
253
|
+
stability: @instabilities.values.any? { |start| (time - start) > stability_ttl }
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
end
|
|
145
257
|
|
|
146
|
-
|
|
258
|
+
# @return [Integer] effective stability ttl in ms. Resolved lazily (instead of in
|
|
259
|
+
# `#initialize`) because the default derives from the routing subscription groups,
|
|
260
|
+
# which may not be drawn yet when the listener is constructed in karafka.rb. By the
|
|
261
|
+
# time health is evaluated, routing is final.
|
|
262
|
+
def stability_ttl
|
|
263
|
+
@stability_ttl ||= default_stability_ttl
|
|
147
264
|
end
|
|
148
265
|
|
|
149
|
-
# @return [
|
|
150
|
-
|
|
151
|
-
|
|
266
|
+
# @return [Integer] default stability_ttl derived from the configured
|
|
267
|
+
# max.poll.interval.ms, multiplied by 2 for headroom above the slowest legitimate
|
|
268
|
+
# rebalance phase. Uses the maximum value across all active subscription groups
|
|
269
|
+
# (their kafka configs already have librdkafka defaults injected), so per
|
|
270
|
+
# subscription group overrides set via the routing DSL are respected. Falls back to
|
|
271
|
+
# the root kafka config when no subscription groups are available.
|
|
272
|
+
def default_stability_ttl
|
|
273
|
+
max_poll_interval = Karafka::App
|
|
274
|
+
.subscription_groups
|
|
275
|
+
.values
|
|
276
|
+
.flatten
|
|
277
|
+
.map { |sg| sg.kafka.fetch(:"max.poll.interval.ms") }
|
|
278
|
+
.max
|
|
279
|
+
|
|
280
|
+
max_poll_interval ||= begin
|
|
281
|
+
# DefaultsInjector#consumer mutates a kafka config hash to ensure
|
|
282
|
+
# max.poll.interval.ms is set (using the same librdkafka default Karafka injects
|
|
283
|
+
# per subscription group). We dup so we don't mutate the global config.
|
|
284
|
+
kafka_config = Karafka::App.config.kafka.dup
|
|
285
|
+
Karafka::Setup::DefaultsInjector.consumer(kafka_config)
|
|
286
|
+
kafka_config.fetch(:"max.poll.interval.ms")
|
|
287
|
+
end
|
|
152
288
|
|
|
153
|
-
|
|
289
|
+
max_poll_interval * STABILITY_TTL_MAX_POLL_MULTIPLIER
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Removes join_state tracking for the given subscription group. Used when its listener
|
|
293
|
+
# stops, since no further statistics events will arrive to move the state back to
|
|
294
|
+
# steady.
|
|
295
|
+
# @param subscription_group [Karafka::Routing::SubscriptionGroup, nil] subscription
|
|
296
|
+
# group from the event payload (nil-safe for events emitted without one)
|
|
297
|
+
def clear_instability_tracking(subscription_group)
|
|
298
|
+
return unless subscription_group
|
|
299
|
+
|
|
300
|
+
sg_id = subscription_group.id
|
|
301
|
+
|
|
302
|
+
synchronize do
|
|
303
|
+
@instabilities.delete(sg_id)
|
|
304
|
+
@join_states.delete(sg_id)
|
|
305
|
+
end
|
|
154
306
|
end
|
|
155
307
|
|
|
156
308
|
# Wraps the logic with a mutex
|
|
@@ -194,15 +346,26 @@ module Karafka
|
|
|
194
346
|
end
|
|
195
347
|
end
|
|
196
348
|
|
|
197
|
-
# @return [Hash] response body status
|
|
349
|
+
# @return [Hash] response body status. Takes a single locked snapshot of all three TTL
|
|
350
|
+
# flags and caches it for the duration of this call so that `super` (which calls
|
|
351
|
+
# `healthy?` to populate the status field) and the merged `errors` hash see the same
|
|
352
|
+
# values. Avoids the previous pattern of taking the lock six times per response, where
|
|
353
|
+
# state could change between acquisitions and leave the status field disagreeing with
|
|
354
|
+
# the errors hash. The base envelope (timestamp, port, process_id, ...) still comes
|
|
355
|
+
# from `super` so any future field added to `BaseListener#status_body` is preserved.
|
|
198
356
|
def status_body
|
|
357
|
+
@evaluation_snapshot = evaluate_ttl_flags
|
|
358
|
+
|
|
199
359
|
super.merge!(
|
|
200
360
|
errors: {
|
|
201
|
-
polling_ttl_exceeded:
|
|
202
|
-
consumption_ttl_exceeded:
|
|
361
|
+
polling_ttl_exceeded: @evaluation_snapshot[:polling],
|
|
362
|
+
consumption_ttl_exceeded: @evaluation_snapshot[:consuming],
|
|
363
|
+
stability_ttl_exceeded: @evaluation_snapshot[:stability],
|
|
203
364
|
unrecoverable: @unrecoverable
|
|
204
365
|
}
|
|
205
366
|
)
|
|
367
|
+
ensure
|
|
368
|
+
@evaluation_snapshot = nil
|
|
206
369
|
end
|
|
207
370
|
end
|
|
208
371
|
end
|
|
@@ -159,14 +159,16 @@ module Karafka
|
|
|
159
159
|
# )
|
|
160
160
|
#
|
|
161
161
|
# @example Read offsets from a specific point in time
|
|
162
|
-
# Karafka::Admin::Recovery.read_committed_offsets(
|
|
162
|
+
# Karafka::Admin::Recovery.read_committed_offsets(
|
|
163
|
+
# 'sync', last_committed_at: Time.new(2025, 3, 1)
|
|
164
|
+
# )
|
|
163
165
|
#
|
|
164
166
|
# @example Migrate a stuck consumer group to a new name (two-step workflow)
|
|
165
167
|
# # Step 1: Read committed offsets from the broken group (bypasses coordinator)
|
|
166
168
|
# offsets = Karafka::Admin::Recovery.read_committed_offsets('sync')
|
|
167
169
|
# #=> { 'events' => { 0 => 1400, 1 => 1402 }, 'orders' => { 0 => 890 } }
|
|
168
170
|
#
|
|
169
|
-
# # Step 2: Inspect the recovered offsets
|
|
171
|
+
# # Step 2: Inspect the recovered offsets - verify all expected topics and partitions
|
|
170
172
|
# # are present and the offset values look reasonable before committing them
|
|
171
173
|
#
|
|
172
174
|
# # Step 3: Write the offsets to the target group using standard Admin APIs
|
|
@@ -193,11 +195,11 @@ module Karafka
|
|
|
193
195
|
next unless parsed[:group] == group_id
|
|
194
196
|
|
|
195
197
|
if parsed[:offset].nil?
|
|
196
|
-
# Tombstone
|
|
198
|
+
# Tombstone - offset was deleted, remove from results
|
|
197
199
|
committed[parsed[:topic]].delete(parsed[:partition])
|
|
198
200
|
committed.delete(parsed[:topic]) if committed[parsed[:topic]].empty?
|
|
199
201
|
else
|
|
200
|
-
# Last write wins
|
|
202
|
+
# Last write wins - scanning forward means we naturally end up with the most
|
|
201
203
|
# recent commit per partition
|
|
202
204
|
committed[parsed[:topic]][parsed[:partition]] = parsed[:offset]
|
|
203
205
|
end
|