radiator 0.3.4 → 0.3.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|