beowulf-ruby-testnet 0.0.1

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.
@@ -0,0 +1,845 @@
1
+ require 'uri'
2
+ require 'base64'
3
+ require 'hashie'
4
+ require 'hashie/logger'
5
+ require 'openssl'
6
+ require 'open-uri'
7
+ require 'net/http/persistent'
8
+
9
+ module Beowulf
10
+ # Beowulf::Api allows you to call remote methods to interact with the Beowulf blockchain.
11
+ # The `Api` class is a shortened name for
12
+ # `Beowulf::CondenserApi`.
13
+ #
14
+ # Examples:
15
+ #
16
+ # api = Beowulf::Api.new
17
+ # response = api.get_dynamic_global_properties
18
+ # virtual_supply = response.result.virtual_supply
19
+ #
20
+ # ... or ...
21
+ #
22
+ # api = Beowulf::Api.new
23
+ # virtual_supply = api.get_dynamic_global_properties do |prop|
24
+ # prop.virtual_supply
25
+ # end
26
+ #
27
+ # If you need access to the `error` property, they can be accessed as follows:
28
+ #
29
+ # api = Beowulf::Api.new
30
+ # response = api.get_dynamic_global_properties
31
+ # if response.result.nil?
32
+ # puts response.error
33
+ # exit
34
+ # end
35
+ #
36
+ # virtual_supply = response.result.virtual_supply
37
+ #
38
+ # ... or ...
39
+ #
40
+ # api = Beowulf::Api.new
41
+ # virtual_supply = api.get_dynamic_global_properties do |prop, error|
42
+ # if prop.nil?
43
+ # puts error
44
+ # exis
45
+ # end
46
+ #
47
+ # prop.virtual_supply
48
+ # end
49
+ #
50
+ # List of remote methods:
51
+ #
52
+ # get_version
53
+ # get_block_header
54
+ # get_block
55
+ # get_config
56
+ # get_dynamic_global_properties
57
+ # get_supernode_schedule
58
+ # get_hardfork_version
59
+ # get_next_scheduled_hardfork
60
+ # get_accounts
61
+ # lookup_account_names
62
+ # lookup_accounts
63
+ # get_account_count
64
+ # get_owner_history
65
+ # get_transaction_hex
66
+ # get_transaction
67
+ # get_transaction_with_status
68
+ # get_pending_transaction_count
69
+ # get_required_signatures
70
+ # get_potential_signatures
71
+ # verify_authority
72
+ # get_supernodes
73
+ # get_supernode_by_accounts
74
+ # get_supernodes_by_vote
75
+ # lookup_supernode_accounts
76
+ # get_supernode_count
77
+ # get_active_supernodes
78
+ #
79
+ class Api
80
+ include Utils
81
+
82
+ DEFAULT_BEOWULF_URL = 'https://testnet-bw.beowulfchain.com/rpc'
83
+
84
+ DEFAULT_BEOWULF_FAILOVER_URLS = [
85
+ DEFAULT_BEOWULF_URL
86
+ ]
87
+
88
+ DEFAULT_RESTFUL_URL = 'https://testnet-bw.beowulfchain.com/rpc'
89
+
90
+ # @private
91
+ POST_HEADERS = {
92
+ 'Content-Type' => 'application/json',
93
+ 'User-Agent' => Beowulf::AGENT_ID
94
+ }
95
+
96
+ # @private
97
+ HEALTH_URI = '/health'
98
+
99
+ def self.default_url(chain)
100
+ case chain.to_sym
101
+ when :beowulf then DEFAULT_BEOWULF_URL
102
+ else; raise ApiError, "Unsupported chain: #{chain}"
103
+ end
104
+ end
105
+
106
+ def self.default_restful_url(chain)
107
+ case chain.to_sym
108
+ when :beowulf then DEFAULT_RESTFUL_URL
109
+ else; raise ApiError, "Unsupported chain: #{chain}"
110
+ end
111
+ end
112
+
113
+ def self.default_failover_urls(chain)
114
+ case chain.to_sym
115
+ when :beowulf then DEFAULT_BEOWULF_FAILOVER_URLS
116
+ else; raise ApiError, "Unsupported chain: #{chain}"
117
+ end
118
+ end
119
+
120
+ # Create a new instance of Beowulf::Api.
121
+ #
122
+ # Examples:
123
+ #
124
+ # api = Beowulf::Api.new(url: 'https://api.example.com')
125
+ #
126
+ # @param options [::Hash] The attributes to initialize the Beowulf::Api with.
127
+ # @option options [String] :url URL that points at a full node, like `https://testnet-bw.beowulfchain.com/rpc`. Default from DEFAULT_URL.
128
+ # @option options [::Array<String>] :failover_urls An array that contains one or more full nodes to fall back on. Default from DEFAULT_FAILOVER_URLS.
129
+ # @option options [Logger] :logger An instance of `Logger` to send debug messages to.
130
+ # @option options [Boolean] :recover_transactions_on_error Have Beowulf try to recover transactions that are accepted but could not be confirmed due to an error like network timeout. Default: `true`
131
+ # @option options [Integer] :max_requests Maximum number of requests on a connection before it is considered expired and automatically closed.
132
+ # @option options [Integer] :pool_size Maximum number of connections allowed.
133
+ # @option options [Boolean] :reuse_ssl_sessions Reuse a previously opened SSL session for a new connection. There's a slight performance improvement by enabling this, but at the expense of reliability during long execution. Default false.
134
+ # @option options [Boolean] :persist Enable or disable Persistent HTTP. Using Persistent HTTP keeps the connection alive between API calls. Default: `true`
135
+ def initialize(options = {})
136
+ @user = options[:user]
137
+ @password = options[:password]
138
+ @chain = options[:chain] || :beowulf
139
+ @url = options[:url] || Api::default_url(@chain)
140
+ @restful_url = options[:restful_url] || Api::default_restful_url(@chain)
141
+ @preferred_url = @url.dup
142
+ @failover_urls = options[:failover_urls]
143
+ @debug = !!options[:debug]
144
+ @max_requests = options[:max_requests] || 30
145
+ @ssl_verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
146
+ @ssl_version = options[:ssl_version]
147
+
148
+ @self_logger = false
149
+ @logger = if options[:logger].nil?
150
+ @self_logger = true
151
+ Beowulf.logger
152
+ else
153
+ options[:logger]
154
+ end
155
+
156
+ @self_hashie_logger = false
157
+ @hashie_logger = if options[:hashie_logger].nil?
158
+ @self_hashie_logger = true
159
+ Logger.new(nil)
160
+ else
161
+ options[:hashie_logger]
162
+ end
163
+
164
+ if @failover_urls.nil?
165
+ @failover_urls = Api::default_failover_urls(@chain) - [@url]
166
+ end
167
+
168
+ @failover_urls = [@failover_urls].flatten.compact
169
+ @preferred_failover_urls = @failover_urls.dup
170
+
171
+ unless @hashie_logger.respond_to? :warn
172
+ @hashie_logger = Logger.new(@hashie_logger)
173
+ end
174
+
175
+ @recover_transactions_on_error = if options.keys.include? :recover_transactions_on_error
176
+ options[:recover_transactions_on_error]
177
+ else
178
+ true
179
+ end
180
+
181
+ @persist_error_count = 0
182
+ @persist = if options.keys.include? :persist
183
+ options[:persist]
184
+ else
185
+ true
186
+ end
187
+
188
+ @reuse_ssl_sessions = if options.keys.include? :reuse_ssl_sessions
189
+ options[:reuse_ssl_sessions]
190
+ else
191
+ true
192
+ end
193
+
194
+ @use_condenser_namespace = if options.keys.include? :use_condenser_namespace
195
+ options[:use_condenser_namespace]
196
+ else
197
+ true
198
+ end
199
+
200
+ if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
201
+ @pool_size = options[:pool_size] || Net::HTTP::Persistent::DEFAULT_POOL_SIZE
202
+ end
203
+
204
+ Hashie.logger = @hashie_logger
205
+ @method_names = nil
206
+ @uri = nil
207
+ @http_id = nil
208
+ @http_memo = {}
209
+ @api_options = options.dup.merge(chain: @chain)
210
+ @api = nil
211
+ @block_api = nil
212
+ @backoff_at = nil
213
+ @jussi_supported = []
214
+ end
215
+
216
+ # Get a specific block or range of blocks.
217
+ #
218
+ # Example:
219
+ #
220
+ # api = Beowulf::Api.new
221
+ # blocks = api.get_blocks(10..20)
222
+ # transactions = blocks.flat_map(&:transactions)
223
+ #
224
+ # ... or ...
225
+ #
226
+ # api = Beowulf::Api.new
227
+ # transactions = []
228
+ # api.get_blocks(10..20) do |block|
229
+ # transactions += block.transactions
230
+ # end
231
+ #
232
+ # @param block_number [Fixnum || ::Array<Fixnum>]
233
+ # @param block the block to execute for each result, optional.
234
+ # @return [::Array]
235
+ def get_blocks(block_number, &block)
236
+ block_number = [*(block_number)].flatten
237
+
238
+ if !!block
239
+ block_number.each do |i|
240
+ if use_condenser_namespace?
241
+ yield api.get_block(i)
242
+ else
243
+ yield block_api.get_block(block_num: i).result, i
244
+ end
245
+ end
246
+ else
247
+ block_number.map do |i|
248
+ if use_condenser_namespace?
249
+ api.get_block(i)
250
+ else
251
+ block_api.get_block(block_num: i).result
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ # Stops the persistent http connections.
258
+ #
259
+ def shutdown
260
+ @uri = nil
261
+ @http_id = nil
262
+ @http_memo.each do |k|
263
+ v = @http_memo.delete(k)
264
+ if defined?(v.shutdown)
265
+ debug "Shutting down instance #{k} (#{v})"
266
+ v.shutdown
267
+ end
268
+ end
269
+ @api.shutdown if !!@api && @api != self
270
+ @api = nil
271
+ @block_api.shutdown if !!@block_api && @block_api != self
272
+ @block_api = nil
273
+
274
+ if @self_logger
275
+ if !!@logger && defined?(@logger.close)
276
+ if defined?(@logger.closed?)
277
+ @logger.close unless @logger.closed?
278
+ end
279
+ end
280
+ end
281
+
282
+ if @self_hashie_logger
283
+ if !!@hashie_logger && defined?(@hashie_logger.close)
284
+ if defined?(@hashie_logger.closed?)
285
+ @hashie_logger.close unless @hashie_logger.closed?
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ # @private
292
+ def method_names
293
+ return @method_names if !!@method_names
294
+ return CondenserApi::METHOD_NAMES if api_name == :condenser_api
295
+
296
+ @method_names = Beowulf::Api.methods(api_name).map do |e|
297
+ e['method'].to_sym
298
+ end
299
+ end
300
+
301
+ # @private
302
+ def api_name
303
+ :condenser_api
304
+ end
305
+
306
+ # @private
307
+ def respond_to_missing?(m, include_private = false)
308
+ method_names.nil? ? false : method_names.include?(m.to_sym)
309
+ end
310
+
311
+ # @private
312
+ def method_missing(m, *args, &block)
313
+ super unless respond_to_missing?(m)
314
+
315
+ current_rpc_id = rpc_id
316
+ method_name = [api_name, m].join('.')
317
+ response = nil
318
+ options = if api_name == :condenser_api
319
+ {
320
+ jsonrpc: "2.0",
321
+ method: method_name,
322
+ params: args,
323
+ id: current_rpc_id,
324
+ }
325
+ else
326
+ rpc_args = if args.empty?
327
+ {}
328
+ else
329
+ args.first
330
+ end
331
+ {
332
+ jsonrpc: "2.0",
333
+ method: method_name,
334
+ params: rpc_args,
335
+ id: current_rpc_id,
336
+ }
337
+ end
338
+
339
+ tries = 0
340
+ timestamp = Time.now.utc
341
+
342
+ loop do
343
+ tries += 1
344
+
345
+ if tries > 5 && flappy? && !check_file_open?
346
+ raise ApiError, 'PANIC: Out of file resources'
347
+ end
348
+
349
+ begin
350
+ if tries > 1 && @recover_transactions_on_error && api_name == :network_broadcast_api
351
+ signatures, exp = extract_signatures(options)
352
+
353
+ if !!signatures && signatures.any?
354
+ offset = [(exp - timestamp).abs, 30].min
355
+
356
+ if !!(response = recover_transaction(signatures, current_rpc_id, timestamp - offset))
357
+ response = Hashie::Mash.new(response)
358
+ end
359
+ end
360
+ end
361
+
362
+ if response.nil?
363
+ response = request(options)
364
+
365
+ response = if response.nil?
366
+ error "No response, retrying ...", method_name
367
+ elsif !response.kind_of? Net::HTTPSuccess
368
+ warning "Unexpected response (code: #{response.code}): #{response.inspect}, retrying ...", method_name, true
369
+ else
370
+ detect_jussi(response)
371
+
372
+ case response.code
373
+ when '200'
374
+ body = response.body
375
+ response = JSON[body]
376
+
377
+ if response['id'] != options[:id]
378
+ debug_payload(options, body) if ENV['DEBUG'] == 'true'
379
+
380
+ if !!response['id']
381
+ warning "Unexpected rpc_id (expected: #{options[:id]}, got: #{response['id']}), retrying ...", method_name, true
382
+ else
383
+ # The node has broken the jsonrpc spec.
384
+ warning "Node did not provide jsonrpc id (expected: #{options[:id]}, got: nothing), retrying ...", method_name, true
385
+ end
386
+
387
+ if response.keys.include?('error')
388
+ handle_error(response, options, method_name, tries)
389
+ end
390
+ elsif response.keys.include?('error')
391
+ handle_error(response, options, method_name, tries)
392
+ else
393
+ Hashie::Mash.new(response)
394
+ end
395
+ when '400' then warning 'Code 400: Bad Request, retrying ...', method_name, true
396
+ when '429' then warning 'Code 429: Too Many Requests, retrying ...', method_name, true
397
+ when '502' then warning 'Code 502: Bad Gateway, retrying ...', method_name, true
398
+ when '503' then warning 'Code 503: Service Unavailable, retrying ...', method_name, true
399
+ when '504' then warning 'Code 504: Gateway Timeout, retrying ...', method_name, true
400
+ else
401
+ warning "Unknown code #{response.code}, retrying ...", method_name, true
402
+ warning response
403
+ end
404
+ end
405
+ end
406
+ rescue Net::HTTP::Persistent::Error => e
407
+ warning "Unable to perform request: #{e} :: #{!!e.cause ? "cause: #{e.cause.message}" : ''}, retrying ...", method_name, true
408
+ if e.cause.class == Net::HTTPMethodNotAllowed
409
+ warning 'Node upstream is misconfigured.'
410
+ drop_current_failover_url method_name
411
+ end
412
+
413
+ @persist_error_count += 1
414
+ rescue ConnectionPool::Error => e
415
+ warning "Connection Pool Error (#{e.message}), retrying ...", method_name, true
416
+ rescue Errno::ECONNREFUSED => e
417
+ warning 'Connection refused, retrying ...', method_name, true
418
+ rescue Errno::EADDRNOTAVAIL => e
419
+ warning 'Node not available, retrying ...', method_name, true
420
+ rescue Errno::ECONNRESET => e
421
+ warning "Connection Reset (#{e.message}), retrying ...", method_name, true
422
+ rescue Errno::EBUSY => e
423
+ warning "Resource busy (#{e.message}), retrying ...", method_name, true
424
+ rescue Errno::ENETDOWN => e
425
+ warning "Network down (#{e.message}), retrying ...", method_name, true
426
+ rescue Net::ReadTimeout => e
427
+ warning 'Node read timeout, retrying ...', method_name, true
428
+ rescue Net::OpenTimeout => e
429
+ warning 'Node timeout, retrying ...', method_name, true
430
+ rescue RangeError => e
431
+ warning 'Range Error, retrying ...', method_name, true
432
+ rescue OpenSSL::SSL::SSLError => e
433
+ warning "SSL Error (#{e.message}), retrying ...", method_name, true
434
+ rescue SocketError => e
435
+ warning "Socket Error (#{e.message}), retrying ...", method_name, true
436
+ rescue JSON::ParserError => e
437
+ warning "JSON Parse Error (#{e.message}), retrying ...", method_name, true
438
+ drop_current_failover_url method_name if tries > 5
439
+ response = nil
440
+ rescue ApiError => e
441
+ warning "ApiError (#{e.message}), retrying ...", method_name, true
442
+ # rescue => e
443
+ # warning "Unknown exception from request, retrying ...", method_name, true
444
+ # warning e
445
+ end
446
+
447
+ if !!response
448
+ @persist_error_count = 0
449
+
450
+ if !!block
451
+ if api_name == :condenser_api
452
+ return yield(response.result, response.error, response.id)
453
+ else
454
+ if defined?(response.result.size) && response.result.size == 0
455
+ return yield(nil, response.error, response.id)
456
+ elsif (defined?(response.result.size) && response.result.size == 1 && defined?(response.result.values))
457
+ return yield(response.result.values.first, response.error, response.id)
458
+ else
459
+ return yield(response.result, response.error, response.id)
460
+ end
461
+ end
462
+ else
463
+ return response
464
+ end
465
+ end
466
+
467
+ backoff
468
+ end # loop
469
+ end
470
+
471
+ def inspect
472
+ properties = %w(
473
+ chain url backoff_at max_requests ssl_verify_mode ssl_version persist
474
+ recover_transactions_on_error reuse_ssl_sessions pool_size
475
+ use_condenser_namespace
476
+ ).map do |prop|
477
+ if !!(v = instance_variable_get("@#{prop}"))
478
+ "@#{prop}=#{v}"
479
+ end
480
+ end.compact.join(', ')
481
+
482
+ "#<#{self.class.name} [#{properties}]>"
483
+ end
484
+
485
+ def stopped?
486
+ http_active = if @http_memo.nil?
487
+ false
488
+ else
489
+ @http_memo.values.map do |http|
490
+ if defined?(http.active?)
491
+ http.active?
492
+ else
493
+ false
494
+ end
495
+ end.include?(true)
496
+ end
497
+
498
+ @uri.nil? && @http_id.nil? && !http_active && @api.nil? && @block_api.nil?
499
+ end
500
+
501
+ def use_condenser_namespace?
502
+ @use_condenser_namespace
503
+ end
504
+ private
505
+ def self.methods_json_path
506
+ @methods_json_path ||= "#{File.dirname(__FILE__)}/methods.json"
507
+ end
508
+
509
+ def self.methods(api_name)
510
+ @methods ||= {}
511
+ @methods[api_name] ||= JSON[File.read methods_json_path].map do |e|
512
+ e if e['api'].to_sym == api_name
513
+ end.compact.freeze
514
+ end
515
+
516
+ def self.apply_http_defaults(http, ssl_verify_mode)
517
+ http.read_timeout = 10
518
+ http.open_timeout = 10
519
+ http.verify_mode = ssl_verify_mode
520
+ http.ssl_timeout = 30 if defined? http.ssl_timeout
521
+ http
522
+ end
523
+
524
+ def api_options
525
+ @api_options.merge(failover_urls: @failover_urls, logger: @logger, hashie_logger: @hashie_logger)
526
+ end
527
+
528
+ def api
529
+ @api ||= self.class == Api ? self : Api.new(api_options)
530
+ end
531
+
532
+ def block_api
533
+ @block_api ||= self.class == BlockApi ? self : BlockApi.new(api_options)
534
+ end
535
+
536
+ def rpc_id
537
+ @rpc_id ||= 0
538
+ @rpc_id = @rpc_id + 1
539
+ end
540
+
541
+ def uri
542
+ @uri ||= URI.parse(@url)
543
+ end
544
+
545
+ def http_id
546
+ @http_id ||= "beowulf-#{Beowulf::VERSION}-#{api_name}-#{SecureRandom.uuid}"
547
+ end
548
+
549
+ def http
550
+ return @http_memo[http_id] if @http_memo.keys.include? http_id
551
+
552
+ @http_memo[http_id] = if @persist && @persist_error_count < 10
553
+ idempotent = api_name != :network_broadcast_api
554
+
555
+ http = if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
556
+ Net::HTTP::Persistent.new(name: http_id, pool_size: @pool_size)
557
+ else
558
+ # net-http-persistent < 3.0
559
+ Net::HTTP::Persistent.new(http_id)
560
+ end
561
+
562
+ http.keep_alive = 30
563
+ http.idle_timeout = idempotent ? 10 : nil
564
+ http.max_requests = @max_requests
565
+ http.retry_change_requests = idempotent
566
+ http.reuse_ssl_sessions = @reuse_ssl_sessions
567
+
568
+ http
569
+ else
570
+ http = Net::HTTP.new(uri.host, uri.port)
571
+ http.use_ssl = uri.scheme == 'https'
572
+ http
573
+ end
574
+
575
+ Api::apply_http_defaults(@http_memo[http_id], @ssl_verify_mode)
576
+ end
577
+
578
+ def post_request
579
+ Net::HTTP::Post.new uri.request_uri, POST_HEADERS
580
+ end
581
+
582
+ def request(options)
583
+ request = post_request
584
+ request.body = JSON[options]
585
+
586
+ case http
587
+ when Net::HTTP::Persistent then http.request(uri, request)
588
+ when Net::HTTP then http.request(request)
589
+ else; raise ApiError, "Unsuppored scheme: #{http.inspect}"
590
+ end
591
+ end
592
+
593
+ def jussi_supported?(url = @url)
594
+ @jussi_supported.include? url
595
+ end
596
+
597
+ def detect_jussi(response)
598
+ return if jussi_supported?(@url)
599
+
600
+ jussi_response_id = response['x-jussi-response-id']
601
+
602
+ if !!jussi_response_id
603
+ debug "Found a node that supports jussi: #{@url}"
604
+ @jussi_supported << @url
605
+ end
606
+ end
607
+
608
+ def recover_transaction(signatures, expected_rpc_id, after)
609
+ debug "Looking for signatures: #{signatures.map{|s| s[0..5]}} since: #{after}"
610
+
611
+ count = 0
612
+ start = Time.now.utc
613
+ block_range = api.get_dynamic_global_properties do |properties|
614
+ high = properties.head_block_number
615
+ low = high - 100
616
+ [*(low..(high))].reverse
617
+ end
618
+
619
+ # At most, we read 100 blocks
620
+ # but we also give up once the block time is before the `after` argument.
621
+ api.get_blocks(block_range) do |block, block_num|
622
+ unless defined? block.transaction_ids
623
+ error "Blockchain does not provide transaction ids in blocks, giving up."
624
+ return nil
625
+ end
626
+
627
+ count += 1
628
+ raise ApiError, "Race condition detected on remote node at: #{block_num}" if block.nil?
629
+
630
+ # In the future, it would be better to decode the operation and signature
631
+ # into the transaction id.
632
+ unless defined? block.transaction_ids
633
+ @recover_transactions_on_error = false
634
+ return
635
+ end
636
+
637
+ timestamp = Time.parse(block.timestamp + 'Z')
638
+ break if timestamp < after
639
+
640
+ block.transactions.each_with_index do |tx, index|
641
+ next unless ((tx['signatures'] || []) & signatures).any?
642
+
643
+ debug "Found transaction #{count} block(s) ago; took #{(Time.now.utc - start)} seconds to scan."
644
+
645
+ return {
646
+ id: expected_rpc_id,
647
+ recovered_by: http_id,
648
+ result: {
649
+ id: block.transaction_ids[index],
650
+ block_num: block_num,
651
+ trx_num: index,
652
+ expired: false
653
+ }
654
+ }
655
+ end
656
+ end
657
+
658
+ debug "Could not find transaction in #{count} block(s); took #{(Time.now.utc - start)} seconds to scan."
659
+
660
+ return nil
661
+ end
662
+
663
+ def reset_failover
664
+ @url = @preferred_url.dup
665
+ @failover_urls = @preferred_failover_urls.dup
666
+ warning "Failover reset, going back to #{@url} ..."
667
+ end
668
+
669
+ def pop_failover_url
670
+ reset_failover if @failover_urls.none?
671
+
672
+ until @failover_urls.none? || healthy?(url = @failover_urls.sample)
673
+ @failover_urls.delete(url)
674
+ end
675
+
676
+ url || (uri || @url).to_s
677
+ end
678
+
679
+ def bump_failover
680
+ @uri = nil
681
+ @url = pop_failover_url
682
+ warning "Failing over to #{@url} ..."
683
+ end
684
+
685
+ def flappy?
686
+ !!@backoff_at && Time.now.utc - @backoff_at < 300
687
+ end
688
+
689
+ # Note, this methods only removes the uri.to_s if present but it does not
690
+ # call bump_failover, in order to avoid a race condition.
691
+ def drop_current_failover_url(prefix)
692
+ if @preferred_failover_urls.size == 1
693
+ warning "Node #{uri} appears to be misconfigured but no other node is available, retrying ...", prefix
694
+ else
695
+ warning "Removing misconfigured node from failover urls: #{uri}, retrying ...", prefix
696
+ @preferred_failover_urls.delete(uri.to_s)
697
+ @failover_urls.delete(uri.to_s)
698
+ end
699
+ end
700
+
701
+ def handle_error(response, request_options, method_name, tries)
702
+ parser = ErrorParser.new(response)
703
+ _signatures, exp = extract_signatures(request_options)
704
+
705
+ if (!!exp && exp < Time.now.utc) || (tries > 2 && !parser.node_degraded?)
706
+ # Whatever the error was, it is already expired or tried too much. No need to try to recover.
707
+ debug "Error code #{parser} but transaction already expired or too many tries, giving up (attempt: #{tries})."
708
+ elsif parser.can_retry?
709
+ drop_current_failover_url method_name if !!exp && parser.expiry?
710
+ drop_current_failover_url method_name if parser.node_degraded?
711
+ debug "Error code #{parser} (attempt: #{tries}), retrying ..."
712
+ return nil
713
+ end
714
+
715
+ if !!parser.trx_id
716
+ # Turns out, the ErrorParser found a transaction id. It might come in
717
+ # handy, so let's append this to the result along with the error.
718
+
719
+ response[:result] = {
720
+ id: parser.trx_id,
721
+ block_num: -1,
722
+ trx_num: -1,
723
+ expired: false
724
+ }
725
+
726
+ if @recover_transactions_on_error
727
+ begin
728
+ if !!@restful_url
729
+ JSON[open("#{@restful_url}/account_history_api/get_transaction?id=#{parser.trx_id}").read].tap do |tx|
730
+ response[:result][:block_num] = tx['block_num']
731
+ response[:result][:trx_num] = tx['transaction_num']
732
+ end
733
+ else
734
+ # Node operators often disable this operation.
735
+ api.get_transaction(parser.trx_id) do |tx|
736
+ if !!tx
737
+ response[:result][:block_num] = tx.block_num
738
+ response[:result][:trx_num] = tx.transaction_num
739
+ end
740
+ end
741
+ end
742
+
743
+ response[:recovered_by] = http_id
744
+ response.delete('error') # no need for this, now
745
+ rescue
746
+ debug "Couldn't find block for trx_id: #{parser.trx_id}, giving up."
747
+ end
748
+ end
749
+ end
750
+
751
+ Hashie::Mash.new(response)
752
+ end
753
+
754
+ def healthy?(url)
755
+ begin
756
+ # Note, not all nodes support the /health uri. But even if they don't,
757
+ # they'll respond status code 200 OK, even if the body shows an error.
758
+ #
759
+ # But if the node supports the /health uri, it will do additional
760
+ # verifications on the block height.
761
+ #
762
+ # Also note, this check is done **without** net-http-persistent.
763
+
764
+ response = open(url + HEALTH_URI)
765
+ response = JSON[response.read]
766
+
767
+ if !!response['error']
768
+ if !!response['error']['data']
769
+ if !!response['error']['data']['message']
770
+ error "#{url} error: #{response['error']['data']['message']}"
771
+ end
772
+ elsif !!response['error']['message']
773
+ error "#{url} error: #{response['error']['message']}"
774
+ else
775
+ error "#{url} error: #{response['error']}"
776
+ end
777
+
778
+ false
779
+ elsif response['status'] == 'OK'
780
+ true
781
+ else
782
+ error "#{url} status: #{response['status']}"
783
+
784
+ false
785
+ end
786
+ rescue JSON::ParserError
787
+ # No JSON, but also no HTTP error code, so we're OK.
788
+
789
+ true
790
+ rescue => e
791
+ error "Health check failure for #{url}: #{e.inspect}"
792
+ sleep 0.2
793
+ false
794
+ end
795
+ end
796
+
797
+ def check_file_open?
798
+ File.exists?('.')
799
+ rescue
800
+ false
801
+ end
802
+
803
+ def debug_payload(request, response)
804
+ request = JSON.pretty_generate(request)
805
+ response = JSON.parse(response) rescue response
806
+ response = JSON.pretty_generate(response) rescue response
807
+
808
+ puts '=' * 80
809
+ puts "Request:"
810
+ puts request
811
+ puts '=' * 80
812
+ puts "Response:"
813
+ puts response
814
+ puts '=' * 80
815
+ end
816
+
817
+ def backoff
818
+ shutdown
819
+ bump_failover if flappy? || !healthy?(uri)
820
+ @backoff_at ||= Time.now.utc
821
+ @backoff_sleep ||= 0.01
822
+
823
+ @backoff_sleep *= 2
824
+ GC.start
825
+ sleep @backoff_sleep
826
+ ensure
827
+ if !!@backoff_at && Time.now.utc - @backoff_at > 300
828
+ @backoff_at = nil
829
+ @backoff_sleep = nil
830
+ end
831
+ end
832
+
833
+ def self.finalize(logger, hashie_logger)
834
+ proc {
835
+ if !!logger && defined?(logger.close) && !logger.closed?
836
+ logger.close
837
+ end
838
+
839
+ if !!hashie_logger && defined?(hashie_logger.close) && !hashie_logger.closed?
840
+ hashie_logger.close
841
+ end
842
+ }
843
+ end
844
+ end
845
+ end