unleash 6.3.1 → 6.4.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 +14 -1
- data/README.md +7 -5
- data/bin/unleash-client +1 -1
- data/examples/simple.rb +3 -4
- data/examples/streaming.rb +50 -0
- data/lib/unleash/backup_file_reader.rb +24 -0
- data/lib/unleash/backup_file_writer.rb +21 -0
- data/lib/unleash/client.rb +22 -4
- data/lib/unleash/configuration.rb +28 -3
- data/lib/unleash/spec_version.rb +1 -1
- data/lib/unleash/streaming_client_executor.rb +114 -0
- data/lib/unleash/streaming_event_processor.rb +55 -0
- data/lib/unleash/toggle_fetcher.rb +8 -40
- data/lib/unleash/util/event_source_wrapper.rb +17 -0
- data/lib/unleash/version.rb +1 -1
- data/unleash-client.gemspec +4 -3
- metadata +31 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c28fb398c92bd470282a94e7fe07b910fa4afed65381da67231c60c2b2960c2
|
|
4
|
+
data.tar.gz: 3a5b78a63ccfcec23ed8eba1e2168a5be7f183ca0c3beac0eb6497d1d4d6678c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d66480a2211bc60a0ddb2899d189a7966be1659f8618873233c8e419f2261dc5cf13cf79f747d36039af5f9ce1ead45fb4e3bc4140d420d4d59369a9a995bc4
|
|
7
|
+
data.tar.gz: ba06afc53ff6d2806d6bbf18cc93e2287ae327f42cb6699f663a3f2f5bb9abc6e3f71ce02ba7fe18f48bdcb9fb1913c2d9cc4a6be2d05bae6f740def1a4281d5
|
data/CHANGELOG.md
CHANGED
|
@@ -13,7 +13,20 @@ Note: These changes are not considered notable:
|
|
|
13
13
|
|
|
14
14
|
## [Unreleased]
|
|
15
15
|
|
|
16
|
-
## [6.
|
|
16
|
+
## [6.4.1] - 2025-12-04
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Bump yggdrasil-engine
|
|
20
|
+
|
|
21
|
+
## [6.4.0] - 2025-08-12
|
|
22
|
+
### Added
|
|
23
|
+
- Experimental streaming support
|
|
24
|
+
|
|
25
|
+
#### Changed
|
|
26
|
+
- SDK registration name
|
|
27
|
+
- base64 dependency version
|
|
28
|
+
|
|
29
|
+
## [6.3.1] - 2025-05-27
|
|
17
30
|
### Fixed
|
|
18
31
|
- Upgraded Yggdrasil engine to fix a memory leak in metrics.
|
|
19
32
|
|
data/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Unleash::Client
|
|
2
2
|
|
|
3
|
-

