meeseeker 0.0.8 → 2.0.1.pre.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.
@@ -32,7 +32,7 @@ when 'sync', 'witness:schedule'
32
32
  Rake::Task['sync'].reenable
33
33
  end; end
34
34
  when 'find' then Rake::Task['find'].invoke(*ARGV[1..-1])
35
- when 'reset' then Rake::Task['reset'].invoke
35
+ when 'reset' then Rake::Task['reset'].invoke(*ARGV[1..-1])
36
36
  else
37
37
  puts "\nBegin/resume sync:"
38
38
  puts "\t#{filename} sync [chain] [block_num]\n\n"
@@ -44,7 +44,7 @@ else
44
44
  puts "\t#{filename} find block 3044538"
45
45
  puts "\t#{filename} find trx 983c5e5c6aef52f1647d952a18771f76b885e6de\n\n"
46
46
  puts 'Clear all keys:'
47
- puts "\t#{filename} reset\n\n"
47
+ puts "\t#{filename} reset [chain]\n\n"
48
48
  puts "Note, using find and reset is not intended for routine operations. It is intended for analysis only."
49
49
  puts "See: https://github.com/inertia186/meeseeker#usage"
50
50
  end
@@ -1,28 +1,200 @@
1
1
  require 'redis'
2
2
  require 'steem'
3
+ require 'hive'
3
4
 
4
5
  require 'meeseeker/version'
5
6
  require 'meeseeker/block_follower_job'
6
7
  require 'meeseeker/witness_schedule_job'
7
8
  require 'meeseeker/steem_engine/agent'
8
9
  require 'meeseeker/steem_engine/follower_job'
10
+ require 'meeseeker/hive_engine'
9
11
 
10
12
  module Meeseeker
11
- LAST_BLOCK_NUM_KEY = 'steem:meeseeker:last_block_num'
12
- LAST_STEEM_ENGINE_BLOCK_NUM_KEY = 'steem_engine:meeseeker:last_block_num'
13
+ STEEM_CHAIN_ID = '0000000000000000000000000000000000000000000000000000000000000000'
14
+ HIVE_LEGACY_CHAIN_ID = '0000000000000000000000000000000000000000000000000000000000000000'
15
+ HIVE_CHAIN_ID = 'beeab0de00000000000000000000000000000000000000000000000000000000'
16
+ STEEM_CHAIN_KEY_PREFIX = 'steem'
17
+ HIVE_CHAIN_KEY_PREFIX = 'hive'
18
+ STEEM_ENGINE_CHAIN_KEY_PREFIX = 'steem_engine'
19
+ HIVE_ENGINE_CHAIN_KEY_PREFIX = 'hive_engine'
20
+ LAST_BLOCK_NUM_KEY_SUFFIX = ':meeseeker:last_block_num'
21
+ LAST_STEEM_ENGINE_BLOCK_NUM_KEY_SUFFIX = ':meeseeker:last_block_num'
13
22
  BLOCKS_PER_DAY = 28800
14
23
  VIRTUAL_TRX_ID = '0000000000000000000000000000000000000000'
