فهرست مقاله
یک عامل صوتی ممکن است در یک نوبت چند درخواست ابزار بسازد. برای آزمودن مرز پذیرش این درخواستها، میتوان ظرفیت جهش و نرخ بازپرشدن بودجه را جدا تعریف کرد. این درس یک سطل توکن کوچک با زمان ساختگی میسازد و پاسخهایش را با حساب دستی و مرجع کسری مستقل میسنجد. برنامه هیچ ابزار، صف، تماس یا سرویس واقعی اجرا نمیکند؛ accepted فقط تصمیم مدل دربارهٔ بودجهٔ فرضی است.
۱. ظرفیت جهش، نرخ پایدار و هزینهٔ درخواست را جدا کنیم

ظرفیت نمونه سه واحد بودجه و نرخ بازپرشدن دو واحد در ثانیه است. سطل در زمان صفر پُر شروع میشود. یک درخواست میتواند یک یا چند واحد هزینهٔ قراردادی داشته باشد؛ در مثال نخست، هزینه دو واحد است. این واحدها توکنِ مدل زبانی، هزینهٔ پولی یا مدت اجرای ابزار نیستند. وقتی همهٔ درخواستها هزینهٔ یک داشته باشند، شمار واحد مصرفشده با شمار درخواست پذیرفتهشده برابر میشود.
ضمیمهٔ A.4 در RFC 3290 مدلی ریاضی برای ظرفیت B، نرخ R و اعتبار موجود بیان میکند: اعتبار با زمان زیاد میشود، در B سقف میخورد و فقط برای ورودیِ پذیرفتهشده کم میشود. آن سند دربارهٔ مدل شبکه است؛ ما واحد بودجهٔ درخواستِ ساختگی را انتخاب کردهایم. این کد مسیریاب، پروتکل شبکه یا پیادهسازی سازگار با تمام آن سند نیست.
در شروع، سه درخواست تکواحدی در یک زمان میتوانند پذیرفته شوند؛ چهارمی باید رد شود، هرچند نرخ بازپرشدن دو واحد در ثانیه است. ظرفیت اولیه امکان جهش میدهد. بنابراین عبارت «دو درخواست در ثانیه» بدون توضیح ظرفیت و هزینه، رفتار کوتاهمدت این مدل را کامل توصیف نمیکند. اندازه و نرخِ مثال، تنظیم پیشنهادی برای سامانهٔ واقعی شما نیستند.
۲. اعتبار صحیح را با زمان میلیثانیه حساب کنیم

برای حفظ کسر واحدها، هر واحد را هزار میلیواحد میگیریم. ظرفیت سه، ۳۰۰۰ میلیواحد است. اگر نرخ دو واحد در ثانیه باشد، گذشت هر میلیثانیه دو میلیواحد اضافه میکند. پس بازپرشدن با ضرب نرخ در فاصلهٔ صحیحِ میلیثانیه انجام میشود و برای این شبکهٔ زمانی نیازی به گردکردن شناور نداریم. زمانهای مساوی به ترتیب فراخوانی پردازش میشوند.
قاعدهٔ بهروزرسانی، کمینهٔ ظرفیت و اعتبار قبلی بهعلاوهٔ بازپرشدن فاصلهٔ گذشته است. پس از سکوت طولانی، سطل بیشتر از ظرفیت ذخیره نمیکند؛ اعتبارِ سرریز برای جهش بزرگتر آینده کنار گذاشته نمیشود. درخواست ردشده نیز زمان آخرین بررسی و اعتبارِ بازپرشده را ثبت میکند، اما هزینه را کم نمیکند. در نتیجه، بررسیهای پیاپی یک فاصلهٔ زمانی را دو بار به حساب نمیآورند.
زمان در این مثال عدد صحیح از صفر تا توان شصتوسومِ دو منهای یک میلیثانیه است و عقب نمیرود؛ این مبدأ، تاریخ تقویمی یا ساعت تهران نیست. ظرفیت و نرخ اعداد صحیح مثبت تا دههزار و هزینه از یک تا ظرفیتاند. هزینهٔ بزرگتر از ظرفیت خطای قرارداد است، چون با صبر کردن در همین سطل جا نمیشود. نوع bool، عدد شناور و رشته برای این فیلدها پذیرفته نیستند.
۳. جواب دستیِ پذیرش، رد و کمترین فاصلهٔ انتظار

