rapidity 0.0.7.362805 → 0.0.8.373200
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/lib/rapidity/share/base.rb +38 -5
- data/lib/rapidity/share/lua_scripts/acquire.lua +27 -23
- data/lib/rapidity/share/lua_scripts/available_in.lua +2 -1
- data/lib/rapidity/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3def70e3c5647129987af0c2be0161ec2b848dadbe6cbad46f75b8fea3af67e
|
|
4
|
+
data.tar.gz: 01dd8c0f29563020a1b6f6d0aa0e89ff3d809dbe5694ca257b478b32a8005d12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db79ce100ae836e9c992629c23fdb6c163b79e282ec5fbb98bbd60a3709ed85fe0968e61f808f444feb26858fda71a3812e89c429d06215eec2fd050dea045ef
|
|
7
|
+
data.tar.gz: 337db88fdb29d7f6a7a0eb5af05cdf1944a962fab384fcdb8ec1a9968029c2fecbde0cbd186223b08b38d6d6e60e1a1371474c466111bf0b8c867f7cb2cace9a
|
data/Gemfile.lock
CHANGED
data/lib/rapidity/share/base.rb
CHANGED
|
@@ -8,12 +8,15 @@ module Rapidity
|
|
|
8
8
|
BASE_SCRIPTS = [:list, :info, :reset, :delete]
|
|
9
9
|
DEFAULT_KEY_TTL = 6000
|
|
10
10
|
|
|
11
|
+
MX = Monitor.new
|
|
12
|
+
|
|
11
13
|
def initialize(pool, ttl: DEFAULT_KEY_TTL.to_i, logger: nil)
|
|
12
14
|
@pool = pool
|
|
13
15
|
@ttl = ttl
|
|
14
16
|
@logger = logger || Logger.new(STDOUT)
|
|
15
17
|
@logger.level = Logger::DEBUG
|
|
16
18
|
# load_redis_scripts
|
|
19
|
+
restore_lua_hashes
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
# Returns a list of limits matching the pattern
|
|
@@ -154,15 +157,45 @@ module Rapidity
|
|
|
154
157
|
#
|
|
155
158
|
# @return [void]
|
|
156
159
|
def load_redis_scripts
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
cls = self.class
|
|
161
|
+
MX.synchronize do
|
|
162
|
+
@pool.with do |conn|
|
|
163
|
+
list_lua_scripts.each do |script|
|
|
164
|
+
cls.instance_variable_set(lua_script_var(script),
|
|
165
|
+
conn.with {|r| r.script(:load, File.read(File.join(__dir__, 'lua_scripts', "#{script.to_s}.lua"))) }
|
|
166
|
+
)
|
|
167
|
+
end
|
|
162
168
|
end
|
|
163
169
|
end
|
|
170
|
+
restore_lua_hashes
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Restore instance script hashes from class cache
|
|
174
|
+
def restore_lua_hashes
|
|
175
|
+
cls = self.class
|
|
176
|
+
list_lua_scripts.each do |script|
|
|
177
|
+
instance_variable_set(lua_script_var(script), cls.instance_variable_get(lua_script_var(script)))
|
|
178
|
+
end
|
|
164
179
|
end
|
|
165
180
|
|
|
181
|
+
|
|
182
|
+
# это для тестов чтобы проверить перезугрузку сриптов
|
|
183
|
+
def reset_lua_scripts
|
|
184
|
+
cls = self.class
|
|
185
|
+
list_lua_scripts.each do |script|
|
|
186
|
+
instance_variable_set(lua_script_var(script), nil)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def list_lua_scripts
|
|
191
|
+
(BASE_SCRIPTS + self.class::LUA_SCRIPTS)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def lua_script_var(script)
|
|
195
|
+
"@lua_#{script}".to_sym
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
|
|
166
199
|
end
|
|
167
200
|
end
|
|
168
201
|
end
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
-- данные между чтением и записью) и высокую производительность.
|
|
8
8
|
--
|
|
9
9
|
-- Описание функционала файла acquire.lua
|
|
10
|
-
-- 1. Атомарный захват по нескольким лимитам ("Всё или ничего"):
|
|
10
|
+
-- 1. Атомарный захват по нескольким лимитам ("Всё или ничего"):
|
|
11
11
|
-- Скрипт принимает список ключей (лимитов) и запрошенное количество токенов.
|
|
12
12
|
-- Он проверяет, есть ли нужное количество токенов во всех переданных лимитах одновременно.
|
|
13
13
|
-- 2. Ленивое пополнение (Lazy Refill): Токены не пополняются каким-то фоновым процессом.
|
|
14
14
|
-- Вместо этого при каждом обращении (в функции Limit:update) вычисляется разница во времени
|
|
15
15
|
-- с момента последнего обращения (current_time - self.last_used) и добавляется количество токенов, пропорциональное скорости rate.
|
|
16
|
-
-- 3. Проверка условий (Validation):
|
|
16
|
+
-- 3. Проверка условий (Validation):
|
|
17
17
|
-- - Если запрашивается <= 0 токенов или передан пустой список ключей — сразу возвращается ошибка.
|
|
18
18
|
-- - Если хотя бы одного ключа не существует в Redis, возвращается key_not_found.
|
|
19
19
|
-- - Если хотя бы в одном лимите не хватает токенов, скрипт возвращает ошибку not_limits и ничего не списывает из других лимитов (откат транзакции).
|
|
@@ -48,7 +48,7 @@ function Limit:new(limit_key)
|
|
|
48
48
|
local obj = { key = limit_key, exists = false }
|
|
49
49
|
setmetatable(obj, self)
|
|
50
50
|
|
|
51
|
-
-- Оптимизация: вместо EXISTS + HMGET делаем только HMGET.
|
|
51
|
+
-- Оптимизация: вместо EXISTS + HMGET делаем только HMGET.
|
|
52
52
|
-- Если ключа нет, values[1] будет nil.
|
|
53
53
|
local values = redis.call("HMGET", limit_key,
|
|
54
54
|
"max_tokens",
|
|
@@ -57,7 +57,7 @@ function Limit:new(limit_key)
|
|
|
57
57
|
"last_used",
|
|
58
58
|
"rate"
|
|
59
59
|
)
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
if not values[1] then
|
|
62
62
|
return obj -- Ключ не существует
|
|
63
63
|
end
|
|
@@ -68,24 +68,28 @@ function Limit:new(limit_key)
|
|
|
68
68
|
obj.last_used = tonumber(values[4]) or 0
|
|
69
69
|
obj.rate = tonumber(values[5]) or 0
|
|
70
70
|
|
|
71
|
-
obj.exists
|
|
72
|
-
|
|
71
|
+
obj.exists = (obj.max_tokens > 0) and (obj.interval > 0)
|
|
72
|
+
|
|
73
73
|
return obj
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
-- "Ленивое" пополнение корзины токенов на основе прошедшего времени
|
|
77
77
|
function Limit:update(current_time)
|
|
78
|
-
if not self.exists then return self end
|
|
79
|
-
|
|
78
|
+
if not self.exists then return self end
|
|
79
|
+
|
|
80
80
|
local time_passed = current_time - self.last_used
|
|
81
81
|
if time_passed <= 0 then return self end
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
local tokens_to_add = math.floor(time_passed * self.rate)
|
|
84
84
|
self.tokens = math.min(self.tokens + tokens_to_add, self.max_tokens)
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
if tokens_to_add > 0 then
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
if self.tokens == self.max_tokens then
|
|
88
|
+
self.last_used = current_time
|
|
89
|
+
else
|
|
90
|
+
local time_consumed = tokens_to_add / self.rate
|
|
91
|
+
self.last_used = math.min(self.last_used + time_consumed, current_time)
|
|
92
|
+
end
|
|
89
93
|
end
|
|
90
94
|
|
|
91
95
|
return self
|
|
@@ -94,10 +98,10 @@ end
|
|
|
94
98
|
-- Сохранение обновленного стейта лимита в Redis
|
|
95
99
|
function Limit:save()
|
|
96
100
|
if not self.exists then return end
|
|
97
|
-
|
|
101
|
+
|
|
98
102
|
redis.call("HMSET", self.key,
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
"tokens", self.tokens,
|
|
104
|
+
"last_used", self.last_used
|
|
101
105
|
)
|
|
102
106
|
end
|
|
103
107
|
|
|
@@ -128,14 +132,14 @@ local function process_all_limits(limit_keys, requested, current_time)
|
|
|
128
132
|
if #limit_keys == 0 then
|
|
129
133
|
return { "result", "false", "retryable", "false", "error", "no keys passed" }
|
|
130
134
|
end
|
|
131
|
-
|
|
135
|
+
|
|
132
136
|
local limits = {}
|
|
133
|
-
|
|
137
|
+
|
|
134
138
|
-- Фаза 1: Проверка всех лимитов (Read & Validate)
|
|
135
139
|
for i = 1, #limit_keys do
|
|
136
140
|
local key = limit_keys[i]
|
|
137
141
|
local limit = Limit:new(key)
|
|
138
|
-
|
|
142
|
+
|
|
139
143
|
if not limit.exists then
|
|
140
144
|
return { "result", "false", "retryable", "false", "error", "key_not_found", "key", key }
|
|
141
145
|
end
|
|
@@ -153,22 +157,22 @@ local function process_all_limits(limit_keys, requested, current_time)
|
|
|
153
157
|
"key", key
|
|
154
158
|
}
|
|
155
159
|
end
|
|
156
|
-
|
|
160
|
+
|
|
157
161
|
table.insert(limits, limit)
|
|
158
162
|
end
|
|
159
|
-
|
|
163
|
+
|
|
160
164
|
-- Фаза 2: Применение изменений (Write)
|
|
161
165
|
-- Выполняется только если всем лимитам хватило токенов
|
|
162
166
|
for i = 1, #limits do
|
|
163
167
|
local limit = limits[i]
|
|
164
168
|
limit:acquire(requested)
|
|
165
169
|
limit:save()
|
|
166
|
-
|
|
167
|
-
-- Продлеваем жизнь ключу, чтобы он не удалился, если к нему активно обращаются.
|
|
170
|
+
|
|
171
|
+
-- Продлеваем жизнь ключу, чтобы он не удалился, если к нему активно обращаются.
|
|
168
172
|
-- "GT" обновляет TTL только если новый TTL больше текущего.
|
|
169
173
|
redis.call("EXPIRE", limit.key, key_ttl, "GT")
|
|
170
174
|
end
|
|
171
|
-
|
|
175
|
+
|
|
172
176
|
return { "result", "true", "keys", limit_keys }
|
|
173
177
|
end
|
|
174
178
|
|
|
@@ -100,8 +100,9 @@ function Limit:available_in(needed)
|
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
local deficit_tokens = needed - self.tokens
|
|
103
|
+
local time_passed = current_time - self.last_used
|
|
103
104
|
-- Округляем вверх (math.ceil), так как нам нужно дождаться появления ЦЕЛОГО токена
|
|
104
|
-
return math.ceil(deficit_tokens / self.rate)
|
|
105
|
+
return math.ceil(deficit_tokens / self.rate) - time_passed
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
-------------------------------------------------------------------------------
|
data/lib/rapidity/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rapidity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.8.373200
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yurusov Vlad
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-05-06 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|