24
+ BLOCK_INTERVAL = 3
25
+ SHUFFLE_URL = 'shuffle'
26
+ DEFAULT_STEEM_URL = 'https://api.steemit.com'
27
+ DEFAULT_STEEM_FAILOVER_URLS = [
28
+ DEFAULT_STEEM_URL,
29
+ # 'https://steemd.minnowsupportproject.org',
30
+ # 'https://anyx.io',
31
+ # 'http://anyx.io',
32
+ # 'https://steemd.privex.io',
33
+ # 'https://api.steem.house'
34
+ ]
35
+ DEFAULT_HIVE_URL = 'https://api.openhive.network'
36
+ DEFAULT_HIVE_FAILOVER_URLS = [
37
+ DEFAULT_HIVE_URL,
38
+ 'https://api.hivekings.com',
39
+ 'https://anyx.io',
40
+ 'http://anyx.io',
41
+ 'https://techcoderx.com',
42
+ 'https://rpc.esteem.app',
43
+ 'https://hived.privex.io',
44
+ 'https://api.pharesim.me',
45
+ 'https://api.hive.blog',
46
+ 'https://rpc.ausbit.dev'
47
+ ]
48
+
49
+ def default_chain_key_prefix
50
+ ENV.fetch('MEESEEKER_CHAIN_KEY_PREFIX', chain_key_prefix)
51
+ end
52
+
53
+ def self.chain_key_prefix
54
+ @chain_key_prefix ||= {}
55
+ url = default_url(HIVE_CHAIN_KEY_PREFIX)
56
+
57
+ return @chain_key_prefix[url] if !!@chain_key_prefix[url]
58
+
59
+ # Just use the Hive API for either chain, until we know which one we're
60
+ # using.
61
+ api = Hive::DatabaseApi.new(url: url)
62
+
63
+ api.get_config do |config|
64
+ @chain_key_prefix[node_url] = if !!config.HIVE_CHAIN_ID && config.HIVE_CHAIN_ID == HIVE_CHAIN_ID
65
+ HIVE_CHAIN_KEY_PREFIX
66
+ elsif !!config.HIVE_CHAIN_ID && config.HIVE_CHAIN_ID == HIVE_LEGACY_CHAIN_ID
67
+ HIVE_CHAIN_KEY_PREFIX
68
+ elsif !!config.STEEM_CHAIN_ID && config.STEEM_CHAIN_ID == STEEM_CHAIN_ID
69
+ STEEM_CHAIN_KEY_PREFIX
70
+ else
71
+ config.keys.find{|k| k.end_with? '_CHAIN_ID'}.split('_').first.downcase.tap do |guess|
72
+ warn "Guessing chain_key_prefix = '#{guess}' for unknown chain on: #{node_url}"
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def self.default_url(chain = default_chain_key_prefix)
79
+ ENV.fetch('MEESEEKER_NODE_URL') do
80
+ case chain.to_s
81
+ when STEEM_CHAIN_KEY_PREFIX then DEFAULT_STEEM_URL
82
+ when HIVE_CHAIN_KEY_PREFIX then DEFAULT_HIVE_URL
83
+ else
84
+ raise "Unknown chain: #{chain}"
85
+ end
86
+ end
87
+ end
88
+
89
+ @problem_node_urls = []
90
+
15
91
  @redis = Redis.new(url: ENV.fetch('MEESEEKER_REDIS_URL', 'redis://127.0.0.1:6379/0'))
16
- @node_url = ENV.fetch('MEESEEKER_NODE_URL', 'https://api.steemit.com')
92
+ @node_url = default_url(ENV.fetch('MEESEEKER_CHAIN_KEY_PREFIX', HIVE_CHAIN_KEY_PREFIX))
17
93
  @steem_engine_node_url = ENV.fetch('MEESEEKER_STEEM_ENGINE_NODE_URL', 'https://api.steem-engine.com/rpc')
94
+ @hive_engine_node_url = ENV.fetch('MEESEEKER_HIVE_ENGINE_NODE_URL', 'https://api.hive-engine.com/rpc')
18
95
  @stream_mode = ENV.fetch('MEESEEKER_STREAM_MODE', 'head').downcase.to_sym
19
96
  @include_virtual = ENV.fetch('MEESEEKER_INCLUDE_VIRTUAL', 'true').downcase == 'true'
20
97
  @include_block_header = ENV.fetch('MEESEEKER_INCLUDE_BLOCK_HEADER', 'true').downcase == 'true'
21
98
  @publish_op_custom_id = ENV.fetch('MEESEEKER_PUBLISH_OP_CUSTOM_ID', 'false').downcase == 'true'
22
- @expire_keys = ENV.fetch('MEESEEKER_EXPIRE_KEYS', BLOCKS_PER_DAY * 3).to_i
99
+ @expire_keys = ENV.fetch('MEESEEKER_EXPIRE_KEYS', BLOCKS_PER_DAY * BLOCK_INTERVAL).to_i
100
+ @max_keys = ENV.fetch('MEESEEKER_MAX_KEYS', '-1').to_i
23
101
 
24
102
  extend self
25
103
 
