intrinio-realtime 5.3.0 → 5.4.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 +4 -4
- data/lib/intrinio-realtime.rb +453 -155
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4489672939eedb16281b3cafdf67b88e272017854716b482be7c2e4f8da705be
|
|
4
|
+
data.tar.gz: 9aca80494ec93e780e033edf64fbf4ef9ce87ab2fb0d86756748be8ca3d04344
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33bed178619655282ae60f6a10bb2e48f0b448c2b3d43c5aef8df82c9dfefff76d6e54fcc6f74b347513b9b461de1ce2859e60e6691349dde957993334832619
|
|
7
|
+
data.tar.gz: 84eaa2fad299104bd9980ac49ef5303c068f3545f03bfbcf56303044be5ba0301bbbb3922618557efcec499eb052baaa8d60f9d82c3a03437472c0219bc86120
|
data/lib/intrinio-realtime.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'logger'
|
|
2
2
|
require 'uri'
|
|
3
|
+
require 'timeout'
|
|
3
4
|
#require 'http'
|
|
4
5
|
require 'net/http'
|
|
5
6
|
require 'eventmachine'
|
|
@@ -25,9 +26,13 @@ module Intrinio
|
|
|
25
26
|
ASK = "Ask".freeze
|
|
26
27
|
BID = "Bid".freeze
|
|
27
28
|
CLIENT_INFO_HEADER_KEY = "Client-Information".freeze
|
|
28
|
-
CLIENT_INFO_HEADER_VALUE = "IntrinioRealtimeRubySDKv5.
|
|
29
|
+
CLIENT_INFO_HEADER_VALUE = "IntrinioRealtimeRubySDKv5.4".freeze
|
|
29
30
|
MESSAGE_VERSION_HEADER_KEY = "UseNewEquitiesFormat".freeze
|
|
30
31
|
MESSAGE_VERSION_HEADER_VALUE = "v2".freeze
|
|
32
|
+
CONNECT_TIMEOUT_SECONDS = 30
|
|
33
|
+
HTTP_TIMEOUT_SECONDS = 30
|
|
34
|
+
CLOSE_TIMEOUT_SECONDS = 5
|
|
35
|
+
WORKER_JOIN_TIMEOUT_SECONDS = 5
|
|
31
36
|
|
|
32
37
|
def self.connect(options, on_trade, on_quote)
|
|
33
38
|
EM.run do
|
|
@@ -150,8 +155,10 @@ module Intrinio
|
|
|
150
155
|
def initialize(options, on_trade, on_quote)
|
|
151
156
|
raise "Options parameter is required" if options.nil? || !options.is_a?(Hash)
|
|
152
157
|
@stop = false
|
|
153
|
-
@
|
|
154
|
-
|
|
158
|
+
@trade_messages = Queue.new
|
|
159
|
+
@quote_messages = Queue.new
|
|
160
|
+
@message_available = Queue.new
|
|
161
|
+
@stop_message = Object.new
|
|
155
162
|
@on_trade = on_trade
|
|
156
163
|
@on_quote = on_quote
|
|
157
164
|
|
|
@@ -189,11 +196,14 @@ module Intrinio
|
|
|
189
196
|
end
|
|
190
197
|
|
|
191
198
|
@threads = []
|
|
199
|
+
@worker_generation = 0
|
|
200
|
+
@worker_mutex = Mutex.new
|
|
192
201
|
|
|
193
202
|
@channels = []
|
|
194
203
|
@channels = parse_channels(options[:channels]) if options[:channels]
|
|
195
204
|
bad_channels = @channels.select{|x| !x.is_a?(String)}
|
|
196
205
|
raise "Invalid channels to join: #{bad_channels}" unless bad_channels.empty?
|
|
206
|
+
@channels_mutex = Mutex.new
|
|
197
207
|
|
|
198
208
|
if options[:logger] == false
|
|
199
209
|
@logger = nil
|
|
@@ -205,10 +215,19 @@ module Intrinio
|
|
|
205
215
|
end
|
|
206
216
|
|
|
207
217
|
@ready = false
|
|
218
|
+
@connecting = false
|
|
219
|
+
@closing = false
|
|
208
220
|
@joined_channels = []
|
|
209
221
|
@selfheal_timer = nil
|
|
222
|
+
@connect_timer = nil
|
|
210
223
|
@selfheal_backoffs = Array.new(SELF_HEAL_BACKOFFS)
|
|
211
224
|
@ws = nil
|
|
225
|
+
@connection_generation = 0
|
|
226
|
+
@state_mutex = Mutex.new
|
|
227
|
+
@lifecycle_mutex = Mutex.new
|
|
228
|
+
@connect_timeout_seconds = CONNECT_TIMEOUT_SECONDS
|
|
229
|
+
@http_timeout_seconds = HTTP_TIMEOUT_SECONDS
|
|
230
|
+
@close_timeout_seconds = CLOSE_TIMEOUT_SECONDS
|
|
212
231
|
end
|
|
213
232
|
|
|
214
233
|
def provider
|
|
@@ -219,9 +238,11 @@ module Intrinio
|
|
|
219
238
|
channels = parse_channels(channels)
|
|
220
239
|
nonconforming = channels.select{|x| !x.is_a?(String)}
|
|
221
240
|
return error("Invalid channels to join: #{nonconforming}") unless nonconforming.empty?
|
|
222
|
-
|
|
223
|
-
@
|
|
224
|
-
|
|
241
|
+
|
|
242
|
+
@channels_mutex.synchronize do
|
|
243
|
+
@channels.concat(channels)
|
|
244
|
+
@channels.uniq!
|
|
245
|
+
end
|
|
225
246
|
debug "Joining channels #{channels}"
|
|
226
247
|
|
|
227
248
|
refresh_channels()
|
|
@@ -232,54 +253,78 @@ module Intrinio
|
|
|
232
253
|
nonconforming = channels.find{|x| !x.is_a?(String)}
|
|
233
254
|
return error("Invalid channels to leave: #{nonconforming}") unless nonconforming.empty?
|
|
234
255
|
|
|
235
|
-
|
|
256
|
+
@channels_mutex.synchronize do
|
|
257
|
+
channels.each{|c| @channels.delete(c)}
|
|
258
|
+
end
|
|
236
259
|
debug "Leaving channels #{channels}"
|
|
237
260
|
|
|
238
261
|
refresh_channels()
|
|
239
262
|
end
|
|
240
263
|
|
|
241
264
|
def leave_all
|
|
242
|
-
@channels = []
|
|
265
|
+
@channels_mutex.synchronize {@channels = []}
|
|
243
266
|
debug "Leaving all channels"
|
|
244
267
|
refresh_channels()
|
|
245
268
|
end
|
|
246
269
|
|
|
247
270
|
def connect
|
|
248
271
|
raise "Must be run from within an EventMachine run loop" unless EM.reactor_running?
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
@
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
272
|
+
|
|
273
|
+
generation = nil
|
|
274
|
+
reconnect_timer = nil
|
|
275
|
+
already_connected = @lifecycle_mutex.synchronize do
|
|
276
|
+
connected = @state_mutex.synchronize do
|
|
277
|
+
if @ready || @connecting
|
|
278
|
+
true
|
|
279
|
+
else
|
|
280
|
+
@closing = false
|
|
281
|
+
@connecting = true
|
|
282
|
+
@connection_generation += 1
|
|
283
|
+
generation = @connection_generation
|
|
284
|
+
reconnect_timer = @selfheal_timer
|
|
285
|
+
@selfheal_timer = nil
|
|
286
|
+
false
|
|
287
|
+
end
|
|
261
288
|
end
|
|
289
|
+
start_workers unless connected
|
|
290
|
+
connected
|
|
262
291
|
end
|
|
292
|
+
return warn("Already connected!") if already_connected
|
|
293
|
+
|
|
294
|
+
cancel_timer(reconnect_timer)
|
|
295
|
+
debug "Connecting..."
|
|
296
|
+
|
|
297
|
+
EM.defer(
|
|
298
|
+
proc { refresh_token },
|
|
299
|
+
proc { |token| finish_authorization(generation, token) },
|
|
300
|
+
proc { |exception| connection_attempt_failed(generation, exception) }
|
|
301
|
+
)
|
|
263
302
|
end
|
|
264
303
|
|
|
265
304
|
def disconnect
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
305
|
+
@lifecycle_mutex.synchronize do
|
|
306
|
+
socket = nil
|
|
307
|
+
reconnect_timer = nil
|
|
308
|
+
connect_timer = nil
|
|
309
|
+
@state_mutex.synchronize do
|
|
310
|
+
@closing = true
|
|
311
|
+
@ready = false
|
|
312
|
+
@connecting = false
|
|
313
|
+
@connection_generation += 1
|
|
314
|
+
socket = @ws
|
|
315
|
+
@ws = nil
|
|
316
|
+
reconnect_timer = @selfheal_timer
|
|
317
|
+
connect_timer = @connect_timer
|
|
318
|
+
@selfheal_timer = nil
|
|
319
|
+
@connect_timer = nil
|
|
279
320
|
end
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
@channels_mutex.synchronize {@joined_channels = []}
|
|
322
|
+
|
|
323
|
+
cancel_timer(reconnect_timer)
|
|
324
|
+
cancel_timer(connect_timer)
|
|
325
|
+
close_websocket(socket, true)
|
|
326
|
+
stop_workers
|
|
327
|
+
end
|
|
283
328
|
info "Connection closed"
|
|
284
329
|
end
|
|
285
330
|
|
|
@@ -294,7 +339,37 @@ module Intrinio
|
|
|
294
339
|
private
|
|
295
340
|
|
|
296
341
|
def queue_message(message)
|
|
297
|
-
|
|
342
|
+
return if message.nil? || message.empty?
|
|
343
|
+
|
|
344
|
+
message_count = message.getbyte(0)
|
|
345
|
+
raise "Message does not contain an event count" if message_count.nil?
|
|
346
|
+
|
|
347
|
+
start_index = 1
|
|
348
|
+
trades = []
|
|
349
|
+
quotes = []
|
|
350
|
+
message_count.times do
|
|
351
|
+
raise "Message header is truncated at byte #{start_index}" if start_index + 2 > message.bytesize
|
|
352
|
+
|
|
353
|
+
message_type = message.getbyte(start_index)
|
|
354
|
+
message_length = message.getbyte(start_index + 1)
|
|
355
|
+
if message_length.nil? || message_length < 2 || start_index + message_length > message.bytesize
|
|
356
|
+
raise "Message body is truncated at byte #{start_index}"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
chunk = message.byteslice(start_index, message_length)
|
|
360
|
+
if message_type == 0
|
|
361
|
+
trades << chunk
|
|
362
|
+
else
|
|
363
|
+
quotes << chunk
|
|
364
|
+
end
|
|
365
|
+
start_index += message_length
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
trades.each {|chunk| @trade_messages.enq(chunk)}
|
|
369
|
+
quotes.each {|chunk| @quote_messages.enq(chunk)}
|
|
370
|
+
(trades.length + quotes.length).times {@message_available.enq(true)}
|
|
371
|
+
rescue StandardError => e
|
|
372
|
+
error "Error adding message to queues: #{e}"
|
|
298
373
|
end
|
|
299
374
|
|
|
300
375
|
def parse_uint64(data)
|
|
@@ -376,7 +451,7 @@ module Intrinio
|
|
|
376
451
|
trade = parse_trade(data, start_index)
|
|
377
452
|
@on_trade.call(trade)
|
|
378
453
|
return start_index + msg_length
|
|
379
|
-
when 1
|
|
454
|
+
when 1, 2 then
|
|
380
455
|
quote = parse_quote(data, start_index, msg_type)
|
|
381
456
|
@on_quote.call(quote)
|
|
382
457
|
return start_index + msg_length
|
|
@@ -384,47 +459,38 @@ module Intrinio
|
|
|
384
459
|
return start_index + msg_length
|
|
385
460
|
end
|
|
386
461
|
|
|
387
|
-
def handle_data
|
|
388
|
-
Thread.current.priority -= 1
|
|
389
|
-
me = self
|
|
462
|
+
def handle_data(generation)
|
|
463
|
+
Thread.current.priority -= 1 rescue nil
|
|
390
464
|
pop = nil
|
|
391
|
-
until @stop do
|
|
465
|
+
until @stop || generation != @worker_generation do
|
|
392
466
|
begin
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
end
|
|
405
|
-
if !data then me.send :error, "Cannot process data. Data is nil. #{pop}" end
|
|
406
|
-
start_index = 1
|
|
407
|
-
count = data[0]
|
|
408
|
-
# These are grouped (many) messages.
|
|
409
|
-
# The first byte tells us how many there are.
|
|
410
|
-
# From there, check the type and symbol length at index 0 of each chunk to know how many bytes each message has.
|
|
411
|
-
count.times {start_index = handle_message(data, start_index)}
|
|
412
|
-
end
|
|
467
|
+
signal = @message_available.deq
|
|
468
|
+
break if signal.equal?(@stop_message) || @stop || generation != @worker_generation
|
|
469
|
+
|
|
470
|
+
# Always drain a trade first. Quotes can be extremely high volume, so a
|
|
471
|
+
# shared FIFO lets them delay trades even when processing capacity is free.
|
|
472
|
+
pop = nonblocking_dequeue(@trade_messages)
|
|
473
|
+
pop = nonblocking_dequeue(@quote_messages) if pop.nil?
|
|
474
|
+
next if pop.nil?
|
|
475
|
+
|
|
476
|
+
data = pop.unpack('C*')
|
|
477
|
+
handle_message(data, 0)
|
|
413
478
|
rescue StandardError => e
|
|
414
|
-
|
|
479
|
+
error "Error handling message from queue: #{e} #{pop}"
|
|
415
480
|
rescue Exception => e
|
|
416
|
-
|
|
481
|
+
break if @stop || generation != @worker_generation
|
|
482
|
+
error "General error handling message from queue: #{e} #{pop}"
|
|
417
483
|
end
|
|
418
484
|
end
|
|
419
485
|
end
|
|
420
486
|
|
|
421
487
|
def refresh_token
|
|
422
|
-
@token = nil
|
|
423
|
-
|
|
424
488
|
uri = URI.parse(auth_url)
|
|
425
489
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
426
490
|
http.use_ssl = true if (auth_url.include?("https"))
|
|
427
|
-
http.
|
|
491
|
+
http.open_timeout = @http_timeout_seconds
|
|
492
|
+
http.read_timeout = @http_timeout_seconds
|
|
493
|
+
http.write_timeout = @http_timeout_seconds if http.respond_to?(:write_timeout=)
|
|
428
494
|
request = Net::HTTP::Get.new(uri.request_uri)
|
|
429
495
|
request.add_field(CLIENT_INFO_HEADER_KEY, CLIENT_INFO_HEADER_VALUE)
|
|
430
496
|
|
|
@@ -432,13 +498,13 @@ module Intrinio
|
|
|
432
498
|
request.basic_auth(@username, @password)
|
|
433
499
|
end
|
|
434
500
|
|
|
435
|
-
response = http.request(request)
|
|
501
|
+
response = http.start {|session| session.request(request)}
|
|
436
502
|
|
|
437
|
-
|
|
438
|
-
|
|
503
|
+
raise "Unable to authorize" if response.code == "401"
|
|
504
|
+
raise "Could not get auth token (HTTP #{response.code})" if response.code != "200"
|
|
439
505
|
|
|
440
|
-
@token = response.body
|
|
441
506
|
debug "Token refreshed"
|
|
507
|
+
response.body
|
|
442
508
|
end
|
|
443
509
|
|
|
444
510
|
def auth_url
|
|
@@ -484,118 +550,350 @@ module Intrinio
|
|
|
484
550
|
end
|
|
485
551
|
end
|
|
486
552
|
|
|
487
|
-
def refresh_websocket
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
553
|
+
def refresh_websocket(generation)
|
|
554
|
+
headers = {
|
|
555
|
+
:headers => {
|
|
556
|
+
CLIENT_INFO_HEADER_KEY => CLIENT_INFO_HEADER_VALUE,
|
|
557
|
+
MESSAGE_VERSION_HEADER_KEY => MESSAGE_VERSION_HEADER_VALUE
|
|
558
|
+
}
|
|
559
|
+
}
|
|
493
560
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
elsif !thread.nil?
|
|
500
|
-
then thread.kill
|
|
561
|
+
old_socket = @state_mutex.synchronize {@ws}
|
|
562
|
+
ws = open_websocket(socket_url, headers) do |socket|
|
|
563
|
+
install_websocket_handlers(socket, generation)
|
|
564
|
+
@state_mutex.synchronize do
|
|
565
|
+
@ws = socket if generation == @connection_generation && !@closing
|
|
501
566
|
end
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
unless current_connection?(generation, ws)
|
|
570
|
+
close_websocket(ws, false)
|
|
571
|
+
return
|
|
572
|
+
end
|
|
506
573
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
@ws = ws = WebSocket::Client::Simple.connect(socket_url, headers)
|
|
574
|
+
close_websocket(old_socket, false) if old_socket && !old_socket.equal?(ws)
|
|
575
|
+
schedule_connect_timeout(generation, ws)
|
|
576
|
+
info "Connection opening"
|
|
577
|
+
end
|
|
512
578
|
|
|
513
|
-
|
|
579
|
+
def open_websocket(url, headers, &block)
|
|
580
|
+
WebSocket::Client::Simple.connect(url, headers, &block)
|
|
581
|
+
end
|
|
514
582
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
end
|
|
521
|
-
me.send :stop_self_heal
|
|
583
|
+
def install_websocket_handlers(ws, generation)
|
|
584
|
+
# websocket-client-simple emits with instance_exec, so self inside a
|
|
585
|
+
# raw handler is the socket. Rebind before touching client methods.
|
|
586
|
+
bind_socket_event(ws, :open) do
|
|
587
|
+
schedule_on_reactor {websocket_opened(generation, ws)}
|
|
522
588
|
end
|
|
523
589
|
|
|
524
|
-
ws
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
then me.send :queue_message, data_message
|
|
530
|
-
end
|
|
531
|
-
rescue StandardError => e
|
|
532
|
-
me.send :error, "Error adding message to queue: #{data_message} #{e}"
|
|
590
|
+
bind_socket_event(ws, :message) do |frame|
|
|
591
|
+
next unless current_connection?(generation, ws)
|
|
592
|
+
if frame.respond_to?(:type) && frame.type != :binary
|
|
593
|
+
debug "Ignoring non-binary websocket message"
|
|
594
|
+
next
|
|
533
595
|
end
|
|
596
|
+
queue_message(frame.data) unless frame.data.nil?
|
|
534
597
|
end
|
|
535
|
-
|
|
536
|
-
ws
|
|
537
|
-
|
|
538
|
-
me.send :info, "Connection closing...: #{e}"
|
|
539
|
-
me.send :try_self_heal
|
|
598
|
+
|
|
599
|
+
bind_socket_event(ws, :close) do |exception|
|
|
600
|
+
schedule_on_reactor {websocket_closed(generation, ws, exception)}
|
|
540
601
|
end
|
|
541
602
|
|
|
542
|
-
ws
|
|
543
|
-
|
|
544
|
-
me.send :error, "Connection error: #{e}"
|
|
545
|
-
me.send :try_self_heal
|
|
603
|
+
bind_socket_event(ws, :error) do |exception|
|
|
604
|
+
schedule_on_reactor {websocket_errored(generation, ws, exception)}
|
|
546
605
|
end
|
|
547
606
|
end
|
|
607
|
+
|
|
608
|
+
def bind_socket_event(ws, event, &callback)
|
|
609
|
+
client = self
|
|
610
|
+
ws.on(event) do |*args|
|
|
611
|
+
client.instance_exec(*args, &callback)
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def websocket_opened(generation, ws)
|
|
616
|
+
timer = nil
|
|
617
|
+
opened = @state_mutex.synchronize do
|
|
618
|
+
if generation == @connection_generation && @ws.equal?(ws) && !@closing
|
|
619
|
+
@ready = true
|
|
620
|
+
@connecting = false
|
|
621
|
+
timer = @connect_timer
|
|
622
|
+
@connect_timer = nil
|
|
623
|
+
true
|
|
624
|
+
else
|
|
625
|
+
false
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
return close_websocket(ws, false) unless opened
|
|
629
|
+
|
|
630
|
+
cancel_timer(timer)
|
|
631
|
+
@channels_mutex.synchronize {@joined_channels = []}
|
|
632
|
+
info "Connection established"
|
|
633
|
+
|
|
634
|
+
begin
|
|
635
|
+
channels_refreshed = !PROVIDERS.include?(provider) || refresh_channels
|
|
636
|
+
return unless channels_refreshed
|
|
637
|
+
stop_self_heal
|
|
638
|
+
rescue StandardError => e
|
|
639
|
+
connection_attempt_failed(generation, e, ws)
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def websocket_closed(generation, ws, exception)
|
|
644
|
+
return unless current_connection?(generation, ws)
|
|
645
|
+
info "Connection closing...: #{exception}"
|
|
646
|
+
connection_attempt_failed(generation, exception || StandardError.new("Websocket closed"), ws)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def websocket_errored(generation, ws, exception)
|
|
650
|
+
return unless current_connection?(generation, ws)
|
|
651
|
+
error "Connection error: #{exception}"
|
|
652
|
+
connection_attempt_failed(generation, exception || StandardError.new("Websocket error"), ws)
|
|
653
|
+
end
|
|
548
654
|
|
|
549
655
|
def refresh_channels
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
# Join new channels
|
|
554
|
-
new_channels = @channels - @joined_channels
|
|
555
|
-
new_channels.each do |channel|
|
|
556
|
-
#msg = join_message(channel)
|
|
557
|
-
#@ws.send(msg.to_json)
|
|
558
|
-
msg = join_binary_message(channel)
|
|
559
|
-
@ws.send(msg)
|
|
560
|
-
info "Joined #{channel}"
|
|
656
|
+
state = @state_mutex.synchronize do
|
|
657
|
+
[@ready, @ws, @connection_generation]
|
|
561
658
|
end
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
659
|
+
return unless state[0] && state[1]
|
|
660
|
+
|
|
661
|
+
ws = state[1]
|
|
662
|
+
generation = state[2]
|
|
663
|
+
debug "Refreshing channels"
|
|
664
|
+
|
|
665
|
+
desired_channels = nil
|
|
666
|
+
@channels_mutex.synchronize do
|
|
667
|
+
desired_channels = Array.new(@channels)
|
|
668
|
+
joined_channels = Array.new(@joined_channels)
|
|
669
|
+
|
|
670
|
+
# Keep the desired and server-side subscription sets in lockstep. Calls
|
|
671
|
+
# to join/leave wait briefly while these sends complete, then perform
|
|
672
|
+
# another refresh with their newer desired set.
|
|
673
|
+
new_channels = desired_channels - joined_channels
|
|
674
|
+
new_channels.each do |channel|
|
|
675
|
+
msg = join_binary_message(channel)
|
|
676
|
+
ws.send(msg, :type => :binary)
|
|
677
|
+
raise "Websocket changed while joining channels" unless current_connection?(generation, ws)
|
|
678
|
+
@joined_channels << channel unless @joined_channels.include?(channel)
|
|
679
|
+
info "Joined #{channel}"
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
old_channels = joined_channels - desired_channels
|
|
683
|
+
old_channels.each do |channel|
|
|
684
|
+
msg = leave_binary_message(channel)
|
|
685
|
+
ws.send(msg, :type => :binary)
|
|
686
|
+
raise "Websocket changed while leaving channels" unless current_connection?(generation, ws)
|
|
687
|
+
@joined_channels.delete(channel)
|
|
688
|
+
info "Left #{channel}"
|
|
689
|
+
end
|
|
571
690
|
end
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
691
|
+
|
|
692
|
+
debug "Current channels: #{desired_channels}"
|
|
693
|
+
true
|
|
694
|
+
rescue StandardError => e
|
|
695
|
+
connection_attempt_failed(generation, e, ws) if generation && ws
|
|
696
|
+
false
|
|
576
697
|
end
|
|
577
698
|
|
|
578
699
|
def try_self_heal
|
|
579
|
-
|
|
700
|
+
delay = nil
|
|
701
|
+
@state_mutex.synchronize do
|
|
702
|
+
return if @closing || @ready || @connecting || @selfheal_timer
|
|
703
|
+
|
|
704
|
+
delay = @selfheal_backoffs.first
|
|
705
|
+
@selfheal_backoffs.delete_at(0) if @selfheal_backoffs.count > 1
|
|
706
|
+
# A marker prevents close and error callbacks for the same failed socket
|
|
707
|
+
# from scheduling two reconnect loops before EM.add_timer returns.
|
|
708
|
+
@selfheal_timer = :scheduling
|
|
709
|
+
end
|
|
580
710
|
debug "Attempting to self-heal"
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
711
|
+
|
|
712
|
+
timer = nil
|
|
713
|
+
timer = EM.add_timer(delay / 1000.0) do
|
|
714
|
+
should_connect = @state_mutex.synchronize do
|
|
715
|
+
active = @selfheal_timer.equal?(timer) || @selfheal_timer == :scheduling
|
|
716
|
+
@selfheal_timer = nil if active
|
|
717
|
+
active && !@closing && !@ready && !@connecting
|
|
718
|
+
end
|
|
719
|
+
connect if should_connect
|
|
720
|
+
end
|
|
721
|
+
@state_mutex.synchronize do
|
|
722
|
+
@selfheal_timer = timer if @selfheal_timer == :scheduling
|
|
589
723
|
end
|
|
590
724
|
end
|
|
591
725
|
|
|
592
726
|
def stop_self_heal
|
|
593
|
-
|
|
594
|
-
|
|
727
|
+
timer = @state_mutex.synchronize do
|
|
728
|
+
value = @selfheal_timer
|
|
729
|
+
@selfheal_timer = nil
|
|
730
|
+
@selfheal_backoffs = Array.new(SELF_HEAL_BACKOFFS)
|
|
731
|
+
value
|
|
732
|
+
end
|
|
733
|
+
cancel_timer(timer)
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def finish_authorization(generation, token)
|
|
737
|
+
authorized = @state_mutex.synchronize do
|
|
738
|
+
if generation == @connection_generation && @connecting && !@closing
|
|
739
|
+
@token = token
|
|
740
|
+
true
|
|
741
|
+
else
|
|
742
|
+
false
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
return unless authorized
|
|
746
|
+
|
|
747
|
+
refresh_websocket(generation)
|
|
748
|
+
rescue StandardError => e
|
|
749
|
+
connection_attempt_failed(generation, e)
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def connection_attempt_failed(generation, exception, socket = nil)
|
|
753
|
+
failed_socket = nil
|
|
754
|
+
connect_timer = nil
|
|
755
|
+
current = @state_mutex.synchronize do
|
|
756
|
+
socket_matches = socket.nil? || @ws.equal?(socket)
|
|
757
|
+
if generation == @connection_generation && socket_matches && !@closing
|
|
758
|
+
failed_socket = @ws
|
|
759
|
+
@ws = nil
|
|
760
|
+
@ready = false
|
|
761
|
+
@connecting = false
|
|
762
|
+
connect_timer = @connect_timer
|
|
763
|
+
@connect_timer = nil
|
|
764
|
+
true
|
|
765
|
+
else
|
|
766
|
+
false
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
return unless current
|
|
770
|
+
|
|
771
|
+
@channels_mutex.synchronize {@joined_channels = []}
|
|
772
|
+
cancel_timer(connect_timer)
|
|
773
|
+
close_websocket(failed_socket, false)
|
|
774
|
+
message = exception.respond_to?(:message) ? exception.message : exception.to_s
|
|
775
|
+
error "Connection attempt failed: #{message}"
|
|
776
|
+
try_self_heal
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def schedule_connect_timeout(generation, ws)
|
|
780
|
+
timer = EM.add_timer(@connect_timeout_seconds) do
|
|
781
|
+
connection_attempt_failed(
|
|
782
|
+
generation,
|
|
783
|
+
Timeout::Error.new("Websocket connect timed out after #{@connect_timeout_seconds} seconds"),
|
|
784
|
+
ws
|
|
785
|
+
)
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
previous_timer = nil
|
|
789
|
+
keep_timer = @state_mutex.synchronize do
|
|
790
|
+
if generation == @connection_generation && @ws.equal?(ws) && @connecting && !@closing
|
|
791
|
+
previous_timer = @connect_timer
|
|
792
|
+
@connect_timer = timer
|
|
793
|
+
true
|
|
794
|
+
else
|
|
795
|
+
false
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
cancel_timer(previous_timer)
|
|
799
|
+
cancel_timer(timer) unless keep_timer
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def current_connection?(generation, ws)
|
|
803
|
+
@state_mutex.synchronize do
|
|
804
|
+
generation == @connection_generation && @ws.equal?(ws) && !@closing
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def start_workers
|
|
809
|
+
@worker_mutex.synchronize do
|
|
810
|
+
live_threads = @threads.select(&:alive?)
|
|
811
|
+
return if live_threads.length == @thread_quantity
|
|
812
|
+
|
|
813
|
+
live_threads.each(&:kill)
|
|
814
|
+
@threads.each {|thread| thread.join(0.1) if thread.alive?}
|
|
815
|
+
@threads = []
|
|
816
|
+
clear_message_queues
|
|
817
|
+
@stop = false
|
|
818
|
+
@worker_generation += 1
|
|
819
|
+
generation = @worker_generation
|
|
820
|
+
@thread_quantity.times do
|
|
821
|
+
@threads << Thread.new {handle_data(generation)}
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def stop_workers
|
|
827
|
+
@worker_mutex.synchronize do
|
|
828
|
+
@stop = true
|
|
829
|
+
@worker_generation += 1
|
|
830
|
+
threads = Array.new(@threads)
|
|
831
|
+
threads.length.times {@message_available.enq(@stop_message)}
|
|
832
|
+
|
|
833
|
+
threads.each do |thread|
|
|
834
|
+
next unless thread.alive?
|
|
835
|
+
next if thread.equal?(Thread.current)
|
|
836
|
+
unless thread.join(WORKER_JOIN_TIMEOUT_SECONDS)
|
|
837
|
+
warn "Worker thread timed out while stopping"
|
|
838
|
+
thread.kill
|
|
839
|
+
thread.join(0.1)
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
@threads = []
|
|
844
|
+
clear_message_queues
|
|
845
|
+
@stop = false
|
|
846
|
+
end
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
def clear_message_queues
|
|
850
|
+
@trade_messages.clear
|
|
851
|
+
@quote_messages.clear
|
|
852
|
+
@message_available.clear
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def nonblocking_dequeue(queue)
|
|
856
|
+
queue.deq(true)
|
|
857
|
+
rescue ThreadError
|
|
858
|
+
nil
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def close_websocket(ws, wait_for_close)
|
|
862
|
+
return if ws.nil?
|
|
863
|
+
|
|
864
|
+
close_thread = Thread.new do
|
|
865
|
+
begin
|
|
866
|
+
ws.close
|
|
867
|
+
rescue StandardError => e
|
|
868
|
+
error "Error closing websocket: #{e.message}"
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
return unless wait_for_close
|
|
872
|
+
|
|
873
|
+
unless close_thread.join(@close_timeout_seconds)
|
|
874
|
+
warn "Websocket close timed out"
|
|
875
|
+
close_thread.kill
|
|
876
|
+
close_thread.join(0.1)
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
def cancel_timer(timer)
|
|
881
|
+
return if timer.nil? || timer == :scheduling
|
|
882
|
+
EM.cancel_timer(timer)
|
|
883
|
+
rescue StandardError
|
|
884
|
+
nil
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
def schedule_on_reactor(&block)
|
|
888
|
+
if EM.reactor_running?
|
|
889
|
+
EM.schedule(&block)
|
|
890
|
+
else
|
|
891
|
+
block.call
|
|
892
|
+
end
|
|
595
893
|
end
|
|
596
894
|
|
|
597
895
|
def ready(val)
|
|
598
|
-
@ready = val
|
|
896
|
+
@state_mutex.synchronize {@ready = val}
|
|
599
897
|
end
|
|
600
898
|
|
|
601
899
|
def debug(message)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: intrinio-realtime
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Intrinio
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: eventmachine
|