|
|
4
|
+
[](https://coveralls.io/github/Unleash/unleash-ruby-sdk?branch=main)
|
|
5
5
|
[](https://badge.fury.io/rb/unleash)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Unleash is a private, secure, and scalable [feature management platform](https://www.getunleash.io/) built to reduce the risk of releasing new features and accelerate software development. This Ruby SDK is designed to help you integrate with Unleash and evaluate feature flags inside your application.
|
|
8
|
+
|
|
9
|
+
You can use this client with [Unleash Enterprise](https://www.getunleash.io/pricing?utm_source=readme&utm_medium=ruby) or [Unleash Open Source](https://github.com/Unleash/unleash).
|
|
8
10
|
|
|
9
11
|
> **Migrating to v6**
|
|
10
12
|
>
|
|
@@ -26,7 +28,7 @@ Ruby client for the [Unleash](https://github.com/Unleash/unleash) feature manage
|
|
|
26
28
|
Add this line to your application's Gemfile:
|
|
27
29
|
|
|
28
30
|
```ruby
|
|
29
|
-
gem 'unleash', '~> 6.
|
|
31
|
+
gem 'unleash', '~> 6.4.1'
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
And then execute:
|
|
@@ -563,7 +565,7 @@ To release a new version, follow these steps:
|
|
|
563
565
|
|
|
564
566
|
## Contributing
|
|
565
567
|
|
|
566
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/unleash/unleash-
|
|
568
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/unleash/unleash-ruby-sdk.
|
|
567
569
|
|
|
568
570
|
Be sure to run both `bundle exec rspec` and `bundle exec rubocop` in your branch before creating a pull request.
|
|
569
571
|
|
data/bin/unleash-client
CHANGED
|
@@ -77,7 +77,7 @@ log_level = \
|
|
|
77
77
|
|
|
78
78
|
@unleash = Unleash::Client.new(
|
|
79
79
|
url: options[:url],
|
|
80
|
-
app_name: 'unleash-
|
|
80
|
+
app_name: 'unleash-ruby-sdk-cli',
|
|
81
81
|
disable_metrics: options[:metrics],
|
|
82
82
|
custom_http_headers: options[:custom_http_headers],
|
|
83
83
|
log_level: log_level
|
data/examples/simple.rb
CHANGED
|
@@ -18,8 +18,8 @@ puts ">> START simple.rb"
|
|
|
18
18
|
# or:
|
|
19
19
|
|
|
20
20
|
@unleash = Unleash::Client.new(
|
|
21
|
-
url: 'https://unleash.
|
|
22
|
-
custom_http_headers: { 'Authorization': '
|
|
21
|
+
url: 'https://app.unleash-hosted.com/demo/api',
|
|
22
|
+
custom_http_headers: { 'Authorization': 'demo-app:dev.9fc74dd72d2b88bea5253c04240b21a54841f08d9918046ed55a06b5' },
|
|
23
23
|
app_name: 'simple-test',
|
|
24
24
|
instance_id: 'local-test-cli',
|
|
25
25
|
refresh_interval: 2,
|
|
@@ -27,8 +27,7 @@ puts ">> START simple.rb"
|
|
|
27
27
|
retry_limit: 2
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
feature_name = "4343443"
|
|
30
|
+
feature_name = "example-flag"
|
|
32
31
|
unleash_context = Unleash::Context.new
|
|
33
32
|
unleash_context.user_id = 123
|
|
34
33
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'unleash'
|
|
4
|
+
require 'unleash/context'
|
|
5
|
+
|
|
6
|
+
puts ">> START streaming.rb"
|
|
7
|
+
|
|
8
|
+
@unleash = Unleash::Client.new(
|
|
9
|
+
url: 'https://app.unleash-hosted.com/demo/api',
|
|
10
|
+
custom_http_headers: { 'Authorization': 'demo-app:dev.9fc74dd72d2b88bea5253c04240b21a54841f08d9918046ed55a06b5' },
|
|
11
|
+
app_name: 'streaming-test',
|
|
12
|
+
instance_id: 'local-streaming-cli',
|
|
13
|
+
refresh_interval: 2,
|
|
14
|
+
metrics_interval: 2,
|
|
15
|
+
retry_limit: 2,
|
|
16
|
+
experimental_mode: { type: 'streaming' },
|
|
17
|
+
timeout: 5,
|
|
18
|
+
log_level: Logger::DEBUG
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
feature_name = "example-flag"
|
|
22
|
+
unleash_context = Unleash::Context.new
|
|
23
|
+
unleash_context.user_id = 123
|
|
24
|
+
|
|
25
|
+
puts "Waiting for client to initialize..."
|
|
26
|
+
sleep 2
|
|
27
|
+
|
|
28
|
+
100.times do
|
|
29
|
+
if @unleash.is_enabled?(feature_name, unleash_context)
|
|
30
|
+
puts "> #{feature_name} is enabled"
|
|
31
|
+
else
|
|
32
|
+
puts "> #{feature_name} is not enabled"
|
|
33
|
+
end
|
|
34
|
+
sleep 1
|
|
35
|
+
puts "---"
|
|
36
|
+
puts ""
|
|
37
|
+
puts ""
|
|
38
|
+
end
|
|
39
|
+
feature_name = "foobar"
|
|
40
|
+
if @unleash.is_enabled?(feature_name, unleash_context, true)
|
|
41
|
+
puts "> #{feature_name} is enabled"
|
|
42
|
+
else
|
|
43
|
+
puts "> #{feature_name} is not enabled"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
puts "> shutting down client..."
|
|
47
|
+
|
|
48
|
+
@unleash.shutdown
|
|
49
|
+
|
|
50
|
+
puts ">> END streaming.rb"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'unleash/configuration'
|
|
2
|
+
|
|
3
|
+
module Unleash
|
|
4
|
+
class BackupFileReader
|
|
5
|
+
def self.read!
|
|
6
|
+
Unleash.logger.debug "read!()"
|
|
7
|
+
|
|
8
|
+
backup_file = Unleash.configuration.backup_file
|
|
9
|
+
return nil unless File.exist?(backup_file)
|
|
10
|
+
|
|
11
|
+
File.read(backup_file)
|
|
12
|
+
rescue IOError => e
|
|
13
|
+
# :nocov:
|
|
14
|
+
Unleash.logger.error "Unable to read the backup_file: #{e}"
|
|
15
|
+
# :nocov:
|
|
16
|
+
nil
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
# :nocov:
|
|
19
|
+
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
|
|
20
|
+
# :nocov:
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'unleash/configuration'
|
|
2
|
+
|
|
3
|
+
module Unleash
|
|
4
|
+
class BackupFileWriter
|
|
5
|
+
def self.save!(toggle_data)
|
|
6
|
+
Unleash.logger.debug "Will save toggles to disk now"
|
|
7
|
+
|
|
8
|
+
backup_file = Unleash.configuration.backup_file
|
|
9
|
+
backup_file_tmp = "#{backup_file}.tmp-#{Process.pid}"
|
|
10
|
+
|
|
11
|
+
File.open(backup_file_tmp, "w") do |file|
|
|
12
|
+
file.write(toggle_data)
|
|
13
|
+
end
|
|
14
|
+
File.rename(backup_file_tmp, backup_file)
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
# This is not really the end of the world. Swallowing the exception.
|
|
17
|
+
Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
|
|
18
|
+
Unleash.logger.error "stacktrace: #{e.backtrace}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/unleash/client.rb
CHANGED
|
@@ -2,8 +2,10 @@ require 'unleash/configuration'
|
|
|
2
2
|
require 'unleash/toggle_fetcher'
|
|
3
3
|
require 'unleash/metrics_reporter'
|
|
4
4
|
require 'unleash/scheduled_executor'
|
|
5
|
+
require 'unleash/streaming_client_executor'
|
|
5
6
|
require 'unleash/variant'
|
|
6
7
|
require 'unleash/util/http'
|
|
8
|
+
require 'unleash/util/event_source_wrapper'
|
|
7
9
|
require 'logger'
|
|
8
10
|
require 'time'
|
|
9
11
|
|
|
@@ -21,7 +23,8 @@ module Unleash
|
|
|
21
23
|
Unleash.engine = YggdrasilEngine.new
|
|
22
24
|
Unleash.engine.register_custom_strategies(Unleash.configuration.strategies.custom_strategies)
|
|
23
25
|
|
|
24
|
-
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new Unleash.engine
|
|
26
|
+
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new Unleash.engine unless Unleash.configuration.streaming_mode?
|
|
27
|
+
|
|
25
28
|
if Unleash.configuration.disable_client
|
|
26
29
|
Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!"
|
|
27
30
|
Unleash.logger.warn "Unleash::Client is disabled! Metrics and MetricsReporter are also disabled!"
|
|
@@ -30,7 +33,9 @@ module Unleash
|
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
register
|
|
33
|
-
|
|
36
|
+
|
|
37
|
+
initialize_client_mode
|
|
38
|
+
|
|
34
39
|
start_metrics unless Unleash.configuration.disable_metrics
|
|
35
40
|
end
|
|
36
41
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -105,7 +110,7 @@ module Unleash
|
|
|
105
110
|
# quick shutdown: just kill running threads
|
|
106
111
|
def shutdown!
|
|
107
112
|
unless Unleash.configuration.disable_client
|
|
108
|
-
self.fetcher_scheduled_executor
|
|
113
|
+
self.fetcher_scheduled_executor&.exit
|
|
109
114
|
self.metrics_scheduled_executor.exit unless Unleash.configuration.disable_metrics
|
|
110
115
|
end
|
|
111
116
|
end
|
|
@@ -117,7 +122,7 @@ module Unleash
|
|
|
117
122
|
'appName': Unleash.configuration.app_name,
|
|
118
123
|
'instanceId': Unleash.configuration.instance_id,
|
|
119
124
|
'connectionId': Unleash.configuration.connection_id,
|
|
120
|
-
'sdkVersion': "unleash-
|
|
125
|
+
'sdkVersion': "unleash-ruby-sdk:" + Unleash::VERSION,
|
|
121
126
|
'strategies': Unleash.strategies.known_strategies,
|
|
122
127
|
'started': Time.now.iso8601(Unleash::TIME_RESOLUTION),
|
|
123
128
|
'interval': Unleash.configuration.metrics_interval_in_millis,
|
|
@@ -140,6 +145,11 @@ module Unleash
|
|
|
140
145
|
end
|
|
141
146
|
end
|
|
142
147
|
|
|
148
|
+
def start_streaming_client
|
|
149
|
+
self.fetcher_scheduled_executor = Unleash::StreamingClientExecutor.new('StreamingExecutor', Unleash.engine)
|
|
150
|
+
self.fetcher_scheduled_executor.run
|
|
151
|
+
end
|
|
152
|
+
|
|
143
153
|
def start_metrics
|
|
144
154
|
Unleash.reporter = Unleash::MetricsReporter.new
|
|
145
155
|
self.metrics_scheduled_executor = Unleash::ScheduledExecutor.new(
|
|
@@ -172,5 +182,13 @@ module Unleash
|
|
|
172
182
|
def first_fetch_is_eager
|
|
173
183
|
Unleash.configuration.use_bootstrap?
|
|
174
184
|
end
|
|
185
|
+
|
|
186
|
+
def initialize_client_mode
|
|
187
|
+
if Unleash.configuration.streaming_mode?
|
|
188
|
+
start_streaming_client
|
|
189
|
+
else
|
|
190
|
+
start_toggle_fetcher
|
|
191
|
+
end
|
|
192
|
+
end
|
|
175
193
|
end
|
|
176
194
|
end
|
|
@@ -22,7 +22,8 @@ module Unleash
|
|
|
22
22
|
:log_level,
|
|
23
23
|
:bootstrap_config,
|
|
24
24
|
:strategies,
|
|
25
|
-
:use_delta_api
|
|
25
|
+
:use_delta_api,
|
|
26
|
+
:experimental_mode
|
|
26
27
|
attr_reader :connection_id
|
|
27
28
|
|
|
28
29
|
def initialize(opts = {})
|
|
@@ -57,7 +58,7 @@ module Unleash
|
|
|
57
58
|
'UNLEASH-INSTANCEID' => self.instance_id,
|
|
58
59
|
'UNLEASH-APPNAME' => self.app_name,
|
|
59
60
|
'Unleash-Client-Spec' => CLIENT_SPECIFICATION_VERSION,
|
|
60
|
-
'UNLEASH-SDK' => "unleash-
|
|
61
|
+
'UNLEASH-SDK' => "unleash-ruby-sdk:#{Unleash::VERSION}"
|
|
61
62
|
}.merge!(generate_custom_http_headers)
|
|
62
63
|
headers['UNLEASH-CONNECTION-ID'] = @connection_id
|
|
63
64
|
headers
|
|
@@ -67,7 +68,9 @@ module Unleash
|
|
|
67
68
|
uri = nil
|
|
68
69
|
## Personal feeling but Rubocop's suggestion here is too dense to be properly readable
|
|
69
70
|
# rubocop:disable Style/ConditionalAssignment
|
|
70
|
-
if
|
|
71
|
+
if streaming_mode?
|
|
72
|
+
uri = URI("#{self.url_stripped_of_slash}/client/streaming")
|
|
73
|
+
elsif self.use_delta_api || polling_with_delta?
|
|
71
74
|
uri = URI("#{self.url_stripped_of_slash}/client/delta")
|
|
72
75
|
else
|
|
73
76
|
uri = URI("#{self.url_stripped_of_slash}/client/features")
|
|
@@ -93,6 +96,17 @@ module Unleash
|
|
|
93
96
|
self.bootstrap_config&.valid?
|
|
94
97
|
end
|
|
95
98
|
|
|
99
|
+
def streaming_mode?
|
|
100
|
+
validate_streaming_support! if streaming_configured?
|
|
101
|
+
streaming_configured?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def polling_with_delta?
|
|
105
|
+
self.experimental_mode.is_a?(Hash) &&
|
|
106
|
+
self.experimental_mode[:type] == 'polling' &&
|
|
107
|
+
self.experimental_mode[:format] == 'delta'
|
|
108
|
+
end
|
|
109
|
+
|
|
96
110
|
private
|
|
97
111
|
|
|
98
112
|
def set_defaults
|
|
@@ -112,6 +126,7 @@ module Unleash
|
|
|
112
126
|
self.bootstrap_config = nil
|
|
113
127
|
self.strategies = Unleash::Strategies.new
|
|
114
128
|
self.use_delta_api = false
|
|
129
|
+
self.experimental_mode = nil
|
|
115
130
|
|
|
116
131
|
self.custom_http_headers = {}
|
|
117
132
|
@connection_id = SecureRandom.uuid
|
|
@@ -149,5 +164,15 @@ module Unleash
|
|
|
149
164
|
rescue NoMethodError
|
|
150
165
|
raise ArgumentError, "unknown configuration parameter '#{val}'"
|
|
151
166
|
end
|
|
167
|
+
|
|
168
|
+
def streaming_configured?
|
|
169
|
+
self.experimental_mode.is_a?(Hash) && self.experimental_mode[:type] == 'streaming'
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def validate_streaming_support!
|
|
173
|
+
return unless RUBY_ENGINE == 'jruby'
|
|
174
|
+
|
|
175
|
+
raise "Streaming mode is not supported on JRuby. Please use polling mode instead or switch to MRI/CRuby."
|
|
176
|
+
end
|
|
152
177
|
end
|
|
153
178
|
end
|
data/lib/unleash/spec_version.rb
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require 'unleash/streaming_event_processor'
|
|
2
|
+
require 'unleash/bootstrap/handler'
|
|
3
|
+
require 'unleash/backup_file_reader'
|
|
4
|
+
require 'unleash/util/event_source_wrapper'
|
|
5
|
+
|
|
6
|
+
module Unleash
|
|
7
|
+
class StreamingClientExecutor
|
|
8
|
+
attr_accessor :name, :event_source, :event_processor, :running
|
|
9
|
+
|
|
10
|
+
def initialize(name, engine)
|
|
11
|
+
self.name = name || 'StreamingClientExecutor'
|
|
12
|
+
self.event_source = nil
|
|
13
|
+
self.event_processor = Unleash::StreamingEventProcessor.new(engine)
|
|
14
|
+
self.running = false
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
# if bootstrap configuration is available, initialize. Otherwise read backup file
|
|
18
|
+
if Unleash.configuration.use_bootstrap?
|
|
19
|
+
bootstrap(engine)
|
|
20
|
+
else
|
|
21
|
+
read_backup_file!(engine)
|
|
22
|
+
end
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
# fall back to reading the backup file
|
|
25
|
+
Unleash.logger.warn "StreamingClientExecutor was unable to initialize, attempting to read from backup file."
|
|
26
|
+
Unleash.logger.debug "Exception Caught: #{e}"
|
|
27
|
+
read_backup_file!(engine)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run(&_block)
|
|
32
|
+
start
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def start
|
|
36
|
+
return if self.running || Unleash.configuration.disable_client
|
|
37
|
+
|
|
38
|
+
Unleash.logger.debug "Streaming client #{self.name} starting connection to: #{Unleash.configuration.fetch_toggles_uri}"
|
|
39
|
+
|
|
40
|
+
self.event_source = create_event_source
|
|
41
|
+
setup_event_handlers
|
|
42
|
+
|
|
43
|
+
self.running = true
|
|
44
|
+
Unleash.logger.debug "Streaming client #{self.name} connection established"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def stop
|
|
48
|
+
return unless self.running
|
|
49
|
+
|
|
50
|
+
Unleash.logger.debug "Streaming client #{self.name} stopping connection"
|
|
51
|
+
self.running = false
|
|
52
|
+
self.event_source&.close
|
|
53
|
+
self.event_source = nil
|
|
54
|
+
Unleash.logger.debug "Streaming client #{self.name} connection closed"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
alias exit stop
|
|
58
|
+
|
|
59
|
+
def running?
|
|
60
|
+
self.running
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def create_event_source
|
|
66
|
+
sse_client = Unleash::Util::EventSourceWrapper.client
|
|
67
|
+
if sse_client.nil?
|
|
68
|
+
raise "Streaming mode is configured but EventSource client is not available. " \
|
|
69
|
+
"Please install the 'ld-eventsource' gem or switch to polling mode."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
headers = (Unleash.configuration.http_headers || {}).dup
|
|
73
|
+
|
|
74
|
+
sse_client.new(
|
|
75
|
+
Unleash.configuration.fetch_toggles_uri.to_s,
|
|
76
|
+
headers: headers,
|
|
77
|
+
read_timeout: 60,
|
|
78
|
+
reconnect_time: 2,
|
|
79
|
+
connect_timeout: 10,
|
|
80
|
+
logger: Unleash.logger
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def setup_event_handlers
|
|
85
|
+
self.event_source.on_event do |event|
|
|
86
|
+
handle_event(event)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
self.event_source.on_error do |error|
|
|
90
|
+
Unleash.logger.warn "Streaming client #{self.name} error: #{error}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_event(event)
|
|
95
|
+
self.event_processor.process_event(event)
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
Unleash.logger.error "Streaming client #{self.name} threw exception #{e.class}: '#{e}'"
|
|
98
|
+
Unleash.logger.debug "stacktrace: #{e.backtrace}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def read_backup_file!(engine)
|
|
102
|
+
backup_data = Unleash::BackupFileReader.read!
|
|
103
|
+
engine.take_state(backup_data) if backup_data
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def bootstrap(engine)
|
|
107
|
+
bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles
|
|
108
|
+
engine.take_state(bootstrap_payload)
|
|
109
|
+
|
|
110
|
+
# reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again
|
|
111
|
+
Unleash.configuration.bootstrap_config = nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'unleash/backup_file_writer'
|
|
3
|
+
|
|
4
|
+
module Unleash
|
|
5
|
+
class StreamingEventProcessor
|
|
6
|
+
attr_accessor :toggle_engine, :mutex
|
|
7
|
+
|
|
8
|
+
def initialize(toggle_engine)
|
|
9
|
+
self.toggle_engine = toggle_engine
|
|
10
|
+
self.mutex = Mutex.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def process_event(event)
|
|
14
|
+
case event.type.to_s
|
|
15
|
+
when 'unleash-connected'
|
|
16
|
+
Unleash.logger.debug "Streaming client connected"
|
|
17
|
+
handle_connected_event(event)
|
|
18
|
+
when 'unleash-updated'
|
|
19
|
+
Unleash.logger.debug "Received streaming update"
|
|
20
|
+
handle_updated_event(event)
|
|
21
|
+
else
|
|
22
|
+
Unleash.logger.debug "Received unknown event type: #{event.type}"
|
|
23
|
+
end
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Unleash.logger.error "Error handling streaming event threw exception #{e.class}: '#{e}'"
|
|
26
|
+
Unleash.logger.debug "stacktrace: #{e.backtrace}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def handle_delta_event(event_data)
|
|
30
|
+
self.mutex.synchronize do
|
|
31
|
+
self.toggle_engine.take_state(event_data)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def handle_connected_event(event)
|
|
38
|
+
Unleash.logger.debug "Processing initial hydration data"
|
|
39
|
+
handle_updated_event(event)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def handle_updated_event(event)
|
|
43
|
+
handle_delta_event(event.data)
|
|
44
|
+
|
|
45
|
+
full_state = @toggle_engine.get_state
|
|
46
|
+
Unleash::BackupFileWriter.save!(full_state)
|
|
47
|
+
rescue JSON::ParserError => e
|
|
48
|
+
Unleash.logger.error "Unable to parse JSON from streaming event data. Exception thrown #{e.class}: '#{e}'"
|
|
49
|
+
Unleash.logger.debug "stacktrace: #{e.backtrace}"
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
Unleash.logger.error "Error processing delta update threw exception #{e.class}: '#{e}'"
|
|
52
|
+
Unleash.logger.debug "stacktrace: #{e.backtrace}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require 'unleash/configuration'
|
|
2
2
|
require 'unleash/bootstrap/handler'
|
|
3
|
+
require 'unleash/backup_file_writer'
|
|
4
|
+
require 'unleash/backup_file_reader'
|
|
3
5
|
require 'net/http'
|
|
4
6
|
require 'json'
|
|
5
7
|
require 'yggdrasil_engine'
|
|
@@ -23,10 +25,10 @@ module Unleash
|
|
|
23
25
|
fetch
|
|
24
26
|
end
|
|
25
27
|
rescue StandardError => e
|
|
26
|
-
#
|
|
28
|
+
# fall back to reading the backup file
|
|
27
29
|
Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
|
|
28
30
|
Unleash.logger.debug "Exception Caught: #{e}"
|
|
29
|
-
|
|
31
|
+
read_backup_file!
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# once initialized, somewhere else you will want to start a loop with fetch()
|
|
@@ -54,25 +56,7 @@ module Unleash
|
|
|
54
56
|
# always synchronize with the local cache when fetching:
|
|
55
57
|
update_engine_state!(response.body)
|
|
56
58
|
|
|
57
|
-
save! response.body
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def save!(toggle_data)
|
|
61
|
-
Unleash.logger.debug "Will save toggles to disk now"
|
|
62
|
-
|
|
63
|
-
backup_file = Unleash.configuration.backup_file
|
|
64
|
-
backup_file_tmp = "#{backup_file}.tmp"
|
|
65
|
-
|
|
66
|
-
self.toggle_lock.synchronize do
|
|
67
|
-
File.open(backup_file_tmp, "w") do |file|
|
|
68
|
-
file.write(toggle_data)
|
|
69
|
-
end
|
|
70
|
-
File.rename(backup_file_tmp, backup_file)
|
|
71
|
-
end
|
|
72
|
-
rescue StandardError => e
|
|
73
|
-
# This is not really the end of the world. Swallowing the exception.
|
|
74
|
-
Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
|
|
75
|
-
Unleash.logger.error "stacktrace: #{e.backtrace}"
|
|
59
|
+
Unleash::BackupFileWriter.save! response.body
|
|
76
60
|
end
|
|
77
61
|
|
|
78
62
|
private
|
|
@@ -88,25 +72,9 @@ module Unleash
|
|
|
88
72
|
Unleash.logger.error "Failed to hydrate state: #{e.backtrace}"
|
|
89
73
|
end
|
|
90
74
|
|
|
91
|
-
def
|
|
92
|
-
Unleash.
|
|
93
|
-
|
|
94
|
-
return nil unless File.exist?(backup_file)
|
|
95
|
-
|
|
96
|
-
backup_data = File.read(backup_file)
|
|
97
|
-
update_engine_state!(backup_data)
|
|
98
|
-
rescue IOError => e
|
|
99
|
-
# :nocov:
|
|
100
|
-
Unleash.logger.error "Unable to read the backup_file: #{e}"
|
|
101
|
-
# :nocov:
|
|
102
|
-
rescue JSON::ParserError => e
|
|
103
|
-
# :nocov:
|
|
104
|
-
Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
|
|
105
|
-
# :nocov:
|
|
106
|
-
rescue StandardError => e
|
|
107
|
-
# :nocov:
|
|
108
|
-
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
|
|
109
|
-
# :nocov:
|
|
75
|
+
def read_backup_file!
|
|
76
|
+
backup_data = Unleash::BackupFileReader.read!
|
|
77
|
+
update_engine_state!(backup_data) if backup_data
|
|
110
78
|
end
|
|
111
79
|
|
|
112
80
|
def bootstrap
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Unleash
|
|
2
|
+
module Util
|
|
3
|
+
module EventSourceWrapper
|
|
4
|
+
def self.client
|
|
5
|
+
return nil if RUBY_ENGINE == 'jruby'
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
require 'ld-eventsource'
|
|
9
|
+
SSE::Client
|
|
10
|
+
rescue LoadError => e
|
|
11
|
+
Unleash.logger.error "Failed to load ld-eventsource: #{e.message}"
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/unleash/version.rb
CHANGED
data/unleash-client.gemspec
CHANGED
|
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
|
|
|
13
13
|
spec.description = "This is the ruby client for Unleash, a powerful feature toggle system
|
|
14
14
|
that gives you a great overview over all feature toggles across all your applications and services."
|
|
15
15
|
|
|
16
|
-
spec.homepage = "https://github.com/unleash/unleash-
|
|
16
|
+
spec.homepage = "https://github.com/unleash/unleash-ruby-sdk"
|
|
17
17
|
|
|
18
18
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
19
19
|
f.match(%r{^(test|spec|features)/})
|
|
@@ -23,9 +23,10 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
spec.require_paths = ["lib"]
|
|
24
24
|
spec.required_ruby_version = ">= 2.7"
|
|
25
25
|
|
|
26
|
-
spec.add_dependency "
|
|
26
|
+
spec.add_dependency "ld-eventsource", "2.2.4" unless RUBY_ENGINE == 'jruby'
|
|
27
|
+
spec.add_dependency "yggdrasil-engine", "~> 1.1.1"
|
|
27
28
|
|
|
28
|
-
spec.add_dependency "base64", "~> 0.
|
|
29
|
+
spec.add_dependency "base64", "~> 0.3.0"
|
|
29
30
|
spec.add_dependency "logger", "~> 1.6"
|
|
30
31
|
|
|
31
32
|
spec.add_development_dependency "bundler", "~> 2.1"
|
metadata
CHANGED
|
@@ -1,43 +1,57 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: unleash
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.
|
|
4
|
+
version: 6.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Renato Arruda
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-12-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: ld-eventsource
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - '='
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 2.2.4
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - '='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 2.2.4
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: yggdrasil-engine
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
16
30
|
requirements:
|
|
17
31
|
- - "~>"
|
|
18
32
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 1.
|
|
33
|
+
version: 1.1.1
|
|
20
34
|
type: :runtime
|
|
21
35
|
prerelease: false
|
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
37
|
requirements:
|
|
24
38
|
- - "~>"
|
|
25
39
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 1.
|
|
40
|
+
version: 1.1.1
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: base64
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
30
44
|
requirements:
|
|
31
45
|
- - "~>"
|
|
32
46
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.
|
|
47
|
+
version: 0.3.0
|
|
34
48
|
type: :runtime
|
|
35
49
|
prerelease: false
|
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
51
|
requirements:
|
|
38
52
|
- - "~>"
|
|
39
53
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.
|
|
54
|
+
version: 0.3.0
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: logger
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -193,7 +207,10 @@ files:
|
|
|
193
207
|
- examples/default-toggles.json
|
|
194
208
|
- examples/extending_unleash_with_opentelemetry.rb
|
|
195
209
|
- examples/simple.rb
|
|
210
|
+
- examples/streaming.rb
|
|
196
211
|
- lib/unleash.rb
|
|
212
|
+
- lib/unleash/backup_file_reader.rb
|
|
213
|
+
- lib/unleash/backup_file_writer.rb
|
|
197
214
|
- lib/unleash/bootstrap/configuration.rb
|
|
198
215
|
- lib/unleash/bootstrap/handler.rb
|
|
199
216
|
- lib/unleash/bootstrap/provider/base.rb
|
|
@@ -206,17 +223,20 @@ files:
|
|
|
206
223
|
- lib/unleash/scheduled_executor.rb
|
|
207
224
|
- lib/unleash/spec_version.rb
|
|
208
225
|
- lib/unleash/strategies.rb
|
|
226
|
+
- lib/unleash/streaming_client_executor.rb
|
|
227
|
+
- lib/unleash/streaming_event_processor.rb
|
|
209
228
|
- lib/unleash/toggle_fetcher.rb
|
|
229
|
+
- lib/unleash/util/event_source_wrapper.rb
|
|
210
230
|
- lib/unleash/util/http.rb
|
|
211
231
|
- lib/unleash/variant.rb
|
|
212
232
|
- lib/unleash/version.rb
|
|
213
233
|
- unleash-client.gemspec
|
|
214
234
|
- v6_MIGRATION_GUIDE.md
|
|
215
|
-
homepage: https://github.com/unleash/unleash-
|
|
235
|
+
homepage: https://github.com/unleash/unleash-ruby-sdk
|
|
216
236
|
licenses:
|
|
217
237
|
- Apache-2.0
|
|
218
238
|
metadata: {}
|
|
219
|
-
post_install_message:
|
|
239
|
+
post_install_message:
|
|
220
240
|
rdoc_options: []
|
|
221
241
|
require_paths:
|
|
222
242
|
- lib
|
|
@@ -231,8 +251,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
231
251
|
- !ruby/object:Gem::Version
|
|
232
252
|
version: '0'
|
|
233
253
|
requirements: []
|
|
234
|
-
rubygems_version: 3.
|
|
235
|
-
signing_key:
|
|
254
|
+
rubygems_version: 3.4.20
|
|
255
|
+
signing_key:
|
|
236
256
|
specification_version: 4
|
|
237
257
|
summary: Unleash feature toggle client.
|
|
238
258
|
test_files: []
|