rox-rollout 6.0.1 → 6.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73d64c9e8123a69712c15cada3f6c9b2642c12953a8c25250eb9963d845dae41
4
- data.tar.gz: 1e14bc76699a72e0f34dc3a5039025ab184049807b930a2c0fa407915c5380b1
3
+ metadata.gz: bffb064d1154d2a9a765e1cd7592cabaf2433c385945694f7373a62ddf6b052f
4
+ data.tar.gz: 682bc11092f95c2a1ac04a4daac434316f8cbf93ee897f60738117d2d5a932a3
5
5
  SHA512:
6
- metadata.gz: a615f9162c302b51d2359d0bc645fa1965a0d28c556678ee7e8a471cc7d1e5cbfef1ca73b06aaaaa91335665c30309286747c31e3930ad4d09fb4547936f1e53
7
- data.tar.gz: c92c4de2381d270ab77dfc543efe00eaa0182d217a0e1e792a5934c58d8cf1c9f0e8d22138839dfd3d63c0ca434835cb8eabe28fdc76a3335bb4e7867da09a44
6
+ metadata.gz: 772253981c9790addeb0995d564053493c70db175a8404c9e7200a2521206f801e11c1c44327b6cd2d03cf4db4d7e84007d10139ff57893b17a8bbcf50ce621b
7
+ data.tar.gz: b9941e8442d8d66bd91373d5f076a24a92da617798af776d46c99e11f000d338fef6186bcbc642107af80e59d88c47e094053bf191912e9a06ff6938c0220f16
@@ -7,7 +7,7 @@ DIR=`pwd`
7
7
  gem install sinatra --conservative
8
8
  gem install sinatra-contrib --conservative
9
9
  gem install json --conservative
10
- gem install em-eventsource --conservative
10
+ gem install server_sent_events --conservative
11
11
  gem install rackup
12
12
  gem install webrick
13
13
 
data/lib/rox/core/core.rb CHANGED
@@ -201,7 +201,8 @@ module Rox
201
201
  def start_or_stop_push_updated_listener
202
202
  if @internal_flags.enabled?('rox.internal.pushUpdates')
203
203
  if @push_updates_listener.nil?
204
- @push_updates_listener = NotificationListener.new(Environment.notifications_path, @sdk_settings.api_key)
204
+ logger = @rox_options&.logger
205
+ @push_updates_listener = NotificationListener.new(Environment.notifications_path, @sdk_settings.api_key, logger: logger)
205
206
  @push_updates_listener.on 'changed' do |_data|
206
207
  fetch
207
208
  end
@@ -1,12 +1,61 @@
1
- require 'em-eventsource'
1
+ require 'server_sent_events'
2
+ require 'net/http'
3
+ require 'uri'
2
4
 
3
5
  module Rox
4
6
  module Core
5
7
  class NotificationListener
6
- def initialize(listen_url, app_key)
8
+ # Initial reconnection delay in seconds (SSE spec recommends "a few seconds")
9
+ INITIAL_RECONNECT_DELAY = 3
10
+ # Maximum reconnection delay in seconds
11
+ MAX_RECONNECT_DELAY = 60
12
+ # Reconnection backoff multiplier
13
+ RECONNECT_MULTIPLIER = 2
14
+ # Jitter percentage to prevent thundering herd (±20%)
15
+ JITTER_FACTOR = 0.2
16
+
17
+ # Extended SSE Client that adds proper headers and timeout configuration
18
+ # The base ServerSentEvents::Client lacks these features
19
+ class SSEClient < ServerSentEvents::Client
20
+ def initialize(address, parser, open_timeout: 10, read_timeout: nil)
21
+ super(address, parser)
22
+ @open_timeout = open_timeout
23
+ @read_timeout = read_timeout
24
+ end
25
+
26
+ def listen(&block)
27
+ Net::HTTP.start(
28
+ @address.host,
29
+ @address.port,
30
+ use_ssl: @address.scheme == 'https',
31
+ open_timeout: @open_timeout,
32
+ read_timeout: @read_timeout
33
+ ) do |http|
34
+ request = Net::HTTP::Get.new(@address)
35
+ request['Accept'] = 'text/event-stream'
36
+ request['Cache-Control'] = 'no-cache'
37
+
38
+ http.request(request) do |response|
39
+ unless response.code == '200'
40
+ raise "HTTP #{response.code}: #{response.message}"
41
+ end
42
+
43
+ response.read_body do |chunk|
44
+ @parser.push(chunk).each { |event| block.call(event) }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def initialize(listen_url, app_key, logger: nil)
7
52
  @listen_url = listen_url
8
53
  @app_key = app_key
9
54
  @handlers = {}
55
+ @logger = logger
56
+ @thread = nil
57
+ @running = false
58
+ @reconnect_delay = INITIAL_RECONNECT_DELAY
10
59
  end
11
60
 
12
61
  def on(event_name, &handler)
@@ -15,31 +64,113 @@ module Rox
15
64
  end
16
65
 
17
66
  def start
67
+ return if @running
68
+
69
+ @running = true
18
70
  sse_url = "#{@listen_url.chomp('/')}/#{@app_key}"