26
- attr_accessor :redis, :node_url, :steem_engine_node_url, :expire_keys,
27
- :stream_mode, :include_virtual, :include_block_header, :publish_op_custom_id
104
+ attr_accessor :redis, :node_url, :steem_engine_node_url,
105
+ :hive_engine_node_url, :expire_keys, :max_keys, :stream_mode,
106
+ :include_virtual, :include_block_header, :publish_op_custom_id
107
+
108
+ def self.shuffle_node_url(chain = ENV.fetch('MEESEEKER_CHAIN_KEY_PREFIX', HIVE_CHAIN_KEY_PREFIX))
109
+ chain = chain.to_s
110
+ node_url = ENV.fetch('MEESEEKER_NODE_URL', default_url(ENV.fetch('MEESEEKER_CHAIN_KEY_PREFIX', chain)))
111
+ return node_url unless node_url == SHUFFLE_URL
112
+
113
+ @problem_node_urls = [] if rand(1..1000) == 13
114
+ shuffle_node_url!(chain)
115
+ end
116
+
117
+ def self.api_class(chain = default_chain_key_prefix)
118
+ case chain.to_s
119
+ when STEEM_CHAIN_KEY_PREFIX then Steem::Api
120
+ when HIVE_CHAIN_KEY_PREFIX then Hive::Api
121
+ else
122
+ raise "Unknown chain: #{chain}"
123
+ end
124
+ end
125
+
126
+ def self.condenser_api_class(chain = default_chain_key_prefix)
127
+ case chain.to_s
128
+ when STEEM_CHAIN_KEY_PREFIX then Steem::CondenserApi
129
+ when HIVE_CHAIN_KEY_PREFIX then Hive::CondenserApi
130
+ else
131
+ raise "Unknown chain: #{chain}"
132
+ end
133
+ end
134
+
135
+ def self.block_api_class(chain = default_chain_key_prefix)
136
+ case chain.to_s
137
+ when STEEM_CHAIN_KEY_PREFIX then Steem::BlockApi
138
+ when HIVE_CHAIN_KEY_PREFIX then Hive::BlockApi
139
+ else
140
+ raise "Unknown chain: #{chain}"
141
+ end
142
+ end
143
+
144
+ def self.database_api_class(chain = default_chain_key_prefix)
145
+ case chain.to_s
146
+ when STEEM_CHAIN_KEY_PREFIX then Steem::DatabaseApi
147
+ when HIVE_CHAIN_KEY_PREFIX then Hive::DatabaseApi
148
+ else
149
+ raise "Unknown chain: #{chain}"
150
+ end
151
+ end
152
+
153
+ def self.stream_class(chain = default_chain_key_prefix)
154
+ case chain.to_s
155
+ when STEEM_CHAIN_KEY_PREFIX then Steem::Stream
156
+ when HIVE_CHAIN_KEY_PREFIX then Hive::Stream
157
+ else
158
+ raise "Unknown chain: #{chain}"
159
+ end
160
+ end
161
+
162
+ def self.shuffle_node_url!(chain = ENV.fetch('MEESEEKER_CHAIN_KEY_PREFIX', HIVE_CHAIN_KEY_PREFIX))
163
+ chain = chain.to_s
164
+ failover_urls = case chain
165
+ when STEEM_CHAIN_KEY_PREFIX then DEFAULT_STEEM_FAILOVER_URLS - @problem_node_urls
166
+ when HIVE_CHAIN_KEY_PREFIX then DEFAULT_HIVE_FAILOVER_URLS - @problem_node_urls
167
+ else; []
168
+ end
169
+ url = failover_urls.sample
170
+ api = api_class(chain).new(url: url)
171
+
172
+ api.get_accounts(['fullnodeupdate']) do |accounts|
173
+ fullnodeupdate = accounts.first
174
+ metadata = (JSON[fullnodeupdate.json_metadata] rescue nil) || {}
175
+
176
+ nodes = metadata.fetch('report', []).map do |report|
177
+ next if chain == HIVE_CHAIN_KEY_PREFIX && !report[HIVE_CHAIN_KEY_PREFIX]
178
+ next if chain != HIVE_CHAIN_KEY_PREFIX && !!report[HIVE_CHAIN_KEY_PREFIX]
179
+
180
+ report['node']
181
+ end.compact.uniq
182
+
183
+ nodes -= @problem_node_urls
184
+
185
+ if nodes.any?
186
+ nodes.sample
187
+ else
188
+ @node_url = failover_urls.sample
189
+ end
190
+ end
191
+ rescue => e
192
+ puts "#{url}: #{e}"
193
+
194
+ @problem_node_urls << url
195
+ failover_urls -= @problem_node_urls
196
+ failover_urls.sample
197
+ end
198
+
199
+ shuffle_node_url! if @node_url == SHUFFLE_URL
28
200
  end
