karafka 2.6.0 → 2.6.1
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 +19 -0
- data/README.md +1 -1
- data/config/locales/pro_errors.yml +2 -0
- data/lib/karafka/connection/pauses_manager.rb +27 -4
- data/lib/karafka/constraints.rb +59 -19
- data/lib/karafka/messages/metadata.rb +11 -2
- data/lib/karafka/pro/encryption/cipher.rb +51 -21
- data/lib/karafka/pro/encryption/ciphers/base.rb +92 -0
- data/lib/karafka/pro/encryption/ciphers/direct.rb +74 -0
- data/lib/karafka/pro/encryption/ciphers/envelope.rb +164 -0
- data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
- data/lib/karafka/pro/encryption/setup/config.rb +22 -0
- data/lib/karafka/pro/encryption.rb +34 -0
- data/lib/karafka/pro/iterator.rb +5 -0
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/default.rb +1 -1
- data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/vp.rb +1 -1
- data/lib/karafka/pro/processing/consumer_groups/strategies/ftr/default.rb +1 -1
- data/lib/karafka/pro/processing/jobs_queue.rb +13 -3
- data/lib/karafka/pro/routing/features/consumer_groups/patterns/detector.rb +27 -4
- data/lib/karafka/pro/scheduled_messages/consumer.rb +9 -12
- data/lib/karafka/pro/scheduled_messages/contracts/message.rb +17 -2
- data/lib/karafka/pro/scheduled_messages/dispatcher.rb +46 -5
- data/lib/karafka/processing/consumer_groups/coordinators_buffer.rb +5 -4
- data/lib/karafka/server.rb +5 -2
- data/lib/karafka/setup/config.rb +5 -37
- data/lib/karafka/swarm/node.rb +2 -0
- data/lib/karafka/swarm/supervisor.rb +4 -2
- data/lib/karafka/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2d4a5f8788806e379defbbbd60e3ba605396b2cfed7fd78211ea5f14724c1b4
|
|
4
|
+
data.tar.gz: d9a183f86bb901603eec8dcaa1b98861fa48ded21ef9b8d95e47d3e100bd9f85
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f418e4dfac658f95fb12d1190e26d903e31d5ea28e38234c3bcb9eb90aee242e11af8f344544d51f3dfa0fe560aa9328a4517c01152c41773c625c99e3118c1
|
|
7
|
+
data.tar.gz: f15f728a174c61cb9d1ab56280f69d98dcee9d4f2e515b5c085bc6b69f2e87b6c8ad4c521f8cb0cf844252eb1d5dec813c4efda2954c7d3b7304c8973fb44cf1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Karafka Framework Changelog
|
|
2
2
|
|
|
3
|
+
## 2.6.1 (2026-08-24)
|
|
4
|
+
- **[Feature]** [Pro] Add an opt-in envelope encryption mode for Messages At Rest encryption (`config.encryption.mode = :envelope`, requires the openssl gem `>= 3.0`). It removes the RSA payload size ceiling of the default `:direct` mode (~245 bytes for 2048-bit keys) by encrypting each message with a one-time RSA-wrapped AES-256-GCM key. Both formats are recognized automatically on decryption, so data already at rest stays readable with no migration. Deployment: upgrade all consuming processes before enabling - older versions cannot decrypt envelope payloads, while upgraded consumers read both formats and keep producing `:direct` until switched, so the rollout can be staged safely. The default will switch to `:envelope` in a future release with prior notice; the `:direct` format stays decryptable indefinitely as data at rest never expires. Cipher key material is now pre-parsed (warmed) during setup - this introduces no new boot failure modes, as the configuration contract has always parsed and validated all configured keys at boot.
|
|
5
|
+
- [Fix] [Pro] Add a missing comma in the filtering strategy's unsupported-action guard so it raises `Karafka::Errors::UnsupportedCaseError` instead of a `NoMethodError`.
|
|
6
|
+
- [Fix] [Pro] Evict scheduled messages from the daily buffer per confirmed flush chunk instead of only after the whole flush. A broker error partway through a multi-chunk flush left the already-produced chunks in the buffer, re-dispatching them on the next tick (a duplicate window for non-transactional producers). Chunk size is now rounded up to even before shifting off the buffer, so a message's target and its own tombstone (always buffered as an adjacent pair) can no longer land in different chunks - with an odd or `1` `flush_batch_size`, the target's chunk could confirm and evict the key before the tombstone chunk ran, leaving the schedule non-tombstoned in Kafka (and re-dispatched after a restart/reload) if that later chunk then failed.
|
|
7
|
+
- [Fix] Prevent the `Karafka::Server#stop` specs from leaking a sub-second `shutdown_timeout` into the global config. The `#stop` examples mutate `Karafka::App.config.shutdown_timeout` (down to `500` ms, below the default `max_wait_time` of `1_000` ms) but never restored it, so with a random run order the invalid value bled into unrelated specs and made them fail contract validation with `shutdown_timeout must be more than max_wait_time`. The original value is now captured and restored (and the memoized class-level value cleared) in the example teardown.
|
|
8
|
+
- [Fix] [Pro] Reject a `schedule_target_epoch` set implausibly far in the future (e.g. a milliseconds value passed as seconds) at publish time instead of silently dropping it on the consumer.
|
|
9
|
+
- [Fix] [Pro] Raise `Karafka::Errors::UnsupportedCaseError` instead of a `NameError` (wrong constant namespace) when a custom DLQ strategy returns an unsupported flow, in both the default and virtual-partitions DLQ strategies.
|
|
10
|
+
- [Enhancement] Support named `Karafka::Constraints` registered in two phases: `:load` (require time) and `:config` (during setup, after contract validation), so feature-specific environment requirements are verified in one place.
|
|
11
|
+
- [Fix] Message `key` and `headers` deserialization results were not cached when the deserializer returned `nil` or `false` (e.g. keyless messages), re-running the deserializer on every access. Falsy results are now cached like truthy ones, matching the payload behavior and the documented contract.
|
|
12
|
+
- [Fix] [Pro] Reset the `Pro::Iterator` stored-offsets latch between runs. `#mark_as_consumed` set an internal `@stored_offsets` flag that was never cleared, so once a reused iterator marked anything, the teardown guard fired a spurious blocking sync `commit_offsets` on the end of every subsequent `#each` - each running on a brand-new consumer with no stored offsets. The flag is now reset alongside the other per-run state in the `#each` `ensure` block, so it reflects only the current run's marking activity (benign no-op commit removed; no correctness impact).
|
|
13
|
+
- [Fix] [Pro] Deduplicate runtime pattern discovery under multiplexing. Multiplexed subscription groups share a single consumer group but each runs its own listener thread and independently discovers the same topic; `ConsumerGroup#topic=` appended unconditionally, so the shared consumer group accumulated up to `multiplex_factor` duplicate `Topic` objects per discovered topic (polluting the routing tree and slowing linear topic scans, though the per-subscription-group consume path stayed correct). The detector now registers each discovered topic in the shared group only once (under its existing mutex) while still giving every subscription group its own `Topic` instance, mirroring how the multiplexing subscription groups builder dups topics per group at boot.
|
|
14
|
+
- [Fix] Fix sub-second `shutdown_timeout` values collapsing to a zero-iteration supervision loop. The graceful-shutdown wait computed its iteration count with integer division (`shutdown_timeout / 1_000`), so any configured `shutdown_timeout` below 1000ms (valid config, as long as it exceeds `max_wait_time`) floored to zero iterations, skipping the grace period entirely and forcing an immediate forceful shutdown (killing in-flight jobs before their offset commits). The count is now computed with float math and `ceil`, guaranteeing at least one supervision check for any positive timeout. The swarm supervisor shared the same formula (masked by the added grace period) and was fixed the same way.
|
|
15
|
+
- [Fix] Remove the backwards-compatible forwarding shim that kept the pre-nesting `config.internal.processing.*` paths (`coordinator_class`, `errors_tracker_class`, `partitioner_class`, `strategy_selector`, `expansions_selector`, `executor_class`, `jobs_builder`) resolving after the 2.6.0 move to `config.internal.processing.consumer_groups.*`. The shim was shipped by accident; the nesting was announced as having no backward compatibility. Use the nested `config.internal.processing.consumer_groups.*` paths instead. Requires `karafka-testing >= 2.6.2`, which reads these settings from their nested location.
|
|
16
|
+
- [Fix] Stop `PausesManager#@pauses` from growing unbounded across rebalances. `CoordinatorsBuffer#revoke` reset the pause tracker's retry attempt count on revocation but never removed it, and nothing else pruned the hash outside of critical-error recovery, so a `Pause` (and its `Mutex`) was retained forever for every distinct topic-partition ever assigned - harmless for static routing (`Topic` objects are stable and get reused on reassignment) but unbounded under regex pattern subscriptions with ephemeral, per-discovery topic names. `#revoke` now removes a tracker outright when it is not currently paused (added `PausesManager#delete`), since it carries no state worth preserving; a tracker that is paused is still reset-and-kept, unchanged, because its remaining backoff may need to be honoured again if the same partition is reclaimed (routine under eager rebalancing, where every previously owned partition is revoked and reassigned even when nothing has actually changed for it).
|
|
17
|
+
- [Fix] Allow-list the benign auto-create `TOPIC_ALREADY_EXISTS` broker warning in `unexpected_patterns_loop_spec.rb`, the same pre-existing broker-side race already allow-listed for other pattern-matched-topic specs, unrelated to this release's other fixes.
|
|
18
|
+
- [Fix] [Pro] Fix unbounded memory growth from `Pro::Processing::JobsQueue`'s per-group semaphore accumulating unconsumed `#tick` signals under non-blocking/LRJ and async-locking workloads.
|
|
19
|
+
- [Fix] [Pro] Stop `Pro::Processing::JobsQueue#unlock` from decrementing `@statistics[:waiting]` before confirming the job was actually tracked. It unconditionally decremented first and only afterwards checked whether the job was still present in `@in_waiting`, so a job unlocked after its subscription group had already been reset via `#clear` (which decrements `waiting` itself as part of recovery) both over-decremented the counter a second time and raised `JobsQueueSynchronizationError` - the counter corruption was silent while the (still legitimate) synchronization error masked it. `#unlock` now only decrements when the job is found, mirroring `#unlock_async`'s existing check-then-act pattern; both raise consistently when the group has already been cleared.
|
|
20
|
+
- [Fix] `Swarm::Node#signal` (used by `#stop`/`#terminate`/`#quiet`) could send a signal to a reaped node's stale `@pid`, which the OS may have already reassigned to an unrelated process. It now returns `false` without signaling if the node is already known to be dead.
|
|
21
|
+
|
|
3
22
|
## 2.6.0 (2026-08-05)
|
|
4
23
|
- **[Breaking]** Remove the flat global pause configuration accessors (`config.pause_timeout`, `config.pause_max_timeout`, `config.pause_with_exponential_backoff`) deprecated in 2.5.2. Use the nested `config.pause.*` namespace (`config.pause.timeout`, `config.pause.max_timeout`, `config.pause.with_exponential_backoff`) instead.
|
|
5
24
|
- **[Breaking]** Nest the per-topic pause configuration under `topic.pause`. The flat `topic.pause_timeout`, `topic.pause_max_timeout`, and `topic.pause_with_exponential_backoff` readers are removed in favor of `topic.pause.timeout`, `topic.pause.max_timeout`, and `topic.pause.with_exponential_backoff`; `topic.to_h` now emits a nested `pause:` hash instead of the flat keys. The Pro Granular Backoffs override DSL is unchanged (`pause(timeout:, max_timeout:, with_exponential_backoff:)`), and `topic.pausing`/`topic.pausing?` are now `topic.pause`/`topic.pause?`. In OSS `topic.pause` reflects the global `config.pause.*` settings; per-topic overriding remains a Pro feature.
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Karafka is a Ruby and Rails multi-threaded efficient Kafka processing framework
|
|
|
14
14
|
- Has [ActiveJob backend](https://karafka.io/docs/Active-Job) support (including [ordered jobs](https://karafka.io/docs/Pro-Enhanced-Active-Job#ordered-jobs))
|
|
15
15
|
- Has a seamless [Dead Letter Queue](https://karafka.io/docs/Dead-Letter-Queue/) functionality built-in
|
|
16
16
|
- Supports in-development [code reloading](https://karafka.io/docs/Auto-reload-of-code-changes-in-development)
|
|
17
|
-
- Is powered by [librdkafka](https://github.com/
|
|
17
|
+
- Is powered by [librdkafka](https://github.com/confluentinc/librdkafka) (the Apache Kafka C/C++ client library)
|
|
18
18
|
- Has an out-of the box [AppSignal](https://karafka.io/docs/Monitoring-and-Logging/#appsignal-metrics-and-error-tracking) and [StatsD/DataDog](https://karafka.io/docs/Monitoring-and-Logging/#datadog-and-statsd-integration) monitoring with dashboard templates.
|
|
19
19
|
|
|
20
20
|
```ruby
|
|
@@ -115,6 +115,7 @@ en:
|
|
|
115
115
|
setup:
|
|
116
116
|
config:
|
|
117
117
|
encryption.active_format: 'needs to be either true or false'
|
|
118
|
+
encryption.mode_format: 'needs to be either :envelope or :direct'
|
|
118
119
|
encryption.public_key_invalid: 'is not a valid public RSA key'
|
|
119
120
|
encryption.public_key_needs_to_be_public: 'is a private RSA key not a public one'
|
|
120
121
|
encryption.private_keys_format: 'needs to be a hash of version and private key value'
|
|
@@ -156,5 +157,6 @@ en:
|
|
|
156
157
|
key_missing: must be present and should be unique within the partition
|
|
157
158
|
key_format: needs to be a non-empty string unique within the partition
|
|
158
159
|
headers_schedule_target_epoch_in_the_past: 'scheduling cannot happen in the past'
|
|
160
|
+
headers_schedule_target_epoch_too_far_in_future: 'scheduling is too far in the future (schedule_target_epoch must be Unix time in seconds)'
|
|
159
161
|
headers_format: are not correct
|
|
160
162
|
not_a_scheduled_messages_topic: 'the envelope topic is not a scheduled messages topic'
|
|
@@ -25,12 +25,19 @@ module Karafka
|
|
|
25
25
|
)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Resets the attempt count of a given topic partition pause tracker
|
|
28
|
+
# Resets the attempt count of a given topic partition pause tracker, or removes it entirely
|
|
29
|
+
# if it is not currently paused.
|
|
29
30
|
#
|
|
30
31
|
# Used on revocation so that a later reclaim of the same partition starts counting retry
|
|
31
32
|
# attempts from zero instead of carrying the stale count across the rebalance. We reset
|
|
32
|
-
# rather than remove
|
|
33
|
-
# the reclaim (the partition can be re-paused via the retained
|
|
33
|
+
# rather than remove a tracker that is currently paused because the pause itself may still
|
|
34
|
+
# need to be resumed after the reclaim (the partition can be re-paused via the retained
|
|
35
|
+
# paused offsets on rebalance) - this matters under eager rebalancing, where every
|
|
36
|
+
# previously owned partition is revoked and then reassigned even when nothing has actually
|
|
37
|
+
# changed for it. A tracker that is not paused has no state worth preserving, so we remove
|
|
38
|
+
# it instead via `#delete` - this is what actually bounds `@pauses`, since otherwise entries
|
|
39
|
+
# accumulate forever for topics whose routing `Topic` object is never reused across
|
|
40
|
+
# reassignment (e.g. regex pattern subscriptions with ephemeral, per-discovery topic names).
|
|
34
41
|
#
|
|
35
42
|
# A coordinator and its pause tracker are created together in
|
|
36
43
|
# `CoordinatorsBuffer#find_or_create`, and `CoordinatorsBuffer#revoke` only calls us once it
|
|
@@ -39,7 +46,23 @@ module Karafka
|
|
|
39
46
|
# @param topic [::Karafka::Routing::Topic] topic
|
|
40
47
|
# @param partition [Integer] partition number
|
|
41
48
|
def revoke(topic, partition)
|
|
42
|
-
@pauses[topic][partition]
|
|
49
|
+
pause = @pauses[topic][partition]
|
|
50
|
+
|
|
51
|
+
return delete(topic, partition) unless pause.paused?
|
|
52
|
+
|
|
53
|
+
pause.reset
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Removes the pause tracker of a given topic partition, dropping the topic entry entirely
|
|
57
|
+
# once it no longer tracks any partitions.
|
|
58
|
+
#
|
|
59
|
+
# @param topic [::Karafka::Routing::Topic] topic
|
|
60
|
+
# @param partition [Integer] partition number
|
|
61
|
+
def delete(topic, partition)
|
|
62
|
+
partitions = @pauses[topic]
|
|
63
|
+
partitions.delete(partition)
|
|
64
|
+
|
|
65
|
+
@pauses.delete(topic) if partitions.empty?
|
|
43
66
|
end
|
|
44
67
|
|
|
45
68
|
# Resumes processing of partitions for which pause time has ended.
|
data/lib/karafka/constraints.rb
CHANGED
|
@@ -1,32 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Karafka
|
|
4
|
-
# Module used to check
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# changes in karafka are not used.
|
|
4
|
+
# Module used to check optional requirements (constraints) that cannot be easily defined by
|
|
5
|
+
# Bundler. Constraints are registered under one of two phases and verified centrally, so all
|
|
6
|
+
# environment requirements live and surface in one place:
|
|
8
7
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# - `:load` - verified when karafka itself is required, for requirements independent of the
|
|
9
|
+
# configuration (like ecosystem gems version compatibility)
|
|
10
|
+
# - `:config` - verified during `Karafka::App.setup` right after the configuration is
|
|
11
|
+
# validated, for requirements that depend on what features are actually enabled (features
|
|
12
|
+
# register those themselves, keeping their specifics out of this generic module)
|
|
11
13
|
module Constraints
|
|
14
|
+
# Phases in which constraints can be verified
|
|
15
|
+
PHASES = %i[load config].freeze
|
|
16
|
+
|
|
17
|
+
private_constant :PHASES
|
|
18
|
+
|
|
12
19
|
class << self
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
# Registers a constraint for verification. Registrations are keyed by name, so
|
|
21
|
+
# re-registration (e.g. when setup runs multiple times in tests) overwrites a previous
|
|
22
|
+
# one instead of accumulating duplicates.
|
|
23
|
+
#
|
|
24
|
+
# @param name [Symbol] unique constraint name
|
|
25
|
+
# @param phase [Symbol] `:load` or `:config`
|
|
26
|
+
# @param block [Proc] verification receiving the config node (`nil` in the `:load`
|
|
27
|
+
# phase) and expected to raise `Karafka::Errors::DependencyConstraintsError` when the
|
|
28
|
+
# requirement is not met
|
|
29
|
+
def register(name, phase:, &block)
|
|
30
|
+
raise(Errors::UnsupportedCaseError, phase) unless PHASES.include?(phase)
|
|
31
|
+
|
|
32
|
+
constraints[phase][name] = block
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Verifies all the constraints registered for a given phase
|
|
36
|
+
#
|
|
37
|
+
# @param phase [Symbol] `:load` or `:config`
|
|
38
|
+
# @param config [Karafka::Core::Configurable::Node, nil] config node for the `:config`
|
|
39
|
+
# phase, `nil` in the `:load` phase
|
|
40
|
+
def verify!(phase = :load, config = nil)
|
|
41
|
+
raise(Errors::UnsupportedCaseError, phase) unless PHASES.include?(phase)
|
|
42
|
+
|
|
43
|
+
constraints[phase].each_value { |constraint| constraint.call(config) }
|
|
26
44
|
end
|
|
27
45
|
|
|
28
46
|
private
|
|
29
47
|
|
|
48
|
+
# @return [Hash{Symbol => Hash{Symbol => Proc}}] registered constraints per phase
|
|
49
|
+
def constraints
|
|
50
|
+
@constraints ||= PHASES.to_h { |phase| [phase, {}] }
|
|
51
|
+
end
|
|
52
|
+
|
|
30
53
|
# Requires given version file from a gem location
|
|
31
54
|
# @param version_location [String]
|
|
32
55
|
# @return [Boolean] true if it was required or false if not reachable
|
|
@@ -45,5 +68,22 @@ module Karafka
|
|
|
45
68
|
Gem::Version.new(string)
|
|
46
69
|
end
|
|
47
70
|
end
|
|
71
|
+
|
|
72
|
+
# If Karafka is used with the Web UI, it needs to be in a version compatible with the API
|
|
73
|
+
# changes in this karafka version. We can make Web UI require a certain karafka version
|
|
74
|
+
# range, but at the moment we do not have a strict 1:1 release pattern matching those two.
|
|
75
|
+
register(:karafka_web_version, phase: :load) do |_config|
|
|
76
|
+
# Skip verification if web is not used at all
|
|
77
|
+
next unless require_version("karafka/web")
|
|
78
|
+
|
|
79
|
+
# All good if version higher than 1.0.0.rc1 because we expect 1.0.0.rc2 or higher
|
|
80
|
+
next if version(Karafka::Web::VERSION) >= version("1.0.0.rc2")
|
|
81
|
+
|
|
82
|
+
# If older web-ui used, we cannot allow it
|
|
83
|
+
raise(
|
|
84
|
+
Errors::DependencyConstraintsError,
|
|
85
|
+
"karafka-web < 1.0.0.rc2 is not compatible with this karafka version"
|
|
86
|
+
)
|
|
87
|
+
end
|
|
48
88
|
end
|
|
49
89
|
end
|
|
@@ -16,18 +16,27 @@ module Karafka
|
|
|
16
16
|
keyword_init: true
|
|
17
17
|
) do
|
|
18
18
|
# @return [Object] deserialized key. By default in the raw string format.
|
|
19
|
+
# @note The result is cached after the first deserialization, including `nil` and other
|
|
20
|
+
# falsy results (e.g. keyless messages), hence the explicit flag instead of a
|
|
21
|
+
# truthiness check. As with `Message#payload`, the flag is set only after a successful
|
|
22
|
+
# deserialization, so an error is not cached and the next access retries.
|
|
19
23
|
def key
|
|
20
|
-
return @key if @
|
|
24
|
+
return @key if @key_deserialized
|
|
21
25
|
|
|
22
26
|
@key = deserializers.key.call(self)
|
|
27
|
+
@key_deserialized = true
|
|
28
|
+
@key
|
|
23
29
|
end
|
|
24
30
|
|
|
25
31
|
# @return [Object] deserialized headers. By default its a hash with keys and payload being
|
|
26
32
|
# strings
|
|
33
|
+
# @note Caching works as in {#key}, including falsy results
|
|
27
34
|
def headers
|
|
28
|
-
return @headers if @
|
|
35
|
+
return @headers if @headers_deserialized
|
|
29
36
|
|
|
30
37
|
@headers = deserializers.headers.call(self)
|
|
38
|
+
@headers_deserialized = true
|
|
39
|
+
@headers
|
|
31
40
|
end
|
|
32
41
|
end
|
|
33
42
|
end
|
|
@@ -32,47 +32,77 @@ module Karafka
|
|
|
32
32
|
module Pro
|
|
33
33
|
module Encryption
|
|
34
34
|
# Cipher for encrypting and decrypting data
|
|
35
|
+
#
|
|
36
|
+
# A facade over the cipher implementations in {Ciphers}:
|
|
37
|
+
#
|
|
38
|
+
# - {Ciphers::Direct} (default) - legacy scheme with the payload RSA-encrypted directly,
|
|
39
|
+
# limited to payloads smaller than the RSA key capacity
|
|
40
|
+
# - {Ciphers::Envelope} - hybrid scheme with a one-time AES-256-GCM key per payload,
|
|
41
|
+
# supporting payloads of any size
|
|
42
|
+
#
|
|
43
|
+
# Encryption follows the `encryption.mode` setting. Decryption is mode-independent: the
|
|
44
|
+
# format is recognized per message (a valid direct RSA ciphertext is always exactly the
|
|
45
|
+
# key modulus size, while an envelope is always at least 29 bytes longer), so consumers
|
|
46
|
+
# decrypt both formats transparently regardless of the configured mode, making staged
|
|
47
|
+
# producer-side rollout of the envelope mode safe.
|
|
48
|
+
#
|
|
49
|
+
# Format detection is deliberately payload-based rather than header-based, even though
|
|
50
|
+
# the encryption middleware already writes message headers that could carry a format
|
|
51
|
+
# marker. A header marker would remove the truncation blind spot documented on
|
|
52
|
+
# {Ciphers::Direct#owns?}, but the payload would stop being self-describing: it must
|
|
53
|
+
# remain decryptable also when it leaves Kafka through channels that do not preserve
|
|
54
|
+
# headers (mirroring and replication tools, dumps, storage sinks) and when handled by
|
|
55
|
+
# custom parsers or ciphers that only receive the payload through the stable
|
|
56
|
+
# `#decrypt(version, content)` contract. We accept the blind spot as the cheaper cost.
|
|
35
57
|
class Cipher
|
|
36
58
|
include Helpers::ConfigImporter.new(
|
|
37
59
|
encryption: %i[encryption]
|
|
38
60
|
)
|
|
39
61
|
|
|
40
|
-
#
|
|
41
|
-
def initialize
|
|
42
|
-
@private_pems = {}
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Encrypts given string content with the public key
|
|
62
|
+
# Encrypts given string content according to the configured `encryption.mode`
|
|
46
63
|
# @param content [String]
|
|
47
64
|
# @return [String]
|
|
48
65
|
def encrypt(content)
|
|
49
|
-
|
|
66
|
+
(encryption.mode == :envelope) ? envelope.encrypt(content) : direct.encrypt(content)
|
|
50
67
|
end
|
|
51
68
|
|
|
52
|
-
# Decrypts provided content using `version` key
|
|
69
|
+
# Decrypts provided content using `version` key with the cipher implementation that
|
|
70
|
+
# recognizes the content format, independently of the configured mode
|
|
53
71
|
# @param version [String] encryption version
|
|
54
72
|
# @param content [String] encrypted content
|
|
55
73
|
# @return [String] decrypted content
|
|
56
74
|
def decrypt(version, content)
|
|
57
|
-
|
|
75
|
+
if direct.owns?(version, content)
|
|
76
|
+
direct.decrypt(version, content)
|
|
77
|
+
else
|
|
78
|
+
envelope.decrypt(version, content)
|
|
79
|
+
end
|
|
58
80
|
end
|
|
59
81
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
# Eagerly builds the underlying ciphers and parses the key material of the given
|
|
83
|
+
# config. Invoked during the single-threaded setup phase so that runtime encryption
|
|
84
|
+
# and decryption only read already-built, effectively frozen state and the lazy
|
|
85
|
+
# initialization below never races across worker threads.
|
|
86
|
+
#
|
|
87
|
+
# @param root_config [Karafka::Core::Configurable::Node] config whose key material to
|
|
88
|
+
# warm. During setup this is the same app config the ciphers read at runtime.
|
|
89
|
+
def warmup(root_config)
|
|
90
|
+
direct.warmup(root_config.encryption)
|
|
91
|
+
envelope.warmup(root_config.encryption)
|
|
65
92
|
end
|
|
66
93
|
|
|
67
|
-
|
|
68
|
-
# @return [::OpenSSL::PKey::RSA] rsa private key
|
|
69
|
-
def private_pem(version)
|
|
70
|
-
return @private_pems[version] if @private_pems.key?(version)
|
|
94
|
+
private
|
|
71
95
|
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
# Lazily built so this facade can be instantiated as a config default while the cipher
|
|
97
|
+
# implementation files may not be loaded yet
|
|
98
|
+
# @return [Ciphers::Direct]
|
|
99
|
+
def direct
|
|
100
|
+
@direct ||= Ciphers::Direct.new
|
|
101
|
+
end
|
|
74
102
|
|
|
75
|
-
|
|
103
|
+
# @return [Ciphers::Envelope]
|
|
104
|
+
def envelope
|
|
105
|
+
@envelope ||= Ciphers::Envelope.new
|
|
76
106
|
end
|
|
77
107
|
end
|
|
78
108
|
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Karafka Pro - Source Available Commercial Software
|
|
4
|
+
# Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# This software is NOT open source. It is source-available commercial software
|
|
7
|
+
# requiring a paid license for use. It is NOT covered by LGPL.
|
|
8
|
+
#
|
|
9
|
+
# The author retains all right, title, and interest in this software,
|
|
10
|
+
# including all copyrights, patents, and other intellectual property rights.
|
|
11
|
+
# No patent rights are granted under this license.
|
|
12
|
+
#
|
|
13
|
+
# PROHIBITED:
|
|
14
|
+
# - Use without a valid commercial license
|
|
15
|
+
# - Redistribution, modification, or derivative works without authorization
|
|
16
|
+
# - Reverse engineering, decompilation, or disassembly of this software
|
|
17
|
+
# - Use as training data for AI/ML models or inclusion in datasets
|
|
18
|
+
# - Scraping, crawling, or automated collection for any purpose
|
|
19
|
+
#
|
|
20
|
+
# PERMITTED:
|
|
21
|
+
# - Reading, referencing, and linking for personal or commercial use
|
|
22
|
+
# - Runtime retrieval by AI assistants, coding agents, and RAG systems
|
|
23
|
+
# for the purpose of providing contextual help to Karafka users
|
|
24
|
+
#
|
|
25
|
+
# Receipt, viewing, or possession of this software does not convey or
|
|
26
|
+
# imply any license or right beyond those expressly stated above.
|
|
27
|
+
#
|
|
28
|
+
# License: https://karafka.io/docs/Pro-License-Comm/
|
|
29
|
+
# Contact: contact@karafka.io
|
|
30
|
+
|
|
31
|
+
module Karafka
|
|
32
|
+
module Pro
|
|
33
|
+
module Encryption
|
|
34
|
+
# Namespace for the cipher implementations behind the encryption modes
|
|
35
|
+
module Ciphers
|
|
36
|
+
# Base for the cipher implementations, providing shared access to the configured RSA
|
|
37
|
+
# key material with per-version private key resolution
|
|
38
|
+
class Base
|
|
39
|
+
include Helpers::ConfigImporter.new(
|
|
40
|
+
encryption: %i[encryption]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Initializes the cipher with an empty private keys cache
|
|
44
|
+
#
|
|
45
|
+
# @note Each cipher instance holds its own tiny cache of parsed pem objects. With two
|
|
46
|
+
# cipher implementations composed by {Encryption::Cipher} this means the material
|
|
47
|
+
# is parsed at most twice per version, which we accept over introducing a shared
|
|
48
|
+
# keyring concept
|
|
49
|
+
#
|
|
50
|
+
# @note The caches are populated via {#warmup} during the single-threaded setup
|
|
51
|
+
# phase, so under normal operations runtime access is read-only. Should a key
|
|
52
|
+
# version appear only at runtime, the lazy population is idempotent and benign
|
|
53
|
+
# under MRI (worst case the same pem is parsed twice)
|
|
54
|
+
def initialize
|
|
55
|
+
@private_pems = {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Eagerly parses the given encryption config key material into the instance caches
|
|
59
|
+
#
|
|
60
|
+
# @param encryption_config [Karafka::Core::Configurable::Node] encryption config
|
|
61
|
+
# node. During setup it is the same node the lazy readers resolve at runtime,
|
|
62
|
+
# passed explicitly so this method does not silently couple to the global state
|
|
63
|
+
def warmup(encryption_config)
|
|
64
|
+
@public_pem ||= OpenSSL::PKey::RSA.new(encryption_config.public_key)
|
|
65
|
+
|
|
66
|
+
encryption_config.private_keys.each do |version, key|
|
|
67
|
+
@private_pems[version] ||= OpenSSL::PKey::RSA.new(key)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# @return [::OpenSSL::PKey::RSA] rsa public key
|
|
74
|
+
def public_pem
|
|
75
|
+
@public_pem ||= OpenSSL::PKey::RSA.new(encryption.public_key)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param version [String] version for which we want to get the rsa key
|
|
79
|
+
# @return [::OpenSSL::PKey::RSA] rsa private key
|
|
80
|
+
def private_pem(version)
|
|
81
|
+
return @private_pems[version] if @private_pems.key?(version)
|
|
82
|
+
|
|
83
|
+
key_string = encryption.private_keys[version]
|
|
84
|
+
key_string || raise(Errors::PrivateKeyNotFoundError, version)
|
|
85
|
+
|
|
86
|
+
@private_pems[version] = OpenSSL::PKey::RSA.new(key_string)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Karafka Pro - Source Available Commercial Software
|
|
4
|
+
# Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# This software is NOT open source. It is source-available commercial software
|
|
7
|
+
# requiring a paid license for use. It is NOT covered by LGPL.
|
|
8
|
+
#
|
|
9
|
+
# The author retains all right, title, and interest in this software,
|
|
10
|
+
# including all copyrights, patents, and other intellectual property rights.
|
|
11
|
+
# No patent rights are granted under this license.
|
|
12
|
+
#
|
|
13
|
+
# PROHIBITED:
|
|
14
|
+
# - Use without a valid commercial license
|
|
15
|
+
# - Redistribution, modification, or derivative works without authorization
|
|
16
|
+
# - Reverse engineering, decompilation, or disassembly of this software
|
|
17
|
+
# - Use as training data for AI/ML models or inclusion in datasets
|
|
18
|
+
# - Scraping, crawling, or automated collection for any purpose
|
|
19
|
+
#
|
|
20
|
+
# PERMITTED:
|
|
21
|
+
# - Reading, referencing, and linking for personal or commercial use
|
|
22
|
+
# - Runtime retrieval by AI assistants, coding agents, and RAG systems
|
|
23
|
+
# for the purpose of providing contextual help to Karafka users
|
|
24
|
+
#
|
|
25
|
+
# Receipt, viewing, or possession of this software does not convey or
|
|
26
|
+
# imply any license or right beyond those expressly stated above.
|
|
27
|
+
#
|
|
28
|
+
# License: https://karafka.io/docs/Pro-License-Comm/
|
|
29
|
+
# Contact: contact@karafka.io
|
|
30
|
+
|
|
31
|
+
module Karafka
|
|
32
|
+
module Pro
|
|
33
|
+
module Encryption
|
|
34
|
+
module Ciphers
|
|
35
|
+
# Legacy cipher where the payload is RSA-encrypted directly with PKCS1 v1.5 padding.
|
|
36
|
+
#
|
|
37
|
+
# RSA can only encrypt data smaller than the key size minus padding (e.g. ~245 bytes
|
|
38
|
+
# for a 2048-bit key, ~501 bytes for a 4096-bit key), so it is unsuitable for larger
|
|
39
|
+
# payloads and remains available only for backwards compatibility with data already
|
|
40
|
+
# encrypted at rest and with fleets not yet fully upgraded.
|
|
41
|
+
class Direct < Base
|
|
42
|
+
# Encrypts given content with the public key
|
|
43
|
+
# @param content [String]
|
|
44
|
+
# @return [String] RSA ciphertext, always exactly the key modulus size
|
|
45
|
+
def encrypt(content)
|
|
46
|
+
public_pem.public_encrypt(content)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Decrypts provided content using the `version` private key
|
|
50
|
+
# @param version [String] encryption version
|
|
51
|
+
# @param content [String] encrypted content
|
|
52
|
+
# @return [String] decrypted content
|
|
53
|
+
def decrypt(version, content)
|
|
54
|
+
private_pem(version).private_decrypt(content)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param version [String] encryption version
|
|
58
|
+
# @param content [String] encrypted content
|
|
59
|
+
# @return [Boolean] true if the content matches this cipher's format. A valid direct
|
|
60
|
+
# RSA ciphertext is always exactly the key modulus size
|
|
61
|
+
#
|
|
62
|
+
# @note One inherent blind spot: an envelope truncated to exactly the modulus size is
|
|
63
|
+
# indistinguishable from a direct ciphertext. It surfaces as an RSA padding error
|
|
64
|
+
# or - when the PKCS1 v1.5 padding coincidentally validates - as garbage output,
|
|
65
|
+
# never as the envelope diagnostics. The legacy direct format carries no marker
|
|
66
|
+
# that could disambiguate this.
|
|
67
|
+
def owns?(version, content)
|
|
68
|
+
content.bytesize == private_pem(version).n.num_bytes
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Karafka Pro - Source Available Commercial Software
|
|
4
|
+
# Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# This software is NOT open source. It is source-available commercial software
|
|
7
|
+
# requiring a paid license for use. It is NOT covered by LGPL.
|
|
8
|
+
#
|
|
9
|
+
# The author retains all right, title, and interest in this software,
|
|
10
|
+
# including all copyrights, patents, and other intellectual property rights.
|
|
11
|
+
# No patent rights are granted under this license.
|
|
12
|
+
#
|
|
13
|
+
# PROHIBITED:
|
|
14
|
+
# - Use without a valid commercial license
|
|
15
|
+
# - Redistribution, modification, or derivative works without authorization
|
|
16
|
+
# - Reverse engineering, decompilation, or disassembly of this software
|
|
17
|
+
# - Use as training data for AI/ML models or inclusion in datasets
|
|
18
|
+
# - Scraping, crawling, or automated collection for any purpose
|
|
19
|
+
#
|
|
20
|
+
# PERMITTED:
|
|
21
|
+
# - Reading, referencing, and linking for personal or commercial use
|
|
22
|
+
# - Runtime retrieval by AI assistants, coding agents, and RAG systems
|
|
23
|
+
# for the purpose of providing contextual help to Karafka users
|
|
24
|
+
#
|
|
25
|
+
# Receipt, viewing, or possession of this software does not convey or
|
|
26
|
+
# imply any license or right beyond those expressly stated above.
|
|
27
|
+
#
|
|
28
|
+
# License: https://karafka.io/docs/Pro-License-Comm/
|
|
29
|
+
# Contact: contact@karafka.io
|
|
30
|
+
|
|
31
|
+
module Karafka
|
|
32
|
+
module Pro
|
|
33
|
+
module Encryption
|
|
34
|
+
module Ciphers
|
|
35
|
+
# Hybrid cipher where each payload is encrypted with a one-time AES-256-GCM key and
|
|
36
|
+
# only that key is RSA-wrapped (OAEP padding). Handles payloads of any size.
|
|
37
|
+
#
|
|
38
|
+
# The GCM auth tag covers the whole envelope (header included), so corruption and
|
|
39
|
+
# truncation are detected reliably. Note this is corruption detection, not
|
|
40
|
+
# authenticity: the RSA public key is distributed to all producers, so any of its
|
|
41
|
+
# holders can construct a valid envelope.
|
|
42
|
+
class Envelope < Base
|
|
43
|
+
# Envelope binary format:
|
|
44
|
+
#
|
|
45
|
+
# [1B format version][RSA-wrapped AES key (modulus size)][12B iv][16B tag][ciphertext]
|
|
46
|
+
#
|
|
47
|
+
# The wrapped key needs no size prefix as OAEP output is always exactly the modulus
|
|
48
|
+
# size of the wrapping key. The version byte allows introducing new envelope layouts
|
|
49
|
+
# (different AEAD, compression, etc.) without falling back to length arithmetic.
|
|
50
|
+
VERSION = "\x01".b.freeze
|
|
51
|
+
|
|
52
|
+
# Number of bytes of the leading format version marker
|
|
53
|
+
VERSION_BYTES = 1
|
|
54
|
+
|
|
55
|
+
# AES cipher used for the envelope payload encryption
|
|
56
|
+
AES = "aes-256-gcm"
|
|
57
|
+
|
|
58
|
+
# Number of bytes of the AES key wrapped inside the envelope. Derived from the
|
|
59
|
+
# cipher so a future AES change cannot silently break the invariant
|
|
60
|
+
KEY_BYTES = OpenSSL::Cipher.new(AES).key_len
|
|
61
|
+
|
|
62
|
+
# Number of bytes of the AES-GCM initialization vector, derived like the key size
|
|
63
|
+
IV_BYTES = OpenSSL::Cipher.new(AES).iv_len
|
|
64
|
+
|
|
65
|
+
# Number of bytes of the AES-GCM authentication tag. This is a choice (GCM supports
|
|
66
|
+
# shorter tags), not a cipher-derived property - 16 is the full, recommended size
|
|
67
|
+
TAG_BYTES = 16
|
|
68
|
+
|
|
69
|
+
# OAEP options for the AES key wrapping. SHA-256 for both MGF1 and the label hash -
|
|
70
|
+
# OAEP does not lean on collision resistance, but the SHA-1 default draws flags from
|
|
71
|
+
# scanners and compliance checklists
|
|
72
|
+
OAEP_OPTIONS = {
|
|
73
|
+
rsa_padding_mode: "oaep",
|
|
74
|
+
rsa_oaep_md: "sha256",
|
|
75
|
+
rsa_mgf1_md: "sha256"
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
private_constant :VERSION, :VERSION_BYTES, :AES, :KEY_BYTES, :IV_BYTES, :TAG_BYTES,
|
|
79
|
+
:OAEP_OPTIONS
|
|
80
|
+
|
|
81
|
+
# Encrypts content with a one-time AES-256-GCM key and RSA-wraps that key using OAEP
|
|
82
|
+
# padding. Unlike PKCS1 v1.5, OAEP unwrapping with a non-matching key fails reliably
|
|
83
|
+
# instead of occasionally yielding garbage. The GCM tag additionally authenticates
|
|
84
|
+
# the whole envelope header, so any bit flip in the version byte, wrapped key or iv
|
|
85
|
+
# is detected, not only ciphertext corruption.
|
|
86
|
+
#
|
|
87
|
+
# @param content [String] content to encrypt
|
|
88
|
+
# @return [String] binary envelope (see {VERSION} for the format)
|
|
89
|
+
def encrypt(content)
|
|
90
|
+
aes = OpenSSL::Cipher.new(AES).encrypt
|
|
91
|
+
aes_key = aes.random_key
|
|
92
|
+
iv = aes.random_iv
|
|
93
|
+
|
|
94
|
+
wrapped_key = public_pem.encrypt(aes_key, OAEP_OPTIONS)
|
|
95
|
+
|
|
96
|
+
header = VERSION + wrapped_key + iv
|
|
97
|
+
aes.auth_data = header
|
|
98
|
+
|
|
99
|
+
# `Cipher#update` rejects empty input on openssl gem < 3.1 (`data must not be
|
|
100
|
+
# empty`), so empty payloads go straight to `#final`
|
|
101
|
+
ciphertext = content.empty? ? aes.final : aes.update(content) + aes.final
|
|
102
|
+
|
|
103
|
+
header + aes.auth_tag(TAG_BYTES) + ciphertext
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Decrypts an envelope produced by {#encrypt}
|
|
107
|
+
# @param version [String] encryption version
|
|
108
|
+
# @param content [String] binary envelope
|
|
109
|
+
# @return [String] decrypted content
|
|
110
|
+
# @note All failure paths stay within the `OpenSSL::PKey` error family: the explicit
|
|
111
|
+
# guards raise `RSAError`, while an OAEP unwrap failure (e.g. non-matching private
|
|
112
|
+
# key) surfaces from the EVP API as its parent `PKeyError`
|
|
113
|
+
def decrypt(version, content)
|
|
114
|
+
content = content.b
|
|
115
|
+
|
|
116
|
+
if content.bytesize < VERSION_BYTES
|
|
117
|
+
raise(OpenSSL::PKey::RSAError, "corrupted or truncated envelope")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Version goes next so future layouts of different sizes are reported as
|
|
121
|
+
# unsupported to older consumers instead of as corrupted
|
|
122
|
+
if content[0, VERSION_BYTES] != VERSION
|
|
123
|
+
raise(OpenSSL::PKey::RSAError, "unsupported envelope version")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
pem = private_pem(version)
|
|
127
|
+
wrapped_size = pem.n.num_bytes
|
|
128
|
+
header_size = VERSION_BYTES + wrapped_size + IV_BYTES
|
|
129
|
+
min_size = header_size + TAG_BYTES
|
|
130
|
+
|
|
131
|
+
if content.bytesize < min_size
|
|
132
|
+
raise(OpenSSL::PKey::RSAError, "corrupted or truncated envelope")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
wrapped_key = content[VERSION_BYTES, wrapped_size]
|
|
136
|
+
iv = content[VERSION_BYTES + wrapped_size, IV_BYTES]
|
|
137
|
+
tag = content[header_size, TAG_BYTES]
|
|
138
|
+
ciphertext = content[min_size..]
|
|
139
|
+
|
|
140
|
+
aes_key = pem.decrypt(wrapped_key, OAEP_OPTIONS)
|
|
141
|
+
|
|
142
|
+
# OAEP unwrapping of a foreign envelope fails reliably, but anyone holding the
|
|
143
|
+
# public key can wrap a string of arbitrary length. Without this guard such input
|
|
144
|
+
# would surface as an ArgumentError from the AES key assignment, escaping the
|
|
145
|
+
# OpenSSL error family this method otherwise normalizes to
|
|
146
|
+
unless aes_key.bytesize == KEY_BYTES
|
|
147
|
+
raise(OpenSSL::PKey::RSAError, "invalid envelope key size")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
aes = OpenSSL::Cipher.new(AES).decrypt
|
|
151
|
+
aes.key = aes_key
|
|
152
|
+
aes.iv = iv
|
|
153
|
+
aes.auth_tag = tag
|
|
154
|
+
aes.auth_data = content[0, header_size]
|
|
155
|
+
|
|
156
|
+
# Same empty-input guard as on the encryption side; `#final` still runs and thus
|
|
157
|
+
# still verifies the auth tag for empty payloads
|
|
158
|
+
ciphertext.empty? ? aes.final : aes.update(ciphertext) + aes.final
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -43,6 +43,7 @@ module Karafka
|
|
|
43
43
|
|
|
44
44
|
nested(:encryption) do
|
|
45
45
|
required(:active) { |val| [true, false].include?(val) }
|
|
46
|
+
required(:mode) { |val| %i[envelope direct].include?(val) }
|
|
46
47
|
required(:version) { |val| val.is_a?(String) && !val.empty? }
|
|
47
48
|
required(:public_key) { |val| val.is_a?(String) }
|
|
48
49
|
required(:fingerprinter) { |val| val == false || val.respond_to?(:hexdigest) }
|
|
@@ -54,6 +54,28 @@ module Karafka
|
|
|
54
54
|
# This allows us to support key rotation
|
|
55
55
|
setting(:private_keys, default: {})
|
|
56
56
|
|
|
57
|
+
# Encryption mode used when producing messages:
|
|
58
|
+
#
|
|
59
|
+
# - `:direct` (default) - payload is RSA-encrypted directly, which limits it to the
|
|
60
|
+
# RSA key capacity (key size minus padding, ~245 bytes for 2048-bit keys). Default
|
|
61
|
+
# for backwards compatibility with already running deployments. The default will
|
|
62
|
+
# switch to `:envelope` in a future release (with prior notice); decryption of the
|
|
63
|
+
# `:direct` format is never planned for removal, as data at rest never expires.
|
|
64
|
+
# - `:envelope` - payload is encrypted with a one-time AES-256-GCM key and only that
|
|
65
|
+
# key is RSA-wrapped, so payloads of any size are supported and the GCM auth tag
|
|
66
|
+
# detects corruption and truncation. Note this is not authenticity: the public key
|
|
67
|
+
# is distributed to all producers, so any of its holders can build a valid envelope
|
|
68
|
+
#
|
|
69
|
+
# Decryption always supports both formats regardless of this setting. Since processes
|
|
70
|
+
# older than the version that introduced this setting cannot decrypt envelope
|
|
71
|
+
# payloads, when enabling `:envelope` upgrade all consuming processes first and only
|
|
72
|
+
# then switch producers to the envelope mode.
|
|
73
|
+
#
|
|
74
|
+
# The envelope openssl gem requirement (>= 3.0) is verified during setup. Flipping
|
|
75
|
+
# this setting to `:envelope` at runtime on an unsupported openssl bypasses that
|
|
76
|
+
# friendly boot error and fails on first use instead.
|
|
77
|
+
setting(:mode, default: :direct)
|
|
78
|
+
|
|
57
79
|
# Cipher used to encrypt and decrypt data
|
|
58
80
|
setting(:cipher, default: Encryption::Cipher.new)
|
|
59
81
|
|
|
@@ -43,6 +43,12 @@ module Karafka
|
|
|
43
43
|
config.instance_eval do
|
|
44
44
|
setting(:encryption, default: Setup::Config.config)
|
|
45
45
|
end
|
|
46
|
+
|
|
47
|
+
# Registered as a config-phase constraint (verified centrally during setup together
|
|
48
|
+
# with all other environment requirements) instead of being checked ad hoc here
|
|
49
|
+
Karafka::Constraints.register(:pro_encryption_envelope_openssl, phase: :config) do |cfg|
|
|
50
|
+
verify_envelope_requirements!(cfg)
|
|
51
|
+
end
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
# @param config [Karafka::Core::Configurable::Node] root node config
|
|
@@ -60,6 +66,13 @@ module Karafka
|
|
|
60
66
|
|
|
61
67
|
# Encryption for WaterDrop
|
|
62
68
|
config.producer.middleware.append(Messages::Middleware.new)
|
|
69
|
+
|
|
70
|
+
# Warm the cipher internals (sub-ciphers, parsed key material) in this
|
|
71
|
+
# single-threaded phase so runtime encryption and decryption across worker threads
|
|
72
|
+
# only read already-built state. Custom ciphers can opt in by exposing
|
|
73
|
+
# #warmup(config)
|
|
74
|
+
cipher = config.encryption.cipher
|
|
75
|
+
cipher.warmup(config) if cipher.respond_to?(:warmup)
|
|
63
76
|
end
|
|
64
77
|
|
|
65
78
|
# This feature does not need any changes post-fork
|
|
@@ -69,6 +82,27 @@ module Karafka
|
|
|
69
82
|
def post_fork(_config, _pre_fork_producer)
|
|
70
83
|
true
|
|
71
84
|
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# The envelope mode relies on the EVP PKey API (`PKey#encrypt`/`#decrypt` with an
|
|
89
|
+
# options hash), available since the openssl gem 3.0. All supported Rubies bundle a
|
|
90
|
+
# sufficient version as a default gem, but it can be pinned lower in a Gemfile, hence
|
|
91
|
+
# this runtime constraint instead of a gemspec dependency that everyone would carry
|
|
92
|
+
# for a single opt-in feature.
|
|
93
|
+
#
|
|
94
|
+
# @param config [Karafka::Core::Configurable::Node] root node config
|
|
95
|
+
def verify_envelope_requirements!(config)
|
|
96
|
+
return unless config.encryption.active
|
|
97
|
+
return unless config.encryption.mode == :envelope
|
|
98
|
+
return if Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new("3.0.0")
|
|
99
|
+
|
|
100
|
+
raise(
|
|
101
|
+
Karafka::Errors::DependencyConstraintsError,
|
|
102
|
+
"encryption.mode = :envelope requires the openssl gem >= 3.0, " \
|
|
103
|
+
"#{OpenSSL::VERSION} detected"
|
|
104
|
+
)
|
|
105
|
+
end
|
|
72
106
|
end
|
|
73
107
|
end
|
|
74
108
|
end
|
data/lib/karafka/pro/iterator.rb
CHANGED
|
@@ -128,6 +128,11 @@ module Karafka
|
|
|
128
128
|
# and the whole iteration becomes a silent no-op.
|
|
129
129
|
@stopped_partitions = Set.new
|
|
130
130
|
@stopped = false
|
|
131
|
+
# Reset the stored-offsets latch so it reflects only the current run's marking activity.
|
|
132
|
+
# Otherwise a single `mark_as_consumed` would keep firing a spurious blocking sync commit on
|
|
133
|
+
# the teardown of every subsequent `#each`, each of which runs on a brand-new consumer that
|
|
134
|
+
# has no stored offsets.
|
|
135
|
+
@stored_offsets = false
|
|
131
136
|
end
|
|
132
137
|
|
|
133
138
|
# Stops the partition we're currently yielded into
|
|
@@ -262,7 +262,7 @@ module Karafka
|
|
|
262
262
|
# Use custom topic if it was returned from the strategy
|
|
263
263
|
@_dispatch_to_dlq_topic = target_topic || topic.dead_letter_queue.topic
|
|
264
264
|
else
|
|
265
|
-
raise Karafka::UnsupportedCaseError, flow
|
|
265
|
+
raise Karafka::Errors::UnsupportedCaseError, flow
|
|
266
266
|
end
|
|
267
267
|
|
|
268
268
|
yield
|
|
@@ -97,7 +97,7 @@ module Karafka
|
|
|
97
97
|
# Use custom topic if it was returned from the strategy
|
|
98
98
|
@_dispatch_to_dlq_topic = target_topic || topic.dead_letter_queue.topic
|
|
99
99
|
else
|
|
100
|
-
raise Karafka::UnsupportedCaseError, flow
|
|
100
|
+
raise Karafka::Errors::UnsupportedCaseError, flow
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
yield
|
|
@@ -90,12 +90,14 @@ module Karafka
|
|
|
90
90
|
# @param job [Jobs::Base] job that locked the queue
|
|
91
91
|
def unlock(job)
|
|
92
92
|
@mutex.synchronize do
|
|
93
|
-
@
|
|
93
|
+
if @in_waiting[job.group_id].delete(job)
|
|
94
|
+
@statistics[:waiting] -= 1
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
return
|
|
97
|
+
end
|
|
96
98
|
|
|
97
99
|
# This should never happen. It means there was a job being unlocked that was never
|
|
98
|
-
# locked in the first place
|
|
100
|
+
# locked in the first place (or its group was already reset via `#clear`)
|
|
99
101
|
raise(Errors::JobsQueueSynchronizationError, job.group_id)
|
|
100
102
|
end
|
|
101
103
|
end
|
|
@@ -185,6 +187,14 @@ module Karafka
|
|
|
185
187
|
# Thanks to the fact that we use the minimum lock time as a timeout, we do not have to
|
|
186
188
|
# wait a whole ticking period to unlock async locks.
|
|
187
189
|
def wait(group_id)
|
|
190
|
+
# Drain stale `#tick` signals up front: for non-blocking/LRJ jobs and released async
|
|
191
|
+
# locks (both Pro-only) `#wait?` can be false from the start, so the loop below - the
|
|
192
|
+
# only other consumer - never runs and signals would otherwise accumulate unbounded.
|
|
193
|
+
# Safe without locking as `#wait` runs only on the single listener thread that owns
|
|
194
|
+
# (and has already registered) this group.
|
|
195
|
+
semaphore = @semaphores.fetch(group_id)
|
|
196
|
+
semaphore.pop(timeout: 0) until semaphore.empty?
|
|
197
|
+
|
|
188
198
|
return super unless @async_locking
|
|
189
199
|
|
|
190
200
|
# We do not generalize this flow because this one is more expensive as it has to allocate
|
|
@@ -79,11 +79,34 @@ module Karafka
|
|
|
79
79
|
def install(pattern, discovered_topic, sg_topics)
|
|
80
80
|
group = pattern.topic.group
|
|
81
81
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
topic
|
|
82
|
+
# Under multiplexing several subscription groups share the same consumer group but
|
|
83
|
+
# each runs its own listener thread and independently discovers the same topic.
|
|
84
|
+
# ConsumerGroup#topic= appends unconditionally, so without this guard the shared
|
|
85
|
+
# group would accumulate up to `multiplex_factor` duplicate Topic objects per
|
|
86
|
+
# discovered topic, polluting the routing tree. We run under the process-wide
|
|
87
|
+
# MUTEX (see #expand), so the find + register below is atomic across subscription
|
|
88
|
+
# groups: the discovered topic is registered in the shared group only once.
|
|
89
|
+
topic = group.topics.detect { |existing| existing.name == discovered_topic }
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
if topic
|
|
92
|
+
# A sibling multiplexed subscription group already registered this topic in the
|
|
93
|
+
# shared consumer group. Each subscription group must still hold its OWN Topic
|
|
94
|
+
# instance in its topics array: the Topic doubles as a per-subscription-group
|
|
95
|
+
# key (Instrumentation::AssignmentsTracker keys assignments on the instance and
|
|
96
|
+
# reads topic.subscription_group.id), so reusing one instance across
|
|
97
|
+
# subscription groups would collide their assignments. We inject a
|
|
98
|
+
# per-subscription-group copy, mirroring how the multiplexing subscription
|
|
99
|
+
# groups builder dups topics per group at boot.
|
|
100
|
+
topic = topic.dup
|
|
101
|
+
else
|
|
102
|
+
# Build new topic and register within the owning group
|
|
103
|
+
topic = group.public_send(:topic=, discovered_topic, &pattern.config)
|
|
104
|
+
topic.patterns(active: true, type: :discovered)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Assign the appropriate subscription group to this topic. Each subscription group
|
|
108
|
+
# owns its own Topic instance (freshly built or dup'd above), so this never leaks
|
|
109
|
+
# across multiplexed subscription groups.
|
|
87
110
|
topic.subscription_group = pattern.topic.subscription_group
|
|
88
111
|
|
|
89
112
|
# Inject into subscription group topics array always, so everything is reflected
|
|
@@ -121,22 +121,19 @@ module Karafka
|
|
|
121
121
|
# that all tombstone events are loaded not to duplicate dispatches
|
|
122
122
|
return unless @state.loaded?
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
# from the daily buffer. That way we ensure the at least once delivery and in case of
|
|
131
|
-
# a transactional producer, exactly once delivery.
|
|
124
|
+
# We first collect all the data for dispatch and then dispatch it in sync batches. Each
|
|
125
|
+
# key is removed from the daily buffer only once the batch carrying it is confirmed
|
|
126
|
+
# delivered - not after the whole flush - so a broker error partway through does not
|
|
127
|
+
# leave already-produced batches in the buffer to be re-dispatched on the next tick
|
|
128
|
+
# (a duplicate-dispatch window for non-transactional producers). This keeps the
|
|
129
|
+
# at-least-once guarantee, and exactly-once when a transactional producer is used.
|
|
132
130
|
@daily_buffer.for_dispatch do |message|
|
|
133
|
-
keys << message.key
|
|
134
131
|
@dispatcher << message
|
|
135
132
|
end
|
|
136
133
|
|
|
137
|
-
@dispatcher.flush
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
@dispatcher.flush do |dispatched_keys|
|
|
135
|
+
dispatched_keys.each { |key| @daily_buffer.delete(key) }
|
|
136
|
+
end
|
|
140
137
|
|
|
141
138
|
@states_reporter.call
|
|
142
139
|
end
|
|
@@ -58,6 +58,15 @@ module Karafka
|
|
|
58
58
|
schedule_source_type
|
|
59
59
|
].freeze
|
|
60
60
|
|
|
61
|
+
# Upper bound (in seconds) for how far ahead a schedule may target: 100 years.
|
|
62
|
+
# schedule_target_epoch is Unix time in seconds; anything beyond this is almost certainly
|
|
63
|
+
# a unit mistake (for example milliseconds passed as seconds, ~1000x too large). We reject
|
|
64
|
+
# it here so it fails loudly at publish time instead of being silently dropped by the
|
|
65
|
+
# consumer.
|
|
66
|
+
MAX_FUTURE_INTERVAL = 100 * 365 * 24 * 60 * 60
|
|
67
|
+
|
|
68
|
+
private_constant :MAX_FUTURE_INTERVAL
|
|
69
|
+
|
|
61
70
|
required(:key) { |val| val.is_a?(String) && val.size.positive? }
|
|
62
71
|
|
|
63
72
|
# Ensure that schedule has all correct keys and that others have other related data
|
|
@@ -84,9 +93,15 @@ module Karafka
|
|
|
84
93
|
|
|
85
94
|
# We allow for small lag as those will be dispatched but we should prevent dispatching
|
|
86
95
|
# in the past in general as often it is a source of errors
|
|
87
|
-
|
|
96
|
+
if epoch_time < Time.now.to_i - 10
|
|
97
|
+
next [[[:headers], :schedule_target_epoch_in_the_past]]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Reject epochs implausibly far ahead (e.g. milliseconds passed as seconds), which would
|
|
101
|
+
# otherwise validate here and then be silently dropped by the consumer, never scheduled
|
|
102
|
+
next if epoch_time <= Time.now.to_i + MAX_FUTURE_INTERVAL
|
|
88
103
|
|
|
89
|
-
[[[:headers], :
|
|
104
|
+
[[[:headers], :schedule_target_epoch_too_far_in_future]]
|
|
90
105
|
end
|
|
91
106
|
|
|
92
107
|
# Makes sure, that the target envelope topic we dispatch to is a scheduled messages topic
|
|
@@ -46,6 +46,10 @@ module Karafka
|
|
|
46
46
|
@topic = topic
|
|
47
47
|
@partition = partition
|
|
48
48
|
@buffer = []
|
|
49
|
+
# Source (daily buffer) key aligned 1:1 with each `@buffer` entry, so `#flush` can report
|
|
50
|
+
# which keys were confirmed delivered per chunk and the consumer can evict them
|
|
51
|
+
# incrementally instead of only after the whole flush succeeds
|
|
52
|
+
@keys = []
|
|
49
53
|
@serializer = Serializer.new
|
|
50
54
|
end
|
|
51
55
|
|
|
@@ -78,12 +82,21 @@ module Karafka
|
|
|
78
82
|
extract(target, message.headers, :key)
|
|
79
83
|
extract(target, message.headers, :partition_key)
|
|
80
84
|
|
|
85
|
+
# `message.key` is pushed to `@keys` once per `@buffer` entry (here and below), not once
|
|
86
|
+
# per message: `@keys` must stay 1:1 aligned with `@buffer` (see `#flush`) so that
|
|
87
|
+
# shifting a chunk off both arrays together always yields the correct keys for that
|
|
88
|
+
# chunk. The target and its tombstone are two separate `@buffer` entries for the same
|
|
89
|
+
# schedule, so the same key is intentionally paired with each. `DailyBuffer#delete` is a
|
|
90
|
+
# plain `Hash#delete`, so a key evicted twice within the same yielded chunk (once per
|
|
91
|
+
# entry) is a harmless no-op the second time.
|
|
81
92
|
@buffer << target
|
|
93
|
+
@keys << message.key
|
|
82
94
|
|
|
83
95
|
# Tombstone message so this schedule is no longer in use and gets removed from Kafka by
|
|
84
96
|
# Kafka itself during compacting. It will not cancel it because already dispatched but
|
|
85
97
|
# will cause it not to be sent again and will be marked as dispatched.
|
|
86
98
|
@buffer << Proxy.tombstone(message: message)
|
|
99
|
+
@keys << message.key
|
|
87
100
|
end
|
|
88
101
|
|
|
89
102
|
# Builds and dispatches the state report message with schedules details
|
|
@@ -105,14 +118,42 @@ module Karafka
|
|
|
105
118
|
# Sends all messages to Kafka in a sync way.
|
|
106
119
|
# We use sync with batches to prevent overloading.
|
|
107
120
|
# When transactional producer in use, this will be wrapped in a transaction automatically.
|
|
121
|
+
#
|
|
122
|
+
# @yieldparam [Array<String>] keys of the chunk that was just confirmed delivered. Yielded
|
|
123
|
+
# after each chunk's sync produce returns, so the caller can evict those keys from the
|
|
124
|
+
# daily buffer incrementally. If a later chunk raises, the chunks already produced have
|
|
125
|
+
# still been reported, so a non-transactional producer will not re-dispatch them.
|
|
126
|
+
# @raise [ArgumentError] when called without a block, since a caller that cannot observe
|
|
127
|
+
# per-chunk confirmations cannot evict incrementally and would reintroduce the
|
|
128
|
+
# whole-flush-or-nothing duplicate-dispatch window this method exists to close
|
|
108
129
|
def flush
|
|
130
|
+
raise ArgumentError, "#flush requires a block to report per-chunk confirmations" unless block_given?
|
|
131
|
+
|
|
109
132
|
until @buffer.empty?
|
|
110
|
-
config.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
133
|
+
batch_size = config.flush_batch_size
|
|
134
|
+
|
|
135
|
+
# A message's target and its tombstone are always buffered as an adjacent pair (see
|
|
136
|
+
# `#<<`). Rounding the chunk size up to even guarantees a chunk boundary can never
|
|
137
|
+
# fall between them - with an odd (or 1) `flush_batch_size`, a chunk could otherwise
|
|
138
|
+
# confirm and yield a key whose target was produced but whose tombstone was not, and a
|
|
139
|
+
# later chunk failing would then leave that schedule non-tombstoned in Kafka, to be
|
|
140
|
+
# re-dispatched after a restart/reload.
|
|
141
|
+
batch_size += 1 if batch_size.odd?
|
|
142
|
+
|
|
143
|
+
messages = @buffer.shift(batch_size)
|
|
144
|
+
keys = @keys.shift(batch_size)
|
|
145
|
+
|
|
146
|
+
config.producer.produce_many_sync(messages)
|
|
147
|
+
|
|
148
|
+
yield(keys)
|
|
115
149
|
end
|
|
150
|
+
ensure
|
|
151
|
+
# Whether flush finished normally (buffer already empty here, so this is a no-op) or
|
|
152
|
+
# raised partway through, drop anything left. Those messages are still in the daily
|
|
153
|
+
# buffer (their keys were never yielded, so never evicted) and will be re-buffered on
|
|
154
|
+
# the next tick, so stale leftovers here must not be dispatched a second time.
|
|
155
|
+
@buffer.clear
|
|
156
|
+
@keys.clear
|
|
116
157
|
end
|
|
117
158
|
|
|
118
159
|
private
|
|
@@ -48,10 +48,11 @@ module Karafka
|
|
|
48
48
|
def revoke(topic_name, partition)
|
|
49
49
|
return unless @coordinators[topic_name].key?(partition)
|
|
50
50
|
|
|
51
|
-
# Reset the partition's pause tracker
|
|
52
|
-
#
|
|
53
|
-
# partition,
|
|
54
|
-
#
|
|
51
|
+
# Reset (or, if not currently paused, remove) the partition's pause tracker. This
|
|
52
|
+
# prevents a stale retry attempt count from being reused as-is if we reclaim this
|
|
53
|
+
# partition (which, with DLQ, would send the next failure straight to the dead letter
|
|
54
|
+
# queue, skipping the retries) while also keeping `PausesManager#@pauses` from growing
|
|
55
|
+
# unbounded. See `PausesManager#revoke` for the full reasoning
|
|
55
56
|
@pauses_manager.revoke(@topics.find(topic_name), partition)
|
|
56
57
|
|
|
57
58
|
# The fact that we delete here does not change the fact that the executor still holds the
|
data/lib/karafka/server.rb
CHANGED
|
@@ -115,8 +115,11 @@ module Karafka
|
|
|
115
115
|
|
|
116
116
|
# We check from time to time (for the timeout period) if all the threads finished
|
|
117
117
|
# their work and if so, we can just return and normal shutdown process will take place
|
|
118
|
-
# We divide
|
|
119
|
-
|
|
118
|
+
# We use float math and divide by 1000 (ms -> s) so that sub-second `shutdown_timeout`
|
|
119
|
+
# values do not collapse to zero iterations (integer division would floor them to 0,
|
|
120
|
+
# skipping the grace period entirely and forcing an immediate forceful shutdown). We
|
|
121
|
+
# `ceil` to guarantee at least one supervision check for any positive timeout.
|
|
122
|
+
(timeout / (supervision_sleep * 1_000)).ceil.times do
|
|
120
123
|
all_listeners_stopped = listeners.all?(&:stopped?)
|
|
121
124
|
all_workers_stopped = workers.stopped?
|
|
122
125
|
|
data/lib/karafka/setup/config.rb
CHANGED
|
@@ -430,12 +430,12 @@ module Karafka
|
|
|
430
430
|
scope: %w[config]
|
|
431
431
|
)
|
|
432
432
|
|
|
433
|
-
|
|
433
|
+
# Verify config-dependent constraints (registered by features for their optional
|
|
434
|
+
# environment requirements) once the config shape is guaranteed by the contract but
|
|
435
|
+
# before any components spin up
|
|
436
|
+
Karafka::Constraints.verify!(:config, config)
|
|
434
437
|
|
|
435
|
-
|
|
436
|
-
# still access config.internal.processing.strategy_selector (etc.) keep working after
|
|
437
|
-
# the move to config.internal.processing.consumer_groups.*
|
|
438
|
-
install_processing_cg_forwarders(config)
|
|
438
|
+
configure_components(proxy)
|
|
439
439
|
|
|
440
440
|
# Refreshes the references that are cached that might have been changed by the config
|
|
441
441
|
Karafka.refresh!
|
|
@@ -460,38 +460,6 @@ module Karafka
|
|
|
460
460
|
|
|
461
461
|
private
|
|
462
462
|
|
|
463
|
-
# Installs forwarding reader methods on the processing config node so that the old
|
|
464
|
-
# (pre-nesting) paths like `config.internal.processing.strategy_selector` still resolve
|
|
465
|
-
# by delegating to `config.internal.processing.consumer_groups.strategy_selector`.
|
|
466
|
-
# This keeps external gems (e.g. karafka-testing) working until they migrate.
|
|
467
|
-
#
|
|
468
|
-
# @param config [Karafka::Core::Configurable::Node] root config node
|
|
469
|
-
def install_processing_cg_forwarders(config)
|
|
470
|
-
processing = config.internal.processing
|
|
471
|
-
cg_node = processing.consumer_groups
|
|
472
|
-
|
|
473
|
-
%i[
|
|
474
|
-
jobs_builder
|
|
475
|
-
coordinator_class
|
|
476
|
-
errors_tracker_class
|
|
477
|
-
partitioner_class
|
|
478
|
-
strategy_selector
|
|
479
|
-
expansions_selector
|
|
480
|
-
executor_class
|
|
481
|
-
].each do |setting_name|
|
|
482
|
-
writer = :"#{setting_name}="
|
|
483
|
-
|
|
484
|
-
# Remove previous definitions (if setup runs more than once) to avoid
|
|
485
|
-
# "method redefined" warnings that spec_helper promotes to errors
|
|
486
|
-
sc = processing.singleton_class
|
|
487
|
-
sc.remove_method(setting_name) if processing.respond_to?(setting_name)
|
|
488
|
-
sc.remove_method(writer) if processing.respond_to?(writer)
|
|
489
|
-
|
|
490
|
-
processing.define_singleton_method(setting_name) { cg_node.public_send(setting_name) }
|
|
491
|
-
processing.define_singleton_method(writer) { |val| cg_node.public_send(writer, val) }
|
|
492
|
-
end
|
|
493
|
-
end
|
|
494
|
-
|
|
495
463
|
# Sets up all the components that are based on the user configuration
|
|
496
464
|
# @param config_proxy [ConfigProxy] the configuration proxy containing deferred setup
|
|
497
465
|
# blocks
|
data/lib/karafka/swarm/node.rb
CHANGED
|
@@ -190,6 +190,8 @@ module Karafka
|
|
|
190
190
|
# @param signal [String]
|
|
191
191
|
# @return [Boolean] true if signal was sent, false if process doesn't exist
|
|
192
192
|
def signal(signal)
|
|
193
|
+
@mutex.synchronize { return false if @alive == false }
|
|
194
|
+
|
|
193
195
|
::Process.kill(signal, @pid)
|
|
194
196
|
true
|
|
195
197
|
rescue Errno::ESRCH
|
|
@@ -143,8 +143,10 @@ module Karafka
|
|
|
143
143
|
|
|
144
144
|
# We check from time to time (for the timeout period) if all the threads finished
|
|
145
145
|
# their work and if so, we can just return and normal shutdown process will take place
|
|
146
|
-
# We divide
|
|
147
|
-
|
|
146
|
+
# We use float math and divide by 1000 (ms -> s) so that sub-second timeout values do not
|
|
147
|
+
# collapse to zero iterations via integer division. We `ceil` to guarantee at least one
|
|
148
|
+
# supervision check for any positive timeout.
|
|
149
|
+
(total_shutdown_timeout / (supervision_sleep * 1_000)).ceil.times do
|
|
148
150
|
if manager.stopped?
|
|
149
151
|
manager.cleanup
|
|
150
152
|
return
|
data/lib/karafka/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: karafka
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.6.
|
|
4
|
+
version: 2.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maciej Mensfeld
|
|
@@ -234,6 +234,9 @@ files:
|
|
|
234
234
|
- lib/karafka/pro/contracts/base.rb
|
|
235
235
|
- lib/karafka/pro/encryption.rb
|
|
236
236
|
- lib/karafka/pro/encryption/cipher.rb
|
|
237
|
+
- lib/karafka/pro/encryption/ciphers/base.rb
|
|
238
|
+
- lib/karafka/pro/encryption/ciphers/direct.rb
|
|
239
|
+
- lib/karafka/pro/encryption/ciphers/envelope.rb
|
|
237
240
|
- lib/karafka/pro/encryption/contracts/config.rb
|
|
238
241
|
- lib/karafka/pro/encryption/errors.rb
|
|
239
242
|
- lib/karafka/pro/encryption/messages/middleware.rb
|