در زمان صفر، درخواست دوواحدی پذیرفته و هزار میلیواحد باقی میماند. درخواست دوواحدیِ دوم در همان زمان رد میشود و همان هزار باقی است. تا زمان ۴۹۹، ۹۹۸ میلیواحد دیگر جمع شده و موجودی ۱۹۹۸ است؛ هنوز دو میلیواحد کم داریم. در زمان ۵۰۰، اعتبار به ۲۰۰۰ میرسد و درخواست دوواحدی پذیرفته میشود. موجودی پس از آن صفر است.
در زمان ۷۵۰، فقط ۵۰۰ میلیواحد داریم؛ درخواست تکواحدی رد میشود و ۲۵۰ میلیثانیهٔ دیگر لازم دارد. در زمان هزار، همان درخواست با موجودی کامل یک واحد پذیرفته میشود. پاسخ retry_after_ms از سقفِ تقسیم کسری اعتبار بر نرخ محاسبه میشود. این کمترین فاصلهٔ صحیح برای همان هزینه، با فرض نبود مصرفِ پذیرفتهشدهٔ میانراهی و ادامهٔ زمان مجاز است؛ جای رزرو نوبت یا تضمین اجرای آینده را نمیگیرد.
در آزمون جدا با ظرفیت دو و نرخ سه، ابتدا دو واحد را مصرف میکنیم. برای یک واحد تازه، ۳۳۳ میلیثانیه فقط ۹۹۹ میلیواحد میسازد؛ باید یک میلیثانیهٔ دیگر صبر کرد. در ۳۳۴، درخواست پذیرفته و دو میلیواحد باقی میماند. این مرز نشان میدهد گردکردن فاصله به پایین، درخواست را زودتر از بودجهٔ لازم مجاز میکند.
۴. کد کامل و مرجع کسری برای هجده بررسی

