simple_http_service 0.2.1 → 3.0.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/.gitignore +9 -1
- data/Gemfile.lock +2 -2
- data/README.md +48 -0
- data/lib/simple_http_service/client.rb +20 -1
- data/lib/simple_http_service/rate_limiter.rb +107 -0
- data/lib/simple_http_service/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bca219ad4f3a280dfea6a35fa241481c04414f2e85f553756e7a8612c8f81da
|
|
4
|
+
data.tar.gz: 647ce34790aaea4de53b84f852415d274b26cb51bd093173b8d2b5612381bf92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c2dd8b1ea07c4a43612df855b64595447e219ffa950c3340c8fa271e4ac72323e7ccc507db62f6f4266924e2f8d194dff109075ebb6432dfbb2a02622266819
|
|
7
|
+
data.tar.gz: dfeea32e3a2db839a96165cd9b18da540d014d7a7115a3afb0ccc23f4272c87fdd0b901c71cae80c910a8b9bb49fbe94f3f569d1b19935c75b87701cf3dc7611
|
data/.gitignore
CHANGED
|
@@ -9,4 +9,12 @@
|
|
|
9
9
|
# rspec failure tracking
|
|
10
10
|
.rspec_status
|
|
11
11
|
|
|
12
|
-
.idea
|
|
12
|
+
.idea
|
|
13
|
+
|
|
14
|
+
# CCE (code-context-engine)
|
|
15
|
+
# CCE local cache (per-machine, not for version control)
|
|
16
|
+
.cce/
|
|
17
|
+
# Claude Code local settings written by cce init
|
|
18
|
+
.claude/settings.local.json
|
|
19
|
+
# .mcp.json contains absolute paths regenerated by `cce init`
|
|
20
|
+
.mcp.json
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
simple_http_service (0.
|
|
4
|
+
simple_http_service (3.0.0)
|
|
5
5
|
net-http (>= 0.4)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -32,7 +32,7 @@ PLATFORMS
|
|
|
32
32
|
|
|
33
33
|
DEPENDENCIES
|
|
34
34
|
bundler (~> 2.0)
|
|
35
|
-
rake (
|
|
35
|
+
rake (>= 10.0)
|
|
36
36
|
rspec (~> 3.13.0)
|
|
37
37
|
rspec-core (~> 3.13.1)
|
|
38
38
|
simple_http_service!
|
data/README.md
CHANGED
|
@@ -45,6 +45,10 @@ client = SimpleHttpService.new(
|
|
|
45
45
|
max_retries: 3,
|
|
46
46
|
additional_headers: {
|
|
47
47
|
'X-Request-Id': '12345'
|
|
48
|
+
},
|
|
49
|
+
rate_limit: {
|
|
50
|
+
limit: 2,
|
|
51
|
+
interval: 60
|
|
48
52
|
}
|
|
49
53
|
)
|
|
50
54
|
```
|
|
@@ -65,6 +69,50 @@ puts response.body
|
|
|
65
69
|
- `max_retries`: The number of times to retry the request in case of failure.
|
|
66
70
|
- `request_body`: The body of the request (used for POST and PUT requests).
|
|
67
71
|
- `additional_headers`: Additional headers to include in the request.
|
|
72
|
+
- `rate_limit`: A hash enabling client side throttling. Omit it and no limiting is applied.
|
|
73
|
+
- `limit` (required to enable): Maximum number of requests allowed per window.
|
|
74
|
+
- `interval`: Length of the window in seconds (default is `60`).
|
|
75
|
+
- `wait`: When `true`, `call` sleeps until a slot frees up instead of raising (default is `false`).
|
|
76
|
+
- `key`: Bucket the window under a custom key (default is `scheme://host:port`).
|
|
77
|
+
|
|
78
|
+
### Rate Limiting
|
|
79
|
+
Rate limiting is opt in. Pass a `rate_limit` hash to cap how often the client fires requests:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
client = SimpleHttpService.new(
|
|
83
|
+
url: 'https://api.example.com/endpoint',
|
|
84
|
+
http_method: :get,
|
|
85
|
+
rate_limit: { limit: 2, interval: 60 }
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
2.times { client.call }
|
|
89
|
+
client.call # raises SimpleHttpService::RateLimitExceeded
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The window is a sliding one, shared across every client in the process that resolves to the same
|
|
93
|
+
key, so two clients pointing at the same host draw from the same budget. Use `key` to share (or
|
|
94
|
+
separate) buckets explicitly.
|
|
95
|
+
|
|
96
|
+
To block instead of raising when the budget is used up:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
client = SimpleHttpService.new(
|
|
100
|
+
url: 'https://api.example.com/endpoint',
|
|
101
|
+
http_method: :get,
|
|
102
|
+
rate_limit: { limit: 2, interval: 60, wait: true }
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`SimpleHttpService::RateLimitExceeded` inherits from `SimpleHttpService::Error` and exposes
|
|
107
|
+
`retry_after`, the number of seconds until the next slot opens:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
begin
|
|
111
|
+
client.call
|
|
112
|
+
rescue SimpleHttpService::RateLimitExceeded => e
|
|
113
|
+
puts "Throttled, retry in #{e.retry_after.ceil}s"
|
|
114
|
+
end
|
|
115
|
+
```
|
|
68
116
|
|
|
69
117
|
### Example
|
|
70
118
|
Here's a complete example of using `SimpleHttpService` to make a `GET` request:
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
require 'net/http'
|
|
2
|
+
require 'simple_http_service/rate_limiter'
|
|
3
|
+
|
|
2
4
|
module SimpleHttpService
|
|
3
5
|
class Client
|
|
4
6
|
attr_accessor :uri, :headers, :http_method, :open_timeout, :read_timeout, :write_timeout,
|
|
5
|
-
:max_retries, :request_body, :additional_headers
|
|
7
|
+
:max_retries, :request_body, :additional_headers, :rate_limit
|
|
6
8
|
|
|
7
9
|
def initialize(opts)
|
|
8
10
|
raise 'URL must be present' unless opts[:url]
|
|
@@ -17,9 +19,11 @@ module SimpleHttpService
|
|
|
17
19
|
@write_timeout = opts[:write_timeout]
|
|
18
20
|
@max_retries = opts[:max_retries] || 1
|
|
19
21
|
@additional_headers = opts[:additional_headers] || {}
|
|
22
|
+
@rate_limit = opts[:rate_limit] || {}
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def call
|
|
26
|
+
throttle
|
|
23
27
|
enable_ssl
|
|
24
28
|
set_headers
|
|
25
29
|
set_timeout
|
|
@@ -27,8 +31,23 @@ module SimpleHttpService
|
|
|
27
31
|
http.request(request)
|
|
28
32
|
end
|
|
29
33
|
|
|
34
|
+
# Limiter backing this client, or nil unless rate_limit[:limit] was passed in.
|
|
35
|
+
def rate_limiter
|
|
36
|
+
return unless rate_limit[:limit]
|
|
37
|
+
|
|
38
|
+
@rate_limiter ||= RateLimiter.for(
|
|
39
|
+
rate_limit[:key] || "#{uri.scheme}://#{uri.host}:#{uri.port}",
|
|
40
|
+
limit: rate_limit[:limit],
|
|
41
|
+
interval: rate_limit[:interval] || RateLimiter::DEFAULT_INTERVAL
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
30
45
|
private
|
|
31
46
|
|
|
47
|
+
def throttle
|
|
48
|
+
rate_limiter&.acquire(wait: rate_limit[:wait] || false)
|
|
49
|
+
end
|
|
50
|
+
|
|
32
51
|
def set_headers
|
|
33
52
|
request["Accept"] = headers[:accept] if headers[:accept]
|
|
34
53
|
request["Authorization"] = headers[:authorization] if headers[:authorization]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module SimpleHttpService
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
|
|
4
|
+
# Raised when a request would exceed the configured rate limit and
|
|
5
|
+
# the client is not configured to wait for a free slot.
|
|
6
|
+
class RateLimitExceeded < Error
|
|
7
|
+
attr_reader :retry_after
|
|
8
|
+
|
|
9
|
+
def initialize(msg, retry_after: nil)
|
|
10
|
+
@retry_after = retry_after
|
|
11
|
+
super(msg)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Sliding-window rate limiter, shared per key across all Client instances
|
|
16
|
+
# in the process. Thread safe.
|
|
17
|
+
class RateLimiter
|
|
18
|
+
DEFAULT_INTERVAL = 60
|
|
19
|
+
|
|
20
|
+
attr_reader :limit, :interval
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Returns the limiter registered for +key+, creating it on first use.
|
|
24
|
+
def for(key, limit:, interval: DEFAULT_INTERVAL)
|
|
25
|
+
registry_mutex.synchronize do
|
|
26
|
+
registry[key] ||= new(limit: limit, interval: interval)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Drops every registered limiter. Mainly useful in tests.
|
|
31
|
+
def reset!
|
|
32
|
+
registry_mutex.synchronize { registry.clear }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def registry
|
|
38
|
+
@registry ||= {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def registry_mutex
|
|
42
|
+
@registry_mutex ||= Mutex.new
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(limit:, interval: DEFAULT_INTERVAL)
|
|
47
|
+
raise 'rate limit must be a positive integer' unless limit.to_i.positive?
|
|
48
|
+
raise 'rate limit interval must be positive' unless interval.to_f.positive?
|
|
49
|
+
|
|
50
|
+
@limit = limit.to_i
|
|
51
|
+
@interval = interval.to_f
|
|
52
|
+
@timestamps = []
|
|
53
|
+
@mutex = Mutex.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Consumes one slot. Raises RateLimitExceeded when the window is full,
|
|
57
|
+
# unless +wait+ is true, in which case it sleeps until a slot frees up.
|
|
58
|
+
def acquire(wait: false)
|
|
59
|
+
loop do
|
|
60
|
+
retry_after = try_acquire
|
|
61
|
+
return true unless retry_after
|
|
62
|
+
|
|
63
|
+
unless wait
|
|
64
|
+
raise RateLimitExceeded.new(
|
|
65
|
+
"rate limit of #{limit} request(s) per #{interval.round} seconds exceeded, " \
|
|
66
|
+
"retry in #{retry_after.ceil} second(s)",
|
|
67
|
+
retry_after: retry_after
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sleep(retry_after)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Slots still available in the current window.
|
|
76
|
+
def remaining
|
|
77
|
+
@mutex.synchronize do
|
|
78
|
+
prune
|
|
79
|
+
limit - @timestamps.size
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Returns nil when a slot was taken, otherwise seconds until the next one.
|
|
86
|
+
def try_acquire
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
prune
|
|
89
|
+
if @timestamps.size < limit
|
|
90
|
+
@timestamps << now
|
|
91
|
+
return nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
[@timestamps.first + interval - now, 0.001].max
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def prune
|
|
99
|
+
cutoff = now - interval
|
|
100
|
+
@timestamps.shift while @timestamps.first && @timestamps.first <= cutoff
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def now
|
|
104
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simple_http_service
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gokul (gklsan)
|
|
@@ -105,6 +105,7 @@ files:
|
|
|
105
105
|
- doc/top-level-namespace.html
|
|
106
106
|
- lib/simple_http_service.rb
|
|
107
107
|
- lib/simple_http_service/client.rb
|
|
108
|
+
- lib/simple_http_service/rate_limiter.rb
|
|
108
109
|
- lib/simple_http_service/version.rb
|
|
109
110
|
- simple_http_service.gemspec
|
|
110
111
|
homepage: https://github.com/gklsan/simple_http_service
|