ibm_appconfiguration_ruby_sdk 0.1.0.pre.rc.0
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +9 -0
- data/LICENSE +201 -0
- data/README.md +474 -0
- data/Rakefile +8 -0
- data/examples/README.md +60 -0
- data/examples/app.rb +104 -0
- data/lib/ibm_appconfiguration_ruby_sdk/app_configuration.rb +291 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/configuration_handler.rb +828 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/constants.rb +89 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/file_manager.rb +72 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/logger.rb +98 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/retry_manager/background_retry_manager.rb +284 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/retry_manager/config_fetcher.rb +254 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/utils.rb +240 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/connection_manager.rb +501 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/connectivity.rb +30 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/driver_socket.rb +28 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/retry_policy.rb +42 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/state.rb +24 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/watchdog.rb +50 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/websocket_client.rb +43 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/feature.rb +121 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/property.rb +107 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/rule.rb +87 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/secret_property.rb +81 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/segment.rb +39 -0
- data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/segment_rules.rb +57 -0
- data/lib/ibm_appconfiguration_ruby_sdk/core/api_manager.rb +269 -0
- data/lib/ibm_appconfiguration_ruby_sdk/core/metering.rb +400 -0
- data/lib/ibm_appconfiguration_ruby_sdk/core/url_builder.rb +252 -0
- data/lib/ibm_appconfiguration_ruby_sdk/version.rb +20 -0
- data/lib/ibm_appconfiguration_ruby_sdk.rb +20 -0
- data/sig/ibm_appconfiguration_ruby_sdk.rbs +4 -0
- metadata +209 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 IBM Corp. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require "socket"
|
|
18
|
+
require "openssl"
|
|
19
|
+
require "uri"
|
|
20
|
+
require "websocket/driver"
|
|
21
|
+
|
|
22
|
+
require_relative "driver_socket"
|
|
23
|
+
require_relative "retry_policy"
|
|
24
|
+
require_relative "watchdog"
|
|
25
|
+
require_relative "state"
|
|
26
|
+
require_relative "connectivity"
|
|
27
|
+
require_relative "../../../core/api_manager"
|
|
28
|
+
require_relative "../../../core/url_builder"
|
|
29
|
+
require_relative "../retry_manager/config_fetcher"
|
|
30
|
+
require_relative "../retry_manager/background_retry_manager"
|
|
31
|
+
require_relative "../../configuration_handler"
|
|
32
|
+
require_relative "../utils"
|
|
33
|
+
require_relative "../../../version"
|
|
34
|
+
|
|
35
|
+
class ConnectionManager
|
|
36
|
+
attr_reader :last_heartbeat_at
|
|
37
|
+
|
|
38
|
+
def initialize(region:, guid:, apikey:, collection_id:, environment_id:, start_background_retry: false)
|
|
39
|
+
@region = region
|
|
40
|
+
@guid = guid
|
|
41
|
+
@apikey = apikey
|
|
42
|
+
@collection_id = collection_id
|
|
43
|
+
@environment_id = environment_id
|
|
44
|
+
@start_background_retry = start_background_retry
|
|
45
|
+
|
|
46
|
+
@state = State::DISCONNECTED
|
|
47
|
+
|
|
48
|
+
@state_mutex = Mutex.new
|
|
49
|
+
|
|
50
|
+
@reconnect_attempts = 0
|
|
51
|
+
|
|
52
|
+
@should_reconnect = true
|
|
53
|
+
|
|
54
|
+
@socket = nil
|
|
55
|
+
@driver = nil
|
|
56
|
+
|
|
57
|
+
@reader_thread = nil
|
|
58
|
+
@watchdog_thread = nil
|
|
59
|
+
@connectivity_thread = nil
|
|
60
|
+
|
|
61
|
+
@last_heartbeat_at = Time.now
|
|
62
|
+
|
|
63
|
+
# Initialize ConfigFetcher and BackgroundRetryManager
|
|
64
|
+
@config_fetcher = nil
|
|
65
|
+
@background_retry_manager = nil
|
|
66
|
+
|
|
67
|
+
# Setup SDK components
|
|
68
|
+
setup_sdk
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def connect
|
|
72
|
+
@shutting_down = false
|
|
73
|
+
|
|
74
|
+
transition_state(State::CONNECTING)
|
|
75
|
+
|
|
76
|
+
# Get authentication token
|
|
77
|
+
begin
|
|
78
|
+
puts "⏳ Requesting IAM token..."
|
|
79
|
+
bearer_token = ApiManager.token
|
|
80
|
+
|
|
81
|
+
if bearer_token.nil? || bearer_token.empty?
|
|
82
|
+
puts "❌ Failed to get authentication token"
|
|
83
|
+
transition_state(State::RECONNECTING)
|
|
84
|
+
schedule_reconnect
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
puts "✓ Got authentication token"
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
puts "❌ Exception getting authentication token: #{e.message}"
|
|
91
|
+
puts " Error details: #{e.class.name}"
|
|
92
|
+
transition_state(State::RECONNECTING)
|
|
93
|
+
schedule_reconnect
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get WebSocket URL
|
|
98
|
+
url = @url_builder.websocket_url
|
|
99
|
+
|
|
100
|
+
if url.nil? || url.empty?
|
|
101
|
+
puts "❌ Failed to get WebSocket URL"
|
|
102
|
+
transition_state(State::RECONNECTING)
|
|
103
|
+
schedule_reconnect
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
uri = URI.parse(url)
|
|
108
|
+
|
|
109
|
+
host = uri.host
|
|
110
|
+
port = uri.port || 443 # Default to 443 for wss://
|
|
111
|
+
|
|
112
|
+
puts "Connecting to #{host}:#{port}"
|
|
113
|
+
|
|
114
|
+
# Create TCP socket
|
|
115
|
+
tcp_socket = TCPSocket.new(host, port)
|
|
116
|
+
|
|
117
|
+
# Wrap with SSL for wss://
|
|
118
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
119
|
+
ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
|
120
|
+
|
|
121
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
|
122
|
+
ssl_socket.sync_close = true
|
|
123
|
+
ssl_socket.hostname = host # Set SNI hostname for SSL handshake
|
|
124
|
+
ssl_socket.connect
|
|
125
|
+
|
|
126
|
+
# Create driver socket with full URL
|
|
127
|
+
socket = DriverSocket.new(ssl_socket, url)
|
|
128
|
+
|
|
129
|
+
@socket = ssl_socket
|
|
130
|
+
|
|
131
|
+
@driver = WebSocket::Driver.client(socket)
|
|
132
|
+
|
|
133
|
+
# Set authentication headers
|
|
134
|
+
@driver.set_header("Authorization", bearer_token)
|
|
135
|
+
@driver.set_header("User-Agent", "appconfiguration-ruby-sdk/#{IbmAppconfigurationRubySdk::VERSION}")
|
|
136
|
+
puts "✓ Headers set with authentication"
|
|
137
|
+
|
|
138
|
+
register_callbacks
|
|
139
|
+
|
|
140
|
+
@driver.start
|
|
141
|
+
|
|
142
|
+
start_reader_thread
|
|
143
|
+
|
|
144
|
+
# Start background retry if flag is set (fallback configurations were loaded)
|
|
145
|
+
if @start_background_retry
|
|
146
|
+
puts "🔄 Starting background retry (initial API fetch failed, using fallback config)"
|
|
147
|
+
@background_retry_manager.start(
|
|
148
|
+
reason: "Initial API fetch failed - using fallback configuration"
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
@start_background_retry = true
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
puts "Connection failed: #{e.message}"
|
|
154
|
+
|
|
155
|
+
transition_state(
|
|
156
|
+
State::RECONNECTING
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
schedule_reconnect
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def disconnect
|
|
163
|
+
@should_reconnect = false
|
|
164
|
+
|
|
165
|
+
transition_state(State::CLOSING)
|
|
166
|
+
|
|
167
|
+
cleanup_connection
|
|
168
|
+
|
|
169
|
+
transition_state(State::CLOSED)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def connected?
|
|
173
|
+
@state == State::CONNECTED
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# --------------------------------------------------
|
|
177
|
+
# INTERNAL CALLBACKS
|
|
178
|
+
# --------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def register_callbacks
|
|
181
|
+
@driver.on(:open) do |event|
|
|
182
|
+
puts "WebSocket connected"
|
|
183
|
+
|
|
184
|
+
# Check for HTTP status code during WebSocket handshake
|
|
185
|
+
# The event object may contain status_code for HTTP errors
|
|
186
|
+
if event.respond_to?(:status_code) && event.status_code
|
|
187
|
+
status_code = event.status_code
|
|
188
|
+
|
|
189
|
+
# Check for client-side errors (4xx except 429 Too Many Requests)
|
|
190
|
+
if status_code >= 400 && status_code < 500 && status_code != 429
|
|
191
|
+
puts "❌ WebSocket handshake failed with client error: #{status_code}"
|
|
192
|
+
puts "⛔ Client-side error detected - will not retry connection"
|
|
193
|
+
@should_reconnect = false
|
|
194
|
+
cleanup_connection
|
|
195
|
+
transition_state(State::CLOSED)
|
|
196
|
+
return
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
transition_state(State::CONNECTED)
|
|
201
|
+
|
|
202
|
+
@reconnect_attempts = 0
|
|
203
|
+
|
|
204
|
+
@last_heartbeat_at = Time.now
|
|
205
|
+
|
|
206
|
+
start_watchdog_thread
|
|
207
|
+
start_connectivity_thread
|
|
208
|
+
|
|
209
|
+
# Incase of websocket retry we need to call /config again
|
|
210
|
+
@start_background_retry = true
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
@driver.on(:message) do |event|
|
|
214
|
+
puts "Received: #{event.data}"
|
|
215
|
+
|
|
216
|
+
if event.data == "test message"
|
|
217
|
+
# Heartbeat message
|
|
218
|
+
@last_heartbeat_at = Time.now
|
|
219
|
+
puts "Heartbeat updated"
|
|
220
|
+
else
|
|
221
|
+
# Configuration update message
|
|
222
|
+
puts "📦 Configuration update received"
|
|
223
|
+
|
|
224
|
+
# Stop any active background retry and restart from t=0
|
|
225
|
+
if @background_retry_manager.active?
|
|
226
|
+
puts "🛑 Stopping active background retry to restart from t=0"
|
|
227
|
+
@background_retry_manager.stop
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Start background retry manager which will fetch immediately at t=0
|
|
231
|
+
puts "🔄 Starting background retry for configuration update..."
|
|
232
|
+
@background_retry_manager.start(
|
|
233
|
+
reason: "Configuration update notification received"
|
|
234
|
+
)
|
|
235
|
+
puts "✓ Background retry started (will fetch immediately at t=0)"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
@driver.on(:close) do |event|
|
|
240
|
+
puts "Connection closed"
|
|
241
|
+
|
|
242
|
+
puts "Code: #{event.code}"
|
|
243
|
+
puts "Reason: #{event.reason}"
|
|
244
|
+
|
|
245
|
+
# Check for WebSocket close codes that map to HTTP 4xx client errors
|
|
246
|
+
# Close codes 4000-4499 (except 4429) indicate client-side errors
|
|
247
|
+
if event.code && event.code >= 4000 && event.code < 4500 && event.code != 4429
|
|
248
|
+
puts "❌ WebSocket closed with client error code: #{event.code}"
|
|
249
|
+
puts "⛔ Client-side error detected - will not retry connection"
|
|
250
|
+
@should_reconnect = false
|
|
251
|
+
cleanup_connection
|
|
252
|
+
transition_state(State::CLOSED)
|
|
253
|
+
return
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
@should_reconnect = true
|
|
257
|
+
handle_disconnect("WebSocket close")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
@driver.on(:error) do |event|
|
|
261
|
+
puts "WebSocket error"
|
|
262
|
+
|
|
263
|
+
p event
|
|
264
|
+
|
|
265
|
+
# Check if error contains a status code indicating client-side error
|
|
266
|
+
if event.respond_to?(:status_code) && event.status_code
|
|
267
|
+
status_code = event.status_code
|
|
268
|
+
|
|
269
|
+
# Check for client-side errors (4xx except 429 Too Many Requests)
|
|
270
|
+
if status_code >= 400 && status_code < 500 && status_code != 429
|
|
271
|
+
puts "❌ WebSocket error with client error status: #{status_code}"
|
|
272
|
+
puts "⛔ Client-side error detected - will not retry connection"
|
|
273
|
+
@should_reconnect = false
|
|
274
|
+
cleanup_connection
|
|
275
|
+
transition_state(State::CLOSED)
|
|
276
|
+
return
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
@should_reconnect = true
|
|
281
|
+
|
|
282
|
+
handle_disconnect("WebSocket error")
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def start_reader_thread
|
|
287
|
+
@reader_thread =
|
|
288
|
+
Thread.new do
|
|
289
|
+
loop do
|
|
290
|
+
break if @socket.nil?
|
|
291
|
+
|
|
292
|
+
data = @socket.readpartial(1024)
|
|
293
|
+
@driver.parse(data)
|
|
294
|
+
end
|
|
295
|
+
rescue EOFError
|
|
296
|
+
unless @shutting_down
|
|
297
|
+
|
|
298
|
+
puts "Server disconnected"
|
|
299
|
+
|
|
300
|
+
handle_disconnect("EOF")
|
|
301
|
+
|
|
302
|
+
end
|
|
303
|
+
rescue IOError => e
|
|
304
|
+
if e.message.include?("stream closed")
|
|
305
|
+
|
|
306
|
+
puts "Reader thread stopped"
|
|
307
|
+
|
|
308
|
+
else
|
|
309
|
+
|
|
310
|
+
puts "Reader IO error: #{e.message}"
|
|
311
|
+
|
|
312
|
+
handle_disconnect(
|
|
313
|
+
"Reader IO failure"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
end
|
|
317
|
+
rescue StandardError => e
|
|
318
|
+
unless @shutting_down
|
|
319
|
+
|
|
320
|
+
puts "Reader error: #{e.message}"
|
|
321
|
+
|
|
322
|
+
handle_disconnect(
|
|
323
|
+
"Reader failure"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# --------------------------------------------------
|
|
331
|
+
# WATCHDOG
|
|
332
|
+
# --------------------------------------------------
|
|
333
|
+
|
|
334
|
+
def start_watchdog_thread
|
|
335
|
+
watchdog = Watchdog.new(self)
|
|
336
|
+
|
|
337
|
+
@watchdog_thread = watchdog.start
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# --------------------------------------------------
|
|
341
|
+
# CONNECTIVITY
|
|
342
|
+
# --------------------------------------------------
|
|
343
|
+
|
|
344
|
+
def start_connectivity_thread
|
|
345
|
+
@connectivity_thread = Thread.new do
|
|
346
|
+
is_connected = true
|
|
347
|
+
|
|
348
|
+
loop do
|
|
349
|
+
sleep(30)
|
|
350
|
+
|
|
351
|
+
internet = Connectivity.check_internet
|
|
352
|
+
|
|
353
|
+
if !internet
|
|
354
|
+
|
|
355
|
+
puts "⚠️ No Internet Connection"
|
|
356
|
+
|
|
357
|
+
is_connected = false
|
|
358
|
+
|
|
359
|
+
else
|
|
360
|
+
|
|
361
|
+
unless is_connected
|
|
362
|
+
|
|
363
|
+
puts "✓ Internet connection restored"
|
|
364
|
+
|
|
365
|
+
# Connection will be handled by reconnect logic
|
|
366
|
+
handle_disconnect("Lost iinternet")
|
|
367
|
+
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
is_connected = true
|
|
371
|
+
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
rescue StandardError => e
|
|
375
|
+
puts "Connectivity thread error: #{e.message}"
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def handle_disconnect(reason)
|
|
380
|
+
should_schedule = false
|
|
381
|
+
|
|
382
|
+
@state_mutex.synchronize do
|
|
383
|
+
return if [
|
|
384
|
+
State::RECONNECTING,
|
|
385
|
+
State::CLOSING,
|
|
386
|
+
State::CLOSED
|
|
387
|
+
].include?(@state)
|
|
388
|
+
|
|
389
|
+
puts "Handling disconnect: #{reason}"
|
|
390
|
+
|
|
391
|
+
transition_state(
|
|
392
|
+
State::RECONNECTING
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
should_schedule =
|
|
396
|
+
@should_reconnect
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Schedule reconnect FIRST
|
|
400
|
+
schedule_reconnect if should_schedule
|
|
401
|
+
|
|
402
|
+
# Then cleanup old connection
|
|
403
|
+
cleanup_connection
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# --------------------------------------------------
|
|
407
|
+
# CLEANUP
|
|
408
|
+
# --------------------------------------------------
|
|
409
|
+
|
|
410
|
+
def cleanup_connection
|
|
411
|
+
begin
|
|
412
|
+
@driver&.close
|
|
413
|
+
rescue StandardError
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
begin
|
|
417
|
+
@socket&.close
|
|
418
|
+
rescue StandardError
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# begin
|
|
422
|
+
# @reader_thread&.kill
|
|
423
|
+
# rescue
|
|
424
|
+
@reader_thread.kill if @reader_thread && @reader_thread != Thread.current
|
|
425
|
+
|
|
426
|
+
begin
|
|
427
|
+
@watchdog_thread&.kill
|
|
428
|
+
rescue StandardError
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
@connectivity_thread&.kill if @connectivity_thread && @connectivity_thread != Thread.current
|
|
432
|
+
|
|
433
|
+
@driver = nil
|
|
434
|
+
@socket = nil
|
|
435
|
+
|
|
436
|
+
@reader_thread = nil
|
|
437
|
+
@watchdog_thread = nil
|
|
438
|
+
@connectivity_thread = nil
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# --------------------------------------------------
|
|
442
|
+
# RECONNECT
|
|
443
|
+
# --------------------------------------------------
|
|
444
|
+
|
|
445
|
+
def schedule_reconnect
|
|
446
|
+
delay =
|
|
447
|
+
RetryPolicy.next_delay(
|
|
448
|
+
@reconnect_attempts
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
puts "Reconnect in #{delay.round(2)} sec"
|
|
452
|
+
|
|
453
|
+
@reconnect_attempts += 1
|
|
454
|
+
|
|
455
|
+
Thread.new do
|
|
456
|
+
sleep(delay)
|
|
457
|
+
|
|
458
|
+
connect if @should_reconnect
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# --------------------------------------------------
|
|
463
|
+
# STATE
|
|
464
|
+
# --------------------------------------------------
|
|
465
|
+
|
|
466
|
+
def transition_state(new_state)
|
|
467
|
+
puts "#{@state} -> #{new_state}"
|
|
468
|
+
|
|
469
|
+
@state = new_state
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# --------------------------------------------------
|
|
473
|
+
# SETUP
|
|
474
|
+
# --------------------------------------------------
|
|
475
|
+
|
|
476
|
+
private
|
|
477
|
+
|
|
478
|
+
def setup_sdk
|
|
479
|
+
# Configure UrlBuilder
|
|
480
|
+
@url_builder = UrlBuilder.instance
|
|
481
|
+
@url_builder.region = @region
|
|
482
|
+
@url_builder.guid = @guid
|
|
483
|
+
@url_builder.apikey = @apikey
|
|
484
|
+
@url_builder.set_websocket_url(@collection_id, @environment_id)
|
|
485
|
+
|
|
486
|
+
# Configure ApiManager
|
|
487
|
+
ApiManager.set_authenticator
|
|
488
|
+
|
|
489
|
+
# Initialize ConfigFetcher
|
|
490
|
+
@config_fetcher = ConfigFetcher.new(
|
|
491
|
+
collection_id: @collection_id,
|
|
492
|
+
environment_id: @environment_id
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# Initialize BackgroundRetryManager
|
|
496
|
+
@background_retry_manager = BackgroundRetryManager.new(
|
|
497
|
+
collection_id: @collection_id,
|
|
498
|
+
environment_id: @environment_id
|
|
499
|
+
)
|
|
500
|
+
end
|
|
501
|
+
end
|
data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/connectivity.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 IBM Corp. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require "resolv"
|
|
18
|
+
require "timeout"
|
|
19
|
+
|
|
20
|
+
module Connectivity
|
|
21
|
+
def self.check_internet
|
|
22
|
+
Timeout.timeout(5) do
|
|
23
|
+
dns = Resolv::DNS.new(nameserver: ["8.8.8.8"])
|
|
24
|
+
dns.getaddress("cloud.ibm.com")
|
|
25
|
+
return true
|
|
26
|
+
end
|
|
27
|
+
rescue StandardError
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/driver_socket.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 IBM Corp. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
class DriverSocket
|
|
18
|
+
attr_reader :url
|
|
19
|
+
|
|
20
|
+
def initialize(tcp_socket, url)
|
|
21
|
+
@tcp_socket = tcp_socket
|
|
22
|
+
@url = url
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def write(data)
|
|
26
|
+
@tcp_socket.write(data)
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/retry_policy.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 IBM Corp. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
class RetryPolicy
|
|
18
|
+
RETRY_CONFIG = {
|
|
19
|
+
initial_delay: 15,
|
|
20
|
+
max_delay: 3600,
|
|
21
|
+
multiplier: 2,
|
|
22
|
+
jitter_factor: 0.3
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
def self.next_delay(attempt)
|
|
26
|
+
base_delay =
|
|
27
|
+
RETRY_CONFIG[:initial_delay] *
|
|
28
|
+
(RETRY_CONFIG[:multiplier]**attempt)
|
|
29
|
+
|
|
30
|
+
capped_delay = [
|
|
31
|
+
base_delay,
|
|
32
|
+
RETRY_CONFIG[:max_delay]
|
|
33
|
+
].min
|
|
34
|
+
|
|
35
|
+
jitter =
|
|
36
|
+
capped_delay *
|
|
37
|
+
RETRY_CONFIG[:jitter_factor] *
|
|
38
|
+
rand
|
|
39
|
+
|
|
40
|
+
capped_delay + jitter
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 IBM Corp. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module State
|
|
18
|
+
DISCONNECTED = :disconnected
|
|
19
|
+
CONNECTING = :connecting
|
|
20
|
+
CONNECTED = :connected
|
|
21
|
+
RECONNECTING = :reconnecting
|
|
22
|
+
CLOSING = :closing
|
|
23
|
+
CLOSED = :closed
|
|
24
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 IBM Corp. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
class Watchdog
|
|
18
|
+
WATCHDOG_CONFIG = {
|
|
19
|
+
check_interval: 60,
|
|
20
|
+
heartbeat_timeout: 120
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def initialize(client)
|
|
24
|
+
@client = client
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def start
|
|
28
|
+
Thread.new do
|
|
29
|
+
loop do
|
|
30
|
+
sleep WATCHDOG_CONFIG[:check_interval]
|
|
31
|
+
|
|
32
|
+
break unless @client.connected?
|
|
33
|
+
|
|
34
|
+
heartbeat_age =
|
|
35
|
+
Time.now - @client.last_heartbeat_at
|
|
36
|
+
|
|
37
|
+
next unless heartbeat_age >
|
|
38
|
+
WATCHDOG_CONFIG[:heartbeat_timeout]
|
|
39
|
+
|
|
40
|
+
puts "Heartbeat timeout detected"
|
|
41
|
+
|
|
42
|
+
@client.handle_disconnect(
|
|
43
|
+
"Heartbeat timeout"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
break
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|