۱. تازگی را از موجودبودنِ مدخل جدا کنیم

وجودِ مدخل، تازگی را تضمین نمی‌کند
تصویر مفهومی: وجودِ مدخل، تازگی را تضمین نمی‌کند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

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

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

خروجیِ ما یک گزارشِ عددی و مقدارِ منطقیِ fresh است. این مقدار اجازهٔ ذخیره یا تحویلِ پاسخ نیست؛ فرض می‌کنیم داده‌های زمانی و عمرِ انتخاب‌شده از قبل به فراخواننده رسیده‌اند. برنامه کلیدِ کش، مجوزِ کاربر، تفاوتِ پاسخ برای درخواست‌های مختلف، اعتبارسنجی با سرور یا سیاستِ استفاده از پاسخِ کهنه را پیاده نمی‌کند. بنابراین نتیجهٔ مثبت را موفقیتِ یک سرویس نمی‌نامیم.

برای بررسیِ دستی، Date را صفر، زمانِ آغازِ درخواست را هزار، دریافت را هزاروپانصد و زمانِ بررسی را چهارهزاروپانصد میلی‌ثانیه می‌گذاریم. Age دو ثانیه و max-age پنج ثانیه است. این‌ها زمان‌های مصنوعی با یک مبدأ مشترک‌اند؛ صفرِ مثال یک تاریخِ واقعیِ انتشار نیست و برنامه ساعتِ سیستم را برای پرکردنِ هیچ‌کدام نمی‌خواند.

۲. Date و Age و ساعت‌های درخواست را هم‌واحد کنیم

مبدأهای زمان باید در یک قرارداد مقایسه شوند
تصویر مفهومی: مبدأهای زمان باید در یک قرارداد مقایسه شوند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

بخشِ ۶٫۶٫۱ از RFC 9110، Date را زمانِ ایجادِ پیام تعریف می‌کند؛ این فیلد زمانِ ورود به کشِ محلی نیست. در رابطِ آموزشی، مقدارِ آن از قبل به میلی‌ثانیه تبدیل شده و باید مضربِ هزار باشد. تجزیهٔ متنِ تاریخ، تشخیصِ منطقهٔ زمانی و تصمیم دربارهٔ Dateِ غایب یا نامعتبر، وظیفهٔ این تابع نیست.

هر چهار زمان باید int واقعی، نامنفی و حداکثر ده به توانِ پانزده میلی‌ثانیه باشند. آغازِ درخواست نباید از دریافت دیرتر باشد و دریافت نباید از زمانِ بررسی جلوتر برود. هم‌مبدأبودن نیز فرضِ ورودی است؛ کنارِ هم گذاشتنِ ساعتِ یکنواختِ محلی و تاریخِ تقویمیِ سرور با یک تغییرِ نام درست نمی‌شود. ترتیبِ عددها، منشأ یا همگامیِ واقعیِ ساعت‌ها را اثبات نمی‌کند.

Age و max-age در این رابط برحسبِ ثانیهٔ صحیح‌اند و هنگامِ محاسبه در هزار ضرب می‌شوند. bool، شناور، رشته و None رد می‌شوند. سقفِ ۸۶۴۰۰ ثانیه، انتخابِ آموزشیِ ماست و حدِ استانداردِ HTTP نیست. در RFC 9111، delta-seconds عددِ صحیحِ نامنفی است؛ این درس پارسرِ آن دستور زبان یا رفتارِ سرریزِ یک پیاده‌سازیِ HTTP را ارائه نمی‌کند.

بخشِ ۴٫۲٫۱ همان RFC، انتخابِ عمرِ تازگی را با توجه به دستورهای پاسخ توضیح می‌دهد. ما فقط max-ageِ ازپیش‌انتخاب‌شده را می‌گیریم؛ تقدمِ s-maxage در کشِ مشترک، Expires، قاعدهٔ اکتشافی یا تعارضِ دستورها را حل نمی‌کنیم. اگر Age در پیام موجود نباشد، مسیرِ استاندارد مقدارِ صفر را در محاسبه به کار می‌گیرد؛ رابطِ ما همچنان همان صفر را صریحاً از فراخواننده می‌خواهد.

۳. کد کاملِ سنِ اصلاح‌شده و مهلت

سنِ اصلاح‌شده از چند مشاهده ساخته می‌شود
تصویر مفهومی: سنِ اصلاح‌شده از چند مشاهده ساخته می‌شود؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

