vnai 2.0.2__py3-none-any.whl → 2.0.3__py3-none-any.whl
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.
- vnai/__init__.py +291 -72
- vnai/beam/__init__.py +2 -2
- vnai/beam/metrics.py +207 -57
- vnai/beam/pulse.py +122 -29
- vnai/beam/quota.py +507 -102
- vnai/flow/__init__.py +7 -2
- vnai/flow/queue.py +142 -55
- vnai/flow/relay.py +476 -149
- vnai/scope/__init__.py +7 -2
- vnai/scope/profile.py +858 -219
- vnai/scope/promo.py +197 -55
- vnai/scope/state.py +246 -71
- {vnai-2.0.2.dist-info → vnai-2.0.3.dist-info}/METADATA +1 -1
- vnai-2.0.3.dist-info/RECORD +16 -0
- {vnai-2.0.2.dist-info → vnai-2.0.3.dist-info}/WHEEL +1 -1
- vnai-2.0.2.dist-info/RECORD +0 -16
- {vnai-2.0.2.dist-info → vnai-2.0.3.dist-info}/top_level.txt +0 -0
vnai/beam/quota.py
CHANGED
@@ -1,107 +1,512 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
import
|
1
|
+
##
|
2
|
+
|
3
|
+
##
|
4
|
+
|
5
|
+
|
6
|
+
import time
|
7
|
+
import functools
|
8
|
+
import threading
|
9
9
|
from collections import defaultdict
|
10
10
|
from datetime import datetime
|
11
|
+
|
11
12
|
class RateLimitExceeded(Exception):
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
#--
|
14
|
+
def __init__(self, resource_type, limit_type="min", current_usage=None, limit_value=None, retry_after=None):
|
15
|
+
self.resource_type = resource_type
|
16
|
+
self.limit_type = limit_type
|
17
|
+
self.current_usage = current_usage
|
18
|
+
self.limit_value = limit_value
|
19
|
+
self.retry_after = retry_after
|
20
|
+
|
21
|
+
##
|
22
|
+
|
23
|
+
message = f"Bạn đã gửi quá nhiều request tới {resource_type}. "
|
24
|
+
if retry_after:
|
25
|
+
message += f"Vui lòng thử lại sau {round(retry_after)} giây."
|
26
|
+
else:
|
27
|
+
message += "Vui lòng thêm thời gian chờ giữa các lần gửi request."
|
28
|
+
|
29
|
+
super().__init__(message)
|
30
|
+
|
17
31
|
class Guardian:
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
#--
|
33
|
+
|
34
|
+
_instance = None
|
35
|
+
_lock = threading.Lock()
|
36
|
+
|
37
|
+
def __new__(cls):
|
38
|
+
with cls._lock:
|
39
|
+
if cls._instance is None:
|
40
|
+
cls._instance = super(Guardian, cls).__new__(cls)
|
41
|
+
cls._instance._initialize()
|
42
|
+
return cls._instance
|
43
|
+
|
44
|
+
def _initialize(self):
|
45
|
+
#--
|
46
|
+
self.resource_limits = defaultdict(lambda: defaultdict(int))
|
47
|
+
self.usage_counters = defaultdict(lambda: defaultdict(list))
|
48
|
+
|
49
|
+
##
|
50
|
+
|
51
|
+
self.resource_limits["default"] = {"min": 60, "hour": 3000}
|
52
|
+
self.resource_limits["TCBS"] = {"min": 60, "hour": 3000}
|
53
|
+
self.resource_limits["VCI"] = {"min": 60, "hour": 3000}
|
54
|
+
self.resource_limits["VCI.ext"] = {"min": 600, "hour": 36000}
|
55
|
+
self.resource_limits["VND.ext"] = {"min": 600, "hour": 36000}
|
56
|
+
self.resource_limits["CAF.ext"] = {"min": 600, "hour": 36000}
|
57
|
+
self.resource_limits["SPL.ext"] = {"min": 600, "hour": 36000}
|
58
|
+
self.resource_limits["VDS.ext"] = {"min": 600, "hour": 36000}
|
59
|
+
self.resource_limits["FAD.ext"] = {"min": 600, "hour": 36000}
|
60
|
+
|
61
|
+
def verify(self, operation_id, resource_type="default"):
|
62
|
+
#--
|
63
|
+
current_time = time.time()
|
64
|
+
|
65
|
+
##
|
66
|
+
|
67
|
+
limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
|
68
|
+
|
69
|
+
##
|
70
|
+
|
71
|
+
minute_cutoff = current_time - 60
|
72
|
+
self.usage_counters[resource_type]["min"] = [
|
73
|
+
t for t in self.usage_counters[resource_type]["min"]
|
74
|
+
if t > minute_cutoff
|
75
|
+
]
|
76
|
+
|
77
|
+
minute_usage = len(self.usage_counters[resource_type]["min"])
|
78
|
+
minute_exceeded = minute_usage >= limits["min"]
|
79
|
+
|
80
|
+
if minute_exceeded:
|
81
|
+
##
|
82
|
+
|
83
|
+
from vnai.beam.metrics import collector
|
84
|
+
collector.record(
|
85
|
+
"rate_limit",
|
86
|
+
{
|
87
|
+
"resource_type": resource_type,
|
88
|
+
"limit_type": "min",
|
89
|
+
"limit_value": limits["min"],
|
90
|
+
"current_usage": minute_usage,
|
91
|
+
"is_exceeded": True
|
92
|
+
},
|
93
|
+
priority="high"
|
94
|
+
)
|
95
|
+
##
|
96
|
+
|
97
|
+
raise RateLimitExceeded(
|
98
|
+
resource_type=resource_type,
|
99
|
+
limit_type="min",
|
100
|
+
current_usage=minute_usage,
|
101
|
+
limit_value=limits["min"],
|
102
|
+
retry_after=60 - (current_time % 60) ##
|
103
|
+
|
104
|
+
)
|
105
|
+
|
106
|
+
##
|
107
|
+
|
108
|
+
hour_cutoff = current_time - 3600
|
109
|
+
self.usage_counters[resource_type]["hour"] = [
|
110
|
+
t for t in self.usage_counters[resource_type]["hour"]
|
111
|
+
if t > hour_cutoff
|
112
|
+
]
|
113
|
+
|
114
|
+
hour_usage = len(self.usage_counters[resource_type]["hour"])
|
115
|
+
hour_exceeded = hour_usage >= limits["hour"]
|
116
|
+
|
117
|
+
##
|
118
|
+
|
119
|
+
from vnai.beam.metrics import collector
|
120
|
+
collector.record(
|
121
|
+
"rate_limit",
|
122
|
+
{
|
123
|
+
"resource_type": resource_type,
|
124
|
+
"limit_type": "hour" if hour_exceeded else "min",
|
125
|
+
"limit_value": limits["hour"] if hour_exceeded else limits["min"],
|
126
|
+
"current_usage": hour_usage if hour_exceeded else minute_usage,
|
127
|
+
"is_exceeded": hour_exceeded
|
128
|
+
}
|
129
|
+
)
|
130
|
+
|
131
|
+
if hour_exceeded:
|
132
|
+
##
|
133
|
+
|
134
|
+
raise RateLimitExceeded(
|
135
|
+
resource_type=resource_type,
|
136
|
+
limit_type="hour",
|
137
|
+
current_usage=hour_usage,
|
138
|
+
limit_value=limits["hour"],
|
139
|
+
retry_after=3600 - (current_time % 3600) ##
|
140
|
+
|
141
|
+
)
|
142
|
+
|
143
|
+
##
|
144
|
+
|
145
|
+
self.usage_counters[resource_type]["min"].append(current_time)
|
146
|
+
self.usage_counters[resource_type]["hour"].append(current_time)
|
147
|
+
return True
|
148
|
+
|
149
|
+
def usage(self, resource_type="default"):
|
150
|
+
#--
|
151
|
+
current_time = time.time()
|
152
|
+
limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
|
153
|
+
|
154
|
+
##
|
155
|
+
|
156
|
+
minute_cutoff = current_time - 60
|
157
|
+
hour_cutoff = current_time - 3600
|
158
|
+
|
159
|
+
self.usage_counters[resource_type]["min"] = [
|
160
|
+
t for t in self.usage_counters[resource_type]["min"]
|
161
|
+
if t > minute_cutoff
|
162
|
+
]
|
163
|
+
|
164
|
+
self.usage_counters[resource_type]["hour"] = [
|
165
|
+
t for t in self.usage_counters[resource_type]["hour"]
|
166
|
+
if t > hour_cutoff
|
167
|
+
]
|
168
|
+
|
169
|
+
##
|
170
|
+
|
171
|
+
minute_usage = len(self.usage_counters[resource_type]["min"])
|
172
|
+
hour_usage = len(self.usage_counters[resource_type]["hour"])
|
173
|
+
|
174
|
+
minute_percentage = (minute_usage / limits["min"]) * 100 if limits["min"] > 0 else 0
|
175
|
+
hour_percentage = (hour_usage / limits["hour"]) * 100 if limits["hour"] > 0 else 0
|
176
|
+
|
177
|
+
##
|
178
|
+
|
179
|
+
return max(minute_percentage, hour_percentage)
|
180
|
+
|
181
|
+
def get_limit_status(self, resource_type="default"):
|
182
|
+
#--
|
183
|
+
current_time = time.time()
|
184
|
+
limits = self.resource_limits.get(resource_type, self.resource_limits["default"])
|
185
|
+
|
186
|
+
##
|
187
|
+
|
188
|
+
minute_cutoff = current_time - 60
|
189
|
+
hour_cutoff = current_time - 3600
|
190
|
+
|
191
|
+
minute_usage = len([t for t in self.usage_counters[resource_type]["min"] if t > minute_cutoff])
|
192
|
+
hour_usage = len([t for t in self.usage_counters[resource_type]["hour"] if t > hour_cutoff])
|
193
|
+
|
194
|
+
return {
|
195
|
+
"resource_type": resource_type,
|
196
|
+
"minute_limit": {
|
197
|
+
"usage": minute_usage,
|
198
|
+
"limit": limits["min"],
|
199
|
+
"percentage": (minute_usage / limits["min"]) * 100 if limits["min"] > 0 else 0,
|
200
|
+
"remaining": max(0, limits["min"] - minute_usage),
|
201
|
+
"reset_in_seconds": 60 - (current_time % 60)
|
202
|
+
},
|
203
|
+
"hour_limit": {
|
204
|
+
"usage": hour_usage,
|
205
|
+
"limit": limits["hour"],
|
206
|
+
"percentage": (hour_usage / limits["hour"]) * 100 if limits["hour"] > 0 else 0,
|
207
|
+
"remaining": max(0, limits["hour"] - hour_usage),
|
208
|
+
"reset_in_seconds": 3600 - (current_time % 3600)
|
209
|
+
}
|
210
|
+
}
|
211
|
+
|
212
|
+
##
|
213
|
+
|
214
|
+
guardian = Guardian()
|
215
|
+
|
33
216
|
class CleanErrorContext:
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
217
|
+
#--
|
218
|
+
##
|
219
|
+
|
220
|
+
_last_message_time = 0
|
221
|
+
_message_cooldown = 5 ##
|
222
|
+
|
223
|
+
|
224
|
+
def __enter__(self):
|
225
|
+
return self
|
226
|
+
|
227
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
228
|
+
if exc_type is RateLimitExceeded:
|
229
|
+
current_time = time.time()
|
230
|
+
|
231
|
+
##
|
232
|
+
|
233
|
+
if current_time - CleanErrorContext._last_message_time >= CleanErrorContext._message_cooldown:
|
234
|
+
print(f"\n⚠️ {str(exc_val)}\n")
|
235
|
+
CleanErrorContext._last_message_time = current_time
|
236
|
+
|
237
|
+
##
|
238
|
+
|
239
|
+
##
|
240
|
+
|
241
|
+
import sys
|
242
|
+
sys.exit(f"Rate limit exceeded. {str(exc_val)} Process terminated.")
|
243
|
+
|
244
|
+
##
|
245
|
+
|
246
|
+
return False
|
247
|
+
return False
|
248
|
+
|
249
|
+
|
250
|
+
def optimize(resource_type='default', loop_threshold=10, time_window=5, ad_cooldown=150, content_trigger_threshold=3,
|
251
|
+
max_retries=2, backoff_factor=2, debug=False):
|
252
|
+
#--
|
253
|
+
##
|
254
|
+
|
255
|
+
if callable(resource_type):
|
256
|
+
func = resource_type
|
257
|
+
return _create_wrapper(func, 'default', loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
258
|
+
max_retries, backoff_factor, debug)
|
259
|
+
|
260
|
+
##
|
261
|
+
|
262
|
+
if loop_threshold < 2:
|
263
|
+
raise ValueError(f"loop_threshold must be at least 2, got {loop_threshold}")
|
264
|
+
if time_window <= 0:
|
265
|
+
raise ValueError(f"time_window must be positive, got {time_window}")
|
266
|
+
if content_trigger_threshold < 1:
|
267
|
+
raise ValueError(f"content_trigger_threshold must be at least 1, got {content_trigger_threshold}")
|
268
|
+
if max_retries < 0:
|
269
|
+
raise ValueError(f"max_retries must be non-negative, got {max_retries}")
|
270
|
+
if backoff_factor <= 0:
|
271
|
+
raise ValueError(f"backoff_factor must be positive, got {backoff_factor}")
|
272
|
+
|
273
|
+
##
|
274
|
+
|
275
|
+
def decorator(func):
|
276
|
+
return _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
277
|
+
max_retries, backoff_factor, debug)
|
278
|
+
return decorator
|
279
|
+
|
280
|
+
def _create_wrapper(func, resource_type, loop_threshold, time_window, ad_cooldown, content_trigger_threshold,
|
281
|
+
max_retries, backoff_factor, debug):
|
282
|
+
#--
|
283
|
+
##
|
284
|
+
|
285
|
+
call_history = []
|
286
|
+
last_ad_time = 0
|
287
|
+
consecutive_loop_detections = 0
|
288
|
+
session_displayed = False ##
|
289
|
+
|
290
|
+
session_start_time = time.time()
|
291
|
+
session_timeout = 1800 ##
|
292
|
+
|
293
|
+
|
294
|
+
@functools.wraps(func)
|
295
|
+
def wrapper(*args, **kwargs):
|
296
|
+
nonlocal last_ad_time, consecutive_loop_detections, session_displayed, session_start_time
|
297
|
+
current_time = time.time()
|
298
|
+
content_triggered = False
|
299
|
+
|
300
|
+
##
|
301
|
+
|
302
|
+
if current_time - session_start_time > session_timeout:
|
303
|
+
session_displayed = False
|
304
|
+
session_start_time = current_time
|
305
|
+
|
306
|
+
##
|
307
|
+
|
308
|
+
retries = 0
|
309
|
+
while True:
|
310
|
+
##
|
311
|
+
|
312
|
+
##
|
313
|
+
|
314
|
+
call_history.append(current_time)
|
315
|
+
|
316
|
+
##
|
317
|
+
|
318
|
+
while call_history and current_time - call_history[0] > time_window:
|
319
|
+
call_history.pop(0)
|
320
|
+
|
321
|
+
##
|
322
|
+
|
323
|
+
loop_detected = len(call_history) >= loop_threshold
|
324
|
+
|
325
|
+
if debug and loop_detected:
|
326
|
+
print(f"[OPTIMIZE] Đã phát hiện vòng lặp cho {func.__name__}: {len(call_history)} lần gọi trong {time_window}s")
|
327
|
+
|
328
|
+
##
|
329
|
+
|
330
|
+
if loop_detected:
|
331
|
+
consecutive_loop_detections += 1
|
332
|
+
if debug:
|
333
|
+
print(f"[OPTIMIZE] Số lần phát hiện vòng lặp liên tiếp: {consecutive_loop_detections}/{content_trigger_threshold}")
|
334
|
+
else:
|
335
|
+
consecutive_loop_detections = 0
|
336
|
+
|
337
|
+
##
|
338
|
+
|
339
|
+
should_show_content = (consecutive_loop_detections >= content_trigger_threshold) and (current_time - last_ad_time >= ad_cooldown) and not session_displayed
|
340
|
+
|
341
|
+
##
|
342
|
+
|
343
|
+
if should_show_content:
|
344
|
+
last_ad_time = current_time
|
345
|
+
consecutive_loop_detections = 0
|
346
|
+
content_triggered = True
|
347
|
+
session_displayed = True ##
|
348
|
+
|
349
|
+
|
350
|
+
if debug:
|
351
|
+
print(f"[OPTIMIZE] Đã kích hoạt nội dung cho {func.__name__}")
|
352
|
+
|
353
|
+
##
|
354
|
+
|
355
|
+
try:
|
356
|
+
from vnai.scope.promo import manager
|
357
|
+
|
358
|
+
##
|
359
|
+
|
360
|
+
try:
|
361
|
+
from vnai.scope.profile import inspector
|
362
|
+
environment = inspector.examine().get("environment", None)
|
363
|
+
manager.present_content(environment=environment, context="loop")
|
364
|
+
except ImportError:
|
365
|
+
manager.present_content(context="loop")
|
366
|
+
|
367
|
+
except ImportError:
|
368
|
+
##
|
369
|
+
|
370
|
+
print(f"Phát hiện vòng lặp: Hàm '{func.__name__}' đang được gọi trong một vòng lặp")
|
371
|
+
except Exception as e:
|
372
|
+
##
|
373
|
+
|
374
|
+
if debug:
|
375
|
+
print(f"[OPTIMIZE] Lỗi khi hiển thị nội dung: {str(e)}")
|
376
|
+
|
377
|
+
##
|
378
|
+
|
379
|
+
try:
|
380
|
+
##
|
381
|
+
|
382
|
+
with CleanErrorContext():
|
383
|
+
guardian.verify(func.__name__, resource_type)
|
384
|
+
|
385
|
+
except RateLimitExceeded as e:
|
386
|
+
##
|
387
|
+
|
388
|
+
from vnai.beam.metrics import collector
|
389
|
+
collector.record(
|
390
|
+
"error",
|
391
|
+
{
|
392
|
+
"function": func.__name__,
|
393
|
+
"error": str(e),
|
394
|
+
"context": "resource_verification",
|
395
|
+
"resource_type": resource_type,
|
396
|
+
"retry_attempt": retries
|
397
|
+
},
|
398
|
+
priority="high"
|
399
|
+
)
|
400
|
+
|
401
|
+
##
|
402
|
+
|
403
|
+
if not session_displayed:
|
404
|
+
try:
|
405
|
+
from vnai.scope.promo import manager
|
406
|
+
try:
|
407
|
+
from vnai.scope.profile import inspector
|
408
|
+
environment = inspector.examine().get("environment", None)
|
409
|
+
manager.present_content(environment=environment, context="loop")
|
410
|
+
session_displayed = True ##
|
411
|
+
|
412
|
+
last_ad_time = current_time
|
413
|
+
except ImportError:
|
414
|
+
manager.present_content(context="loop")
|
415
|
+
session_displayed = True
|
416
|
+
last_ad_time = current_time
|
417
|
+
except Exception:
|
418
|
+
pass ##
|
419
|
+
|
420
|
+
|
421
|
+
##
|
422
|
+
|
423
|
+
if retries < max_retries:
|
424
|
+
wait_time = backoff_factor ** retries
|
425
|
+
retries += 1
|
426
|
+
|
427
|
+
##
|
428
|
+
|
429
|
+
if hasattr(e, "retry_after") and e.retry_after:
|
430
|
+
wait_time = min(wait_time, e.retry_after)
|
431
|
+
|
432
|
+
if debug:
|
433
|
+
print(f"[OPTIMIZE] Đã đạt giới hạn tốc độ cho {func.__name__}, thử lại sau {wait_time} giây (lần thử {retries}/{max_retries})")
|
434
|
+
|
435
|
+
time.sleep(wait_time)
|
436
|
+
continue ##
|
437
|
+
|
438
|
+
else:
|
439
|
+
##
|
440
|
+
|
441
|
+
raise
|
442
|
+
|
443
|
+
##
|
444
|
+
|
445
|
+
start_time = time.time()
|
446
|
+
success = False
|
447
|
+
error = None
|
448
|
+
|
449
|
+
try:
|
450
|
+
##
|
451
|
+
|
452
|
+
result = func(*args, **kwargs)
|
453
|
+
success = True
|
454
|
+
return result
|
455
|
+
except Exception as e:
|
456
|
+
error = str(e)
|
457
|
+
raise
|
458
|
+
finally:
|
459
|
+
##
|
460
|
+
|
461
|
+
execution_time = time.time() - start_time
|
462
|
+
|
463
|
+
##
|
464
|
+
|
465
|
+
try:
|
466
|
+
from vnai.beam.metrics import collector
|
467
|
+
collector.record(
|
468
|
+
"function",
|
469
|
+
{
|
470
|
+
"function": func.__name__,
|
471
|
+
"resource_type": resource_type,
|
472
|
+
"execution_time": execution_time,
|
473
|
+
"success": success,
|
474
|
+
"error": error,
|
475
|
+
"in_loop": loop_detected,
|
476
|
+
"loop_depth": len(call_history),
|
477
|
+
"content_triggered": content_triggered,
|
478
|
+
"timestamp": datetime.now().isoformat(),
|
479
|
+
"retry_count": retries if retries > 0 else None
|
480
|
+
}
|
481
|
+
)
|
482
|
+
|
483
|
+
##
|
484
|
+
|
485
|
+
if content_triggered:
|
486
|
+
collector.record(
|
487
|
+
"ad_opportunity",
|
488
|
+
{
|
489
|
+
"function": func.__name__,
|
490
|
+
"resource_type": resource_type,
|
491
|
+
"call_frequency": len(call_history) / time_window,
|
492
|
+
"consecutive_loops": consecutive_loop_detections,
|
493
|
+
"timestamp": datetime.now().isoformat()
|
494
|
+
}
|
495
|
+
)
|
496
|
+
except ImportError:
|
497
|
+
##
|
498
|
+
|
499
|
+
pass
|
500
|
+
|
501
|
+
##
|
502
|
+
|
503
|
+
break
|
504
|
+
|
505
|
+
return wrapper
|
506
|
+
|
507
|
+
|
508
|
+
##
|
509
|
+
|
510
|
+
def rate_limit_status(resource_type="default"):
|
511
|
+
#--
|
512
|
+
return guardian.get_limit_status(resource_type)
|