@@ -1,18 +1,24 @@
1
+ require 'radiator'
2
+
1
3
  module Meeseeker
2
4
  class BlockFollowerJob
3
5
  MAX_VOP_RETRY = 3
4
6
 
5
7
  def perform(options = {})
6
- block_api = Steem::BlockApi.new(url: Meeseeker.node_url)
8
+ chain = (options[:chain] || 'hive').to_sym
9
+ url = Meeseeker.default_url(chain)
10
+ block_api = Meeseeker.block_api_class(chain).new(url: url)
7
11
  redis = Meeseeker.redis
8
12
  last_key_prefix = nil
9
13
  trx_index = 0
10
14
  current_block_num = nil
11
15
  block_transactions = []
16
+ chain_key_prefix = chain.to_s if !!options[:chain]
17
+ chain_key_prefix ||= Meeseeker.default_chain_key_prefix
12
18
 
13
19
  stream_operations(options) do |op, trx_id, block_num|
14
20
  begin
15
- current_key_prefix = "steem:#{block_num}:#{trx_id}"
21
+ current_key_prefix = "#{chain_key_prefix}:#{block_num}:#{trx_id}"
16
22
 
17
23
  if current_key_prefix == last_key_prefix
18
24
  trx_index += 1
@@ -26,17 +32,28 @@ module Meeseeker
26
32
  }
27
33
 
28
34
  block_transactions << trx_id unless trx_id == VIRTUAL_TRX_ID
29
- redis.publish('steem:transaction', transaction_payload.to_json)
35
+ redis.publish("#{chain_key_prefix}:transaction", transaction_payload.to_json)
30
36
  end
31
- last_key_prefix = "steem:#{block_num}:#{trx_id}"
37
+ last_key_prefix = "#{chain_key_prefix}:#{block_num}:#{trx_id}"
32
38
  trx_index = 0
33
39
  end
34
40
 
35
- op_type = op.type.split('_')[0..-2].join('_')
41
+ op_type = if op.type.end_with? '_operation'
42
+ op.type.split('_')[0..-2].join('_')
43
+ else
44
+ op.type
45
+ end
46
+
36
47
  key = "#{current_key_prefix}:#{trx_index}:#{op_type}"
37
48
  puts key
38
49
  end
39
50
 
51
+ unless Meeseeker.max_keys == -1
52
+ while redis.keys("#{chain_key_prefix}:*").size > Meeseeker.max_keys
53
+ sleep Meeseeker::BLOCK_INTERVAL
54
+ end
55
+ end
56
+
40
57
  redis.set(key, op.to_json)
41
58
  redis.expire(key, Meeseeker.expire_keys) unless Meeseeker.expire_keys == -1
42
59
 
@@ -49,26 +66,30 @@ module Meeseeker
49
66
  if Meeseeker.include_block_header
50
67
  catch :block_header do
51
68
  block_api.get_block_header(block_num: block_num) do |result|
52
- throw :block_header if result.nil || result.header.nil?
69
+ if result.nil? || result.header.nil?
70
+ puts "Node returned empty result for block_header on block_num: #{block_num} (rate limiting?). Retrying ..."
71
+ sleep Meeseeker::BLOCK_INTERVAL
72
+ throw :block_header
73
+ end
53
74
 
54
75
  block_payload.merge!(result.header.to_h)
55
76
  end
56
77
  end
57
78
  end
58
79
 
59
- redis.set(LAST_BLOCK_NUM_KEY, block_num)
60
- redis.publish('steem:block', block_payload.to_json)
80
+ redis.set(chain_key_prefix + LAST_BLOCK_NUM_KEY_SUFFIX, block_num)
81
+ redis.publish("#{chain_key_prefix}:block", block_payload.to_json)
61
82
  current_block_num = block_num
62
83
  end
63
84
 
64
- redis.publish("steem:op:#{op_type}", {key: key}.to_json)
85
+ redis.publish("#{chain_key_prefix}:op:#{op_type}", {key: key}.to_json)
65
86
 
66
87
  if Meeseeker.publish_op_custom_id
67
88
  if %w(custom custom_binary custom_json).include? op_type
68
89
  id = (op["value"]["id"] rescue nil).to_s
69
90
 
70
91
  if id.size > 0
71
- redis.publish("steem:op:#{op_type}:#{id}", {key: key}.to_json)
92
+ redis.publish("#{chain_key_prefix}:op:#{op_type}:#{id}", {key: key}.to_json)
72
93
  end
73
94
  end
74
95
  end
@@ -76,7 +97,10 @@ module Meeseeker
76
97
  end
77
98
  private
78
99
  def stream_operations(options = {}, &block)
100
+ chain = (options[:chain] || 'hive').to_sym
79
101
  redis = Meeseeker.redis
102
+ chain_key_prefix = chain.to_s if !!options[:chain]
103
+ chain_key_prefix ||= Meeseeker.chain_key_prefix
80
104
  last_block_num = nil
81
105
  mode = options.delete(:mode) || Meeseeker.stream_mode
82
106
  options[:include_virtual] ||= Meeseeker.include_virtual
@@ -84,8 +108,9 @@ module Meeseeker
84
108
  if !!options[:at_block_num]
85
109
  last_block_num = options[:at_block_num].to_i
86
110
  else
87
- database_api = Steem::DatabaseApi.new(url: Meeseeker.node_url)
88
- last_block_num = redis.get(LAST_BLOCK_NUM_KEY).to_i + 1
111
+ url = Meeseeker.default_url(chain)
112
+ database_api = Meeseeker.database_api_class(chain).new(url: url)
113
+ last_block_num = redis.get(chain_key_prefix + LAST_BLOCK_NUM_KEY_SUFFIX).to_i + 1
89
114
 
90
115
  block_num = catch :dynamic_global_properties do
91
116
  database_api.get_dynamic_global_properties do |dgpo|
@@ -116,11 +141,12 @@ module Meeseeker
116
141
  end
117
142
 
118
143
  begin
119
- stream_options = {url: Meeseeker.node_url, mode: mode}
120
- options = options.merge(at_block_num: last_block_num)
144
+ url = Meeseeker.default_url(chain)
145
+ stream_options = {chain: chain, url: url}
146
+ stream_args = [last_block_num, mode]
121
147
  condenser_api = nil
122
148
 
123
- Steem::Stream.new(stream_options).tap do |stream|
149
+ Radiator::Stream.new(stream_options).tap do |stream|
124
150
  puts "Stream begin: #{stream_options.to_json}; #{options.to_json}"
125
151
 
126
152
  # Prior to v0.0.4, we only streamed operations with stream.operations.
@@ -129,67 +155,89 @@ module Meeseeker
129
155
  # to embed it into op values. This should also reduce streaming
130
156
  # overhead since we no longer stream block_headers inder the hood.
131
157
 
132
- stream.blocks(options) do |b, n|
133
- b.transactions.each_with_index do |transaction, index|
134
- transaction.operations.each do |op|
135
- op = op.merge(timestamp: b.timestamp)
136
-
137
- yield op, b.transaction_ids[index], n
138
- end
139
- end
140
-
141
- next unless !!Meeseeker.include_virtual
142
-
143
- retries = 0
144
-
145
- # This is where it gets tricky. Virtual ops sometims don't show up
146
- # right away, especially if we're streaming on head blocks. In that
147
- # situation, we might only need to wait about 1 block. This loop
148
- # will likely one execute one iteration, but we have fallback logic
149
- # in case there are complications.
150
- #
151
- # See: https://developers.steem.io/tutorials-recipes/virtual-operations-when-streaming-blockchain-transactions
152
-
153
- loop do
154
- condenser_api ||= Steem::CondenserApi.new(url: Meeseeker.node_url)
155
- condenser_api.get_ops_in_block(n, true) do |vops|
156
- redo if vops.nil?
158
+ loop do
159
+ begin
160
+ stream.blocks(*stream_args) do |b, n, condenser_api|
161
+ redo if b.nil?
157
162
 