ابتدا سنِ ظاهری برابرِ زمانِ دریافت منهای Date است؛ اگر منفی شد، صفر می‌گذاریم. تأخیرِ پاسخ، فاصلهٔ دریافت تا آغازِ درخواست است. به Ageِ تبدیل‌شده به میلی‌ثانیه، این تأخیر را اضافه می‌کنیم. مسیرِ محافظه‌کارانهٔ بخشِ ۴٫۲٫۳، بزرگ‌ترِ سنِ ظاهری و این مقدارِ اصلاح‌شده را به‌عنوانِ سنِ اولیه انتخاب می‌کند؛ این انتخاب را صریح نگه داشته‌ایم.

زمانِ ماندنِ محلی، فاصلهٔ زمانِ بررسی تا دریافت است. با افزودنِ آن به سنِ اولیه، سنِ جاری به دست می‌آید. در مثال، سنِ ظاهری ۱۵۰۰، تأخیر ۵۰۰ و Ageِ اصلاح‌شده ۲۵۰۰ میلی‌ثانیه است. زمانِ ماندن ۳۰۰۰ می‌شود؛ پس پاسخ اکنون ۵۵۰۰ میلی‌ثانیه سن دارد و از عمرِ پنج‌هزار میلی‌ثانیه‌ای گذشته است.

معیارِ تازگی، کوچک‌تر بودنِ اکیدِ سن از عمر است. در همین مثال، زمانِ بررسیِ ۳۹۹۹، سنِ ۴۹۹۹ و یک میلی‌ثانیه فاصله تا مرز می‌دهد؛ در زمانِ ۴۰۰۰، سن دقیقاً ۵۰۰۰ و fresh نادرست است. صفرشدنِ فاصلهٔ باقی‌مانده، به‌تنهایی حالتِ تازه را از کهنه جدا نمی‌کند؛ باید همان مقایسهٔ اکید را نگه داشت.

تمامِ اجزای گزارشِ زیر عددِ صحیحِ میلی‌ثانیه‌اند و تقسیمِ شناوری در کار نیست. این دقتِ عددی، دقتِ فیزیکیِ ساعت یا اندازه‌گیریِ شبکه را تضمین نمی‌کند. گزارش، سرآیندِ Age تولید نمی‌کند؛ نباید مقدارِ میلی‌ثانیه‌ایِ آن را به‌صورتِ عددِ کسری داخلِ فیلدی گذاشت که واحد و دستور زبانِ دیگری دارد. فایلِ کامل با کتابخانهٔ استانداردِ پایتون اجرا می‌شود.

"""Offline bounded RFC9111 conservative age arithmetic, not a full HTTP cache."""
from itertools import product
import json
import platform

TIME_CAP_MS = 10**15
SECONDS_CAP = 86400  # Own teaching cap, not the RFC delta-seconds limit.


def age_report(date_ms, request_ms, response_ms, now_ms, age_s, max_age_s):
    values = (date_ms, request_ms, response_ms, now_ms, age_s, max_age_s)
    if any(type(value) is not int for value in values):
        raise TypeError('all supplied times and durations must be actual ints')
    if any(not 0 <= value <= TIME_CAP_MS for value in values[:4]):
        raise ValueError('time outside own 0..10^15 millisecond domain')
    if date_ms % 1000:
        raise ValueError('supplied Date must represent a whole HTTP second')
    if not 0 <= age_s <= SECONDS_CAP or not 0 <= max_age_s <= SECONDS_CAP:
        raise ValueError('Age and max-age outside own 0..86400 second domain')
    if not request_ms <= response_ms <= now_ms:
        raise ValueError('request <= response <= now required on one clock origin')
    apparent = max(0, response_ms-date_ms)
    delay = response_ms-request_ms
    corrected_age = age_s*1000+delay
    initial = max(apparent, corrected_age)
    resident = now_ms-response_ms
    current = initial+resident
    lifetime = max_age_s*1000
    return dict(apparent_age_ms=apparent,response_delay_ms=delay,
                corrected_age_value_ms=corrected_age,corrected_initial_age_ms=initial,
                resident_time_ms=resident,current_age_ms=current,freshness_lifetime_ms=lifetime,
                fresh=current < lifetime,remaining_fresh_ms=max(0,lifetime-current))


def origin_reference(date_ms, request_ms, response_ms, now_ms, age_s, max_age_s):
    """Test-only effective origin; no production intermediate steps reused."""
    origin = min(response_ms, date_ms, request_ms-1000*age_s)
    current = now_ms-origin
    return current, now_ms < origin+1000*max_age_s


