radiator 0.3.4 → 0.3.6
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/Gemfile.lock +3 -3
- data/README.md +16 -3
- data/Rakefile +50 -0
- data/lib/radiator/api.rb +99 -81
- data/lib/radiator/base_error.rb +7 -2
- data/lib/radiator/chain_config.rb +1 -1
- data/lib/radiator/error_parser.rb +94 -0
- data/lib/radiator/stream.rb +1 -1
- data/lib/radiator/transaction.rb +67 -15
- data/lib/radiator/utils.rb +54 -0
- data/lib/radiator/version.rb +1 -1
- data/lib/radiator.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 956f02673186f4855cd07bc5c5a1a9163c329cbf
|
4
|
+
data.tar.gz: 3a18bd23240d54c747ec4b5735a316c60a9e5ef6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6982a8ac692ee87bb34041d854ca80034745505f389f6f94e12deaafae6975a3336d6147b4d116a4d8a8d03f85f8d051561751ab782e74df511341a8b084169d
|
7
|
+
data.tar.gz: 6a6438a16389022332cda72466bad910f6d1f4b474e892176078dd37c5f6a821f0cdd7422ceebe3140e450b7bf3b5c9b9bae92f207aacce9683337ca2b1af78c
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
radiator (0.3.
|
4
|
+
radiator (0.3.6)
|
5
5
|
awesome_print (~> 1.7, >= 1.7.0)
|
6
6
|
bitcoin-ruby (~> 0.0, >= 0.0.11)
|
7
7
|
ffi (~> 1.9, >= 1.9.18)
|
@@ -16,7 +16,7 @@ GEM
|
|
16
16
|
addressable (2.5.2)
|
17
17
|
public_suffix (>= 2.0.2, < 4.0)
|
18
18
|
awesome_print (1.8.0)
|
19
|
-
bitcoin-ruby (0.0.
|
19
|
+
bitcoin-ruby (0.0.12)
|
20
20
|
connection_pool (2.2.1)
|
21
21
|
crack (0.4.3)
|
22
22
|
safe_yaml (~> 1.0.0)
|
@@ -68,4 +68,4 @@ DEPENDENCIES
|
|
68
68
|
yard (~> 0.9.9)
|
69
69
|
|
70
70
|
BUNDLED WITH
|
71
|
-
1.
|
71
|
+
1.16.0.pre.3
|
data/README.md
CHANGED
@@ -397,9 +397,9 @@ Radiator supports failover for situations where a node has, for example, become
|
|
397
397
|
|
398
398
|
```ruby
|
399
399
|
options = {
|
400
|
-
ur: 'https://
|
400
|
+
ur: 'https://api.steemit.com',
|
401
401
|
failover_urls: [
|
402
|
-
'https://
|
402
|
+
'https://api.steemitstage.com',
|
403
403
|
'https://gtg.steem.house:8090'
|
404
404
|
]
|
405
405
|
}
|
@@ -421,6 +421,16 @@ There is another rare scenario involving `::Transaction` broadcasts that's handl
|
|
421
421
|
tx = Radiator::Transaction.new(wif: wif, recover_transactions_on_error: false)
|
422
422
|
```
|
423
423
|
|
424
|
+
## Debugging
|
425
|
+
|
426
|
+
To enable debugging, set environment `LOG=DEBUG` before launching your app. E.g.:
|
427
|
+
|
428
|
+
```bash
|
429
|
+
$ LOG=DEBUG irb -rradiator
|
430
|
+
```
|
431
|
+
|
432
|
+
This will enable debugging for the `irb` session.
|
433
|
+
|
424
434
|
## Troubleshooting
|
425
435
|
|
426
436
|
## Problem: My log is full of `Unable to perform request ... retrying ...` messages.
|
@@ -465,7 +475,10 @@ Verify your code is not doing too much between blocks.
|
|
465
475
|
* `rake`
|
466
476
|
* To run tests with parallelization and local code coverage:
|
467
477
|
* `HELL_ENABLED=true rake`
|
468
|
-
|
478
|
+
* To run a stream test on the live STEEM blockchain with debug logging enabled:
|
479
|
+
* `LOG=DEBUG rake test_live_stream`
|
480
|
+
* To run a stream test on the live GOLOS blockchain with debug logging enabled:
|
481
|
+
* `LOG=DEBUG rake test_live_stream[golos]`
|
469
482
|
---
|
470
483
|
|
471
484
|
<center>
|
data/Rakefile
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'bundler/gem_tasks'
|
2
2
|
require 'rake/testtask'
|
3
3
|
require 'yard'
|
4
|
+
require 'radiator'
|
4
5
|
|
5
6
|
Rake::TestTask.new(:test) do |t|
|
6
7
|
t.libs << 'test'
|
@@ -19,14 +20,62 @@ end
|
|
19
20
|
|
20
21
|
task default: :test
|
21
22
|
|
23
|
+
desc 'Deletes test/fixtures/vcr_cassettes/*.yml so they can be rebuilt fresh.'
|
24
|
+
task :dump_vcr do |t|
|
25
|
+
exec 'rm -v test/fixtures/vcr_cassettes/*.yml'
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'Tests the ability to stream live data.'
|
29
|
+
task :test_live_stream, :chain do |t, args|
|
30
|
+
chain = (args[:chain] || 'steem').to_sym
|
31
|
+
last_block_number = 0
|
32
|
+
options = {chain: chain}
|
33
|
+
api = Radiator::Api.new(options)
|
34
|
+
total_ops = 0.0
|
35
|
+
total_vops = 0.0
|
36
|
+
|
37
|
+
Radiator::Stream.new(options).blocks do |b, n|
|
38
|
+
if last_block_number == 0
|
39
|
+
# skip test
|
40
|
+
elsif last_block_number + 1 == n
|
41
|
+
t = b.transactions
|
42
|
+
t_size = t.size
|
43
|
+
o = t.map(&:operations)
|
44
|
+
op_size = o.map(&:size).reduce(0, :+)
|
45
|
+
total_ops += op_size
|
46
|
+
api.get_ops_in_block(n, true) do |vops|
|
47
|
+
vop_size = vops.size
|
48
|
+
total_vops += vop_size
|
49
|
+
|
50
|
+
vop_ratio = if total_vops > 0
|
51
|
+
total_vops / total_ops
|
52
|
+
else
|
53
|
+
0
|
54
|
+
end
|
55
|
+
|
56
|
+
puts "#{n}: #{b.witness}; transactions: #{t_size}; operations: #{op_size}, virtual operations: #{vop_size} (cumulative vop ratio: #{('%.2f' % (vop_ratio * 100))} %)"
|
57
|
+
end
|
58
|
+
else
|
59
|
+
# This should not happen. If it does, there's likely a bug in Radiator.
|
60
|
+
|
61
|
+
puts "Error, last block nunber was #{last_block_number}, did not expect #{n}."
|
62
|
+
end
|
63
|
+
|
64
|
+
last_block_number = n
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'Ruby console with radiator already required.'
|
22
69
|
task :console do
|
23
70
|
exec "irb -r radiator -I ./lib"
|
24
71
|
end
|
25
72
|
|
73
|
+
desc 'Build a new version of the radiator gem.'
|
26
74
|
task :build do
|
27
75
|
exec 'gem build radiator.gemspec'
|
28
76
|
end
|
29
77
|
|
78
|
+
desc 'Publish the current version of the radiator gem.'
|
30
79
|
task :push do
|
31
80
|
exec "gem push radiator-#{Radiator::VERSION}.gem"
|
32
81
|
end
|
@@ -34,6 +83,7 @@ end
|
|
34
83
|
# We're not going to yank on a regular basis, but this is how it's done if you
|
35
84
|
# really want a task for that for some reason.
|
36
85
|
|
86
|
+
# desc 'Yank the current version of the radiator gem.'
|
37
87
|
# task :yank do
|
38
88
|
# exec "gem yank radiator -v #{Radiator::VERSION}"
|
39
89
|
# end
|
data/lib/radiator/api.rb
CHANGED
@@ -3,6 +3,7 @@ require 'base64'
|
|
3
3
|
require 'hashie'
|
4
4
|
require 'hashie/logger'
|
5
5
|
require 'openssl'
|
6
|
+
require 'open-uri'
|
6
7
|
require 'net/http/persistent'
|
7
8
|
|
8
9
|
module Radiator
|
@@ -131,14 +132,15 @@ module Radiator
|
|
131
132
|
# @see https://steemit.github.io/steemit-docs/#accounts
|
132
133
|
#
|
133
134
|
class Api
|
134
|
-
|
135
|
+
include Utils
|
136
|
+
|
137
|
+
DEFAULT_STEEM_URL = 'https://api.steemit.com'
|
135
138
|
|
136
139
|
DEFAULT_GOLOS_URL = 'https://ws.golos.io'
|
137
140
|
|
138
141
|
DEFAULT_STEEM_FAILOVER_URLS = [
|
139
142
|
DEFAULT_STEEM_URL,
|
140
|
-
'https://
|
141
|
-
'https://steemd.steemitstage.com',
|
143
|
+
'https://api.steemitstage.com',
|
142
144
|
'https://gtg.steem.house:8090',
|
143
145
|
'https://seed.bitcoiner.me',
|
144
146
|
'https://steemd.minnowsupportproject.org',
|
@@ -156,9 +158,8 @@ module Radiator
|
|
156
158
|
'Content-Type' => 'application/json'
|
157
159
|
}
|
158
160
|
|
159
|
-
#
|
160
|
-
|
161
|
-
SSL_VERSIONS = [:TLSv1_2, :TLSv1_1, :TLSv1, :SSLv3, :SSLv2, :SSLv23]
|
161
|
+
# @private
|
162
|
+
HEALTH_URI = '/health'
|
162
163
|
|
163
164
|
def self.default_url(chain)
|
164
165
|
case chain.to_sym
|
@@ -183,11 +184,12 @@ module Radiator
|
|
183
184
|
# api = Radiator::Api.new(url: 'https://api.example.com')
|
184
185
|
#
|
185
186
|
# @param options [Hash] The attributes to initialize the Radiator::Api with.
|
186
|
-
# @option options [String] :url URL that points at a full node, like `https://
|
187
|
+
# @option options [String] :url URL that points at a full node, like `https://api.steemit.com`. Default from DEFAULT_URL.
|
187
188
|
# @option options [Array<String>] :failover_urls An array that contains one or more full nodes to fall back on. Default from DEFAULT_FAILOVER_URLS.
|
188
189
|
# @option options [Logger] :logger An instance of `Logger` to send debug messages to.
|
189
190
|
# @option options [Boolean] :recover_transactions_on_error Have Radiator try to recover transactions that are accepted but could not be confirmed due to an error like network timeout. Default: `true`
|
190
191
|
# @option options [Integer] :max_requests Maximum number of requests on a connection before it is considered expired and automatically closed.
|
192
|
+
# @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.
|
191
193
|
def initialize(options = {})
|
192
194
|
@user = options[:user]
|
193
195
|
@password = options[:password]
|
@@ -199,7 +201,9 @@ module Radiator
|
|
199
201
|
@logger = options[:logger] || Radiator.logger
|
200
202
|
@hashie_logger = options[:hashie_logger] || Logger.new(nil)
|
201
203
|
@max_requests = options[:max_requests] || 30
|
202
|
-
@
|
204
|
+
@ssl_verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
|
205
|
+
@reuse_ssl_sessions = !!options[:reuse_ssl_sessions]
|
206
|
+
@ssl_version = options[:ssl_version]
|
203
207
|
|
204
208
|
if @failover_urls.nil?
|
205
209
|
@failover_urls = Api::default_failover_urls(@chain) - [@url]
|
@@ -220,6 +224,7 @@ module Radiator
|
|
220
224
|
|
221
225
|
Hashie.logger = @hashie_logger
|
222
226
|
@method_names = nil
|
227
|
+
@http = nil
|
223
228
|
@api_options = options.dup.merge(chain: @chain)
|
224
229
|
@api = nil
|
225
230
|
@block_api = nil
|
@@ -376,12 +381,13 @@ module Radiator
|
|
376
381
|
def method_missing(m, *args, &block)
|
377
382
|
super unless respond_to_missing?(m)
|
378
383
|
|
384
|
+
current_rpc_id = rpc_id
|
379
385
|
method_name = [api_name, m].join('.')
|
380
386
|
response = nil
|
381
387
|
options = {
|
382
388
|
jsonrpc: "2.0",
|
383
389
|
params: [api_name, m, args],
|
384
|
-
id:
|
390
|
+
id: current_rpc_id,
|
385
391
|
method: "call"
|
386
392
|
}
|
387
393
|
|
@@ -398,8 +404,8 @@ module Radiator
|
|
398
404
|
if tries > 1 && !!signatures && signatures.any?
|
399
405
|
offset = [(exp - timestamp).abs, 300].min
|
400
406
|
|
401
|
-
if !!(response = recover_transaction(signatures,
|
402
|
-
warning 'Found recovered transaction after retry.', method_name
|
407
|
+
if !!(response = recover_transaction(signatures, current_rpc_id, timestamp - offset))
|
408
|
+
warning 'Found recovered transaction after retry.', method_name, true
|
403
409
|
response = Hashie::Mash.new(response)
|
404
410
|
end
|
405
411
|
end
|
@@ -411,7 +417,7 @@ module Radiator
|
|
411
417
|
response = if response.nil?
|
412
418
|
error "No response, retrying ...", method_name
|
413
419
|
elsif !response.kind_of? Net::HTTPSuccess
|
414
|
-
warning "Unexpected response (code: #{response.code}): #{response.inspect}, retrying ...", method_name
|
420
|
+
warning "Unexpected response (code: #{response.code}): #{response.inspect}, retrying ...", method_name, true
|
415
421
|
else
|
416
422
|
case response.code
|
417
423
|
when '200'
|
@@ -419,52 +425,48 @@ module Radiator
|
|
419
425
|
response = JSON[body]
|
420
426
|
|
421
427
|
if response['id'] != options[:id]
|
422
|
-
warning "Unexpected rpc_id (expected: #{options[:id]}, got: #{response['id']}), retrying ...", method_name
|
428
|
+
warning "Unexpected rpc_id (expected: #{options[:id]}, got: #{response['id']}), retrying ...", method_name, true
|
423
429
|
elsif response.keys.include?('error')
|
424
|
-
|
425
|
-
when -32601 # Assert Exception:method_itr != api_itr->second.end(): Could not find method ...
|
426
|
-
nil
|
427
|
-
else
|
428
|
-
Hashie::Mash.new(response)
|
429
|
-
end
|
430
|
+
handle_error(response, options, method_name, tries)
|
430
431
|
else
|
431
432
|
Hashie::Mash.new(response)
|
432
433
|
end
|
433
|
-
when '400' then warning 'Code 400: Bad Request, retrying ...', method_name
|
434
|
-
when '429' then warning 'Code 429: Too Many Requests, retrying ...', method_name
|
435
|
-
when '502' then warning 'Code 502: Bad Gateway, retrying ...', method_name
|
436
|
-
when '503' then warning 'Code 503: Service Unavailable, retrying ...', method_name
|
437
|
-
when '504' then warning 'Code 504: Gateway Timeout, retrying ...', method_name
|
434
|
+
when '400' then warning 'Code 400: Bad Request, retrying ...', method_name, true
|
435
|
+
when '429' then warning 'Code 429: Too Many Requests, retrying ...', method_name, true
|
436
|
+
when '502' then warning 'Code 502: Bad Gateway, retrying ...', method_name, true
|
437
|
+
when '503' then warning 'Code 503: Service Unavailable, retrying ...', method_name, true
|
438
|
+
when '504' then warning 'Code 504: Gateway Timeout, retrying ...', method_name, true
|
438
439
|
else
|
439
|
-
warning "Unknown code #{response.code}, retrying ...", method_name
|
440
|
+
warning "Unknown code #{response.code}, retrying ...", method_name, true
|
440
441
|
ap response
|
441
442
|
end
|
442
443
|
end
|
443
444
|
end
|
444
445
|
rescue Net::HTTP::Persistent::Error => e
|
445
|
-
warning "Unable to perform request: #{e} :: #{!!e.cause ? "cause: #{e.cause.message}" : ''}, retrying ...", method_name
|
446
|
+
warning "Unable to perform request: #{e} :: #{!!e.cause ? "cause: #{e.cause.message}" : ''}, retrying ...", method_name, true
|
446
447
|
rescue Errno::ECONNREFUSED => e
|
447
|
-
warning 'Connection refused, retrying ...', method_name
|
448
|
+
warning 'Connection refused, retrying ...', method_name, true
|
448
449
|
rescue Errno::EADDRNOTAVAIL => e
|
449
|
-
warning 'Node not available, retrying ...', method_name
|
450
|
+
warning 'Node not available, retrying ...', method_name, true
|
451
|
+
rescue Errno::ECONNRESET => e
|
452
|
+
warning "Connection Reset (#{e.message}), retrying ...", method_name, true
|
450
453
|
rescue Net::ReadTimeout => e
|
451
|
-
warning 'Node read timeout, retrying ...', method_name
|
454
|
+
warning 'Node read timeout, retrying ...', method_name, true
|
452
455
|
rescue Net::OpenTimeout => e
|
453
|
-
warning 'Node timeout, retrying ...', method_name
|
456
|
+
warning 'Node timeout, retrying ...', method_name, true
|
454
457
|
rescue RangeError => e
|
455
|
-
warning 'Range Error, retrying ...', method_name
|
458
|
+
warning 'Range Error, retrying ...', method_name, true
|
456
459
|
rescue OpenSSL::SSL::SSLError => e
|
457
|
-
|
458
|
-
warning "SSL Error (#{e.message}), switching to #{@ssl_version} and retrying ...", method_name
|
460
|
+
warning "SSL Error (#{e.message}), retrying ...", method_name, true
|
459
461
|
rescue SocketError => e
|
460
|
-
warning "Socket Error (#{e.message}), retrying ...", method_name
|
462
|
+
warning "Socket Error (#{e.message}), retrying ...", method_name, true
|
461
463
|
rescue JSON::ParserError => e
|
462
|
-
warning "JSON Parse Error (#{e.message}), retrying ...", method_name
|
464
|
+
warning "JSON Parse Error (#{e.message}), retrying ...", method_name, true
|
463
465
|
response = nil
|
464
466
|
rescue ApiError => e
|
465
|
-
warning "ApiError (#{e.message}), retrying ...", method_name
|
467
|
+
warning "ApiError (#{e.message}), retrying ...", method_name, true
|
466
468
|
# rescue => e
|
467
|
-
# warning "Unknown exception from request, retrying ...", method_name
|
469
|
+
# warning "Unknown exception from request, retrying ...", method_name, true
|
468
470
|
# ap e if defined? ap
|
469
471
|
end
|
470
472
|
|
@@ -528,13 +530,9 @@ module Radiator
|
|
528
530
|
@http.idle_timeout = idempotent ? 10 : nil
|
529
531
|
@http.max_requests = @max_requests
|
530
532
|
@http.retry_change_requests = idempotent
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
@http.ssl_version = @ssl_version
|
535
|
-
else
|
536
|
-
@http.reuse_ssl_sessions = true
|
537
|
-
end
|
533
|
+
@http.verify_mode = @ssl_verify_mode
|
534
|
+
@http.reuse_ssl_sessions = @reuse_ssl_sessions
|
535
|
+
@http.ssl_version = @ssl_version
|
538
536
|
|
539
537
|
@http
|
540
538
|
end
|
@@ -549,25 +547,7 @@ module Radiator
|
|
549
547
|
http.request(uri, request)
|
550
548
|
end
|
551
549
|
|
552
|
-
def
|
553
|
-
params = options[:params]
|
554
|
-
|
555
|
-
signatures = params.map do |param|
|
556
|
-
next unless defined? param.map
|
557
|
-
|
558
|
-
param.map { |tx| tx[:signatures] }
|
559
|
-
end.flatten.compact
|
560
|
-
|
561
|
-
expirations = params.map do |param|
|
562
|
-
next unless defined? param.map
|
563
|
-
|
564
|
-
param.map { |tx| Time.parse(tx[:expiration] + 'Z') }
|
565
|
-
end.flatten.compact
|
566
|
-
|
567
|
-
[signatures, expirations.min]
|
568
|
-
end
|
569
|
-
|
570
|
-
def recover_transaction(signatures, rpc_id, after)
|
550
|
+
def recover_transaction(signatures, expected_rpc_id, after)
|
571
551
|
block_range = api.get_dynamic_global_properties do |properties|
|
572
552
|
high = properties.head_block_number
|
573
553
|
low = high - 100
|
@@ -589,7 +569,7 @@ module Radiator
|
|
589
569
|
next unless ((tx['signatures'] || []) & signatures).any?
|
590
570
|
|
591
571
|
return {
|
592
|
-
id:
|
572
|
+
id: expected_rpc_id,
|
593
573
|
result: {
|
594
574
|
id: block.transaction_ids[index],
|
595
575
|
block_num: block_num,
|
@@ -610,12 +590,16 @@ module Radiator
|
|
610
590
|
end
|
611
591
|
|
612
592
|
def pop_failover_url
|
613
|
-
@failover_urls.
|
593
|
+
reset_failover if @failover_urls.none?
|
594
|
+
|
595
|
+
until @failover_urls.none? || healthy?(url = @failover_urls.sample)
|
596
|
+
@failover_urls.delete(url)
|
597
|
+
end
|
598
|
+
|
599
|
+
url || @url
|
614
600
|
end
|
615
601
|
|
616
602
|
def bump_failover
|
617
|
-
reset_failover if @failover_urls.none?
|
618
|
-
|
619
603
|
@uri = nil
|
620
604
|
@url = pop_failover_url
|
621
605
|
warning "Failing over to #{@url} ..."
|
@@ -625,9 +609,56 @@ module Radiator
|
|
625
609
|
!!@backoff_at && Time.now - @backoff_at < 300
|
626
610
|
end
|
627
611
|
|
612
|
+
def drop_current_failover_url(prefix)
|
613
|
+
if @preferred_failover_urls.size == 1
|
614
|
+
warning "Node #{@url} appears to be misconfigured but no other node is available, retrying ...", prefix
|
615
|
+
else
|
616
|
+
warning "Removing misconfigured node from failover urls: #{@url}, retrying ...", prefix
|
617
|
+
@preferred_failover_urls.delete(@url)
|
618
|
+
@failover_urls.delete(@url)
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
def handle_error(response, request_options, method_name, tries)
|
623
|
+
parser = ErrorParser.new(response)
|
624
|
+
signatures, exp = extract_signatures(request_options)
|
625
|
+
|
626
|
+
if (!!exp && exp < Time.now.utc) || tries > 2
|
627
|
+
# Whatever the error was, it is already expired or tried too much. No
|
628
|
+
# need to try to recover.
|
629
|
+
|
630
|
+
debug "Error code #{parser} but transaction already expired or too many tries, giving up (attempt: #{tries})."
|
631
|
+
elsif parser.can_retry?
|
632
|
+
drop_current_failover_url method_name if !!exp && parser.expiry?
|
633
|
+
debug "Error code #{parser} (attempt: #{tries}), retrying ..."
|
634
|
+
return nil
|
635
|
+
end
|
636
|
+
|
637
|
+
Hashie::Mash.new(response)
|
638
|
+
end
|
639
|
+
|
640
|
+
def healthy?(url)
|
641
|
+
begin
|
642
|
+
# Note, not all nodes support the /health uri. But even if they don't,
|
643
|
+
# they'll respond status code 200 OK, even if the body shows an error.
|
644
|
+
|
645
|
+
# But if the node supports the /health uri, it will do additional
|
646
|
+
# verifications on the block height.
|
647
|
+
# See: https://github.com/steemit/steem/blob/master/contrib/healthcheck.sh
|
648
|
+
|
649
|
+
# Also note, this check is done **without** net-http-persistent.
|
650
|
+
|
651
|
+
!!open(url + HEALTH_URI)
|
652
|
+
rescue => e
|
653
|
+
error "Health check failure for #{url}: #{e.inspect}"
|
654
|
+
sleep 0.2
|
655
|
+
false
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
628
659
|
def backoff
|
629
660
|
shutdown
|
630
|
-
bump_failover if flappy?
|
661
|
+
bump_failover if flappy? || !healthy?(@url)
|
631
662
|
@backoff_at ||= Time.now
|
632
663
|
@backoff_sleep ||= 0.01
|
633
664
|
|
@@ -639,18 +670,5 @@ module Radiator
|
|
639
670
|
@backoff_sleep = nil
|
640
671
|
end
|
641
672
|
end
|
642
|
-
|
643
|
-
def send_log(level, message, prefix = nil)
|
644
|
-
if !!prefix
|
645
|
-
@logger.send level, "#{prefix} :: #{message}"
|
646
|
-
else
|
647
|
-
@logger.send level, "#{message}"
|
648
|
-
end
|
649
|
-
|
650
|
-
nil
|
651
|
-
end
|
652
|
-
|
653
|
-
def error(message, prefix = nil); send_log(:error, message, prefix); end
|
654
|
-
def warning(message, prefix = nil); send_log(:warn, message, prefix); end
|
655
673
|
end
|
656
674
|
end
|
data/lib/radiator/base_error.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
module Radiator
|
2
2
|
class BaseError < StandardError
|
3
|
-
def initialize(error)
|
3
|
+
def initialize(error, cause = nil)
|
4
4
|
@error = error
|
5
|
+
@cause = cause
|
5
6
|
end
|
6
7
|
|
7
8
|
def to_s
|
8
|
-
|
9
|
+
if !!@cause
|
10
|
+
JSON[error: @error, cause: @cause] rescue {error: @error, cause: @cause}.to_s
|
11
|
+
else
|
12
|
+
JSON[@error] rescue @error
|
13
|
+
end
|
9
14
|
end
|
10
15
|
end
|
11
16
|
end
|
@@ -8,7 +8,7 @@ module Radiator
|
|
8
8
|
NETWORKS_STEEM_CORE_ASSET = 'STEEM'
|
9
9
|
NETWORKS_STEEM_DEBT_ASSET = 'SBD'
|
10
10
|
NETWORKS_STEEM_VEST_ASSET = 'VESTS'
|
11
|
-
NETWORKS_STEEM_DEFAULT_NODE = 'https://
|
11
|
+
NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steemit.com'
|
12
12
|
|
13
13
|
NETWORKS_GOLOS_CHAIN_ID = '782a3039b478c839e4cb0c941ff4eaeb7df40bdd68bd441afd444b9da763de12'
|
14
14
|
NETWORKS_GOLOS_ADDRESS_PREFIX = 'GLS'
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Radiator
|
2
|
+
class ErrorParser
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
attr_reader :response, :error_code, :error_message,
|
6
|
+
:api_name, :api_method, :api_params,
|
7
|
+
:expiry, :can_retry, :can_reprepare, :debug
|
8
|
+
|
9
|
+
alias expiry? expiry
|
10
|
+
alias can_retry? can_retry
|
11
|
+
alias can_reprepare? can_reprepare
|
12
|
+
|
13
|
+
REPREPARE = [
|
14
|
+
'is_canonical( c ): signature is not canonical',
|
15
|
+
'now < trx.expiration: ',
|
16
|
+
'(skip & skip_transaction_dupe_check) || trx_idx.indices().get<by_trx_id>().find(trx_id) == trx_idx.indices().get<by_trx_id>().end(): Duplicate transaction check failed'
|
17
|
+
]
|
18
|
+
|
19
|
+
def initialize(response)
|
20
|
+
@response = response
|
21
|
+
|
22
|
+
@error_code = nil
|
23
|
+
@error_message = nil
|
24
|
+
@api_name = nil
|
25
|
+
@api_method = nil
|
26
|
+
@api_params = nil
|
27
|
+
|
28
|
+
@expiry = nil
|
29
|
+
@can_retry = nil
|
30
|
+
@can_reprepare = nil
|
31
|
+
@debug = nil
|
32
|
+
|
33
|
+
parse_error_response
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_error_response
|
37
|
+
return if response.nil?
|
38
|
+
|
39
|
+
@error_code = response['error']['data']['code']
|
40
|
+
stacks = response['error']['data']['stack']
|
41
|
+
stack_formats = stacks.map { |s| s['format'] }
|
42
|
+
stack_datum = stacks.map { |s| s['data'] }
|
43
|
+
data_call_method = stack_datum.find { |data| data['call.method'] == 'call' }
|
44
|
+
|
45
|
+
@error_message = stack_formats.reject(&:empty?).join('; ')
|
46
|
+
|
47
|
+
@api_name, @api_method, @api_params = if !!data_call_method
|
48
|
+
@api_name = data_call_method['call.params']
|
49
|
+
end
|
50
|
+
|
51
|
+
case @error_code
|
52
|
+
when 10
|
53
|
+
@expiry = false
|
54
|
+
@can_retry = false
|
55
|
+
@can_reprepare = if @api_name == 'network_broadcast_api'
|
56
|
+
(stack_formats & REPREPARE).any?
|
57
|
+
else
|
58
|
+
false
|
59
|
+
end
|
60
|
+
when 4030100
|
61
|
+
# Code 4030100 is "transaction_expiration_exception: transaction
|
62
|
+
# expiration exception". If we assume the expiration was valid, the
|
63
|
+
# node might be bad and needs to be dropped.
|
64
|
+
|
65
|
+
@expiry = true
|
66
|
+
@can_retry = true
|
67
|
+
@can_reprepare = false
|
68
|
+
when 4030200
|
69
|
+
# Code 4030200 is "transaction tapos exception". They are recoverable
|
70
|
+
# if the transaction hasn't expired yet. A tapos exception can be
|
71
|
+
# retried in situations where the node is behind and the tapos is
|
72
|
+
# based on a block the node doesn't know about yet.
|
73
|
+
|
74
|
+
@expiry = false
|
75
|
+
@can_retry = true
|
76
|
+
|
77
|
+
# Allow fall back to reprepare if retry fails.
|
78
|
+
@can_reprepare = true
|
79
|
+
else
|
80
|
+
@expiry = false
|
81
|
+
@can_retry = false
|
82
|
+
@can_reprepare = false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
if !!error_message && !error_message.empty?
|
88
|
+
"#{error_code}: #{error_message}"
|
89
|
+
else
|
90
|
+
error_code.to_s
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/radiator/stream.rb
CHANGED
data/lib/radiator/transaction.rb
CHANGED
@@ -27,12 +27,13 @@ module Radiator
|
|
27
27
|
|
28
28
|
@logger = options[:logger] || Radiator.logger
|
29
29
|
@chain ||= :steem
|
30
|
+
@chain = @chain.to_sym
|
30
31
|
@chain_id = chain_id options[:chain_id]
|
31
32
|
@url = options[:url] || url
|
32
33
|
@operations = options[:operations] || []
|
33
34
|
|
34
35
|
unless NETWORK_CHAIN_IDS.include? @chain_id
|
35
|
-
|
36
|
+
warning "Unknown chain id: #{@chain_id}"
|
36
37
|
end
|
37
38
|
|
38
39
|
if !!wif && !!private_key
|
@@ -70,7 +71,21 @@ module Radiator
|
|
70
71
|
prepare
|
71
72
|
|
72
73
|
if broadcast
|
73
|
-
|
74
|
+
loop do
|
75
|
+
response = @network_broadcast_api.broadcast_transaction_synchronous(payload)
|
76
|
+
|
77
|
+
if !!response.error
|
78
|
+
parser = ErrorParser.new(response)
|
79
|
+
|
80
|
+
if parser.can_reprepare?
|
81
|
+
debug "Repreparing transaction ..."
|
82
|
+
prepare
|
83
|
+
redo
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
return response
|
88
|
+
end
|
74
89
|
else
|
75
90
|
self
|
76
91
|
end
|
@@ -99,19 +114,56 @@ module Radiator
|
|
99
114
|
def prepare
|
100
115
|
raise TransactionError, "No wif or private key." unless !!@wif || !!@private_key
|
101
116
|
|
102
|
-
@
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
117
|
+
@api.get_dynamic_global_properties do |properties, error|
|
118
|
+
if !!error
|
119
|
+
raise TransactionError, "Unable to prepare transaction.", error
|
120
|
+
end
|
121
|
+
|
122
|
+
@properties = properties
|
123
|
+
|
124
|
+
case @chain
|
125
|
+
when :steem, :test
|
126
|
+
# You can actually go back as far as the TaPoS buffer will allow, which
|
127
|
+
# is something like 50,000 blocks.
|
128
|
+
|
129
|
+
block_number = @properties.last_irreversible_block_num
|
130
|
+
|
131
|
+
@api.get_block(block_number) do |block, error|
|
132
|
+
if !!error
|
133
|
+
raise TransactionError, "Unable to prepare transaction.", error
|
134
|
+
end
|
135
|
+
|
136
|
+
if block.nil?
|
137
|
+
raise TransactionError, "Unable to prepare transaction, block missing."
|
138
|
+
end
|
139
|
+
|
140
|
+
if block.block_id.nil?
|
141
|
+
raise TransactionError, "Unable to prepare transaction, block.block_id missing."
|
142
|
+
end
|
143
|
+
|
144
|
+
@ref_block_num = block_number & 0xFFFF
|
145
|
+
@ref_block_prefix = unhexlify(block.block_id[8..-1]).unpack('V*')[0]
|
146
|
+
end
|
147
|
+
when :golos
|
148
|
+
# No support for block_id in get_block on golos (yet), so just use the
|
149
|
+
# head block number.
|
150
|
+
|
151
|
+
@ref_block_num = @properties.head_block_number & 0xFFFF
|
152
|
+
@ref_block_prefix = unhexlify(@properties.head_block_id[8..-1]).unpack('V*')[0]
|
153
|
+
else
|
154
|
+
raise TransactionError, "Unable to prepare transaction, unsupported chain: #{@chain}"
|
155
|
+
end
|
156
|
+
|
157
|
+
# The expiration allows for transactions to expire if they are not
|
158
|
+
# included into a block by that time. Always update it to the current
|
159
|
+
# time + EXPIRE_IN_SECS.
|
160
|
+
#
|
161
|
+
# Note, as of #1215, expiration exactly 'now' will be rejected:
|
162
|
+
# https://github.com/steemit/steem/blob/57451b80d2cf480dcce9b399e48e56aa7af1d818/libraries/chain/database.cpp#L2870
|
163
|
+
# https://github.com/steemit/steem/issues/1215
|
164
|
+
|
165
|
+
@expiration = Time.parse(@properties.time + 'Z') + EXPIRE_IN_SECS
|
166
|
+
end
|
115
167
|
|
116
168
|
self
|
117
169
|
end
|
data/lib/radiator/utils.rb
CHANGED
@@ -1,5 +1,59 @@
|
|
1
1
|
module Radiator
|
2
2
|
module Utils
|
3
|
+
def extract_signatures(options)
|
4
|
+
params = options[:params]
|
5
|
+
|
6
|
+
signatures = params.map do |param|
|
7
|
+
next unless defined? param.map
|
8
|
+
|
9
|
+
param.map do |tx|
|
10
|
+
tx[:signatures] rescue nil
|
11
|
+
end
|
12
|
+
end.flatten.compact
|
13
|
+
|
14
|
+
expirations = params.map do |param|
|
15
|
+
next unless defined? param.map
|
16
|
+
|
17
|
+
param.map do |tx|
|
18
|
+
Time.parse(tx[:expiration] + 'Z') rescue nil
|
19
|
+
end
|
20
|
+
end.flatten.compact
|
21
|
+
|
22
|
+
[signatures, expirations.min]
|
23
|
+
end
|
24
|
+
|
25
|
+
def send_log(level, message, prefix = nil)
|
26
|
+
log_message = if !!prefix
|
27
|
+
"#{prefix} :: #{message}"
|
28
|
+
else
|
29
|
+
message
|
30
|
+
end
|
31
|
+
|
32
|
+
if !!@logger
|
33
|
+
@logger.send level, log_message
|
34
|
+
else
|
35
|
+
puts "#{level}: #{log_message}"
|
36
|
+
end
|
37
|
+
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def error(message, prefix = nil)
|
42
|
+
send_log(:error, message, prefix)
|
43
|
+
end
|
44
|
+
|
45
|
+
def warning(message, prefix = nil, log_debug_node = false)
|
46
|
+
debug("Current node: #{@url}", prefix) if !!log_debug_node && @url
|
47
|
+
|
48
|
+
send_log(:warn, message, prefix)
|
49
|
+
end
|
50
|
+
|
51
|
+
def debug(message, prefix = nil)
|
52
|
+
if ENV['LOG'] == 'DEBUG'
|
53
|
+
send_log(:debug, message, prefix)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
3
57
|
def hexlify(s)
|
4
58
|
a = []
|
5
59
|
if s.respond_to? :each_byte
|
data/lib/radiator/version.rb
CHANGED
data/lib/radiator.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: radiator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anthony Martin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-10-
|
11
|
+
date: 2017-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -335,6 +335,7 @@ files:
|
|
335
335
|
- lib/radiator/chain_stats_api.rb
|
336
336
|
- lib/radiator/condenser_api.rb
|
337
337
|
- lib/radiator/database_api.rb
|
338
|
+
- lib/radiator/error_parser.rb
|
338
339
|
- lib/radiator/follow_api.rb
|
339
340
|
- lib/radiator/logger.rb
|
340
341
|
- lib/radiator/market_history_api.rb
|
@@ -379,7 +380,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
379
380
|
version: '0'
|
380
381
|
requirements: []
|
381
382
|
rubyforge_project:
|
382
|
-
rubygems_version: 2.6.
|
383
|
+
rubygems_version: 2.6.14
|
383
384
|
signing_key:
|
384
385
|
specification_version: 4
|
385
386
|
summary: STEEM RPC Ruby Client
|