158
- if vops.empty? && mode != :head
159
- # Usually, we just need to slow down to allow virtual ops to
160
- # show up after a short delay. Adding this delay doesn't
161
- # impact overall performance because steem-ruby will batch
162
- # when block streams fall behind.
163
-
164
- if retries < MAX_VOP_RETRY
165
- retries = retries + 1
166
- condenser_api = nil
167
- sleep 3 * retries
163
+ b.transactions.each_with_index do |transaction, index|
164
+ transaction.operations.each do |op|
165
+ op_value = op[1].merge(timestamp: b.timestamp)
168
166
 
169
- redo
167
+ yield Hashie::Mash.new(type: op[0], value: op_value), b.transaction_ids[index], n
170
168
  end
171
-
172
- puts "Gave up retrying virtual ops lookup on block #{n}"
173
-
174
- break
175
169
  end
176
170
 
177
- if retries > 0
178
- puts "Found virtual ops for block #{n} aftere #{retries} retrie(s)"
179
- end
171
+ next unless !!Meeseeker.include_virtual
172
+
173
+ retries = 0
174
+
175
+ # This is where it gets tricky. Virtual ops sometims don't show up
176
+ # right away, especially if we're streaming on head blocks. In that
177
+ # situation, we might only need to wait about 1 block. This loop
178
+ # will likely one execute one iteration, but we have fallback logic
179
+ # in case there are complications.
180
+ #
181
+ # See: https://developers.steem.io/tutorials-recipes/virtual-operations-when-streaming-blockchain-transactions
180
182
 
181
- vops.each do |vop|
182
- normalized_op = Hashie::Mash.new(
183
- type: vop.op[0],
184
- value: vop.op[1],
185
- timestamp: vop.timestamp
186
- )
183
+ loop do
184
+ # TODO (HF23) Switch to account_history_api.enum_virtual_ops if supported.
185
+ url = Meeseeker.default_url(chain)
186
+ condenser_api ||= Meeseeker.condenser_api_class(chain).new(url: url)
187
+ condenser_api.get_ops_in_block(n, true) do |vops|
188
+ if vops.nil?
189
+ puts "Node returned empty result for get_ops_in_block on block_num: #{n} (rate limiting?). Retrying ..."
190
+ vops = []
191
+ end
192
+
193
+ if vops.empty? && mode != :head
194
+ # Usually, we just need to slow down to allow virtual ops to
195
+ # show up after a short delay. Adding this delay doesn't
196
+ # impact overall performance because steem-ruby will batch
197
+ # when block streams fall behind.
198
+
199
+ if retries < MAX_VOP_RETRY
200
+ retries = retries + 1
201
+ condenser_api = nil
202
+ sleep Meeseeker::BLOCK_INTERVAL * retries
203
+
204
+ redo
205
+ end
206
+
207
+ puts "Gave up retrying virtual ops lookup on block #{n}"
208
+
209
+ break
210
+ end
211
+
212
+ if retries > 0
213
+ puts "Found virtual ops for block #{n} aftere #{retries} retrie(s)"
214
+ end
215
+
216
+ vops.each do |vop|
217
+ normalized_op = Hashie::Mash.new(
218
+ type: vop.op[0],
219
+ value: vop.op[1],
220
+ timestamp: vop.timestamp
221
+ )
222
+
223
+ yield normalized_op, vop.trx_id, vop.block
224
+ end
225
+ end
187
226
 
188
- yield normalized_op, vop.trx_id, vop.block
227
+ break
189
228
  end
190
229
  end
191
230
 
192
231
  break
232
+ rescue => e
233
+ raise e unless e.to_s.include? 'Request Entity Too Large'
234
+
235
+ # We need to tell steem-ruby to avoid json-rpc-batch on this
236
+ # node.
237
+
238
+ Meeseeker.block_api_class(chain).const_set 'MAX_RANGE_SIZE', 1
239
+ sleep Meeseeker::BLOCK_INTERVAL
240
+ redo
193
241
  end
194
242
  end
195
243
  end