def rejected(exception, values):
    try:
        age_report(*values)
    except exception:
        return True
    return False


def main():
    values = (0,1000,1500,4500,2,5)
    example = age_report(*values)
    checks = {}
    checks['hand_full_conservative_age_components'] = example == dict(
        apparent_age_ms=1500,response_delay_ms=500,corrected_age_value_ms=2500,
        corrected_initial_age_ms=2500,resident_time_ms=3000,current_age_ms=5500,
        freshness_lifetime_ms=5000,fresh=False,remaining_fresh_ms=0)
    apparent = age_report(0,10000,11000,13000,1,20)
    checks['apparent_age_can_dominate_age_field_path'] = (
        apparent['corrected_age_value_ms'] == 2000 and apparent['current_age_ms'] == 13000)
    checks['corrected_age_includes_response_delay'] = (
        age_report(2000,0,500,500,1,1)['current_age_ms'] == 1500)
    before = age_report(0,1000,1500,3999,2,5)
    boundary = age_report(0,1000,1500,4000,2,5)
    checks['strict_freshness_boundary'] = (
        before['fresh'] and before['current_age_ms'] == 4999
        and before['remaining_fresh_ms'] == 1 and not boundary['fresh']
        and boundary['current_age_ms'] == 5000 and boundary['remaining_fresh_ms'] == 0)
    checks['zero_lifetime_is_never_fresh'] = not age_report(0,0,0,0,0,0)['fresh']
    future = age_report(20000,10000,11000,13000,0,5)
    checks['future_date_clamps_only_apparent_age'] = (
        future['apparent_age_ms'] == 0 and future['corrected_initial_age_ms'] == 1000
        and future['current_age_ms'] == 3000)
    checks['same_instant_zero_age_and_positive_lifetime'] = (
        age_report(0,0,0,0,0,1)['current_age_ms'] == 0 and age_report(0,0,0,0,0,1)['fresh'])
    checks['resident_growth_changes_age_one_for_one'] = (
        age_report(0,1000,1500,4501,2,5)['current_age_ms'] == example['current_age_ms']+1)
    shifted = tuple(value+1000000 for value in values[:4])+values[4:]
    checks['common_time_origin_translation_invariance'] = age_report(*shifted) == example
    checks['second_fields_are_converted_to_milliseconds'] = (
        age_report(0,0,0,0,1,2)['current_age_ms'] == 1000
        and age_report(0,0,0,0,1,2)['freshness_lifetime_ms'] == 2000)
    bad_types=[]
    for position in range(6):
        for value in (True,1.0,'1',None):
            candidate=list(values);candidate[position]=value
            bad_types.append(rejected(TypeError,candidate))
    checks['exact_integer_types_excluding_bool'] = all(bad_types)
    checks['own_caps_nonnegative_and_date_precision'] = all([
        rejected(ValueError,(-1000,0,0,0,0,1)),rejected(ValueError,(1,0,0,0,0,1)),
        rejected(ValueError,(TIME_CAP_MS+1000,0,0,0,0,1)),
        rejected(ValueError,(0,0,0,0,-1,1)),rejected(ValueError,(0,0,0,0,0,86401)),
        age_report(TIME_CAP_MS,TIME_CAP_MS,TIME_CAP_MS,TIME_CAP_MS,86400,86400)['current_age_ms'] == 86400000])
    checks['inconsistent_observation_order_rejected'] = all([
        rejected(ValueError,(0,1001,1000,2000,0,1)),
        rejected(ValueError,(0,0,1001,1000,0,1))])
    passed,cases=True,0
    for date,request,delay,resident,age,lifetime in product(
            (0,1000,2000,3000),(0,1000,2000),(0,250,1000),(0,1,500,1500),(0,1,3),(0,1,2,5)):
        response=request+delay;now=response+resident
        actual=age_report(date,request,response,now,age,lifetime)
        expected, fresh=origin_reference(date,request,response,now,age,lifetime)
        passed &= actual['current_age_ms'] == expected and actual['fresh'] == fresh
        cases+=1
    checks['independent_effective_origin_oracle'] = bool(passed and cases == 1728)
    assert len(checks) == 14 and all(type(v) is bool and v for v in checks.values()),checks
    print(json.dumps(dict(python=platform.python_version(),checks=checks,
        supplied_example=dict(date_ms=0,request_ms=1000,response_ms=1500,now_ms=4500,age_s=2,max_age_s=5),
        example=example,before_boundary=before,at_boundary=boundary,future_Date_example=future,
        independent_oracle_cases_in_one_group=cases,
        scope='Supplied numeric metadata only; fresh is not a permission to store or serve. No HTTP header parser, network or clock read.'),indent=2))


