lightrate-rails 1.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 +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +328 -0
- data/README.md +346 -0
- data/Rakefile +8 -0
- data/lib/lightrate_rails/configuration.rb +32 -0
- data/lib/lightrate_rails/controller_helper.rb +230 -0
- data/lib/lightrate_rails/engine.rb +17 -0
- data/lib/lightrate_rails/errors.rb +23 -0
- data/lib/lightrate_rails/version.rb +5 -0
- data/lib/lightrate_rails.rb +45 -0
- metadata +211 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 30dc222ab05943e69909f0fed5361b5b470613bda38b6c2908114e2564d9e2a7
|
|
4
|
+
data.tar.gz: 3fe845e284c5378a8241fac838f885709b2438d466182f41244021afbe3d3d1a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d39ed9be6dc10b7ad1f693c4bdeb0f42f82fd8cc0b93f83c7e6cdfecd7a7a50c049f6e801c3aa6f5c28cd65624aed08d2768d8e4ddc7859f2e72f2e5094b3480
|
|
7
|
+
data.tar.gz: 98014b3bb3baeacff7a236344db56e360c069e3fb1b5b5f9b548d17d2bea2036a4c09580c5a03bc82c113e718e7403e43780e270ff1b660bd9f4484ac05c181d
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
lightrate-rails (1.0.0)
|
|
5
|
+
lightrate-client (~> 1.0)
|
|
6
|
+
rails (>= 6.0)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
actioncable (8.0.3)
|
|
12
|
+
actionpack (= 8.0.3)
|
|
13
|
+
activesupport (= 8.0.3)
|
|
14
|
+
nio4r (~> 2.0)
|
|
15
|
+
websocket-driver (>= 0.6.1)
|
|
16
|
+
zeitwerk (~> 2.6)
|
|
17
|
+
actionmailbox (8.0.3)
|
|
18
|
+
actionpack (= 8.0.3)
|
|
19
|
+
activejob (= 8.0.3)
|
|
20
|
+
activerecord (= 8.0.3)
|
|
21
|
+
activestorage (= 8.0.3)
|
|
22
|
+
activesupport (= 8.0.3)
|
|
23
|
+
mail (>= 2.8.0)
|
|
24
|
+
actionmailer (8.0.3)
|
|
25
|
+
actionpack (= 8.0.3)
|
|
26
|
+
actionview (= 8.0.3)
|
|
27
|
+
activejob (= 8.0.3)
|
|
28
|
+
activesupport (= 8.0.3)
|
|
29
|
+
mail (>= 2.8.0)
|
|
30
|
+
rails-dom-testing (~> 2.2)
|
|
31
|
+
actionpack (8.0.3)
|
|
32
|
+
actionview (= 8.0.3)
|
|
33
|
+
activesupport (= 8.0.3)
|
|
34
|
+
nokogiri (>= 1.8.5)
|
|
35
|
+
rack (>= 2.2.4)
|
|
36
|
+
rack-session (>= 1.0.1)
|
|
37
|
+
rack-test (>= 0.6.3)
|
|
38
|
+
rails-dom-testing (~> 2.2)
|
|
39
|
+
rails-html-sanitizer (~> 1.6)
|
|
40
|
+
useragent (~> 0.16)
|
|
41
|
+
actiontext (8.0.3)
|
|
42
|
+
actionpack (= 8.0.3)
|
|
43
|
+
activerecord (= 8.0.3)
|
|
44
|
+
activestorage (= 8.0.3)
|
|
45
|
+
activesupport (= 8.0.3)
|
|
46
|
+
globalid (>= 0.6.0)
|
|
47
|
+
nokogiri (>= 1.8.5)
|
|
48
|
+
actionview (8.0.3)
|
|
49
|
+
activesupport (= 8.0.3)
|
|
50
|
+
builder (~> 3.1)
|
|
51
|
+
erubi (~> 1.11)
|
|
52
|
+
rails-dom-testing (~> 2.2)
|
|
53
|
+
rails-html-sanitizer (~> 1.6)
|
|
54
|
+
activejob (8.0.3)
|
|
55
|
+
activesupport (= 8.0.3)
|
|
56
|
+
globalid (>= 0.3.6)
|
|
57
|
+
activemodel (8.0.3)
|
|
58
|
+
activesupport (= 8.0.3)
|
|
59
|
+
activerecord (8.0.3)
|
|
60
|
+
activemodel (= 8.0.3)
|
|
61
|
+
activesupport (= 8.0.3)
|
|
62
|
+
timeout (>= 0.4.0)
|
|
63
|
+
activestorage (8.0.3)
|
|
64
|
+
actionpack (= 8.0.3)
|
|
65
|
+
activejob (= 8.0.3)
|
|
66
|
+
activerecord (= 8.0.3)
|
|
67
|
+
activesupport (= 8.0.3)
|
|
68
|
+
marcel (~> 1.0)
|
|
69
|
+
activesupport (8.0.3)
|
|
70
|
+
base64
|
|
71
|
+
benchmark (>= 0.3)
|
|
72
|
+
bigdecimal
|
|
73
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
74
|
+
connection_pool (>= 2.2.5)
|
|
75
|
+
drb
|
|
76
|
+
i18n (>= 1.6, < 2)
|
|
77
|
+
logger (>= 1.4.2)
|
|
78
|
+
minitest (>= 5.1)
|
|
79
|
+
securerandom (>= 0.3)
|
|
80
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
|
81
|
+
uri (>= 0.13.1)
|
|
82
|
+
addressable (2.8.7)
|
|
83
|
+
public_suffix (>= 2.0.2, < 7.0)
|
|
84
|
+
ast (2.4.3)
|
|
85
|
+
base64 (0.3.0)
|
|
86
|
+
benchmark (0.4.1)
|
|
87
|
+
bigdecimal (3.2.3)
|
|
88
|
+
builder (3.3.0)
|
|
89
|
+
concurrent-ruby (1.3.5)
|
|
90
|
+
connection_pool (2.5.4)
|
|
91
|
+
crack (1.0.0)
|
|
92
|
+
bigdecimal
|
|
93
|
+
rexml
|
|
94
|
+
crass (1.0.6)
|
|
95
|
+
date (3.4.1)
|
|
96
|
+
diff-lcs (1.6.2)
|
|
97
|
+
docile (1.4.1)
|
|
98
|
+
drb (2.2.3)
|
|
99
|
+
erb (5.0.2)
|
|
100
|
+
erubi (1.13.1)
|
|
101
|
+
faraday (2.14.0)
|
|
102
|
+
faraday-net_http (>= 2.0, < 3.5)
|
|
103
|
+
json
|
|
104
|
+
logger
|
|
105
|
+
faraday-net_http (3.4.1)
|
|
106
|
+
net-http (>= 0.5.0)
|
|
107
|
+
faraday-retry (2.3.2)
|
|
108
|
+
faraday (~> 2.0)
|
|
109
|
+
globalid (1.3.0)
|
|
110
|
+
activesupport (>= 6.1)
|
|
111
|
+
hashdiff (1.2.1)
|
|
112
|
+
i18n (1.14.7)
|
|
113
|
+
concurrent-ruby (~> 1.0)
|
|
114
|
+
io-console (0.8.1)
|
|
115
|
+
irb (1.15.2)
|
|
116
|
+
pp (>= 0.6.0)
|
|
117
|
+
rdoc (>= 4.0.0)
|
|
118
|
+
reline (>= 0.4.2)
|
|
119
|
+
json (2.15.0)
|
|
120
|
+
language_server-protocol (3.17.0.5)
|
|
121
|
+
lightrate-client (1.0.0)
|
|
122
|
+
faraday (~> 2.0)
|
|
123
|
+
faraday-retry (~> 2.0)
|
|
124
|
+
json (~> 2.0)
|
|
125
|
+
lint_roller (1.1.0)
|
|
126
|
+
logger (1.7.0)
|
|
127
|
+
loofah (2.24.1)
|
|
128
|
+
crass (~> 1.0.2)
|
|
129
|
+
nokogiri (>= 1.12.0)
|
|
130
|
+
mail (2.8.1)
|
|
131
|
+
mini_mime (>= 0.1.1)
|
|
132
|
+
net-imap
|
|
133
|
+
net-pop
|
|
134
|
+
net-smtp
|
|
135
|
+
marcel (1.1.0)
|
|
136
|
+
mini_mime (1.1.5)
|
|
137
|
+
minitest (5.25.5)
|
|
138
|
+
net-http (0.6.0)
|
|
139
|
+
uri
|
|
140
|
+
net-imap (0.5.11)
|
|
141
|
+
date
|
|
142
|
+
net-protocol
|
|
143
|
+
net-pop (0.1.2)
|
|
144
|
+
net-protocol
|
|
145
|
+
net-protocol (0.2.2)
|
|
146
|
+
timeout
|
|
147
|
+
net-smtp (0.5.1)
|
|
148
|
+
net-protocol
|
|
149
|
+
nio4r (2.7.4)
|
|
150
|
+
nokogiri (1.18.10-aarch64-linux-gnu)
|
|
151
|
+
racc (~> 1.4)
|
|
152
|
+
nokogiri (1.18.10-aarch64-linux-musl)
|
|
153
|
+
racc (~> 1.4)
|
|
154
|
+
nokogiri (1.18.10-arm-linux-gnu)
|
|
155
|
+
racc (~> 1.4)
|
|
156
|
+
nokogiri (1.18.10-arm-linux-musl)
|
|
157
|
+
racc (~> 1.4)
|
|
158
|
+
nokogiri (1.18.10-arm64-darwin)
|
|
159
|
+
racc (~> 1.4)
|
|
160
|
+
nokogiri (1.18.10-x86_64-darwin)
|
|
161
|
+
racc (~> 1.4)
|
|
162
|
+
nokogiri (1.18.10-x86_64-linux-gnu)
|
|
163
|
+
racc (~> 1.4)
|
|
164
|
+
nokogiri (1.18.10-x86_64-linux-musl)
|
|
165
|
+
racc (~> 1.4)
|
|
166
|
+
parallel (1.27.0)
|
|
167
|
+
parser (3.3.9.0)
|
|
168
|
+
ast (~> 2.4.1)
|
|
169
|
+
racc
|
|
170
|
+
pp (0.6.2)
|
|
171
|
+
prettyprint
|
|
172
|
+
prettyprint (0.2.0)
|
|
173
|
+
prism (1.5.1)
|
|
174
|
+
psych (5.2.6)
|
|
175
|
+
date
|
|
176
|
+
stringio
|
|
177
|
+
public_suffix (6.0.2)
|
|
178
|
+
racc (1.8.1)
|
|
179
|
+
rack (3.2.1)
|
|
180
|
+
rack-session (2.1.1)
|
|
181
|
+
base64 (>= 0.1.0)
|
|
182
|
+
rack (>= 3.0.0)
|
|
183
|
+
rack-test (2.2.0)
|
|
184
|
+
rack (>= 1.3)
|
|
185
|
+
rackup (2.2.1)
|
|
186
|
+
rack (>= 3)
|
|
187
|
+
rails (8.0.3)
|
|
188
|
+
actioncable (= 8.0.3)
|
|
189
|
+
actionmailbox (= 8.0.3)
|
|
190
|
+
actionmailer (= 8.0.3)
|
|
191
|
+
actionpack (= 8.0.3)
|
|
192
|
+
actiontext (= 8.0.3)
|
|
193
|
+
actionview (= 8.0.3)
|
|
194
|
+
activejob (= 8.0.3)
|
|
195
|
+
activemodel (= 8.0.3)
|
|
196
|
+
activerecord (= 8.0.3)
|
|
197
|
+
activestorage (= 8.0.3)
|
|
198
|
+
activesupport (= 8.0.3)
|
|
199
|
+
bundler (>= 1.15.0)
|
|
200
|
+
railties (= 8.0.3)
|
|
201
|
+
rails-dom-testing (2.3.0)
|
|
202
|
+
activesupport (>= 5.0.0)
|
|
203
|
+
minitest
|
|
204
|
+
nokogiri (>= 1.6)
|
|
205
|
+
rails-html-sanitizer (1.6.2)
|
|
206
|
+
loofah (~> 2.21)
|
|
207
|
+
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
|
208
|
+
railties (8.0.3)
|
|
209
|
+
actionpack (= 8.0.3)
|
|
210
|
+
activesupport (= 8.0.3)
|
|
211
|
+
irb (~> 1.13)
|
|
212
|
+
rackup (>= 1.0.0)
|
|
213
|
+
rake (>= 12.2)
|
|
214
|
+
thor (~> 1.0, >= 1.2.2)
|
|
215
|
+
tsort (>= 0.2)
|
|
216
|
+
zeitwerk (~> 2.6)
|
|
217
|
+
rainbow (3.1.1)
|
|
218
|
+
rake (13.3.0)
|
|
219
|
+
rdoc (6.14.2)
|
|
220
|
+
erb
|
|
221
|
+
psych (>= 4.0.0)
|
|
222
|
+
regexp_parser (2.11.3)
|
|
223
|
+
reline (0.6.2)
|
|
224
|
+
io-console (~> 0.5)
|
|
225
|
+
rexml (3.4.4)
|
|
226
|
+
rspec (3.13.1)
|
|
227
|
+
rspec-core (~> 3.13.0)
|
|
228
|
+
rspec-expectations (~> 3.13.0)
|
|
229
|
+
rspec-mocks (~> 3.13.0)
|
|
230
|
+
rspec-core (3.13.5)
|
|
231
|
+
rspec-support (~> 3.13.0)
|
|
232
|
+
rspec-expectations (3.13.5)
|
|
233
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
234
|
+
rspec-support (~> 3.13.0)
|
|
235
|
+
rspec-mocks (3.13.5)
|
|
236
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
237
|
+
rspec-support (~> 3.13.0)
|
|
238
|
+
rspec-rails (6.1.5)
|
|
239
|
+
actionpack (>= 6.1)
|
|
240
|
+
activesupport (>= 6.1)
|
|
241
|
+
railties (>= 6.1)
|
|
242
|
+
rspec-core (~> 3.13)
|
|
243
|
+
rspec-expectations (~> 3.13)
|
|
244
|
+
rspec-mocks (~> 3.13)
|
|
245
|
+
rspec-support (~> 3.13)
|
|
246
|
+
rspec-support (3.13.6)
|
|
247
|
+
rubocop (1.81.1)
|
|
248
|
+
json (~> 2.3)
|
|
249
|
+
language_server-protocol (~> 3.17.0.2)
|
|
250
|
+
lint_roller (~> 1.1.0)
|
|
251
|
+
parallel (~> 1.10)
|
|
252
|
+
parser (>= 3.3.0.2)
|
|
253
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
254
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
255
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
256
|
+
ruby-progressbar (~> 1.7)
|
|
257
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
258
|
+
rubocop-ast (1.47.1)
|
|
259
|
+
parser (>= 3.3.7.2)
|
|
260
|
+
prism (~> 1.4)
|
|
261
|
+
rubocop-capybara (2.22.1)
|
|
262
|
+
lint_roller (~> 1.1)
|
|
263
|
+
rubocop (~> 1.72, >= 1.72.1)
|
|
264
|
+
rubocop-factory_bot (2.27.1)
|
|
265
|
+
lint_roller (~> 1.1)
|
|
266
|
+
rubocop (~> 1.72, >= 1.72.1)
|
|
267
|
+
rubocop-rspec (2.31.0)
|
|
268
|
+
rubocop (~> 1.40)
|
|
269
|
+
rubocop-capybara (~> 2.17)
|
|
270
|
+
rubocop-factory_bot (~> 2.22)
|
|
271
|
+
rubocop-rspec_rails (~> 2.28)
|
|
272
|
+
rubocop-rspec_rails (2.29.1)
|
|
273
|
+
rubocop (~> 1.61)
|
|
274
|
+
ruby-progressbar (1.13.0)
|
|
275
|
+
securerandom (0.4.1)
|
|
276
|
+
simplecov (0.22.0)
|
|
277
|
+
docile (~> 1.1)
|
|
278
|
+
simplecov-html (~> 0.11)
|
|
279
|
+
simplecov_json_formatter (~> 0.1)
|
|
280
|
+
simplecov-html (0.13.2)
|
|
281
|
+
simplecov_json_formatter (0.1.4)
|
|
282
|
+
stringio (3.1.7)
|
|
283
|
+
thor (1.4.0)
|
|
284
|
+
timeout (0.4.3)
|
|
285
|
+
tsort (0.2.0)
|
|
286
|
+
tzinfo (2.0.6)
|
|
287
|
+
concurrent-ruby (~> 1.0)
|
|
288
|
+
unicode-display_width (3.2.0)
|
|
289
|
+
unicode-emoji (~> 4.1)
|
|
290
|
+
unicode-emoji (4.1.0)
|
|
291
|
+
uri (1.0.3)
|
|
292
|
+
useragent (0.16.11)
|
|
293
|
+
vcr (6.3.1)
|
|
294
|
+
base64
|
|
295
|
+
webmock (3.25.1)
|
|
296
|
+
addressable (>= 2.8.0)
|
|
297
|
+
crack (>= 0.3.2)
|
|
298
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
|
299
|
+
websocket-driver (0.8.0)
|
|
300
|
+
base64
|
|
301
|
+
websocket-extensions (>= 0.1.0)
|
|
302
|
+
websocket-extensions (0.1.5)
|
|
303
|
+
zeitwerk (2.7.3)
|
|
304
|
+
|
|
305
|
+
PLATFORMS
|
|
306
|
+
aarch64-linux-gnu
|
|
307
|
+
aarch64-linux-musl
|
|
308
|
+
arm-linux-gnu
|
|
309
|
+
arm-linux-musl
|
|
310
|
+
arm64-darwin
|
|
311
|
+
x86_64-darwin
|
|
312
|
+
x86_64-linux-gnu
|
|
313
|
+
x86_64-linux-musl
|
|
314
|
+
|
|
315
|
+
DEPENDENCIES
|
|
316
|
+
bundler (~> 2.0)
|
|
317
|
+
lightrate-rails!
|
|
318
|
+
rake (~> 13.0)
|
|
319
|
+
rspec (~> 3.0)
|
|
320
|
+
rspec-rails (~> 6.0)
|
|
321
|
+
rubocop (~> 1.0)
|
|
322
|
+
rubocop-rspec (~> 2.0)
|
|
323
|
+
simplecov (~> 0.21)
|
|
324
|
+
vcr (~> 6.0)
|
|
325
|
+
webmock (~> 3.0)
|
|
326
|
+
|
|
327
|
+
BUNDLED WITH
|
|
328
|
+
2.5.21
|
data/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Lightrate Rails
|
|
2
|
+
|
|
3
|
+
A Ruby on Rails integration gem for LightRate API throttling. This gem provides seamless integration with the LightRate API using controller before_actions to automatically throttle requests using local token buckets that automatically refill from the server when needed.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Controller-Level Throttling**: Use `throttle_with_lightrate` in any controller to enable automatic rate limiting
|
|
8
|
+
- **Flexible User Identification**: Customize user identification per-controller with full access to controller context
|
|
9
|
+
- **Local Token Bucket Only**: Uses only the local token bucket approach for efficient throttling with automatic server refills
|
|
10
|
+
- **Custom Exception Handling**: Raises `LightRateNoTokensAvailable` when no tokens are available
|
|
11
|
+
- **Fine-Grained Control**: Use `:only` or `:except` options to throttle specific actions
|
|
12
|
+
- **Controller Helpers**: Manual token consumption methods for advanced use cases
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this line to your application's Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'lightrate-rails'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
And then execute:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
$ bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install it yourself as:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ gem install lightrate-rails
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
### Basic Setup
|
|
37
|
+
|
|
38
|
+
**Required:** Configure Lightrate in your Rails application initializer:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# config/initializers/lightrate_rails.rb
|
|
42
|
+
|
|
43
|
+
LightrateRails.configure do |config|
|
|
44
|
+
# Required: Your LightRate API key
|
|
45
|
+
config.api_key = ENV['LIGHTRATE_API_KEY']
|
|
46
|
+
|
|
47
|
+
# Required: Your LightRate Application ID
|
|
48
|
+
config.application_id = ENV['LIGHTRATE_APPLICATION_ID']
|
|
49
|
+
|
|
50
|
+
# Optional: Enable/disable throttling globally (default: true)
|
|
51
|
+
config.enabled = Rails.env.production?
|
|
52
|
+
|
|
53
|
+
# Optional: Default bucket size (default: 5)
|
|
54
|
+
config.default_local_bucket_size = 10
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Note:** You must provide both a valid API key and Application ID. The gem will raise a `ConfigurationError` if either is missing.
|
|
59
|
+
|
|
60
|
+
### Advanced Configuration
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
LightrateRails.configure do |config|
|
|
64
|
+
# Required: Your LightRate API key
|
|
65
|
+
config.api_key = ENV['LIGHTRATE_API_KEY']
|
|
66
|
+
|
|
67
|
+
# Required: Your LightRate Application ID
|
|
68
|
+
config.application_id = ENV['LIGHTRATE_APPLICATION_ID']
|
|
69
|
+
|
|
70
|
+
# Enable/disable throttling globally (default: true)
|
|
71
|
+
config.enabled = true
|
|
72
|
+
|
|
73
|
+
# Default local bucket size (default: 5)
|
|
74
|
+
config.default_local_bucket_size = 10
|
|
75
|
+
|
|
76
|
+
# API client configuration
|
|
77
|
+
config.timeout = 30
|
|
78
|
+
config.retry_attempts = 3
|
|
79
|
+
config.logger = Rails.logger
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
### Global Client Architecture
|
|
86
|
+
|
|
87
|
+
The gem creates a single global LightRate client instance during Rails initialization. This approach provides several benefits:
|
|
88
|
+
|
|
89
|
+
- **Efficiency**: No client creation overhead on each request
|
|
90
|
+
- **Shared Token Buckets**: Token buckets are shared across all requests, providing better rate limiting
|
|
91
|
+
- **Memory Efficiency**: Single client instance reduces memory usage
|
|
92
|
+
- **Consistent State**: All parts of your application use the same client configuration
|
|
93
|
+
|
|
94
|
+
### Controller-Level Throttling
|
|
95
|
+
|
|
96
|
+
The gem automatically includes `LightrateRails::ControllerHelper` in all your controllers. To enable throttling, simply call `throttle_with_lightrate` in any controller:
|
|
97
|
+
|
|
98
|
+
#### Basic Usage
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# Throttle all actions in this controller
|
|
102
|
+
class ApiController < ApplicationController
|
|
103
|
+
throttle_with_lightrate
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Only throttle specific actions
|
|
107
|
+
class UsersController < ApplicationController
|
|
108
|
+
throttle_with_lightrate only: [:create, :update, :destroy]
|
|
109
|
+
|
|
110
|
+
def index; end # NOT throttled
|
|
111
|
+
def show; end # NOT throttled
|
|
112
|
+
def create; end # Throttled
|
|
113
|
+
def update; end # Throttled
|
|
114
|
+
def destroy; end # Throttled
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Throttle all actions EXCEPT specific ones
|
|
118
|
+
class PostsController < ApplicationController
|
|
119
|
+
throttle_with_lightrate except: [:index, :show]
|
|
120
|
+
|
|
121
|
+
def index; end # NOT throttled
|
|
122
|
+
def show; end # NOT throttled
|
|
123
|
+
def create; end # Throttled
|
|
124
|
+
def update; end # Throttled
|
|
125
|
+
def destroy; end # Throttled
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Disable throttling for this entire controller
|
|
129
|
+
class HealthController < ApplicationController
|
|
130
|
+
skip_lightrate_throttling
|
|
131
|
+
|
|
132
|
+
def status; end # NOT throttled
|
|
133
|
+
def ping; end # NOT throttled
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Custom User Identification
|
|
138
|
+
|
|
139
|
+
By default, the gem uses `current_user.id` to identify users. You can customize this per-controller:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# Use a different method on your user object
|
|
143
|
+
class ApiController < ApplicationController
|
|
144
|
+
throttle_with_lightrate user_identifier: -> { current_user&.uuid }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Use a symbol for a controller method
|
|
148
|
+
class ApiController < ApplicationController
|
|
149
|
+
throttle_with_lightrate user_identifier: :get_api_user_id
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def get_api_user_id
|
|
154
|
+
request.headers['X-API-User-ID']
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Combine multiple identifiers
|
|
159
|
+
class ApiController < ApplicationController
|
|
160
|
+
throttle_with_lightrate user_identifier: -> {
|
|
161
|
+
"#{current_user&.id}:#{current_organization&.id}"
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Use API token for identification
|
|
166
|
+
class ApiV2Controller < ApplicationController
|
|
167
|
+
throttle_with_lightrate user_identifier: -> {
|
|
168
|
+
current_api_user&.token
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Combine with :only option
|
|
173
|
+
class ComplexController < ApplicationController
|
|
174
|
+
throttle_with_lightrate(
|
|
175
|
+
only: [:create, :update],
|
|
176
|
+
user_identifier: -> { request.headers['X-User-Token'] }
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Manual Token Management in Controllers
|
|
182
|
+
|
|
183
|
+
For advanced use cases, you can manually manage token consumption. The helper methods are automatically available in all controllers:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
class ApiController < ApplicationController
|
|
187
|
+
def expensive_operation
|
|
188
|
+
# Check if tokens are available for current path before proceeding
|
|
189
|
+
if lightrate_tokens_available?
|
|
190
|
+
# Your expensive operation here
|
|
191
|
+
perform_expensive_operation
|
|
192
|
+
else
|
|
193
|
+
render json: { error: 'Rate limit exceeded' }, status: 429
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def path_specific_operation
|
|
198
|
+
# Manually consume a token for a specific path
|
|
199
|
+
consume_lightrate_token_for_path(path: '/api/v1/special')
|
|
200
|
+
|
|
201
|
+
# Your operation here
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def current_path_operation
|
|
205
|
+
# Consume a token for the current request path (most common use case)
|
|
206
|
+
consume_lightrate_token_for_current_path
|
|
207
|
+
|
|
208
|
+
# Your operation here
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def conditional_operation
|
|
212
|
+
# Use the helper to conditionally execute code
|
|
213
|
+
if lightrate_tokens_available?
|
|
214
|
+
# Execute expensive operation
|
|
215
|
+
heavy_computation
|
|
216
|
+
else
|
|
217
|
+
# Fallback to lighter operation
|
|
218
|
+
light_computation
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def custom_user_operation
|
|
223
|
+
# Use a different user identifier
|
|
224
|
+
consume_lightrate_token_for_current_path(
|
|
225
|
+
user_identifier: current_user.uuid
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Your operation here
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def specific_path_operation
|
|
232
|
+
# Consume token for a specific path and method
|
|
233
|
+
consume_lightrate_token_for_path(
|
|
234
|
+
path: "/api/v1/special-endpoint",
|
|
235
|
+
method: "POST",
|
|
236
|
+
user_identifier: current_user.id
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Your operation here
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Controller Configuration Methods
|
|
245
|
+
|
|
246
|
+
Configure throttling behavior at the controller level:
|
|
247
|
+
|
|
248
|
+
| Method | Description | Options |
|
|
249
|
+
|--------|-------------|---------|
|
|
250
|
+
| `throttle_with_lightrate(options = {})` | Enable throttling for this controller | `only:` - Array of action names to throttle<br>`except:` - Array of action names to exclude<br>`user_identifier:` - Proc or Symbol for custom user identification |
|
|
251
|
+
| `skip_lightrate_throttling` | Disable throttling for this entire controller | None |
|
|
252
|
+
|
|
253
|
+
### Available Helper Methods
|
|
254
|
+
|
|
255
|
+
The gem provides several helper methods for manual token management. All methods share a single global LightRate client instance that is created during Rails initialization, ensuring efficient token bucket management across all requests.
|
|
256
|
+
|
|
257
|
+
| Method | Description | Parameters |
|
|
258
|
+
|--------|-------------|------------|
|
|
259
|
+
| `lightrate_tokens_available?` | Check if tokens are available for current path | `user_identifier` |
|
|
260
|
+
| `consume_lightrate_token_for_current_path` | Consume token for current request path | `user_identifier` |
|
|
261
|
+
| `consume_lightrate_token_for_path` | Consume token for specific path | `path`, `method`, `user_identifier` |
|
|
262
|
+
|
|
263
|
+
**Most Common Use Cases:**
|
|
264
|
+
|
|
265
|
+
- **`lightrate_tokens_available?`** - Use this to check before expensive operations
|
|
266
|
+
- **`consume_lightrate_token_for_current_path`** - Use this when you want to consume a token for the current request path
|
|
267
|
+
- **`consume_lightrate_token_for_path`** - Use this for specific paths different from the current request
|
|
268
|
+
|
|
269
|
+
### Exception Handling
|
|
270
|
+
|
|
271
|
+
The gem automatically handles rate limiting through controller callbacks:
|
|
272
|
+
|
|
273
|
+
- **HTML requests**: Redirects with flash message
|
|
274
|
+
- **JSON requests**: Returns 429 status with JSON error
|
|
275
|
+
- **XML requests**: Returns 429 status with XML error
|
|
276
|
+
|
|
277
|
+
#### Customizing Throttling Responses
|
|
278
|
+
|
|
279
|
+
You can customize the throttling response by overriding the `handle_rate_limit_exceeded` method in your controllers:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
class ApiController < ApplicationController
|
|
283
|
+
throttle_with_lightrate
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def handle_rate_limit_exceeded(exception)
|
|
288
|
+
# Custom throttling response
|
|
289
|
+
render json: {
|
|
290
|
+
error: 'Rate Limited',
|
|
291
|
+
message: 'You have exceeded the rate limit for this endpoint.',
|
|
292
|
+
path: exception.path,
|
|
293
|
+
user: exception.user_identifier,
|
|
294
|
+
retry_after: 30
|
|
295
|
+
}, status: 429
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Exception Classes
|
|
301
|
+
|
|
302
|
+
### LightRateNoTokensAvailable
|
|
303
|
+
|
|
304
|
+
Raised when no tokens are available for a request.
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
begin
|
|
308
|
+
consume_lightrate_tokens(operation: 'my_operation')
|
|
309
|
+
rescue LightrateRails::LightRateNoTokensAvailable => e
|
|
310
|
+
puts "No tokens available for path: #{e.path}"
|
|
311
|
+
puts "User: #{e.user_identifier}"
|
|
312
|
+
puts "Response: #{e.response}"
|
|
313
|
+
end
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Configuration Options
|
|
317
|
+
|
|
318
|
+
| Option | Type | Default | Description |
|
|
319
|
+
|--------|------|---------|-------------|
|
|
320
|
+
| `api_key` | String | `nil` | **Required** - Your LightRate API key |
|
|
321
|
+
| `application_id` | String | `nil` | **Required** - Your LightRate Application ID |
|
|
322
|
+
| `enabled` | Boolean | `true` | Enable/disable throttling globally |
|
|
323
|
+
| `default_local_bucket_size` | Integer | `5` | Default size for local token buckets |
|
|
324
|
+
| `timeout` | Integer | `30` | API request timeout in seconds |
|
|
325
|
+
| `retry_attempts` | Integer | `3` | Number of API retry attempts |
|
|
326
|
+
| `logger` | Logger | `Rails.logger` | Logger for API requests |
|
|
327
|
+
|
|
328
|
+
**Note:** User identification is now configured per-controller using the `user_identifier` option in `throttle_with_lightrate`. See [Custom User Identification](#custom-user-identification) for details.
|
|
329
|
+
|
|
330
|
+
## Development
|
|
331
|
+
|
|
332
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
333
|
+
|
|
334
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
335
|
+
|
|
336
|
+
## Contributing
|
|
337
|
+
|
|
338
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/lightbourne-technologies/lightrate-rails. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/lightbourne-technologies/lightrate-rails/blob/main/CODE_OF_CONDUCT.md).
|
|
339
|
+
|
|
340
|
+
## License
|
|
341
|
+
|
|
342
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
343
|
+
|
|
344
|
+
## Code of Conduct
|
|
345
|
+
|
|
346
|
+
Everyone interacting in the Lightrate Rails project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/lightbourne-technologies/lightrate-rails/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LightrateRails
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :api_key, :application_id, :timeout, :retry_attempts, :logger, :default_local_bucket_size,
|
|
6
|
+
:enabled
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@enabled = true
|
|
10
|
+
@timeout = 30
|
|
11
|
+
@retry_attempts = 3
|
|
12
|
+
@logger = nil
|
|
13
|
+
@default_local_bucket_size = 5
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def valid?
|
|
17
|
+
api_key.present? && application_id.present?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
{
|
|
22
|
+
api_key: api_key.present? ? "******" : nil,
|
|
23
|
+
application_id: application_id,
|
|
24
|
+
enabled: enabled,
|
|
25
|
+
timeout: timeout,
|
|
26
|
+
retry_attempts: retry_attempts,
|
|
27
|
+
logger: logger,
|
|
28
|
+
default_local_bucket_size: default_local_bucket_size
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LightrateRails
|
|
4
|
+
module ControllerHelper
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
rescue_from LightrateRails::LightRateNoTokensAvailable, with: :handle_rate_limit_exceeded
|
|
9
|
+
|
|
10
|
+
# Class-level configuration for throttling
|
|
11
|
+
class_attribute :lightrate_throttled_actions, default: []
|
|
12
|
+
class_attribute :lightrate_throttle_only_specified, default: false
|
|
13
|
+
class_attribute :lightrate_user_identifier_method, default: nil
|
|
14
|
+
class_attribute :lightrate_enabled, default: false
|
|
15
|
+
|
|
16
|
+
# Main before_action that handles token consumption
|
|
17
|
+
before_action :consume_lightrate_token, if: :should_throttle_current_action?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class_methods do
|
|
21
|
+
# Enable throttling for this controller
|
|
22
|
+
# @param options [Hash] Configuration options
|
|
23
|
+
# @option options [Array<Symbol, String>] :only List of action names to throttle (optional)
|
|
24
|
+
# @option options [Array<Symbol, String>] :except List of action names to exclude from throttling (optional)
|
|
25
|
+
# @option options [Proc, Symbol] :user_identifier Custom method to get user identifier (optional)
|
|
26
|
+
# Can be a Proc that receives the controller instance, or a Symbol for a controller method
|
|
27
|
+
# @example
|
|
28
|
+
# throttle_with_lightrate only: [:create, :update]
|
|
29
|
+
# throttle_with_lightrate user_identifier: -> { current_api_user.token }
|
|
30
|
+
# throttle_with_lightrate user_identifier: :get_user_id
|
|
31
|
+
def throttle_with_lightrate(options = {})
|
|
32
|
+
self.lightrate_enabled = true
|
|
33
|
+
|
|
34
|
+
if options.key?(:only)
|
|
35
|
+
self.lightrate_throttled_actions = Array(options[:only]).map(&:to_s)
|
|
36
|
+
self.lightrate_throttle_only_specified = true
|
|
37
|
+
elsif options.key?(:except)
|
|
38
|
+
# Store exceptions and handle in should_throttle_action?
|
|
39
|
+
self.lightrate_throttled_actions = Array(options[:except]).map(&:to_s)
|
|
40
|
+
self.lightrate_throttle_only_specified = :except
|
|
41
|
+
else
|
|
42
|
+
self.lightrate_throttled_actions = []
|
|
43
|
+
self.lightrate_throttle_only_specified = false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if options.key?(:user_identifier)
|
|
47
|
+
self.lightrate_user_identifier_method = options[:user_identifier]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Disable throttling for this controller
|
|
52
|
+
def skip_lightrate_throttling
|
|
53
|
+
self.lightrate_enabled = false
|
|
54
|
+
self.lightrate_throttled_actions = []
|
|
55
|
+
self.lightrate_throttle_only_specified = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if the given action should be throttled
|
|
59
|
+
# @param action_name [String] The action name to check
|
|
60
|
+
# @return [Boolean] true if the action should be throttled
|
|
61
|
+
def should_throttle_action?(action_name)
|
|
62
|
+
return false unless lightrate_enabled
|
|
63
|
+
return false if lightrate_throttle_only_specified == true && lightrate_throttled_actions.empty?
|
|
64
|
+
return true if lightrate_throttled_actions.empty? && lightrate_throttle_only_specified == false
|
|
65
|
+
|
|
66
|
+
if lightrate_throttle_only_specified == :except
|
|
67
|
+
# If using :except, throttle everything EXCEPT the listed actions
|
|
68
|
+
!lightrate_throttled_actions.include?(action_name.to_s)
|
|
69
|
+
else
|
|
70
|
+
# If using :only, throttle only the listed actions
|
|
71
|
+
lightrate_throttled_actions.include?(action_name.to_s)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if tokens are available for the current request path
|
|
77
|
+
# Note: This method actually consumes a token to check availability
|
|
78
|
+
# @param user_identifier [String, nil] User identifier (defaults to current_user.id)
|
|
79
|
+
# @return [Boolean] true if tokens are available, false otherwise
|
|
80
|
+
def lightrate_tokens_available?(user_identifier: nil)
|
|
81
|
+
user_id = user_identifier || current_user&.id
|
|
82
|
+
return false unless user_id
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
consume_lightrate_token_for_current_path(user_identifier: user_id)
|
|
86
|
+
true
|
|
87
|
+
rescue LightrateRails::LightRateNoTokensAvailable
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Consume a token for the current request path
|
|
93
|
+
# @param user_identifier [String, nil] User identifier (defaults to current_user.id)
|
|
94
|
+
# @return [LightrateClient::ConsumeLocalBucketTokenResponse] The response object
|
|
95
|
+
# @raise [LightrateRails::LightRateNoTokensAvailable] If no tokens are available
|
|
96
|
+
def consume_lightrate_token_for_current_path(user_identifier: nil)
|
|
97
|
+
user_id = user_identifier || current_user&.id
|
|
98
|
+
raise ArgumentError, "User identifier is required" unless user_id
|
|
99
|
+
|
|
100
|
+
response = lightrate_client.consume_local_bucket_token(
|
|
101
|
+
path: request.path,
|
|
102
|
+
http_method: request.request_method,
|
|
103
|
+
user_identifier: user_id
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
unless response.success
|
|
107
|
+
raise LightrateRails::LightRateNoTokensAvailable.new(request.path, user_id, response)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
response
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Manually consume a token from the local bucket for a specific path
|
|
114
|
+
# @param path [String] The API path
|
|
115
|
+
# @param method [String] The HTTP method (defaults to current request method)
|
|
116
|
+
# @param user_identifier [String, nil] User identifier (defaults to current_user.id)
|
|
117
|
+
def consume_lightrate_token_for_path(path:, method: nil, user_identifier: nil)
|
|
118
|
+
user_id = user_identifier || current_user&.id
|
|
119
|
+
raise ArgumentError, "User identifier is required" unless user_id
|
|
120
|
+
|
|
121
|
+
http_method = method || request.request_method
|
|
122
|
+
|
|
123
|
+
response = lightrate_client.consume_local_bucket_token(
|
|
124
|
+
path: path,
|
|
125
|
+
http_method: http_method,
|
|
126
|
+
user_identifier: user_id
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
unless response.success
|
|
130
|
+
raise LightrateRails::LightRateNoTokensAvailable.new(path, user_id, response)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
response
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
# Check if the current action should be throttled
|
|
139
|
+
# @return [Boolean]
|
|
140
|
+
def should_throttle_current_action?
|
|
141
|
+
return false unless LightrateRails.configuration.enabled
|
|
142
|
+
self.class.should_throttle_action?(action_name)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Main before_action that consumes a token for the current request
|
|
146
|
+
# Raises LightRateNoTokensAvailable if no tokens are available, which is caught by rescue_from
|
|
147
|
+
def consume_lightrate_token
|
|
148
|
+
user_id = get_lightrate_user_identifier
|
|
149
|
+
|
|
150
|
+
# Skip throttling if we can't identify the user
|
|
151
|
+
return if user_id.nil?
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
response = lightrate_client.consume_local_bucket_token(
|
|
155
|
+
path: request.path,
|
|
156
|
+
http_method: request.request_method,
|
|
157
|
+
user_identifier: user_id
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
unless response.success
|
|
161
|
+
raise LightrateRails::LightRateNoTokensAvailable.new(request.path, user_id, response)
|
|
162
|
+
end
|
|
163
|
+
rescue LightrateClient::APIError => e
|
|
164
|
+
# Handle API errors - log and continue with request
|
|
165
|
+
Rails.logger.warn("LightRate API error: #{e.message}") if Rails.logger
|
|
166
|
+
rescue LightrateClient::ConfigurationError => e
|
|
167
|
+
# Handle configuration errors - log and continue with request
|
|
168
|
+
Rails.logger.error("LightRate configuration error: #{e.message}") if Rails.logger
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get the user identifier for the current request
|
|
173
|
+
# Uses controller-level configuration if available, otherwise falls back to current_user.id
|
|
174
|
+
# @return [String, nil]
|
|
175
|
+
def get_lightrate_user_identifier
|
|
176
|
+
if self.class.lightrate_user_identifier_method
|
|
177
|
+
case self.class.lightrate_user_identifier_method
|
|
178
|
+
when Proc
|
|
179
|
+
instance_exec(&self.class.lightrate_user_identifier_method)
|
|
180
|
+
when Symbol
|
|
181
|
+
send(self.class.lightrate_user_identifier_method)
|
|
182
|
+
else
|
|
183
|
+
self.class.lightrate_user_identifier_method
|
|
184
|
+
end
|
|
185
|
+
elsif respond_to?(:current_user)
|
|
186
|
+
current_user&.id
|
|
187
|
+
else
|
|
188
|
+
'Anonymous'
|
|
189
|
+
end
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
Rails.logger.warn("Failed to get user identifier: #{e.message}") if Rails.logger
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def handle_rate_limit_exceeded(exception)
|
|
196
|
+
# Check if we're in an API controller (ActionController::API) or regular controller (ActionController::Base)
|
|
197
|
+
if self.class.ancestors.include?(ActionController::API)
|
|
198
|
+
# API controller - just render JSON
|
|
199
|
+
render json: {
|
|
200
|
+
error: 'Too Many Requests',
|
|
201
|
+
message: 'Rate limit exceeded. Please try again later.',
|
|
202
|
+
}, status: 429
|
|
203
|
+
else
|
|
204
|
+
# Regular controller - use respond_to
|
|
205
|
+
respond_to do |format|
|
|
206
|
+
format.html do
|
|
207
|
+
flash[:error] = "Rate limit exceeded. Please try again later."
|
|
208
|
+
redirect_back(fallback_location: root_path)
|
|
209
|
+
end
|
|
210
|
+
format.json do
|
|
211
|
+
render json: {
|
|
212
|
+
error: 'Too Many Requests',
|
|
213
|
+
message: 'Rate limit exceeded. Please try again later.',
|
|
214
|
+
}, status: 429
|
|
215
|
+
end
|
|
216
|
+
format.xml do
|
|
217
|
+
render xml: {
|
|
218
|
+
error: 'Too Many Requests',
|
|
219
|
+
message: 'Rate limit exceeded. Please try again later.',
|
|
220
|
+
}.to_xml(root: 'error'), status: 429
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def lightrate_client
|
|
227
|
+
LightrateRails.client
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LightrateRails
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
config.to_prepare do
|
|
6
|
+
# Include in ActionController::Base
|
|
7
|
+
if defined?(ActionController::Base)
|
|
8
|
+
ActionController::Base.include LightrateRails::ControllerHelper
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Include in ActionController::API
|
|
12
|
+
if defined?(ActionController::API)
|
|
13
|
+
ActionController::API.include LightrateRails::ControllerHelper
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LightrateRails
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
8
|
+
class LightRateNoTokensAvailable < Error
|
|
9
|
+
attr_reader :path, :user_identifier, :response
|
|
10
|
+
|
|
11
|
+
def initialize(path = nil, user_identifier = nil, response = nil)
|
|
12
|
+
@path = path
|
|
13
|
+
@user_identifier = user_identifier
|
|
14
|
+
@response = response
|
|
15
|
+
|
|
16
|
+
message = "No tokens available for request"
|
|
17
|
+
message += " to #{path}" if path
|
|
18
|
+
message += " for user #{user_identifier}" if user_identifier
|
|
19
|
+
|
|
20
|
+
super(message)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lightrate_client"
|
|
4
|
+
require "lightrate_rails/version"
|
|
5
|
+
require "lightrate_rails/engine"
|
|
6
|
+
require "lightrate_rails/errors"
|
|
7
|
+
require "lightrate_rails/configuration"
|
|
8
|
+
require "lightrate_rails/controller_helper"
|
|
9
|
+
|
|
10
|
+
module LightrateRails
|
|
11
|
+
class << self
|
|
12
|
+
def configure
|
|
13
|
+
yield(configuration)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configuration
|
|
17
|
+
@configuration ||= Configuration.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def client
|
|
21
|
+
@client ||= create_client
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_client
|
|
25
|
+
raise ConfigurationError, "API key is required" unless configuration.api_key
|
|
26
|
+
raise ConfigurationError, "Application ID is required" unless configuration.application_id
|
|
27
|
+
|
|
28
|
+
@client = LightrateClient::Client.new(
|
|
29
|
+
configuration.api_key,
|
|
30
|
+
configuration.application_id,
|
|
31
|
+
{
|
|
32
|
+
timeout: configuration.timeout,
|
|
33
|
+
retry_attempts: configuration.retry_attempts,
|
|
34
|
+
logger: configuration.logger,
|
|
35
|
+
default_local_bucket_size: configuration.default_local_bucket_size
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reset!
|
|
41
|
+
@configuration = nil
|
|
42
|
+
@client = nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lightrate-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Lightbourne Technologies
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: lightrate-client
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: bundler
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rspec-rails
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '6.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '6.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: webmock
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rubocop
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '1.0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '1.0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: rubocop-rspec
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '2.0'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '2.0'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: simplecov
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0.21'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0.21'
|
|
153
|
+
- !ruby/object:Gem::Dependency
|
|
154
|
+
name: vcr
|
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - "~>"
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '6.0'
|
|
160
|
+
type: :development
|
|
161
|
+
prerelease: false
|
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - "~>"
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '6.0'
|
|
167
|
+
description: A Rails gem that provides seamless integration with Lightrate API throttling
|
|
168
|
+
using local token buckets and controller-level rate limiting.
|
|
169
|
+
email:
|
|
170
|
+
- grayden@lightbournetechnologies.ca
|
|
171
|
+
executables: []
|
|
172
|
+
extensions: []
|
|
173
|
+
extra_rdoc_files: []
|
|
174
|
+
files:
|
|
175
|
+
- Gemfile
|
|
176
|
+
- Gemfile.lock
|
|
177
|
+
- README.md
|
|
178
|
+
- Rakefile
|
|
179
|
+
- lib/lightrate_rails.rb
|
|
180
|
+
- lib/lightrate_rails/configuration.rb
|
|
181
|
+
- lib/lightrate_rails/controller_helper.rb
|
|
182
|
+
- lib/lightrate_rails/engine.rb
|
|
183
|
+
- lib/lightrate_rails/errors.rb
|
|
184
|
+
- lib/lightrate_rails/version.rb
|
|
185
|
+
homepage: https://github.com/lightbourne-technologies/lightrate-client-rails
|
|
186
|
+
licenses:
|
|
187
|
+
- MIT
|
|
188
|
+
metadata:
|
|
189
|
+
homepage_uri: https://github.com/lightbourne-technologies/lightrate-client-rails
|
|
190
|
+
source_code_uri: https://github.com/lightbourne-technologies/lightrate-client-rails
|
|
191
|
+
changelog_uri: https://github.com/lightbourne-technologies/lightrate-client-rails/blob/main/CHANGELOG.md
|
|
192
|
+
post_install_message:
|
|
193
|
+
rdoc_options: []
|
|
194
|
+
require_paths:
|
|
195
|
+
- lib
|
|
196
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
197
|
+
requirements:
|
|
198
|
+
- - ">="
|
|
199
|
+
- !ruby/object:Gem::Version
|
|
200
|
+
version: 2.7.0
|
|
201
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
202
|
+
requirements:
|
|
203
|
+
- - ">="
|
|
204
|
+
- !ruby/object:Gem::Version
|
|
205
|
+
version: '0'
|
|
206
|
+
requirements: []
|
|
207
|
+
rubygems_version: 3.5.21
|
|
208
|
+
signing_key:
|
|
209
|
+
specification_version: 4
|
|
210
|
+
summary: Ruby on Rails integration for Lightrate throttling
|
|
211
|
+
test_files: []
|