فایل tool_token_bucket.py با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. پیادهسازی اصلی فقط عدد صحیحِ میلیواحد دارد. مرجع مستقل با Fraction پایتون، خود واحد بودجه را به شکل کسر نگه میدارد و فاصلهٔ زمان را بر هزار تقسیم میکند. در پایان هر گام، خروجی کامل دو مسیر با هم مقایسه میشود.
"""Sequential offline admission model with exact integer milliunit credit.
No actual tool, queue, HTTP service, sleeping, model billing or authentication.
"""
from fractions import Fraction
import json
import math
import platform
MAX_MS=2**63-1
def bounded_integer(value,lower,upper,name):
if type(value) is not int or not lower<=value<=upper:
raise ValueError(name)
return value
class ToolBudget:
def __init__(self,capacity_units,rate_units_per_second):
self.capacity=bounded_integer(capacity_units,1,10_000,'capacity')
self.rate=bounded_integer(rate_units_per_second,1,10_000,'rate')
self.last_ms=0
self._credit=1000*self.capacity
def admit(self,at_ms,cost_units=1):
bounded_integer(at_ms,0,MAX_MS,'time_ms')
bounded_integer(cost_units,1,self.capacity,'cost_units')
if at_ms<self.last_ms:
raise ValueError('backward_time')
self._credit=min(1000*self.capacity,self._credit+self.rate*(at_ms-self.last_ms))
self.last_ms=at_ms
need=1000*cost_units
accepted=self._credit>=need
if accepted:
self._credit-=need
delay=0
else:
deficit=need-self._credit
delay=(deficit+self.rate-1)//self.rate
return {'at_ms':at_ms,'accepted':accepted,'cost_units':cost_units,
'remaining_milliunits':self._credit,'retry_after_ms':delay}
def snapshot(self):
return {'last_ms':self.last_ms,'remaining_milliunits':self._credit}
def exact_fraction_oracle(capacity,rate,events):
"""Independent unit-level rational model; no scaled integer implementation calls."""
balance=Fraction(capacity)
previous=0
trace=[]
for at,cost in events:
balance=min(Fraction(capacity),balance+Fraction(rate*(at-previous),1000))
previous=at
if balance>=cost:
accepted=True
balance-=cost
delay=0
else:
accepted=False
delay=math.ceil((cost-balance)*1000/rate)
assert (balance*1000).denominator==1
trace.append({'at_ms':at,'accepted':accepted,'cost_units':cost,
'remaining_milliunits':int(balance*1000),'retry_after_ms':delay})
return trace
def rejects(call,message):
try:
call()
except ValueError as error:
return str(error)==message
return False
def run_example():
events=[(0,2),(0,2),(499,2),(500,2),(750,1),(1000,1)]
budget=ToolBudget(3,2)
trace=[budget.admit(t,cost) for t,cost in events]
hand=[
{'at_ms':0,'accepted':True,'cost_units':2,'remaining_milliunits':1000,'retry_after_ms':0},
{'at_ms':0,'accepted':False,'cost_units':2,'remaining_milliunits':1000,'retry_after_ms':500},
{'at_ms':499,'accepted':False,'cost_units':2,'remaining_milliunits':1998,'retry_after_ms':1},
{'at_ms':500,'accepted':True,'cost_units':2,'remaining_milliunits':0,'retry_after_ms':0},
{'at_ms':750,'accepted':False,'cost_units':1,'remaining_milliunits':500,'retry_after_ms':250},
{'at_ms':1000,'accepted':True,'cost_units':1,'remaining_milliunits':0,'retry_after_ms':0},
]
burst=ToolBudget(3,2)
burst_results=[burst.admit(0) for _ in range(4)]
idle=ToolBudget(3,2)
idle_first=idle.admit(1_000_000,3)
idle_next=idle.admit(1_000_000,1)
thirds=ToolBudget(2,3)
thirds.admit(0,2)
thirds_denied=thirds.admit(0)
thirds_333=thirds.admit(333)
thirds_334=thirds.admit(334)
repeated=ToolBudget(3,2)
repeated.admit(0,3)
deny100=repeated.admit(100)
deny200=repeated.admit(200)
invalid=ToolBudget(3,2)
invalid.admit(100,1)
invalid_before=invalid.snapshot()
bad_times=all(rejects(lambda value=value:invalid.admit(value),'time_ms')
for value in (-1,True,1.0,'1',None,MAX_MS+1))
state_after_bad_times=invalid.snapshot()
backwards_rejected=rejects(lambda:invalid.admit(99),'backward_time')
after_backwards=invalid.snapshot()
bad_costs=all(rejects(lambda value=value:invalid.admit(200,value),'cost_units')
for value in (0,-1,4,True,1.0,'1',None))
state_after_bad_costs=invalid.snapshot()
oracle_ok=True
envelope_ok=True
case_count=0
gaps=[0,333,1,499,500,1000]
for capacity in range(1,5):
for rate in range(1,5):
for variant in range(5):
time=0
scenario=[]
for index in range(6):
time+=gaps[(index+variant)%len(gaps)]
scenario.append((time,1+(index+variant)%capacity))
tested=ToolBudget(capacity,rate)
actual=[tested.admit(t,cost) for t,cost in scenario]
oracle_ok=oracle_ok and actual==exact_fraction_oracle(capacity,rate,scenario)
for left in range(len(actual)):
for right in range(left,len(actual)):
admitted=sum(row['cost_units'] for row in actual[left:right+1] if row['accepted'])
envelope=Fraction(capacity)+Fraction(rate*(actual[right]['at_ms']-actual[left]['at_ms']),1000)
envelope_ok=envelope_ok and admitted<=envelope
case_count+=1
left,right=ToolBudget(1,1),ToolBudget(1,1)
left.admit(0)
left_again,right_first=left.admit(0),right.admit(0)
repeat=ToolBudget(3,2)
repeated_trace=[repeat.admit(t,cost) for t,cost in events]
returned=repeat.admit(1000,1)
returned['remaining_milliunits']=999999
checks={
'hand_first_weighted_request_consumes_two_of_three_units':trace[0]==hand[0],
'same_time_request_denied_without_consuming_remaining_unit':trace[1]==hand[1],
'one_ms_before_boundary_has1998milliunits_and_one_ms_wait':trace[2]==hand[2],
'exact500ms_boundary_accepts_weighted_cost_and_drains_credit':trace[3]==hand[3],
'fractional_half_unit_at750ms_is_preserved_after_denial':trace[4]==hand[4],
'next_complete_unit_at1000ms_matches_hand_and_fraction_oracle':trace[5]==hand[5] and trace==exact_fraction_oracle(3,2,events),
'initial_same_timestamp_burst_is_capacity_not_refill_rate': [x['accepted'] for x in burst_results]==[True,True,True,False] and burst_results[-1]['retry_after_ms']==500,
'idle_refill_caps_at_capacity_without_banking_excess':idle_first['accepted'] and idle_first['remaining_milliunits']==0 and not idle_next['accepted'] and idle_next['retry_after_ms']==500,
'retry_delay_rounds_up_and_333_334ms_boundary_is_explicit':thirds_denied['retry_after_ms']==334 and thirds_333=={'at_ms':333,'accepted':False,'cost_units':1,'remaining_milliunits':999,'retry_after_ms':1} and thirds_334=={'at_ms':334,'accepted':True,'cost_units':1,'remaining_milliunits':2,'retry_after_ms':0},
'denied_requests_refill_elapsed_time_once_and_never_charge_cost':deny100['remaining_milliunits']==200 and deny200['remaining_milliunits']==400 and not deny100['accepted'] and not deny200['accepted'],
'invalid_time_types_and_range_do_not_mutate_state':bad_times and state_after_bad_times==invalid_before,
'backward_time_is_rejected_without_credit_or_clock_change':backwards_rejected and after_backwards==invalid_before,
'configuration_requires_positive_bounded_exact_integers':all(rejects(lambda value=value:ToolBudget(value,2),'capacity') for value in (0,-1,10001,True,1.0,'1')) and all(rejects(lambda value=value:ToolBudget(3,value),'rate') for value in (0,-1,10001,False,2.0,'2')),
'cost_must_fit_bucket_and_validation_does_not_advance_clock':bad_costs and state_after_bad_costs==invalid_before,
'eighty_small_scenarios_equal_independent_rational_unit_oracle':oracle_ok and case_count==80,
'admitted_cost_obeys_capacity_plus_rate_times_window_in_all_cases':envelope_ok and case_count==80,
'separate_objects_have_separate_credit_not_authenticated_client_isolation':not left_again['accepted'] and right_first['accepted'],
'same_schedule_repeats_and_mutating_reply_does_not_change_bucket':repeated_trace==trace and repeat.snapshot()=={'last_ms':1000,'remaining_milliunits':0},
}
assert len(checks)==18 and all(type(value)is bool and value for value in checks.values()),checks
return {'python':platform.python_version(),'dependencies':'standard library only',
'scope':'Sequential offline weighted admission; fixed integer milliseconds and milliunits, no queue, service, actual action, wall clock, sleep, concurrency, authorization or language-model token billing',
'capacity_units':3,'refill_units_per_second':2,'events':[[t,c] for t,c in events],
'hand_timeline':hand,'observed_timeline':trace,'thirds_boundary':[thirds_denied,thirds_333,thirds_334],
'oracle_scenarios':case_count,'checks':checks}
if __name__=='__main__':
print(json.dumps(run_example(),ensure_ascii=False,indent=2,allow_nan=False))
هشتاد برنامهٔ کوچک با ظرفیت، نرخ، فاصله و هزینهٔ متفاوت ساخته شدهاند؛ هر برنامه شش درخواست دارد. مقایسهٔ کامل با مرجع کسری، یک گروه بررسی است. گروه دیگری در همهٔ پنجرههای متوالی همان دادهها کنترل میکند که هزینهٔ پذیرفتهشده از ظرفیت بهعلاوهٔ نرخ ضرب در زمان بیشتر نشود. این زیرحالتها به تعداد هجده گروه نامدار اضافه نمیشوند؛ اجرای دوباره هم بررسی جدید محسوب نمیشود.
{
"python": "3.14.5",
"dependencies": "standard library only",
"scope": "Sequential offline weighted admission; fixed integer milliseconds and milliunits, no queue, service, actual action, wall clock, sleep, concurrency, authorization or language-model token billing",
"capacity_units": 3,
"refill_units_per_second": 2,
"events": [
[
0,
2
],
[
0,
2
],
[
499,
2
],
[
500,
2
],
[
750,
1
],
[
1000,
1
]
],
"hand_timeline": [
{
"at_ms": 0,
"accepted": true,
"cost_units": 2,
"remaining_milliunits": 1000,
"retry_after_ms": 0
},
{
"at_ms": 0,
"accepted": false,
"cost_units": 2,
"remaining_milliunits": 1000,
"retry_after_ms": 500
},
{
"at_ms": 499,
"accepted": false,
"cost_units": 2,
"remaining_milliunits": 1998,
"retry_after_ms": 1
},
{
"at_ms": 500,
"accepted": true,
"cost_units": 2,
"remaining_milliunits": 0,
"retry_after_ms": 0
},
{
"at_ms": 750,
"accepted": false,
"cost_units": 1,
"remaining_milliunits": 500,
"retry_after_ms": 250
},
{
"at_ms": 1000,
"accepted": true,
"cost_units": 1,
"remaining_milliunits": 0,
"retry_after_ms": 0
}
],
"observed_timeline": [
{
"at_ms": 0,
"accepted": true,
"cost_units": 2,
"remaining_milliunits": 1000,
"retry_after_ms": 0
},
{
"at_ms": 0,
"accepted": false,
"cost_units": 2,
"remaining_milliunits": 1000,
"retry_after_ms": 500
},
{
"at_ms": 499,
"accepted": false,
"cost_units": 2,
"remaining_milliunits": 1998,
"retry_after_ms": 1
},
{
"at_ms": 500,
"accepted": true,
"cost_units": 2,
"remaining_milliunits": 0,
"retry_after_ms": 0
},
{
"at_ms": 750,
"accepted": false,
"cost_units": 1,
"remaining_milliunits": 500,
"retry_after_ms": 250
},
{
"at_ms": 1000,
"accepted": true,
"cost_units": 1,
"remaining_milliunits": 0,
"retry_after_ms": 0
}
],
"thirds_boundary": [
{
"at_ms": 0,
"accepted": false,
"cost_units": 1,
"remaining_milliunits": 0,
"retry_after_ms": 334
},
{
"at_ms": 333,
"accepted": false,
"cost_units": 1,
"remaining_milliunits": 999,
"retry_after_ms": 1
},
{
"at_ms": 334,
"accepted": true,
"cost_units": 1,
"remaining_milliunits": 2,
"retry_after_ms": 0
}
],
"oracle_scenarios": 80,
"checks": {
"hand_first_weighted_request_consumes_two_of_three_units": true,
"same_time_request_denied_without_consuming_remaining_unit": true,
"one_ms_before_boundary_has1998milliunits_and_one_ms_wait": true,
"exact500ms_boundary_accepts_weighted_cost_and_drains_credit": true,
"fractional_half_unit_at750ms_is_preserved_after_denial": true,
"next_complete_unit_at1000ms_matches_hand_and_fraction_oracle": true,
"initial_same_timestamp_burst_is_capacity_not_refill_rate": true,
"idle_refill_caps_at_capacity_without_banking_excess": true,
"retry_delay_rounds_up_and_333_334ms_boundary_is_explicit": true,
"denied_requests_refill_elapsed_time_once_and_never_charge_cost": true,
"invalid_time_types_and_range_do_not_mutate_state": true,
"backward_time_is_rejected_without_credit_or_clock_change": true,
"configuration_requires_positive_bounded_exact_integers": true,
"cost_must_fit_bucket_and_validation_does_not_advance_clock": true,
"eighty_small_scenarios_equal_independent_rational_unit_oracle": true,
"admitted_cost_obeys_capacity_plus_rate_times_window_in_all_cases": true,
"separate_objects_have_separate_credit_not_authenticated_client_isolation": true,
"same_schedule_repeats_and_mutating_reply_does_not_change_bucket": true
}
}
خطای ورودی و زمان عقبرونده، اعتبار و ساعت مدل را تغییر نمیدهد. این خاصیت با تصویر وضعیت پیش و پس از فراخوانی نامعتبر آزموده شده است. دو شیء جدا نیز بودجههای جدا دارند؛ این جدایی حافظهای، احراز هویت مشتری یا کنترل دسترسی نیست. تابع فقط پاسخ میدهد و نه درخواست ردشده را در صف میگذارد، نه sleep میکند و نه خودش دوباره تلاش میکند.
۵. محدودیت عملی و چهار پرسش پاسخدار