if __name__ == '__main__':
    main()

۴. مرزِ برابر و مرجعِ زمانِ مبدأ را بسنجیم

برابری با مهلت، تازگی نیست
تصویر مفهومی: برابری با مهلت، تازگی نیست؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مرجعِ مستقل، اجزای محاسبهٔ اصلی را دوباره فراخوانی نمی‌کند. یک مبدأِ مؤثرِ محاسباتی از کمترینِ سه مقدار می‌سازد: دریافت، Date و آغازِ درخواست منهای Ageِ میلی‌ثانیه‌ای. فاصلهٔ اکنون تا آن مبدأ، سنِ مرجع است. این هم‌ارزی به همان ترتیبِ زمان‌ها و نامنفی‌بودنِ Age وابسته است؛ مبدأِ مؤثر، کشفِ زمانِ واقعیِ تولیدِ پاسخ نیست.

برای چهار Date، سه زمانِ آغاز، سه تأخیر، چهار مدتِ ماندن، سه Age و چهار عمرِ تازگی، ۱۷۲۸ ترکیب با مرجع مقایسه شدند. تمامِ این خانواده یک گروه است. شاهدهای دستی، غلبهٔ سنِ ظاهری، افزودنِ تأخیر به Age، مرزِ برابر، عمرِ صفر، تغییرِ مبدأِ مشترک و خطاهای ورودی گروه‌های مکمل‌اند. تعدادِ گروه‌های نام‌دارِ برنامه چهارده است، نه تعدادِ ترکیب‌ها.

یک Dateِ جلوتر از دریافت، فقط سنِ ظاهری را صفر می‌کند. در شاهدِ جدا، Date بیست‌هزار، آغاز ده‌هزار، دریافت یازده‌هزار و اکنون سیزده‌هزار میلی‌ثانیه است؛ Age صفر است، اما تأخیرِ هزار و ماندنِ دوهزار، سنِ جاریِ سه‌هزار می‌سازند. صفرکردنِ همهٔ سن صرفاً به علتِ جلو بودنِ Date، با قراردادِ این محاسبه سازگار نیست.

اجرای واقعی با Python 3.14.5 چهارده بررسیِ درست و stderr تهی ثبت کرد. دو RFC مربوط به ژوئنِ ۲۰۲۲ هستند و در ۱۸ اکتبرِ ۲۰۲۶ خوانده شدند؛ تاریخِ دسترسی، تاریخِ انتشارِ استاندارد نیست. فایل‌های خروجی، دادهٔ واقعیِ همین اجرای آفلاین‌اند. نه زمانِ پاسخِ یک محصول سنجیده شده، نه افزایشِ سرعت، کاهشِ هزینه یا رفتارِ یک کشِ مستقر از آن نتیجه می‌شود.

