lightrate-client 1.0.1 → 1.0.2
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 +1 -1
- data/examples/basic_usage.rb +21 -1
- data/examples/repeated_calls_example.rb +125 -0
- data/lib/lightrate_client/client.rb +57 -76
- data/lib/lightrate_client/types.rb +71 -26
- data/lib/lightrate_client/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 603315821de731679dd12cf30666482ded677ed36e4fe4c29c6aae078ad7cac7
|
|
4
|
+
data.tar.gz: bb796d172549d872a3a278f765ebea08e7db53721874cf5be126856fb00f4c98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9754e47d78fadbb327059923d779853b6526afbaa4337a724d8d142beb39ea44fd0865024d6befc10172be6849b303c92b35d8772a40deed7a0a6beaa5af7ad4
|
|
7
|
+
data.tar.gz: 4535cd08a0487c99c70a80fac6c20c1f3fbec7cd3f6fefe1343ccb87233bf4497edd01c6ab82d92856054239b927c85ffe54625a9e2c9a5c93c91c8fa230c9bc
|
data/Gemfile.lock
CHANGED
data/examples/basic_usage.rb
CHANGED
|
@@ -103,8 +103,28 @@ begin
|
|
|
103
103
|
puts " (These create separate buckets due to different HTTP methods)"
|
|
104
104
|
puts
|
|
105
105
|
|
|
106
|
+
puts "7. Different users calling same operation:"
|
|
107
|
+
result7a = client.consume_local_bucket_token(
|
|
108
|
+
operation: 'send_notification',
|
|
109
|
+
user_identifier: 'user123'
|
|
110
|
+
)
|
|
111
|
+
result7b = client.consume_local_bucket_token(
|
|
112
|
+
operation: 'send_notification',
|
|
113
|
+
user_identifier: 'user456'
|
|
114
|
+
)
|
|
115
|
+
result7c = client.consume_local_bucket_token(
|
|
116
|
+
operation: 'send_notification',
|
|
117
|
+
user_identifier: 'user789'
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
puts " User 123 - Success: #{result7a.success} #{result7a.bucket_status}"
|
|
121
|
+
puts " User 456 - Success: #{result7b.success} #{result7b.bucket_status}"
|
|
122
|
+
puts " User 789 - Success: #{result7c.success} #{result7c.bucket_status}"
|
|
123
|
+
puts " (These create separate buckets due to different users)"
|
|
124
|
+
puts
|
|
125
|
+
|
|
106
126
|
# Example 7: Direct API call using consume_tokens
|
|
107
|
-
puts "
|
|
127
|
+
puts "8. Direct API call using consume_tokens:"
|
|
108
128
|
api_response = client.consume_tokens(
|
|
109
129
|
operation: 'send_notification',
|
|
110
130
|
user_identifier: 'user789',
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'lightrate_client'
|
|
5
|
+
|
|
6
|
+
# This example demonstrates making repeated calls to the same HTTP endpoint
|
|
7
|
+
# using consume_local_bucket_token. It shows how the local bucket cache
|
|
8
|
+
# reduces API calls and improves performance by reusing tokens locally.
|
|
9
|
+
|
|
10
|
+
# Create a client with default bucket size
|
|
11
|
+
# Note: Both API key and application ID are required for all requests
|
|
12
|
+
client = LightrateClient::Client.new(
|
|
13
|
+
ENV['LIGHTRATE_API_KEY'] || 'your_api_key_here',
|
|
14
|
+
ENV['LIGHTRATE_APPLICATION_ID'] || 'your_application_id_here',
|
|
15
|
+
default_local_bucket_size: 10, # Fetch 10 tokens at a time
|
|
16
|
+
logger: ENV['DEBUG'] ? Logger.new(STDOUT) : nil
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
puts "=" * 80
|
|
20
|
+
puts "Repeated HTTP Target Example"
|
|
21
|
+
puts "=" * 80
|
|
22
|
+
puts
|
|
23
|
+
|
|
24
|
+
# Simulate making 20 calls to the same HTTP endpoint
|
|
25
|
+
target_path = '/posts'
|
|
26
|
+
target_method = 'GET'
|
|
27
|
+
user_id = 'user_12345'
|
|
28
|
+
|
|
29
|
+
puts "Making 20 calls to #{target_method} #{target_path} for user #{user_id}"
|
|
30
|
+
puts
|
|
31
|
+
puts "Breakdown:"
|
|
32
|
+
puts " - First call: Will fetch tokens from API (expected to return 10 tokens)"
|
|
33
|
+
puts " - Calls 2-10: Should consume from local bucket (no API calls)"
|
|
34
|
+
puts " - Call 11: Local bucket empty, will fetch more from API"
|
|
35
|
+
puts " - And so on..."
|
|
36
|
+
puts
|
|
37
|
+
puts "-" * 80
|
|
38
|
+
puts
|
|
39
|
+
|
|
40
|
+
api_calls = 0
|
|
41
|
+
cache_hits = 0
|
|
42
|
+
call_times = []
|
|
43
|
+
|
|
44
|
+
20.times do |i|
|
|
45
|
+
call_number = i + 1
|
|
46
|
+
start_time = Time.now
|
|
47
|
+
|
|
48
|
+
# Make the API call through the client
|
|
49
|
+
result = client.consume_local_bucket_token(
|
|
50
|
+
path: target_path,
|
|
51
|
+
http_method: target_method,
|
|
52
|
+
user_identifier: user_id
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
elapsed_time = (Time.now - start_time) * 1000 # Convert to milliseconds
|
|
56
|
+
|
|
57
|
+
call_times << elapsed_time
|
|
58
|
+
|
|
59
|
+
if result.used_local_token
|
|
60
|
+
cache_hits += 1
|
|
61
|
+
cache_status = "✓ Local cache hit"
|
|
62
|
+
else
|
|
63
|
+
api_calls += 1
|
|
64
|
+
cache_status = "✗ API call made"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if result.bucket_status
|
|
68
|
+
tokens_remaining = result.bucket_status[:tokens_remaining]
|
|
69
|
+
max_tokens = result.bucket_status[:max_tokens]
|
|
70
|
+
bucket_info = " | Bucket: #{tokens_remaining}/#{max_tokens} tokens"
|
|
71
|
+
else
|
|
72
|
+
bucket_info = ""
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
printf "Call %2d: %s (%.2f ms)%s\n",
|
|
76
|
+
call_number,
|
|
77
|
+
cache_status,
|
|
78
|
+
elapsed_time,
|
|
79
|
+
bucket_info
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
puts
|
|
83
|
+
puts "-" * 80
|
|
84
|
+
puts "Summary"
|
|
85
|
+
puts "-" * 80
|
|
86
|
+
puts "Total calls: 20"
|
|
87
|
+
puts "API calls made: #{api_calls}"
|
|
88
|
+
puts "Cache hits: #{cache_hits}"
|
|
89
|
+
puts "Cache hit rate: #{(cache_hits.to_f / 20 * 100).round(1)}%"
|
|
90
|
+
puts
|
|
91
|
+
puts "Timing statistics:"
|
|
92
|
+
puts " Average time: #{(call_times.sum / call_times.length).round(2)} ms"
|
|
93
|
+
puts " Fastest call: #{call_times.min.round(2)} ms"
|
|
94
|
+
puts " Slowest call: #{call_times.max.round(2)} ms"
|
|
95
|
+
puts
|
|
96
|
+
|
|
97
|
+
# Show the difference between cached and non-cached calls
|
|
98
|
+
if call_times.length > 0
|
|
99
|
+
api_call_times = []
|
|
100
|
+
cache_call_times = []
|
|
101
|
+
|
|
102
|
+
20.times do |i|
|
|
103
|
+
if i == 0 || i % 10 == 0 # API calls happen on first call and when bucket is empty
|
|
104
|
+
api_call_times << call_times[i]
|
|
105
|
+
else
|
|
106
|
+
cache_call_times << call_times[i]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if cache_call_times.any?
|
|
111
|
+
avg_cache_time = cache_call_times.sum / cache_call_times.length
|
|
112
|
+
avg_api_time = api_call_times.sum / api_call_times.length if api_call_times.any?
|
|
113
|
+
|
|
114
|
+
if avg_api_time
|
|
115
|
+
speedup = avg_api_time / avg_cache_time
|
|
116
|
+
puts "Performance:"
|
|
117
|
+
puts " Average cached call: #{avg_cache_time.round(2)} ms"
|
|
118
|
+
puts " Average API call: #{avg_api_time.round(2)} ms"
|
|
119
|
+
puts " Speed improvement: #{speedup.round(1)}x faster with cache"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
puts
|
|
125
|
+
puts "=" * 80
|
|
@@ -37,61 +37,50 @@ module LightrateClient
|
|
|
37
37
|
# @param user_identifier [String] The user identifier
|
|
38
38
|
# @param tokens_requested [Integer] Number of tokens to consume
|
|
39
39
|
def consume_local_bucket_token(operation: nil, path: nil, http_method: nil, user_identifier:)
|
|
40
|
-
#
|
|
41
|
-
bucket
|
|
40
|
+
# Synchronize the entire process to prevent race conditions
|
|
41
|
+
# First, try to find an existing bucket that matches this request
|
|
42
|
+
bucket = find_bucket_by_matcher(user_identifier, operation, path, http_method)
|
|
43
|
+
|
|
44
|
+
if bucket && bucket.check_and_consume_token
|
|
45
|
+
return LightrateClient::ConsumeLocalBucketTokenResponse.new(
|
|
46
|
+
success: true,
|
|
47
|
+
used_local_token: true,
|
|
48
|
+
bucket_status: bucket.status
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# No matching bucket or bucket is empty - make API call to get tokens and rule info
|
|
53
|
+
tokens_to_fetch = @configuration.default_local_bucket_size
|
|
54
|
+
|
|
55
|
+
# Make the API call
|
|
56
|
+
response = consume_tokens(operation: operation, path: path, http_method: http_method, user_identifier: user_identifier, tokens_requested: tokens_to_fetch)
|
|
57
|
+
|
|
58
|
+
if response.rule.is_default
|
|
59
|
+
return LightrateClient::ConsumeLocalBucketTokenResponse.new(
|
|
60
|
+
success: response.tokens_consumed > 0,
|
|
61
|
+
used_local_token: false,
|
|
62
|
+
bucket_status: nil
|
|
63
|
+
)
|
|
64
|
+
end
|
|
42
65
|
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
bucket = fill_bucket_and_create_if_not_exists(user_identifier, response.rule, response.tokens_consumed)
|
|
67
|
+
|
|
68
|
+
tokens_available = bucket.check_and_consume_token
|
|
69
|
+
|
|
70
|
+
return LightrateClient::ConsumeLocalBucketTokenResponse.new(
|
|
71
|
+
success: tokens_available,
|
|
72
|
+
used_local_token: false,
|
|
73
|
+
bucket_status: bucket.status
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def consume_token_from_bucket(bucket, provided_tokens = 0)
|
|
45
78
|
bucket.synchronize do
|
|
46
|
-
|
|
47
|
-
has_tokens, consumed_successfully = bucket.check_and_consume_token
|
|
48
|
-
|
|
49
|
-
# If we successfully consumed a local token, return success
|
|
50
|
-
if consumed_successfully
|
|
51
|
-
return LightrateClient::ConsumeLocalBucketTokenResponse.new(
|
|
52
|
-
success: true,
|
|
53
|
-
used_local_token: true,
|
|
54
|
-
bucket_status: bucket.status
|
|
55
|
-
)
|
|
56
|
-
end
|
|
79
|
+
fetch_required = !bucket.has_tokens? || bucket.expired?
|
|
57
80
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# Make API call
|
|
62
|
-
request = LightrateClient::ConsumeTokensRequest.new(
|
|
63
|
-
application_id: @configuration.application_id,
|
|
64
|
-
operation: operation,
|
|
65
|
-
path: path,
|
|
66
|
-
http_method: http_method,
|
|
67
|
-
user_identifier: user_identifier,
|
|
68
|
-
tokens_requested: tokens_to_fetch
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
# Make the API call
|
|
72
|
-
response = post("/api/v1/tokens/consume", request.to_h)
|
|
73
|
-
tokens_consumed = response['tokensConsumed']&.to_i || 0
|
|
74
|
-
|
|
75
|
-
# If we got tokens from API, refill the bucket and try to consume
|
|
76
|
-
if tokens_consumed > 0
|
|
77
|
-
tokens_added, has_tokens_after_refill = bucket.refill_and_check(tokens_consumed)
|
|
78
|
-
|
|
79
|
-
# Try to consume a token after refilling
|
|
80
|
-
_, final_consumed = bucket.check_and_consume_token
|
|
81
|
-
|
|
82
|
-
return LightrateClient::ConsumeLocalBucketTokenResponse.new(
|
|
83
|
-
success: final_consumed,
|
|
84
|
-
used_local_token: false,
|
|
85
|
-
bucket_status: bucket.status
|
|
86
|
-
)
|
|
87
|
-
else
|
|
88
|
-
# No tokens available from API
|
|
89
|
-
return LightrateClient::ConsumeLocalBucketTokenResponse.new(
|
|
90
|
-
success: false,
|
|
91
|
-
used_local_token: false,
|
|
92
|
-
bucket_status: bucket.status
|
|
93
|
-
)
|
|
94
|
-
end
|
|
81
|
+
token_available = bucket.check_and_consume_token
|
|
82
|
+
|
|
83
|
+
[token_available, fetch_required]
|
|
95
84
|
end
|
|
96
85
|
end
|
|
97
86
|
|
|
@@ -102,7 +91,8 @@ module LightrateClient
|
|
|
102
91
|
path: path,
|
|
103
92
|
http_method: http_method,
|
|
104
93
|
user_identifier: user_identifier,
|
|
105
|
-
tokens_requested: tokens_requested
|
|
94
|
+
tokens_requested: tokens_requested,
|
|
95
|
+
tokens_requested_for_default_bucket_match: 1
|
|
106
96
|
)
|
|
107
97
|
consume_tokens_with_request(request)
|
|
108
98
|
end
|
|
@@ -117,6 +107,8 @@ module LightrateClient
|
|
|
117
107
|
raise ArgumentError, "Request validation failed" unless request.valid?
|
|
118
108
|
|
|
119
109
|
response = post("/api/v1/tokens/consume", request.to_h)
|
|
110
|
+
# Parse JSON response if it's a string
|
|
111
|
+
response = JSON.parse(response) if response.is_a?(String)
|
|
120
112
|
LightrateClient::ConsumeTokensResponse.from_hash(response)
|
|
121
113
|
end
|
|
122
114
|
|
|
@@ -125,37 +117,26 @@ module LightrateClient
|
|
|
125
117
|
@buckets_mutex = Mutex.new
|
|
126
118
|
end
|
|
127
119
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
bucket_key = create_bucket_key(user_identifier, operation, path, http_method)
|
|
131
|
-
|
|
132
|
-
# Double-checked locking pattern for thread-safe bucket creation
|
|
133
|
-
return @token_buckets[bucket_key] if @token_buckets[bucket_key]
|
|
120
|
+
def fill_bucket_and_create_if_not_exists(user_identifier, rule, initial_tokens)
|
|
121
|
+
bucket_key = "#{user_identifier}:rule:#{rule.id}"
|
|
134
122
|
|
|
135
123
|
@buckets_mutex.synchronize do
|
|
136
|
-
|
|
124
|
+
return @token_buckets[bucket_key] if @token_buckets[bucket_key] && !@token_buckets[bucket_key].expired?
|
|
125
|
+
|
|
137
126
|
@token_buckets[bucket_key] ||= begin
|
|
138
|
-
bucket_size =
|
|
139
|
-
TokenBucket.new(bucket_size)
|
|
127
|
+
bucket_size = @configuration.default_local_bucket_size
|
|
128
|
+
TokenBucket.new(bucket_size, rule_id: rule.id, matcher: rule.matcher, http_method: rule.http_method, user_identifier: user_identifier)
|
|
140
129
|
end
|
|
141
130
|
end
|
|
142
|
-
|
|
143
|
-
@token_buckets[bucket_key]
|
|
144
|
-
end
|
|
145
131
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
@
|
|
132
|
+
@token_buckets[bucket_key].refill(initial_tokens)
|
|
133
|
+
|
|
134
|
+
@token_buckets[bucket_key]
|
|
149
135
|
end
|
|
150
136
|
|
|
151
|
-
def
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"#{user_identifier}:operation:#{operation}"
|
|
155
|
-
elsif path
|
|
156
|
-
"#{user_identifier}:path:#{path}:#{http_method}"
|
|
157
|
-
else
|
|
158
|
-
raise ArgumentError, "Either operation or path must be specified"
|
|
137
|
+
def find_bucket_by_matcher(user_identifier, operation, path, http_method)
|
|
138
|
+
@token_buckets.values.find do |bucket|
|
|
139
|
+
bucket.matches?(operation, path, http_method) && bucket.user_identifier == user_identifier
|
|
159
140
|
end
|
|
160
141
|
end
|
|
161
142
|
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
module LightrateClient
|
|
4
4
|
# Request types
|
|
5
5
|
class ConsumeTokensRequest
|
|
6
|
-
attr_accessor :application_id, :operation, :path, :http_method, :user_identifier, :tokens_requested, :timestamp
|
|
6
|
+
attr_accessor :application_id, :operation, :path, :http_method, :user_identifier, :tokens_requested, :tokens_requested_for_default_bucket_match, :timestamp
|
|
7
7
|
|
|
8
|
-
def initialize(application_id:, operation: nil, path: nil, http_method: nil, user_identifier:, tokens_requested:, timestamp: nil)
|
|
8
|
+
def initialize(application_id:, operation: nil, path: nil, http_method: nil, user_identifier:, tokens_requested:, tokens_requested_for_default_bucket_match: nil, timestamp: nil)
|
|
9
9
|
@application_id = application_id
|
|
10
10
|
@operation = operation
|
|
11
11
|
@path = path
|
|
12
12
|
@http_method = http_method
|
|
13
13
|
@user_identifier = user_identifier
|
|
14
14
|
@tokens_requested = tokens_requested
|
|
15
|
+
@tokens_requested_for_default_bucket_match = tokens_requested_for_default_bucket_match
|
|
15
16
|
@timestamp = timestamp || Time.now
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -23,6 +24,7 @@ module LightrateClient
|
|
|
23
24
|
httpMethod: @http_method,
|
|
24
25
|
userIdentifier: @user_identifier,
|
|
25
26
|
tokensRequested: @tokens_requested,
|
|
27
|
+
tokensRequestedForDefaultBucketMatch: @tokens_requested_for_default_bucket_match,
|
|
26
28
|
timestamp: @timestamp
|
|
27
29
|
}.compact
|
|
28
30
|
end
|
|
@@ -87,14 +89,16 @@ module LightrateClient
|
|
|
87
89
|
end
|
|
88
90
|
|
|
89
91
|
class Rule
|
|
90
|
-
attr_reader :id, :name, :refill_rate, :burst_rate, :is_default
|
|
92
|
+
attr_reader :id, :name, :refill_rate, :burst_rate, :is_default, :matcher, :http_method
|
|
91
93
|
|
|
92
|
-
def initialize(id:, name:, refill_rate:, burst_rate:, is_default: false)
|
|
94
|
+
def initialize(id:, name:, refill_rate:, burst_rate:, is_default: false, matcher: nil, http_method: nil)
|
|
93
95
|
@id = id
|
|
94
96
|
@name = name
|
|
95
97
|
@refill_rate = refill_rate
|
|
96
98
|
@burst_rate = burst_rate
|
|
97
99
|
@is_default = is_default
|
|
100
|
+
@matcher = matcher
|
|
101
|
+
@http_method = http_method
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
def self.from_hash(hash)
|
|
@@ -103,18 +107,25 @@ module LightrateClient
|
|
|
103
107
|
name: hash['name'] || hash[:name],
|
|
104
108
|
refill_rate: hash['refillRate'] || hash[:refill_rate],
|
|
105
109
|
burst_rate: hash['burstRate'] || hash[:burst_rate],
|
|
106
|
-
is_default: hash['isDefault'] || hash[:is_default] || false
|
|
110
|
+
is_default: hash['isDefault'] || hash[:is_default] || false,
|
|
111
|
+
matcher: hash['matcher'] || hash[:matcher],
|
|
112
|
+
http_method: hash['httpMethod'] || hash[:http_method]
|
|
107
113
|
)
|
|
108
114
|
end
|
|
109
115
|
end
|
|
110
116
|
|
|
111
117
|
# Token bucket for local token management
|
|
112
118
|
class TokenBucket
|
|
113
|
-
attr_reader :available_tokens, :max_tokens
|
|
119
|
+
attr_reader :available_tokens, :max_tokens, :rule_id, :matcher, :http_method, :last_accessed_at, :user_identifier
|
|
114
120
|
|
|
115
|
-
def initialize(max_tokens)
|
|
121
|
+
def initialize(max_tokens, rule_id:, matcher:, http_method: nil, user_identifier:)
|
|
116
122
|
@max_tokens = max_tokens
|
|
117
123
|
@available_tokens = 0
|
|
124
|
+
@rule_id = rule_id
|
|
125
|
+
@matcher = matcher
|
|
126
|
+
@http_method = http_method
|
|
127
|
+
@last_accessed_at = Time.now
|
|
128
|
+
@user_identifier = user_identifier
|
|
118
129
|
@mutex = Mutex.new
|
|
119
130
|
end
|
|
120
131
|
|
|
@@ -145,10 +156,11 @@ module LightrateClient
|
|
|
145
156
|
end
|
|
146
157
|
|
|
147
158
|
# Refill the bucket with tokens from the server (caller must hold lock)
|
|
148
|
-
# @param
|
|
159
|
+
# @param tokens_to_add [Integer] Number of tokens to add
|
|
149
160
|
# @return [Integer] Number of tokens actually added to the bucket
|
|
150
|
-
def refill(
|
|
151
|
-
|
|
161
|
+
def refill(tokens_to_add)
|
|
162
|
+
touch
|
|
163
|
+
tokens_to_add = [tokens_to_add, @max_tokens - @available_tokens].min
|
|
152
164
|
@available_tokens += tokens_to_add
|
|
153
165
|
tokens_to_add
|
|
154
166
|
end
|
|
@@ -167,29 +179,62 @@ module LightrateClient
|
|
|
167
179
|
@available_tokens = 0
|
|
168
180
|
end
|
|
169
181
|
|
|
182
|
+
# Check if this bucket matches the given request
|
|
183
|
+
def matches?(operation, path, http_method)
|
|
184
|
+
return false if expired?
|
|
185
|
+
return false unless @matcher
|
|
186
|
+
|
|
187
|
+
begin
|
|
188
|
+
matcher_regex = Regexp.new(@matcher)
|
|
189
|
+
|
|
190
|
+
# For operation-based requests, match against operation
|
|
191
|
+
if operation
|
|
192
|
+
return matcher_regex.match?(operation) && @http_method.nil?
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# For path-based requests, match against path and HTTP method
|
|
196
|
+
if path
|
|
197
|
+
return matcher_regex.match?(path) && @http_method == http_method
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
false
|
|
201
|
+
rescue RegexpError
|
|
202
|
+
# If matcher is not a valid regex, fall back to exact match
|
|
203
|
+
if operation
|
|
204
|
+
return @matcher == operation && @http_method.nil?
|
|
205
|
+
elsif path
|
|
206
|
+
return @matcher == path && @http_method == http_method
|
|
207
|
+
end
|
|
208
|
+
false
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Check if bucket has expired (not accessed in 60 seconds)
|
|
213
|
+
def expired?
|
|
214
|
+
Time.now - @last_accessed_at > 60
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Update last accessed time
|
|
218
|
+
def touch
|
|
219
|
+
@last_accessed_at = Time.now
|
|
220
|
+
end
|
|
221
|
+
|
|
170
222
|
# Check tokens and consume atomically (caller must hold lock)
|
|
171
223
|
# This prevents race conditions between checking and consuming
|
|
172
224
|
# @return [Array] [has_tokens, consumed_successfully]
|
|
173
225
|
def check_and_consume_token
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
@available_tokens
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
226
|
+
synchronize do
|
|
227
|
+
touch
|
|
228
|
+
has_tokens = @available_tokens > 0
|
|
229
|
+
if has_tokens
|
|
230
|
+
@available_tokens -= 1
|
|
231
|
+
true
|
|
232
|
+
else
|
|
233
|
+
false
|
|
234
|
+
end
|
|
180
235
|
end
|
|
181
236
|
end
|
|
182
237
|
|
|
183
|
-
# Refill and check tokens atomically (caller must hold lock)
|
|
184
|
-
# @param tokens_to_fetch [Integer] Number of tokens to fetch
|
|
185
|
-
# @return [Array] [tokens_added, has_tokens_after_refill]
|
|
186
|
-
def refill_and_check(tokens_to_fetch)
|
|
187
|
-
tokens_to_add = [tokens_to_fetch, @max_tokens - @available_tokens].min
|
|
188
|
-
@available_tokens += tokens_to_add
|
|
189
|
-
has_tokens_after = @available_tokens > 0
|
|
190
|
-
[tokens_to_add, has_tokens_after]
|
|
191
|
-
end
|
|
192
|
-
|
|
193
238
|
# Synchronize access to this bucket for thread-safe operations
|
|
194
239
|
# @yield Block to execute under bucket lock
|
|
195
240
|
def synchronize(&block)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lightrate-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lightbourne Technologies
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-11-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -179,6 +179,7 @@ files:
|
|
|
179
179
|
- README.md
|
|
180
180
|
- Rakefile
|
|
181
181
|
- examples/basic_usage.rb
|
|
182
|
+
- examples/repeated_calls_example.rb
|
|
182
183
|
- lib/lightrate_client.rb
|
|
183
184
|
- lib/lightrate_client/client.rb
|
|
184
185
|
- lib/lightrate_client/configuration.rb
|