مستندات HTTP API در AWS کاربرد سطل توکن برای نرخ و جهش درخواست و ماهیت بهترینتلاشِ محدودیتهای آن سرویس را توضیح میدهد. مدل ترتیبی ما تنظیم یا آزمایش AWS نیست. در سامانهٔ چندفرایندی، ثبت اتمی اعتبار، انتخاب ساعت، بازیابی خرابی و اشتراک بودجه نیاز به طراحی جدا دارند؛ قبولی این تمرین بهتنهایی چنین قابلیتهایی ایجاد نمیکند.
پرسش اول: چرا با نرخ دو، سه درخواست در زمان صفر پذیرفته میشوند؟ پاسخ: ظرفیت اولیه سه است و سطل پُر شروع میشود. نرخ، سرعت اعتبارگیریِ بعدی را تعیین میکند؛ جهش اولیه به ظرفیت وابسته است.
پرسش دوم: آیا رد درخواست، اعتبار باقیمانده را صفر میکند؟ پاسخ: خیر؛ فقط گذشت زمان یک بار حساب میشود و هزینه کم نمیشود. نمونهٔ ۴۹۹ میلیثانیه ۱۹۹۸ میلیواحد را نگه میدارد تا در ۵۰۰ مرز پذیرش برسد.
پرسش سوم: آیا درخواست پذیرفتهشده برای اجرای ابزار مجاز و بیتکرار است؟ پاسخ: این خروجی فقط دربارهٔ بودجه است. قرارداد و مجوز ابزار و هویت درخواست و بازپخش نتیجه قواعد جدا دارند. این مدل حتی هویت درخواست را نگه نمیدارد و یک تکرارِ پذیرفتهشده میتواند دوباره اعتبار مصرف کند.
پرسش چهارم: آیا retry_after_ms زمان اجرای قطعی است؟ پاسخ: خیر؛ فقط فاصلهٔ لازم طبق فرضهای اعلامشده است. ورود مصرفکنندهٔ دیگر یا تغییر سیاست میتواند وضعیت را عوض کند و هیچ صف یا رزروی ساخته نشده است. منابع در ۲۴ مهر ۱۴۰۵ خوانده شدند؛ RFC 3290 سند اطلاعاتی مه ۲۰۰۲ است و این مطلب آموزش پایهٔ تألیفی، بدون ادعای خبر تازه یا ظرفیت واقعی سرویس است.
منابع و مطالعهٔ بیشتر
- RFC3290 AppendixA.4؛ مدل ریاضی سطل توکن سختگیر
- AWS API Gateway؛ نرخ و جهش درخواست و محدودیت بهترینتلاش
- Python3.14 Fraction؛ مرجع کسری مستقل
بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