{
  "python": "3.14.5",
  "checks": {
    "hand_full_conservative_age_components": true,
    "apparent_age_can_dominate_age_field_path": true,
    "corrected_age_includes_response_delay": true,
    "strict_freshness_boundary": true,
    "zero_lifetime_is_never_fresh": true,
    "future_date_clamps_only_apparent_age": true,
    "same_instant_zero_age_and_positive_lifetime": true,
    "resident_growth_changes_age_one_for_one": true,
    "common_time_origin_translation_invariance": true,
    "second_fields_are_converted_to_milliseconds": true,
    "exact_integer_types_excluding_bool": true,
    "own_caps_nonnegative_and_date_precision": true,
    "inconsistent_observation_order_rejected": true,
    "independent_effective_origin_oracle": true
  },
  "supplied_example": {
    "date_ms": 0,
    "request_ms": 1000,
    "response_ms": 1500,
    "now_ms": 4500,
    "age_s": 2,
    "max_age_s": 5
  },
  "example": {
    "apparent_age_ms": 1500,
    "response_delay_ms": 500,
    "corrected_age_value_ms": 2500,
    "corrected_initial_age_ms": 2500,
    "resident_time_ms": 3000,
    "current_age_ms": 5500,
    "freshness_lifetime_ms": 5000,
    "fresh": false,
    "remaining_fresh_ms": 0
  },
  "before_boundary": {
    "apparent_age_ms": 1500,
    "response_delay_ms": 500,
    "corrected_age_value_ms": 2500,
    "corrected_initial_age_ms": 2500,
    "resident_time_ms": 2499,
    "current_age_ms": 4999,
    "freshness_lifetime_ms": 5000,
    "fresh": true,
    "remaining_fresh_ms": 1
  },
  "at_boundary": {
    "apparent_age_ms": 1500,
    "response_delay_ms": 500,
    "corrected_age_value_ms": 2500,
    "corrected_initial_age_ms": 2500,
    "resident_time_ms": 2500,
    "current_age_ms": 5000,
    "freshness_lifetime_ms": 5000,
    "fresh": false,
    "remaining_fresh_ms": 0
  },
  "future_Date_example": {
    "apparent_age_ms": 0,
    "response_delay_ms": 1000,
    "corrected_age_value_ms": 1000,
    "corrected_initial_age_ms": 1000,
    "resident_time_ms": 2000,
    "current_age_ms": 3000,
    "freshness_lifetime_ms": 5000,
    "fresh": true,
    "remaining_fresh_ms": 2000
  },
  "independent_oracle_cases_in_one_group": 1728,
  "scope": "Supplied numeric metadata only; fresh is not a permission to store or serve. No HTTP header parser, network or clock read."
}

۵. چهار پرسش دربارهٔ بازاعتبارسنجی و مجوزِ استفاده

تازگی با مجوزِ استفاده یکی نیست
تصویر مفهومی: تازگی با مجوزِ استفاده یکی نیست؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: آیا پاسخ در زمانِ بررسیِ ۴۰۰۰ میلی‌ثانیهٔ نمونه هنوز تازه است؟ اگر سن را از لحظهٔ دریافت صفر بگیریم، چه قراردادی تغییر می‌کند؟

پاسخ: خیر؛ سنِ اصلاح‌شده دقیقاً به عمرِ ۵۰۰۰ رسیده و شرطِ کوچک‌تر بودن برقرار نیست. آغازِ دوباره از دریافت، سنِ پیشین و تأخیر را حذف می‌کند و مهلت را جابه‌جا می‌برد. یکسان‌بودنِ مدتِ max-age برای درست‌بودنِ مبدأِ شمارش کافی نیست.

پرسش دوم: چرا در شاهدِ اصلی Ageِ دوثانیه‌ای را مستقیماً با سنِ ظاهریِ ۱۵۰۰ میلی‌ثانیه مقایسه نمی‌کنیم؟ کدام فاصله هنوز باید به آن افزوده شود؟

پاسخ: واحدها باید برابر شوند و تأخیرِ دریافت نسبت به آغازِ درخواست نیز وارد شود. دوثانیه به دوهزار میلی‌ثانیه تبدیل و ۵۰۰ به آن اضافه می‌شود؛ حاصلِ ۲۵۰۰ از سنِ ظاهری بزرگ‌تر است. فراموش‌کردنِ این فاصله، نسخهٔ دیگری از فرمول می‌سازد.

پرسش سوم: اگر Date از دریافت جلوتر باشد، آیا می‌توان تازه‌بودنِ پاسخ را قطعی دانست؟ شاهدِ آینده‌بودنِ Date چه مرزی را روشن می‌کند؟

پاسخ: خیر؛ مسیرِ Age و تأخیر، و سپس مدتِ ماندن، همچنان سن می‌سازند. در شاهدِ ما نتیجه سه‌ثانیه است. تابع ساعت‌ها را همگام نمی‌کند و اعتبارِ فراداده را نمی‌سنجد؛ تنها ورودیِ داده‌شده را طبقِ قرارداد حساب می‌کند.

پرسش چهارم: آیا freshِ درست یعنی پاسخ باید تحویل شود و freshِ نادرست یعنی مدخل باید حذف شود؟ چه تصمیم‌هایی بیرونِ این تابع مانده‌اند؟

پاسخ: هیچ‌کدام از این دو اقدام در برنامه نیست. مجوز، کلیدِ مناسب، دستورهای محدودکننده، بازاعتبارسنجی و سیاستِ پاسخِ کهنه باید جدا بررسی شوند. این گزارش نه چیزی ذخیره یا حذف می‌کند، نه نمایشِ کاربر را تازه می‌کند؛ فقط سن و مقایسهٔ زمانی را گزارش می‌دهد.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.