throttled_json_rpc_client 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -15
- data/example.rb +7 -4
- data/lib/json_rpc_client.rb +97 -0
- data/lib/limiter/distributed_rate_queue.rb +1 -0
- data/lib/throttled_json_rpc_client/version.rb +1 -1
- data/lib/throttled_json_rpc_client.rb +75 -7
- metadata +18 -5
- data/lib/json_rpc_client/eth.rb +0 -61
- data/lib/throttled_json_rpc_client/eth.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 59ebf0f85dc101410fcd0a3a665777ed1ffbae6e662e895acf7e338756cb8155
|
4
|
+
data.tar.gz: 58cee4e703c32b3a771b90bb5adc801f96d22b4359ff730ecac9820e8afaab91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f6491a2d5685dae9e50c264271b6c07cb8701cb87c5b65709dfbcc4f594e3be7059deab89cc450a61402a32f7bd1a488dbd6f777bb189066ddc55d2600144a98
|
7
|
+
data.tar.gz: 237c09309c9f13de9b72660af01b8266ebc1f78deb9f31de11c71a7fba263e57611a503c19910091c5333e90bf11ee76f149f92020d4ed0f5509506dda88b2fb
|
data/README.md
CHANGED
@@ -14,33 +14,23 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
17
|
-
## example1, without rate throttling
|
18
|
-
```ruby
|
19
|
-
rpc_url = "https://1rpc.io/eth"
|
20
|
-
|
21
|
-
eth = JsonRpcClient::Eth.new(rpc_url)
|
22
|
-
|
23
|
-
p eth.block_number
|
24
|
-
```
|
25
|
-
|
26
|
-
## example2, with rate throttling
|
27
|
-
> The rate limit on the server is called Rate Limiting, and the rate limit on the client is called Rate Throttling.
|
28
|
-
|
29
17
|
This rate throttling can be used in multi-threaded, multi-process, multi-machine environments.
|
18
|
+
|
30
19
|
```ruby
|
31
|
-
rpc_url = "https://
|
20
|
+
rpc_url = "https://eth.llamarpc.com"
|
32
21
|
|
33
22
|
# default equals to ThrottledJsonRpcClient::Eth.new(url, rate: 5, interval: 1, redis_urls: ["redis://localhost:6379/2"])
|
34
|
-
|
23
|
+
client = ThrottledJsonRpcClient::Client.new(rpc_url)
|
35
24
|
|
36
25
|
threads = []
|
37
26
|
10.times do
|
38
27
|
threads << Thread.new do
|
39
|
-
p
|
28
|
+
p client.call("eth_getBlockByNumber", ["0x1234", false])
|
40
29
|
end
|
41
30
|
end
|
42
31
|
threads.map(&:join)
|
43
32
|
```
|
33
|
+
see [example](./example.rb)
|
44
34
|
|
45
35
|
## Development
|
46
36
|
|
data/example.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
|
-
rpc_url = "https://
|
1
|
+
rpc_url = "https://eth.llamarpc.com"
|
2
2
|
|
3
|
-
|
3
|
+
# limit: 1 request / 5 seconds
|
4
|
+
client = ThrottledJsonRpcClient::Client.new(
|
4
5
|
rpc_url,
|
5
|
-
|
6
|
+
rate: 1,
|
7
|
+
interval: 5,
|
8
|
+
redis_urls: ["redis://localhost:6379/2"]
|
6
9
|
)
|
7
10
|
|
8
11
|
threads = []
|
9
12
|
10.times do
|
10
13
|
threads << Thread.new do
|
11
|
-
p
|
14
|
+
p client.call("eth_blockNumber", [])
|
12
15
|
end
|
13
16
|
end
|
14
17
|
threads.map(&:join)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "faraday"
|
2
|
+
require "json"
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
class JsonRpcClient
|
6
|
+
class RpcError < StandardError; end
|
7
|
+
|
8
|
+
DEFAULT_MAX_BATCH_SIZE = 100
|
9
|
+
DEFAULT_TIMEOUT = 30
|
10
|
+
|
11
|
+
def initialize(
|
12
|
+
endpoint,
|
13
|
+
timeout: DEFAULT_TIMEOUT,
|
14
|
+
max_batch_size: DEFAULT_MAX_BATCH_SIZE,
|
15
|
+
headers: {}
|
16
|
+
)
|
17
|
+
@endpoint = endpoint
|
18
|
+
@max_batch_size = max_batch_size
|
19
|
+
@timeout = timeout
|
20
|
+
@headers = { "Content-Type" => "application/json" }.merge(headers)
|
21
|
+
@request_id = 0
|
22
|
+
@last_request_time = Time.now
|
23
|
+
@request_times = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(method, params = nil)
|
27
|
+
payload = build_request(method, params)
|
28
|
+
response = make_request(payload)
|
29
|
+
|
30
|
+
raise RpcError, "RPC error: #{response["error"]}" if response["error"]
|
31
|
+
|
32
|
+
response["result"]
|
33
|
+
end
|
34
|
+
|
35
|
+
def batch_call(calls)
|
36
|
+
if calls.size > @max_batch_size
|
37
|
+
raise RpcError,
|
38
|
+
"Batch size #{calls.size} exceeds maximum allowed size of #{@max_batch_size}"
|
39
|
+
end
|
40
|
+
|
41
|
+
results = []
|
42
|
+
batch_payload = calls.map do |method, params|
|
43
|
+
build_request(method, params)
|
44
|
+
end
|
45
|
+
|
46
|
+
responses = make_request(batch_payload)
|
47
|
+
|
48
|
+
# Handle single response case
|
49
|
+
responses = [responses] unless responses.is_a?(Array)
|
50
|
+
|
51
|
+
# Sort responses by id to maintain order
|
52
|
+
sorted_responses = responses.sort_by { |r| r["id"] }
|
53
|
+
|
54
|
+
sorted_responses.each do |response|
|
55
|
+
raise RpcError, "RPC error in batch: #{response["error"]}" if response["error"]
|
56
|
+
|
57
|
+
results << response["result"]
|
58
|
+
end
|
59
|
+
|
60
|
+
results
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def build_request(method, params)
|
66
|
+
@request_id += 1
|
67
|
+
{
|
68
|
+
jsonrpc: "2.0",
|
69
|
+
method: method,
|
70
|
+
params: params || [],
|
71
|
+
id: @request_id
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def make_request(payload)
|
76
|
+
connection = Faraday.new(@endpoint) do |conn|
|
77
|
+
conn.options.timeout = @timeout
|
78
|
+
conn.ssl.verify = false
|
79
|
+
conn.adapter Faraday.default_adapter
|
80
|
+
end
|
81
|
+
|
82
|
+
response = connection.post do |req|
|
83
|
+
req.headers = @headers
|
84
|
+
req.body = payload.to_json
|
85
|
+
end
|
86
|
+
|
87
|
+
raise RpcError, "HTTP error: #{response.status} - #{response.body}" unless response.success?
|
88
|
+
|
89
|
+
JSON.parse(response.body)
|
90
|
+
rescue Faraday::TimeoutError => e
|
91
|
+
raise RpcError, "Request timed out after #{@timeout} seconds"
|
92
|
+
rescue Faraday::ConnectionFailed => e
|
93
|
+
raise RpcError, "Connection failed: #{e.message}"
|
94
|
+
rescue Faraday::SSLError => e
|
95
|
+
raise RpcError, "SSL Error: #{e.message}"
|
96
|
+
end
|
97
|
+
end
|
@@ -1,11 +1,79 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
require "delegate"
|
4
|
+
require_relative "limiter/distributed_rate_queue"
|
5
|
+
require_relative "json_rpc_client"
|
7
6
|
require_relative "throttled_json_rpc_client/version"
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
module ThrottledJsonRpcClient
|
9
|
+
class Client < SimpleDelegator
|
10
|
+
DEFAULT_MAX_RETRIES = 3
|
11
|
+
DEFAULT_BASE_DELAY = 1 # Base delay in seconds
|
12
|
+
|
13
|
+
# limit: #{rate} requests / #{interval} seconds
|
14
|
+
def initialize(
|
15
|
+
endpoint,
|
16
|
+
timeout: JsonRpcClient::DEFAULT_TIMEOUT,
|
17
|
+
max_batch_size: JsonRpcClient::DEFAULT_MAX_BATCH_SIZE,
|
18
|
+
headers: {},
|
19
|
+
# retry params
|
20
|
+
max_retries: DEFAULT_MAX_RETRIES,
|
21
|
+
base_delay: DEFAULT_BASE_DELAY,
|
22
|
+
# limit params
|
23
|
+
rate: 5,
|
24
|
+
interval: 1, # in seconds
|
25
|
+
redis_urls: ["redis://localhost:6379/2"]
|
26
|
+
)
|
27
|
+
@max_retries = max_retries
|
28
|
+
@base_delay = base_delay
|
29
|
+
@queue = DistributedRateQueue.new(
|
30
|
+
redis_urls: redis_urls,
|
31
|
+
key: "key:#{endpoint}",
|
32
|
+
rate: rate,
|
33
|
+
interval: interval
|
34
|
+
)
|
35
|
+
super(
|
36
|
+
JsonRpcClient.new(
|
37
|
+
endpoint,
|
38
|
+
timeout: timeout,
|
39
|
+
max_batch_size: max_batch_size,
|
40
|
+
headers: headers
|
41
|
+
)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(method, params = nil)
|
46
|
+
retries = 0
|
47
|
+
begin
|
48
|
+
@queue.shift do
|
49
|
+
super(method, params)
|
50
|
+
end
|
51
|
+
rescue JsonRpcClient::RpcError => e
|
52
|
+
retries += 1
|
53
|
+
if retries <= @max_retries
|
54
|
+
delay = @base_delay * (2**(retries - 1))
|
55
|
+
sleep(delay)
|
56
|
+
retry
|
57
|
+
end
|
58
|
+
raise e
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def batch_call(calls)
|
63
|
+
retries = 0
|
64
|
+
begin
|
65
|
+
@queue.shift do
|
66
|
+
super(calls)
|
67
|
+
end
|
68
|
+
rescue JsonRpcClient::RpcError => e
|
69
|
+
retries += 1
|
70
|
+
if retries <= @max_retries
|
71
|
+
delay = @base_delay * (2**(retries - 1))
|
72
|
+
sleep(delay)
|
73
|
+
retry
|
74
|
+
end
|
75
|
+
raise e
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: throttled_json_rpc_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aki Wu
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-11-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: redlock
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,10 +52,9 @@ files:
|
|
38
52
|
- README.md
|
39
53
|
- Rakefile
|
40
54
|
- example.rb
|
41
|
-
- lib/json_rpc_client
|
55
|
+
- lib/json_rpc_client.rb
|
42
56
|
- lib/limiter/distributed_rate_queue.rb
|
43
57
|
- lib/throttled_json_rpc_client.rb
|
44
|
-
- lib/throttled_json_rpc_client/eth.rb
|
45
58
|
- lib/throttled_json_rpc_client/version.rb
|
46
59
|
- sig/throttled_json_rpc_client.rbs
|
47
60
|
homepage: https://github.com/wuminzhe/throttled_json_rpc_client
|
@@ -66,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
79
|
- !ruby/object:Gem::Version
|
67
80
|
version: '0'
|
68
81
|
requirements: []
|
69
|
-
rubygems_version: 3.
|
82
|
+
rubygems_version: 3.5.21
|
70
83
|
signing_key:
|
71
84
|
specification_version: 4
|
72
85
|
summary: throttled_json_rpc_client
|
data/lib/json_rpc_client/eth.rb
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
module JsonRpcClient
|
2
|
-
class HttpError < StandardError; end
|
3
|
-
class JSONRpcError < StandardError; end
|
4
|
-
|
5
|
-
class << self
|
6
|
-
def request(url, method, params)
|
7
|
-
uri = URI.parse(url)
|
8
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
9
|
-
http.use_ssl = uri.scheme == "https"
|
10
|
-
|
11
|
-
request = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
12
|
-
request.body = { jsonrpc: "2.0", method: method, params: params, id: 1 }.to_json
|
13
|
-
|
14
|
-
# https://docs.ruby-lang.org/en/master/Net/HTTPResponse.html
|
15
|
-
response = http.request(request)
|
16
|
-
raise HttpError, response unless response.is_a?(Net::HTTPOK)
|
17
|
-
|
18
|
-
body = JSON.parse(response.body)
|
19
|
-
raise JSONRpcError, body["error"] if body["error"]
|
20
|
-
|
21
|
-
body["result"]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
class Eth
|
26
|
-
attr_reader :url
|
27
|
-
|
28
|
-
def initialize(url)
|
29
|
-
@url = url
|
30
|
-
end
|
31
|
-
|
32
|
-
def get_block_by_bumber(block_number_or_block_tag, transaction_detail_flag = false)
|
33
|
-
rpc_method = "eth_getBlockByNumber"
|
34
|
-
params = [block_number_or_block_tag, transaction_detail_flag]
|
35
|
-
JsonRpcClient.request(url, rpc_method, params)
|
36
|
-
end
|
37
|
-
|
38
|
-
# == get_block_by_bumber('latest'])['number']
|
39
|
-
def block_number
|
40
|
-
rpc_method = "eth_blockNumber"
|
41
|
-
params = []
|
42
|
-
JsonRpcClient.request(url, rpc_method, params)
|
43
|
-
end
|
44
|
-
|
45
|
-
#############################
|
46
|
-
# use method_missing to define all the methods
|
47
|
-
#############################
|
48
|
-
def respond_to_missing?(*_args)
|
49
|
-
true
|
50
|
-
end
|
51
|
-
|
52
|
-
# example:
|
53
|
-
# eth.get_balance('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 'latest')
|
54
|
-
def method_missing(method, *args)
|
55
|
-
words = method.to_s.split("_")
|
56
|
-
rpc_method = "eth_#{words[0]}#{words[1..].collect(&:capitalize).join}"
|
57
|
-
params = args
|
58
|
-
JsonRpcClient.request(url, rpc_method, params)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
require "delegate"
|
2
|
-
|
3
|
-
module ThrottledJsonRpcClient
|
4
|
-
class Eth < SimpleDelegator
|
5
|
-
# limit: #{rate} requests / #{interval} seconds
|
6
|
-
def initialize(url, rate: 5, interval: 1, redis_urls: ["redis://localhost:6379/2"])
|
7
|
-
@queue = DistributedRateQueue.new(
|
8
|
-
redis_urls: redis_urls,
|
9
|
-
key: "key:#{url}",
|
10
|
-
rate: rate,
|
11
|
-
interval: interval
|
12
|
-
)
|
13
|
-
|
14
|
-
super(JsonRpcClient::Eth.new(url))
|
15
|
-
end
|
16
|
-
|
17
|
-
def method_missing(*args, **kwargs, &block)
|
18
|
-
@queue.shift do
|
19
|
-
super
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|