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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/CONTRIBUTING.md +9 -0
  5. data/LICENSE +201 -0
  6. data/README.md +474 -0
  7. data/Rakefile +8 -0
  8. data/examples/README.md +60 -0
  9. data/examples/app.rb +104 -0
  10. data/lib/ibm_appconfiguration_ruby_sdk/app_configuration.rb +291 -0
  11. data/lib/ibm_appconfiguration_ruby_sdk/configurations/configuration_handler.rb +828 -0
  12. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/constants.rb +89 -0
  13. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/file_manager.rb +72 -0
  14. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/logger.rb +98 -0
  15. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/retry_manager/background_retry_manager.rb +284 -0
  16. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/retry_manager/config_fetcher.rb +254 -0
  17. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/utils.rb +240 -0
  18. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/connection_manager.rb +501 -0
  19. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/connectivity.rb +30 -0
  20. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/driver_socket.rb +28 -0
  21. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/retry_policy.rb +42 -0
  22. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/state.rb +24 -0
  23. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/watchdog.rb +50 -0
  24. data/lib/ibm_appconfiguration_ruby_sdk/configurations/internal/websocket_client/websocket_client.rb +43 -0
  25. data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/feature.rb +121 -0
  26. data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/property.rb +107 -0
  27. data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/rule.rb +87 -0
  28. data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/secret_property.rb +81 -0
  29. data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/segment.rb +39 -0
  30. data/lib/ibm_appconfiguration_ruby_sdk/configurations/models/segment_rules.rb +57 -0
  31. data/lib/ibm_appconfiguration_ruby_sdk/core/api_manager.rb +269 -0
  32. data/lib/ibm_appconfiguration_ruby_sdk/core/metering.rb +400 -0
  33. data/lib/ibm_appconfiguration_ruby_sdk/core/url_builder.rb +252 -0
  34. data/lib/ibm_appconfiguration_ruby_sdk/version.rb +20 -0
  35. data/lib/ibm_appconfiguration_ruby_sdk.rb +20 -0
  36. data/sig/ibm_appconfiguration_ruby_sdk.rbs +4 -0
  37. 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
@@ -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
@@ -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
@@ -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