71
+
19
72
  @thread = Thread.new do
20
- EM.run do
21
- source = EventMachine::EventSource.new(sse_url)
22
- @handlers.each do |event_name, event_handlers|
23
- event_handlers.each do |handler|
24
- source.on event_name do |data|
25
- # Start new thread to allow the handler to stop the Listener (terminate the current thread)
26
- # and continue handler code execution without interruption
27
- handler_thread = Thread.new do
28
- handler.call(data)
29
- end
30
- handler_thread.join
31
- end
32
- end
33
- end
34
- source.start
35
- end
73
+ connect_with_retry(sse_url)
36
74
  end
37
75
  end
38
76
 
39
77
  def stop
78
+ @running = false
40
79
  @thread&.terminate
41
80
  @thread = nil
42
81
  end
82
+
83
+ private
84
+
85
+ def calculate_reconnect_delay
86
+ # Use current delay, capped at maximum
87
+ base_delay = [@reconnect_delay, MAX_RECONNECT_DELAY].min
88
+
89
+ # Add jitter: ±20% randomness to prevent thundering herd
90
+ # Example: 3s becomes 2.4s - 3.6s
91
+ # This spreads out reconnection attempts across clients
92
+ jitter = rand(-JITTER_FACTOR..JITTER_FACTOR) * base_delay
93
+ final_delay = base_delay + jitter
94
+
95
+ # Never less than 1 second
96
+ [final_delay, 1.0].max
97
+ end
98
+
99
+ def connect_with_retry(sse_url)
100
+ while @running
101
+ begin
102
+ log_info("Connecting to SSE endpoint: #{sse_url}")
103
+ connect_to_sse(sse_url)
104
+
105
+ # Connection closed normally - reset to initial delay
106
+ break unless @running
107
+ @reconnect_delay = INITIAL_RECONNECT_DELAY
108
+
109
+ rescue => e
110
+ break unless @running
111
+ log_error("SSE connection error: #{e.class} - #{e.message}")
112
+
113
+ # On error, use exponential backoff for next attempt
114
+ @reconnect_delay = [@reconnect_delay * RECONNECT_MULTIPLIER, MAX_RECONNECT_DELAY].min
115
+ end
116
+
117
+ # ALWAYS delay before reconnecting (both normal close and error cases)
118
+ # This matches em-eventsource behavior which waits ~3s regardless of close reason
119
+ delay = calculate_reconnect_delay
120
+ log_info("Reconnecting in #{delay.round(1)} seconds...")
121
+ sleep(delay)
122
+ end
123
+ end
124
+
125
+ def connect_to_sse(sse_url)
126
+ uri = URI(sse_url)
127
+
128
+ # Create SSE client using the library's client with our extensions
129
+ client = SSEClient.new(
130
+ uri,
131
+ ServerSentEvents::Parser.new,
132
+ open_timeout: 10, # 10 seconds to establish connection
133
+ read_timeout: nil # No timeout for reading (SSE is long-lived)
134
+ )
135
+
136
+ log_info("Connected to SSE endpoint")
137
+ @reconnect_delay = INITIAL_RECONNECT_DELAY
138
+
139
+ # Use the library's listen method to handle connection and parsing
140
+ client.listen do |event|
141
+ break unless @running
142
+ handle_event(event)
143
+ end
144
+ end
145
+
146
+ def handle_event(event)
147
+ # Get event type (default to 'message' if not specified)
148
+ event_type = event.event || 'message'
149
+
150
+ # Call registered handlers for this event type
151
+ handlers = @handlers[event_type]
152
+ return unless handlers
153
+
154
+ handlers.each do |handler|
155
+ begin
156
+ # Execute handler in a new thread to allow it to call stop() without blocking
157
+ handler_thread = Thread.new do
158
+ handler.call(event.data)
159
+ end
160
+ handler_thread.join
161
+ rescue => e
162
+ log_error("Error in event handler: #{e.class} - #{e.message}")
163
+ end
164
+ end
165
+ end
166
+
167
+ def log_info(message)
168
+ @logger&.info("[NotificationListener] #{message}")
169
+ end
170
+
171
+ def log_error(message)
172
+ @logger&.error("[NotificationListener] #{message}")
173
+ end
43
174
  end
44
175
  end
45
176
  end
data/lib/rox/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rox
2
- VERSION = '6.0.1'.freeze
2
+ VERSION = '6.0.2'.freeze
3
3
  end
data/rox.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.required_ruby_version = '>= 2.5'
24
24
 
25
- spec.add_runtime_dependency 'em-eventsource', '~> 0.3.2'
25
+ spec.add_runtime_dependency 'server_sent_events', '~> 0.1.3'
26
26
 
27
27
  spec.add_development_dependency 'bundler', '~> 2.4'
28
28
  spec.add_development_dependency 'minitest', '~> 5.20'
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rox-rollout
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.1
4
+ version: 6.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - CloudBees
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-27 00:00:00.000000000 Z
11
+ date: 2026-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: em-eventsource
14
+ name: server_sent_events
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.3.2
19
+ version: 0.1.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.3.2
26
+ version: 0.1.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement