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 +4 -4
- data/e2e-server/run_server.sh +1 -1
- data/lib/rox/core/core.rb +2 -1
- data/lib/rox/core/notifications/notification_listener.rb +149 -18
- data/lib/rox/version.rb +1 -1
- data/rox.gemspec +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bffb064d1154d2a9a765e1cd7592cabaf2433c385945694f7373a62ddf6b052f
|
|
4
|
+
data.tar.gz: 682bc11092f95c2a1ac04a4daac434316f8cbf93ee897f60738117d2d5a932a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 772253981c9790addeb0995d564053493c70db175a8404c9e7200a2521206f801e11c1c44327b6cd2d03cf4db4d7e84007d10139ff57893b17a8bbcf50ce621b
|
|
7
|
+
data.tar.gz: b9941e8442d8d66bd91373d5f076a24a92da617798af776d46c99e11f000d338fef6186bcbc642107af80e59d88c47e094053bf191912e9a06ff6938c0220f16
|
data/e2e-server/run_server.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
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 '
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-04-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
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
|
|
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
|
|
26
|
+
version: 0.1.3
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: bundler
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|