پرش به مطلب اصلی

بخش ۷: shutdown برنامه

در WaterWall فقط یک بخش مسئول هماهنگی shutdown برنامه است. shutdown controller درخواست‌ها و وضعیت خروج را ثبت می‌کند. بعد از اینکه worker 0 از پردازش عادی event loop خارج شد، thread اصلی مراحل shutdown را بر اساس مالکیت منابع اجرا می‌کند. worker‌ها، loop‌ها، تونل‌ها، device‌ها، صف‌ها و کد پروتکل فقط به عملیات صریح و محلی چرخه‌ی عمر پاسخ می‌دهند که مالکشان شروع کرده است؛ وضعیت process controller را مرتب بررسی نمی‌کنند.

توقف اضطراری مسیر جداگانه‌ای دارد. abortProgramNow() عمداً cleanup عادی را انجام نمی‌دهد. این تابع فقط برای خرابی داده یا ساختار/state، نقض invariant یا شکست در تحویل کنترل هنگام shutdown است؛ وضعیتی که ادامه‌دادن در آن امن نیست.

۱. shutdown controller پردازه​

controller فقط رو به جلو در مراحل زیر حرکت می‌کند:

Running
-> Requested
-> ClosingAdmissions
-> QuiescingWorkers
-> QuiescingExternalProducers
-> DrainingWorkers
-> StoppingComponents
-> DestroyingWorkers
-> Finalizing

requestProgramShutdown(code) رابط عمومی ثبت درخواست منظم است. نخستین درخواست سه واقعیت مستقل را ثبت می‌کند: منشأ تشخیصی، محدوده‌ی پاک‌سازی و وضعیت خروج. محدوده از نقطه‌ی commit‌شدن runtime به دست می‌آید، نه از منشأ درخواست: هر درخواست پذیرفته‌شده پیش از commit، ‏StartupRollback را انتخاب می‌کند و هر درخواست پذیرفته‌شده پس از آن، ProcessShutdown را. بنابراین signal رسیده هنگام startup همچنان منشأ signal دارد، اما پاک‌سازی rollback را دریافت می‌کند.

نخستین درخواست یک تراکنش واحد از controller به worker است. controller در حالی که mutex خود را نگه داشته، scope را انتخاب می‌کند و درخواست quiesce از نوع level-triggered برای worker 0 را، شامل بستن پذیرش عادی و پذیرش message، نصب می‌کند. تنها پس از آن Requested را منتشر می‌کند. اگر worker 0 نتواند درخواست محلی را بپذیرد، controller در Running می‌ماند. پس از پذیرش، wake ناقص درخواست را rollback نمی‌کند. ترتیب lock ابتدا mutex controller و سپس mutex کنترل worker است؛ مسیرهای شکست worker پیش از گزارش سراسری، lock همان worker را آزاد می‌کنند.

پس از موفقیت همه‌ی مراحل startup و پذیرش همه‌ی task‌های الزامی، اما پیش از منتشرشدن workers_run_flag، مالک اصلی startup عملیات commit ‏runtime را اجرا می‌کند. commit و پذیرش درخواست با mutex controller سری می‌شوند؛ بنابراین یا درخواست برنده می‌شود و rollback رخ می‌دهد، یا runtime پیش‌تر commit شده است. درخواست‌های تکراری با هم ادغام می‌شوند. نخستین وضعیت غیرصفر بعدی می‌تواند وضعیت موفق را تغییر دهد، تا زمانی که منشأ، scope و وضعیت هنگام ورود به Finalizing با هم ثابت شوند.

Requested فقط تضمین می‌کند رکورد درخواست وجود دارد و controller اصلی اجرا باید وارد shutdown شود. این state به معنای بسته‌شدن پذیرش، بازگشت callback، مرده‌شدن line یا امن‌بودن بازپس‌گیری یک شیء نیست.

فقط بخش تحویل سیستم‌عامل در SignalManager و هماهنگ‌کننده‌ی اصلی، phase سراسری پردازه را می‌خوانند. هیچ application_stopping_flag، بررسی در اجزای انتهایی با isApplicationTerminating() یا state ثانویه‌ی shutdownِ قابل‌مشاهده برای component‌ها وجود ندارد.

۲. تحویل درخواست و worker 0​

در POSIX، ‏handler async سیگنال نخستین سیگنال را در صندوق پیامی از نوع volatile sig_atomic_t ذخیره می‌کند و با بهترین تلاش یک بایت در self-pipe غیرمسدودکننده می‌نویسد. پر بودن pipe یعنی یک بیدارباش از قبل در انتظار است. handler هیچ قفل برنامه‌ای نمی‌گیرد و پاک‌سازی انجام نمی‌دهد. worker 0 صندوق پیام را مصرف و درخواست نوع‌دار controller را منتشر می‌کند. عملیات برداشتن و پاک‌کردن صندوق پیام، دقیقاً مجموعه‌ی سیگنال‌های خاتمه‌ی منظم را مسدود می‌کند، سیگنال دریافت‌شده را پیش از رفع انسداد منتشر می‌کند و mask قبلی فراخواننده را بدون تغییر بازمی‌گرداند. مرز شکست راه‌اندازی، در حالی که همان مجموعه مسدود است، صندوق پیام را برمی‌دارد، سیگنال را منتشر می‌کند و StartupFailure را نیز منتشر می‌کند. سیگنالی که هنگام گرفتن mask از قبل ثبت شده باشد منشأ تغییرناپذیر را تعیین می‌کند؛ سیگنالی که فقط پس از گرفتن mask تحویل شود، بعد از مرز ترتیب می‌گیرد و نمی‌تواند شکست راه‌اندازی پذیرفته‌شده را جایگزین کند. مسیر اصلی راه‌اندازی نیز صندوق پیام را در نقاط کنترل هر مرحله مصرف می‌کند.

در Windows، یک دروازه‌ی اتمیک با طول عمر پردازه، پذیرش handler و شمار ورودی‌های فعال را با هم مدیریت می‌کند. handler پیش از خواندن حافظه‌ی manager یا controller این دروازه را می‌گیرد. هنگام نابودی، دروازه بسته و ثبت handler سیستم‌عامل لغو می‌شود؛ منتظرها آزاد می‌شوند و سیستم فقط برای ورودی‌های پذیرفته‌شده صبر می‌کند. ورودی دیررس از مسیر جایگزین وضعیت بحرانی که به manager نیاز ندارد استفاده می‌کند. رسیدگی به close/logoff/shutdown کنسول، هر جا قرارداد سیستم‌عامل لازم بداند، می‌تواند مدت محدودی منتظر بماند.

callback، event loop، ‏worker‌ها، node‌ها یا وضعیت سراسری خود را نابود نمی‌کند. wloopRun() درخواست محلی را میان ریشه‌های callback مشاهده می‌کند و نتیجه‌ای نوع‌دار برمی‌گرداند. سپس runMainThread() بیرون از پشته‌ی پردازش رویداد، هماهنگ‌کننده را فراخوانی می‌کند.

سیگنال دوم اپراتور می‌تواند از مسیر فرارِ خروج فوری در SignalManager استفاده کند. abortProgramNow() فقط داوری بدون قفل وضعیت و _Exit/ExitProcess فوری را انجام می‌دهد؛ هیچ پیام تشخیصی‌ای تولید نمی‌کند که ممکن است روی stream پر مسدود شود. در مسیر C11، هنگام کامپایل الزام می‌شود عدد صحیح مربوط به وضعیت بحرانی همیشه بدون قفل باشد. مسیر جایگزین ویندوز از primitive متناظر و بدون قفل نوع Interlocked استفاده می‌کند. عملیات آن relaxed باقی می‌مانند، زیرا این عدد صحیح هیچ state دیگری را منتشر نمی‌کند.

در POSIX، سیگنال‌های crash بحرانی، ‏disposition پیش‌فرض خود را بازمی‌گردانند، رفع انسداد و دوباره raise می‌شوند و فقط در صورت شکست از _Exit(128 + signum) استفاده می‌شود. handler‌های بحرانی در Windows با وضعیت داوری‌شده خاتمه می‌دهند، زیرا معنای سیگنال و فایل core dump در POSIX آنجا کاربرد ندارد.

۳. چرخه‌ی عمر worker​

هر event worker معمولی مراحل زیر را طی می‌کند:

Initialized
-> Running
-> QuiesceRequested
-> Quiesced
-> DrainRequested
-> Drained
-> TeardownRequested
-> Exited
-> Joined

هماهنگ‌کننده‌ی اصلی درخواست‌ها را منتشر می‌کند. worker مالک فقط پس از کامل‌شدن کاری که هر state نمایندگی می‌کند تأیید آن را منتشر می‌کند. mutex و condition variable مربوط به worker، اجازه‌ی ورود به drain و teardown را پس از پایان پردازش عادی event loop منتقل می‌کنند؛ فرمان‌های چرخه‌ی عمر به‌طور پنهانی از queue عادی event عبور داده نمی‌شوند.

worker 0 همان معنا را رعایت می‌کند، اما مراحل quiesce، ‏drain و teardown خود را مستقیماً روی main thread اجرا می‌کند. engine‌های lwIP متعلق به worker‌های عادی‌اند؛ stack هیچ worker یا thread جداگانه‌ای ندارد. worker بدون استفاده به engine نیاز ندارد.

همه‌ی worker‌های ثبت‌شده همین چرخه‌ی عمر را دارند. هویت آن‌ها پس از detachشدن loop و صف پیام تا زمان unbind معتبر می‌ماند. admission پیش از detach و مستقل از مالکیت worker بسته می‌شود. teardown در صورت نیاز باید reference ذخیره‌شده‌ی loop را به کار ببرد و از loop == NULL نتیجه نگیرد که thread دیگر worker نیست.

یک worker در state ‏Quiesced:

  • هیچ کار async عادی تازه‌ای را نمی‌پذیرد؛
  • هیچ root مربوط به callback عادی در حال اجرا ندارد و root بعدی را آغاز نمی‌کند؛
  • message‌های در صف و زمان‌بندی‌شده‌ی پذیرفته‌شده را تسویه کرده است؛
  • timer‌ها و watcher‌های component مربوط به WID خود را detach کرده است؛
  • از پردازش عادی حلقه برگشته است؛
  • همچنان مالک حلقه، صف، تونل، ‏line و pool‌های لازم برای تخلیه و برچیدن است.

۴. مرزهای callback عادی​

ریشه‌ی یک callback عادی، یک ورود از زمان‌بند یا تولیدکننده‌ی خارجی به رفتار معمول برنامه است: callback مربوط به I/O، ‏timer، ‏idle، رویداد سفارشی، پیام در صفِ worker، تحویل دستگاه یا تحویل lwIP. ‏callback‌های همگام و تو‌در‌توی تونل متعلق به ریشه‌ی پذیرفته‌شده برای دقیقاً همان wloop_t هستند؛ callback روی حلقه‌ی A هیچ اختیار ارث‌رسیده‌ای روی حلقه‌ی B ندارد. نوشتن‌های مستقیم روی NIO و IOCP نیز همین قاعده را دارند: فراخواننده‌ی مستقل باید پذیرش حلقه‌ی مقصد را در تمام مدت ارسال سیستمی، ثبت interest، درج در صف و انتشار عملیات async نگه دارد. callback همگام write یک ریشه‌ی callback مقصد است: فقط با اختیار عادی حلقه‌ی مقصد اجرا می‌شود و هنگام بازگشت، هر اختیار بیرونی مربوط به حلقه‌ی مبدأ را بازمی‌گرداند. پذیرش پیش از فراخوانی کد کاربر آزاد می‌شود؛ بنابراین پذیرش بسته، هر ریشه‌ی عادی بعدی را متوقف می‌کند و backend، buffer منتقل‌شده یا عملیات در صف را دقیقاً یک بار تسویه می‌کند.

نوشتن مستقل WIO ممکن است از thread دیگری آغاز شود، اما فراخواننده‌ها باید نوشتن‌های یک wio_t را به‌صورت سری انجام دهند؛ قرارداد mutex بازگشتی برای هر WIO وجود ندارد. نوشتن‌های TCP با طول صفر و شاخه‌های موجود صف stream از همان دروازه‌ی حلقه‌ی مقصد عبور می‌کنند. رد درخواست هیچ بایتی نمی‌فرستد، به‌عنوان اثر جانبی WIO را نمی‌بندد و buffer منتقل‌شده را یک بار مصرف می‌کند. datagram‌های UDP با طول صفر همچنان datagram معتبرند.

event loop درخواست quiescence محلی خود را در نقاط زیر بررسی می‌کند:

  1. پیش از poll؛
  2. بلافاصله پس از poll؛
  3. پیش از ریشه‌ی هر callback مربوط به timer، ‏idle، ‏I/O آماده، رویداد سفارشی، ‏completion و پیام worker؛
  4. میان ورودی‌های هر دسته‌ی گرفته‌شده و میان ورودی‌های هر بار تخلیه‌ی صف.

اگر درخواست هنگام اجرای callback برسد، همان callback عادی برمی‌گردد. حلقه هیچ ریشه‌ی عادی دیگری را شروع نمی‌کند. یک درخواست level-triggered همراه با بیدارباش poller، حتی وقتی هیچ رویداد عادی قابل‌پذیرش نیست، پیشرفت را تضمین می‌کند.

ارسال سفارشیِ wevent_t خام، سازوکار داخلی event loop و worker است و نباید payload تحت مالکیت برنامه را حمل کند. ارسال عادی پس از بسته‌شدن پذیرش رد می‌شود. عملیات ارسال یک کپی محلی ذخیره می‌کند که metadata حلقه‌ی آن همیشه برای مقصد نرمال شده است؛ حافظه‌ی متعلق به فراخواننده نمی‌تواند اختیار callback را جعل کند. صف کنترل محدودتر، پاک‌سازی الزامی WIO را تا زمانی می‌پذیرد که worker هنگام quiescence آن را تخلیه کند؛ سپس پیش از انتشار Quiesced پذیرش خود را می‌بندد. callback‌های کنترل بدون اختیار عادی ارث‌رسیده اجرا می‌شوند؛ بنابراین کار عادی‌ای که عمداً شروع می‌کنند باید پذیرش تازه بگیرد. کار دلخواه برنامه که مالکیت حمل می‌کند باید از API با قرارداد صریح اجرا-یا-لغو (Run-or-cancel) استفاده کند.

۵. مالکیت اجرا-یا-لغو (Run-or-cancel)​

یک پیام worker پذیرفته‌شده دقیقاً یکی از مسیرهای callback یا پاک‌سازی خود را اجرا می‌کند. پذیرش و بسته‌شدن صف زیر قفل طول عمر worker و قفل صف به یک ترتیب قطعی درمی‌آیند. پیام‌های در صف و زمان‌بندی‌شده‌ی در انتظار، پیش از تأیید quiescence روی worker مالک لغو می‌شوند؛ نابودی صف هرگز آن‌ها را بی‌صدا رها نمی‌کند.

API‌های ارسال، دو سبک مالکیت مورد استفاده در زمان اجرا را از هم جدا می‌کنند:

  • API‌های cleanup-on-refusal درخواست را رد و پاک‌سازی ثبت‌شده را بلافاصله اجرا می‌کنند؛
  • API‌های retain-on-refusal وقتی پیام پذیرفته نشده، مالکیت را به فراخواننده برمی‌گردانند؛
  • پس از پذیرش، سامانه‌ی کار تا زمانی که callback یا پاک‌سازی آرگومان‌ها را تسویه کند مالک آن‌ها است.

ارسال با تأخیر نیز همین تفاوت را هنگام آماده‌سازی timer رعایت می‌کند. فراخوانی retain-on-refusal که نتواند timer همان worker را نصب کند، ‏payload را بدون اجرای پاک‌سازی به فراخواننده برمی‌گرداند. اگر wrapper آماده‌سازی در صف پذیرفته شده باشد و سپس تخصیص timer روی worker مقصد شکست بخورد، مالکیت قبلاً منتقل شده است؛ در نتیجه پاک‌سازی یک بار با ResourceFailure اجرا می‌شود.

worker message‌های زمان‌بندی‌شده از مرز داخلی سه‌حالتهٔ wtimerTryAdd() استفاده می‌کنند. storage مربوط به timer آن‌ها با calloc واقعاً fallible در CRT ساخته و با free متناظر در CRT آزاد می‌شود؛ provenance صریح expiry، حذف صریح، cleanup quiesced/deferred، آزادسازی رویداد pending و نابودی loop را به همان خانواده هدایت می‌کند. wtimerAdd() عمومی و storage نامرتبط event همچنان fail-fast هستند. ردشدن نصب به‌دلیل بسته‌بودن پذیرش loop، AdmissionClosed گزارش می‌کند و هرگز به اشتباه ResourceFailure نامیده نمی‌شود.

ارسال‌های بررسی‌شده مقدار worker_message_submit_result_e برمی‌گردانند: پذیرفته‌شده، ردشده با حفظ مالکیت فراخواننده، یا ردشده پس از اجرای پاک‌سازی. پاک‌سازی یک worker_message_cancel_reason_e دریافت می‌کند که دردسترس‌نبودن مقصد، پذیرش بسته، شکست درج در صف/منبع، quiescence و برچیدن را از هم جدا می‌کند. لغو عمدی هنگام quiescence و برچیدن، تسویه‌ی عادی مالکیت است، نه شکست تازه‌ی پردازه.

اجرای inline روی همان worker پس از بسته‌شدن هم‌زمان پذیرش فقط وقتی معتبر است که عملیات همگام و تو‌در‌تو داخل یک ریشه‌ی عادی از قبل پذیرفته‌شده باشد. پاک‌سازی shutdown صرفاً به‌دلیل برابر بودن WID در TLS با مقصد نمی‌تواند پذیرش عادی را دوباره به‌دست آورد.

پاک‌سازی مالکیت را آزاد می‌کند. نباید کار برنامه را دوباره آغاز کند، گراف نامحدود همسایه‌ها را فراخوانی کند یا صرفاً به‌دلیل لغوشدن کار هنگام quiescence عمدی، یک shutdown دیگر درخواست کند.

settlement عمومی line task​

خانوادهٔ lineSchedule*Task*() همین قرارداد را به API عمومی line می‌آورد. cleanup دلیل worker message را به line_task_cancel_reason_e نگاشت می‌کند و وقتی dispatch allocation فیزیکی ولی line منطقی مرده ببیند LineDead می‌دهد. رکورد scheduling پیش از user callback آزاد می‌شود؛ reference line تا پایان notification، settlement buffer و سپس lineUnref() نگه داشته می‌شود.

RejectedSettled یعنی notification همگام و cleanup داخلی تمام شده است؛ AcceptedAsync فقط انتقال مالکیت به مسیر async است. پس از admission پیام setup، مسیر submitter و owner مستقل از هم پیش می‌روند:

submitter lane -> setup admission commits result -> return/observe AcceptedAsync
owner lane -> accepted setup may run -> allocation fails -> ResourceFailure cancellation
cancellation may precede, race with, or follow submitter observation

بنابراین state و latch معلق caller باید پیش از submission منتشر شوند؛ مشاهدهٔ AcceptedAsync نقطهٔ synchronization با cancellation نیست.

setup در صف می‌تواند با Quiesced، timer arm‌شده پیش از expiry و queue جداشده با Teardown settle شود. این‌ها مجوز rearm، restart retry یا درخواست shutdown تازه نیستند. پیشرفت الزامی باید در inventory مالک یا stop نماینده داشته باشد. callback لغو NULL فقط notification را حذف می‌کند و cleanup رکورد، buffer و reference باقی می‌ماند.

۶. مراحل lifecycle و ABI نود خارجی​

هر تونل یک جدول کامل callback چرخه‌ی عمر با context دارد. shutdown پردازه مراحل دقیق زیر را برای هر تونل اجرا می‌کند. NodeManager درون هر chain از ترتیب ذخیره‌سازی آن استفاده می‌کند و این ترتیب تضمین توپولوژیک source-to-tail نیست:

مرحلهcontextقرارداد
onQuiesceRequestthread اصلیپذیرش source محلی، تولیدکننده، فعال‌سازی مجدد و مؤلفه را ببندد و بیدارباش‌های توقف را صادر کند. باید محدود و غیرمسدودکننده باشد.
onWorkerQuiesceworker مالکtimer‌ها، ‏watcher‌ها، ‏handle‌های async و handle‌های در صفِ این WID را در حالی جدا کند که حلقه و state تونل هنوز زنده‌اند.
onQuiesceWaitthread اصلیپس از quiesceشدن همه‌ی worker‌ها، ریشه‌های callback خارجی را join یا detach کند و خالی‌بودن دروازه‌های محلی را اثبات کند. منابع لازم برای تخلیه‌ی line را حفظ کند.
onWorkerStopworker مالکفهرست‌های مرجعِ مالکیت source و line‌های عادی داخلیِ تحت مالکیت را تخلیه کند. برای line‌های قرضی هرگز فهرست تازه‌ای ابداع نکند.
onStopthread اصلیپس از تخلیه‌شدن همه‌ی worker‌ها، نویسنده‌های نهایی، ‏socket‌ها، ‏PCB‌ها، ‏netif‌ها و state مشترک را ببندد. هیچ timer یا watcher وابسته به worker را لمس نکند.
onDestroythread اصلیپس از پایان‌یافتن callback‌ها و ازبین‌رفتن پیام‌ها، ‏line‌ها و thread‌های خارجی، و پس از حذف ارجاع‌های worker، نمونه را بازپس بگیرد.

همه‌ی callback‌ها یک ww_lifecycle_context_t فقط‌خواندنی دریافت می‌کنند. دامنه‌های پشتیبانی‌شده عبارت‌اند از shutdown پردازه، بازگردانی راه‌اندازی، توقف فرزند خصوصی به‌دست مالک و راه‌اندازی مجدد محلی دستگاه. ‏context همچنین سیاست نوع بستن را، منظم (Graceful) یا اجباری/قطع‌کننده (Abortive)، مشخص می‌کند. مؤلفه‌ها رفتار را از این context انتخاب می‌کنند، نه از state محیطی پردازه.

حذف زنده‌ی یک configuration در حال اجرا در این lifecycle پشتیبانی نمی‌شود. چنین قابلیتی به lease اجرای مختص همان configuration و قرارداد publication/removal خود نیاز دارد.

private child tunnel‌ها با helper‌های lifecycle مربوط به owned child هدایت می‌شوند. child‌ای که در chain عادی NodeManager درج شده است دوباره به‌دست parent هدایت نمی‌شود؛ traversal مرکزی از قبل مالک آن است.

node‌های خارجی​

lifecycle v2 عمداً layout خارجی tunnel_t و signature‌های callback را تغییر داد. ABI کامل نود خارجی اکنون نسخهٔ 7 است و درخواست checksum روی sbuf_t، حذف فیلد و accessorهای checksum از line، signature‌های عمومی line task، حذف callbackهای عمومی مالکیت buffer، سازندهٔ سه-master برای buffer pool و APIهای splice inventory و layout ساختار by-value ‏socket_filter_option_t بدون shared_balance_table را پوشش می‌دهد. library یک node دینامیک باید symbol تاریخی زیر را export کند:

uint32_t waterwallNodeLifecycleAbiVersion(void)
{
return WW_EXTERNAL_NODE_ABI_VERSION;
}

loader این symbol را پیش از getter بررسی می‌کند و descriptor گمشده یا ناسازگار را رد می‌کند. WW_EXTERNAL_NODE_LIFECYCLE_ABI_VERSION برای سازگاری source alias مانده و callback‌های lifecycle معنای v2 خود را حفظ می‌کنند. WaterWall prototype قدیمی را از مرز جدید فراخوانی یا layout ناسازگار را normalize نمی‌کند.

۷. توالی مرتب shutdown​

هماهنگ‌کننده‌ی اصلی مراحل زیر را انجام می‌دهد:

  1. همه‌ی onQuiesceRequestها را فراخوانی و پذیرش عادی همه‌ی worker‌ها را ببندد؛
  2. برای همه‌ی worker‌های معمولی quiescence درخواست کند، worker 0 را quiesce کند و سپس منتظر تأیید Quiesced همه‌ی آن‌ها بماند؛
  3. همه‌ی onQuiesceWaitها را برای join یا detachکردن تولیدکننده‌های خارجی فراخوانی کند؛
  4. همه‌ی worker‌ها را وارد تخلیه کند؛ SocketManager پیش از آنکه NodeManager onWorkerStop را به ترتیب ذخیره‌سازی chain اجرا کند، جدول مالک مربوط به source‌های UDP خود را تخلیه کند؛
  5. منتظر تأیید Drained همه بماند و سپس همه‌ی onStopها را فراخوانی کند؛
  6. worker‌ها را وارد برچیدن پیام/engine/حلقه/pool کند، منتظر Exited بماند و worker‌های غیر اصلی را join کند؛
  7. پس از خروج همه‌ی engine‌ها و worker‌ها، pool‌ها، port‌ها و quota‌های مشترک lwIP را نهایی و secret مربوط به ISN را پاک کند؛
  8. ناظرهای دیرهنگام و بدون مالکیت خروج و finalizer صریح برنامه را اجرا کند؛
  9. وضعیت خروج را تثبیت و node‌ها، ‏SocketManager، ‏pool‌ها، ‏shortcut‌ها، سازوکارهای همگام‌سازی worker، زیرساخت سیگنال و حافظه‌ی سراسری را نابود کند؛
  10. با وضعیت تثبیت‌شده خارج شود.

ثبت log تا پیام‌های تشخیصی دیرهنگام پاک‌سازی در دسترس می‌ماند. registry مربوط به ناظر خروج در SignalManager یک پشته‌ی عمومی مالکیت نیست: ناظرها می‌توانند پیام تشخیصی نهایی تولید یا وضعیت هنوز تثبیت‌نشده را تنظیم کنند، اما مالک worker، ‏node، دستگاه یا صف نیستند.

۸. ‏line‌ها هنگام تخلیه​

قواعد line در بخش ۲ تغییر نمی‌کنند. پذیرش source پیش از تخلیه‌ی فهرست‌های مالکیت بسته می‌شود و جدول مالک مربوط به UDP در SocketManager پیش از hook‌های worker تونل تخلیه می‌شود. با این حال، ترتیب ذخیره‌سازی chain تضمین نمی‌شود از توپولوژی callback پیروی کند. بنابراین hook مربوط به onWorkerStop باید فقط inventory مرجع source یا line‌های تحت مالکیت خودش را drain کند؛ نباید بر پایه‌ی ترتیب فرضی همسایه‌ها خالی‌بودن registry مربوط به line‌های قرضی را الزام کند. بررسی نهایی در onDestroy می‌تواند پس از پایان drain همه‌ی hook‌های worker و مالکان line عادی، خالی‌بودن registry قرضی را الزام کند.

event loop، chain، ‏state تونل، منابع tail لازم برای Finish و pool‌های line و buffer در سراسر تخلیه‌ی worker زنده می‌مانند. فقط سازنده‌ی یک line عادی lineDestroy() را صدا می‌زند و تخلیه‌ی هر مالک همه‌ی line‌های عادی آن را از نظر منطقی مرده باقی می‌گذارد. shutdown در زمان اجرا هیچ‌گاه packet line را نابود نمی‌کند.

VmessServer برای backendهای UDP متعلق به خودش در حالت native و packetaddr، inventory جداگانهٔ هر worker دارد. worker stop این inventory معتبر را تا زنده‌بودن pool و chain تخلیه می‌کند؛ carrier borrowed ممکن است هنوز منتظر hook مالک مبدأ باشد و ترتیب همسایه فرض نمی‌شود. بستن carrier packetaddr پیش از اولین callback backend پذیرش را با Closing می‌بندد؛ ورودی مقصد دقیق و پیوند مالکیت پیش از اعلان حذف می‌شوند و پس از callbackهایی که ممکن است sibling را ببندند lookup از نو شروع می‌شود. پاسخ آماده بایت‌های encoded مستقل دارد. ساخت backend تازه به normal admission حلقهٔ مالک نیاز دارد، پس quiescence خط drain‌شده را بازسازی نمی‌کند.

inventory endpoint پویای هر worker​

provider مربوط به endpoint پویای UDP برای هر WID مالک یک registry مرجع دارد. هر entry مالک WIO خود و، پس از نخستین ingress، مالک line عادی ساخته‌شده توسط provider است؛ مصرف‌کننده‌ها فقط value-handle نگه می‌دارند، نه line_t *. onQuiesceRequest پذیرش endpoint تازه را می‌بندد و onWorkerQuiesce، callback خواندن و userdata مربوط به WIO را روی loop مالک جدا می‌کند، در حالی که منابع لازم برای drain هنوز زنده‌اند.

سپس onWorkerStop تمام entry‌های باقی‌مانده را با یک مسیر واحد و idempotent می‌بندد: پیش از callback آن را از registry حذف می‌کند، WIO را جدا و close می‌کند، پیوند endpoint به line را پاک می‌کند، line state محلی را نابود می‌کند، Finish را در جهت دور از source می‌فرستد و line عادی متعلق به provider را از نظر منطقی مرده می‌سازد. handle قدیمی یا تکراری no-op است. provider نباید به بازدید نخستِ wrapper یا همسایه وابسته باشد و assertion نهایی در destroy خالی‌بودن registry همهٔ worker‌ها را بررسی می‌کند.

بستن endpoint می‌تواند به‌صورت synchronous، ‏remote line‌های UDP مربوط به SOCKS را finish کند. drain این remote‌ها، ‏provider-client line جداگانه را در سراسر هر callback retain می‌کند، remote را پیش از Finish از registry جدا می‌کند و اگر close تو‌در‌تو قبلاً client line را از نظر منطقی مرده کرده باشد فوراً متوقف می‌شود. هیچ مسیر shutdown پس از آن مشاهده، line state را بازسازی یا دوباره استفاده نمی‌کند.

ReverseClient هر pair ساخته‌شده را از زمان انتشار تا بسته‌شدن در registry مالکِ هر worker نگه می‌دارد؛ این شامل pair‌های Connecting، ‏Unused و Active است. idle table مشترک آن فقط index مربوط به timeout ‏starvation است؛ فعال‌سازی یک item را بدون حذف مالکیت از این index بیرون می‌آورد. انقضا، ‏Finish همسایه، شکست re-entrant در Init و تخلیه‌ی worker همگی از یک مسیر بستن pair استفاده می‌کنند که پیش از callback همسایه آن را از registry خارج می‌کند و هر دو line ساخته‌شده را مرده می‌سازد. تخلیه‌ی worker ابتدا item‌های idle را تسویه و سپس هر pair باقی‌مانده در registry مالک را می‌بندد.

پیام انقضا در IdleTable موقتاً مالک تحویل است، نه index مربوط به مالک. اگر تحویل در حالی لغو شود که item هنوز متصل است، پاک‌سازی نشانگر در انتظار را پاک و item را با deadline اصلی خود به heap انقضا برمی‌گرداند. پاک‌سازی هرگز جای callback وابسته به worker برای انقضا را نمی‌گیرد. حذف صریح، جداسازی worker یا نابودی table، ‏item را در مالکیت پیام باقی می‌گذارد تا لغو، به‌جای بازگرداندن آن، دقیقاً یک بار آزادش کند. ظرفیت رزروشده‌ی heap برای itemهای متصل، بازگرداندن آن‌ها در cleanup لغو را بدون تخصیص حافظه ممکن می‌کند. drainکردن inventory یک worker باید این رزرو را برای itemهای متصل با پیام انقضای متعلق به workerهای دیگر حفظ کند، حتی اگر آن itemها موقتاً در heap حضور نداشته باشند. مرحله‌بندی انقضا نیز تراکنشی است: شکست رزرو هر دو index را دست‌نخورده باقی می‌گذارد و شکست بعدی در رشد مرحله‌بندی، دقیقاً همان item بیرون‌آمده را پیش از توقف پیمایش برمی‌گرداند. ساخت item تنها پس از آن یک handle را در دسترس می‌گذارد که map و heap انقضا هر دو مالک آن باشند؛ شکست انتشار در heap، درج map را بازمی‌گرداند.

idle table محلی و مشترک مرزهای متفاوتی در shutdown دارند. timer هر local_idle_table_t را روی worker مالک دقیق آن و پیش از drainکردن line‌ها quiesce کنید؛ سپس تمام table محلی را همان‌جا drain و در حالی نابود کنید که loop، ‏callback‌ها، line‌ها و pool‌های همان worker هنوز زنده‌اند. هرگز table محلی یا handle یک item را به worker دیگری منتقل نکنید. callback انقضای طبیعی می‌تواند item فعال خودش را تمدید یا حذف کند؛ callbackها همچنین می‌توانند itemهای فعال دیگر را با رعایت قواعد عمر آن‌ها تغییر دهند. callback مربوط به drain برای item ازپیش‌جداشده نهایی است و نباید همان item را تمدید یا حذف کند. از داخل هیچ‌کدام از این callbackها، همان table محلی را به‌صورت همگام drain، ‏quiesce یا destroy نکنید؛ این عملیات چرخه‌ی عمر را تا بازگشت کامل dispatch انقضا یا drain به تعویق بیندازید. برای idle_table_t، ‏inventory فعال هر WID مالک را روی همان WID و پیش از نابودی نهایی table مشترک drain کنید. item‌ای که از قبل در مالکیت message انقضای ارسال‌شده است، هنگام drain مالک detach می‌شود و در آنجا callback یا free نمی‌شود؛ callback یا cleanup پیام، مالکیت تحویل و table را تا تسویه‌ی دقیقاً یک‌باره نگه می‌دارد.

درخواست shutdown منظم معادل نابودی line نیست. handler مربوط به Finish برای یک line عادی تحت مالکیت، حتی وقتی shutdown پردازه را نیز درخواست می‌کند، باید پیش از بازگشت تولیدکننده‌ها را جدا، ‏line state را نابود و line را از نظر منطقی مرده کند.

۹. ‏timer‌ها، ‏watcher‌ها و بازپس‌گیری معوق timer​

AuthenticationClient مالک line عادی کنترل روی worker صفر است. reconnect مثبت deadline اولیه را حفظ می‌کند؛ retry صفر queue می‌شود و cancellation فقط state محافظت‌شده با mutex را settle می‌کند. timer مستقل، قدیمی‌ترین درخواست بی‌پاسخ را حتی بدون کار دوره‌ای منقضی می‌کند. quiescence timerها را جدا و Stop خط مالک را drain می‌کند. batch مصرف حل‌نشده و بدهی نسبی باقی‌مانده catch-up از پاک‌سازی session/transport جان سالم می‌برند: پس از احراز هویت، نخست replay و ACK و سپس pull الزامی کامل انجام می‌شود. پس از refresh الزامی، push منتظر بر pull عادی اولویت دارد تا گزارش مصرف گرسنه نماند. پس از replay/refresh، بدهی نسبی بایت و مشاهده ACKنشده تا همان نقطه capture می‌شود و یک pass محدود با batchهای محدود و pull پس از هر ACK آن را تخلیه می‌کند؛ aggregate مطلقِ rebaseشونده هدف reconnect نیست. بدهی نسبی تا ACK مطابق حفظ می‌شود، حتی وقتی saturation آن را در شمارنده rebaseشده پنهان کند. مصرف بعد از cut از تنظیم push و trigger اولین استفاده پیروی می‌کند؛ قطع بعدی هم batch و هم بدهی نسبی باقی‌مانده را حفظ می‌کند. پس از replay/refresh، بدهی حفظ‌شده با deltaهای مرئیِ تأییدنشده ادغام می‌شود، بدون اینکه بخش هم‌پوشان دوبار اضافه شود. فقط ACK مطابق، بدهی همان گزارش را مصرف می‌کند؛ نابودی نهایی تونل حافظه RAM آن را بدون تضمین تحویل آزاد می‌کند. timer غیرصفر worker صفر حتی با کار دوره‌ای غیرفعال پیشرفت مستقل و retry refusal منابع را تضمین می‌کند؛ شکست ایجاد trigger الزامی درخواست orderly shutdown است. quiescence آن را بدون rearm جدا می‌کند و Stop پیش از آزادسازی آرایه‌ها، جدول‌ها و قفل‌ها line مالک را drain می‌کند. cache معتبر پذیرش جدید را در قطع عادی ادامه می‌دهد؛ رد صریح credential تا Session معتبر sticky است و accounting اتصال موجود قطع نمی‌شود. origin، سن، مشاهده و marker ACK با پاک‌سازی session یا migration جدول reset نمی‌شوند. checkpoint هویت احراز‌شده از expiry/جایگزینی session مستقل و فقط در RAM است. با از دست رفتن continuity، batch نامطمئن حفظ و همگام‌سازی خودکار متوقف می‌شود، نه اینکه در checkpoint تازه replay شود. ACK فقط پذیرش در حافظه سرور است، نه ذخیره دیسک. ذخیره دوره‌ای/نهایی دیتابیس checkpoint را ذخیره نمی‌کند. restart کلاینت ممکن است مصرف گزارش‌نشده را از دست بدهد؛ restart سرور checkpoint را از دست می‌دهد و ممکن است مصرف ACKشده پس از آخرین ذخیره موفق را از دست بدهد. shutdown تضمین تحویل نهایی مصرف نیست؛ بازیابی حسابداری پس از crash/restart ارائه نمی‌شود.

timer‌ها و watcher‌های مؤلفه به worker وابسته‌اند. onQuiesceRequest، ‏state مربوط به فعال‌سازی مجدد/پذیرش آن‌ها را می‌بندد؛ onWorkerQuiesce، ‏pointer منتشرشده‌ی timer یا watcher همان مؤلفه را روی worker مالک حذف می‌کند.

sampler سراسری منابع پردازه روی worker 0 همین ترتیب را رعایت می‌کند: timer دوره‌ای آن را هنگام quiescence ‏worker متوقف کنید و سپس اجازه دهید آخرین snapshot منتشرشده تا زمان نابودی global state به‌طور طبیعی قدیمی شود. idle table‌های محلی نیز پیش از drainشدن state تونل یا line قابل‌دسترسی از callback‌هایشان quiesce می‌شوند؛ مرحله‌ی stop روی worker مالک، item‌های باقی‌مانده را حذف و table خالی را نابود می‌کند.

quiescence عمومی حلقه هر timer باقی‌مانده را غیرفعال و callback و userdata آن را پاک می‌کند. تخصیص روی فهرست معوق متعلق به حلقه نگه داشته می‌شود، زیرا line state ممکن است هنوز pointer همان timer را داشته باشد و در تخلیه‌ی بعدی line، مالک آن را حذف کند. wtimerDelete() این state را تشخیص می‌دهد و آزادسازی نهایی را دقیقاً یک بار انجام می‌دهد. نابودی حلقه فقط timer‌های معوق باقی‌مانده پس از تخلیه را آزاد می‌کند.

timer یک‌باره‌ی سررسیدشده پیش از اجرای callback برای بازپس‌گیری علامت می‌خورد. اجرای موفق wtimerReset() آن را دوباره زنده می‌کند. اگر فعال‌سازیِ مجددِ خودِ timer رد شود، callback همه‌ی جایگاه‌های مرجعِ مالک را پاک و کار نمایندگی‌شده را تسویه می‌کند و بدون حذف یا نگه‌داشتن timer در حال اجرا برمی‌گردد؛ آزادسازی مربوط به رویداد در انتظار، بازپس‌گیری یکتا را انجام می‌دهد. این با callback‌ای که به‌علت quiescence فراخوانی نشده است فرق دارد؛ تخصیص آن برای پاک‌سازی روی worker مالک در دسترس می‌ماند.

reset کردن timer در انتظار، اجرای سررسید قبلی را لغو می‌کند، اما reference مربوط به عمر تخصیص در صف pending را پاک نمی‌کند. dispatcher ممکن است هنوز pointer timer جاری یا بعدی را نگه داشته باشد. حذف پس از reset موفق، حتی از callback خود timer، مجاز است؛ آزادسازی تخصیص تا پایان استفاده‌ی dispatcher از آن reference به تعویق می‌افتد. اگر timer حذف نشود، برنامه‌ی جایگزین آن فعال می‌ماند و callback در سررسید جدید قابل اجراست. quiescence و پاک‌سازی مستقیم حلقه نیز همین reference را پیش از آزادسازی timer یا نگه‌داشتن آن برای پاک‌سازی مالک تسویه می‌کنند.

در IOCP، لغو به معنی تکمیل نیست. quiescence callback‌های کاربر را متوقف می‌کند، اما رکوردهای عملیات kernel تا زمانی که تکمیل‌های لغوشده بازنشسته شوند زنده می‌مانند. حافظه‌ی WIO فقط پس از تخلیه‌شدن آن reference‌ها بازپس گرفته می‌شود.

۱۰. دستگاه‌های خارجی و lwIP​

دستگاه‌های TUN، ‏raw و capture، توقف را به عملیات درخواست و انتظار تقسیم می‌کنند. مرحله‌ی درخواست، پذیرش محلی تحویل/فعال‌سازی مجدد را می‌بندد و بدون joinکردن به reader‌های خارجی سیگنال می‌دهد. مرحله‌ی انتظار، در حالی که دستگاه و state تونل هنوز معتبرند، ریشه‌های callback خارجی را join یا quiesce می‌کند. توقف نهایی پس از تخلیه‌شدن line‌های worker، منابع باقی‌مانده‌ی interface، ‏writer، ‏route، ‏DNS و platform را آزاد می‌کند.

هر backend مربوط به TUN در مرحله‌ی درخواست deviceReaderSessionEndRequest() را فراخوانی می‌کند و deviceReaderSessionEndWait() را تا BringDown به تأخیر می‌اندازد. دریافت TUN در Linux و Windows روی worker عادی انجام می‌شود. در Linux هر مالک WIO خود را در onWorkerQuiesce جدا می‌کند و BringDown پیش از بازنشسته‌کردن کار باقی‌مانده منتظر پایان خواندن‌های پذیرفته‌شده و watcher‌ها می‌ماند. در Windows، callback خارجی thread-pool فقط به worker پیام می‌دهد؛ توقف باید admission دریافت و rearm را ببندد، wait بومی را غیرفعال و callback‌های آن را quiesce کند و سپس نشست Wintun را ببندد. pipe‌های توقف Darwin/BSD غیرمسدودکننده و close-on-exec می‌مانند و reader آن‌ها باید پیش از بازنشانی مالک pool و بازنشسته‌کردن fragment‌ها join شود. همه‌ی backend‌ها در مرحله‌ی درخواست admission خروجی worker را نیز می‌بندند و پیش از بستن منابع بومی، نوشتن‌های پذیرفته‌شده را تسویه می‌کنند. Down تنها وقتی منتشر می‌شود که همه‌ی مالکیت لازم از بین رفته باشد. UdpStatelessSocket نشست تولیدکننده‌ی خارجی ندارد: پیام‌های worker و درخواست‌های async DNS آن پیش از انتشار Quiesced به‌دست worker‌های مالک تسویه می‌شوند و WIO فقط از onWorkerQuiesce روی io_wid ثبت‌شده بسته می‌شود.

تحویل دستگاه از دروازه‌های محلی نسل/نشست استفاده می‌کند. تولیدکننده‌ای که پیش از بسته‌شدن دروازه وارد شده است می‌تواند کامل شود؛ ورودی بعدی رد می‌شود و مالکیت نگه‌داشته‌شده‌ی خود را آزاد می‌کند. نسل قدیمی reader پیش از بازشدن نسل تازه هنگام راه‌اندازی مجدد quiesce می‌شود و reader خارجی، در صورت وجود، join می‌شود. TUN و raw صف‌های خروجی محدود و متعلق به هر worker دارند؛ انتشار نسل writer پیش از بسته‌شدن admission جدا می‌شود. callback و عملیات بومی پذیرفته‌شده باید پیش از نابودی buffer‌های صف و بستن device یا session تسویه شوند. completion بومی raw در Windows فقط به worker ثبت‌شده پیام می‌دهد و به thread writer اختصاصی نیاز ندارد.

هر join ناموفق، ‏handle بومی و منابعی را که thread می‌تواند به آن‌ها دسترسی داشته باشد حفظ می‌کند. نابودکردن آن‌ها زیر thread احتمالاً زنده هرگز راهکار جایگزین منظم نیست.

quiescence هر worker پذیرش engine همان worker را می‌بندد و timer آن را پیش از wloopQuiesceNormalWork() جدا می‌کند. هنگام drain، node‌ها منابع stack خود را روی worker مالک جدا می‌کنند، در حالی که engine و pool‌ها هنوز زنده‌اند. workerPerformTeardown() پیام‌های جداشده را تسویه می‌کند، engine را آزاد می‌کند و سپس loop ذخیره‌شده و pool‌ها را نابود می‌کند. چون detach پیام‌ها قبلاً worker->loop را پاک کرده است، پاک‌سازی engine از reference صریح و ذخیره‌شده‌ی loop استفاده می‌کند. نهایی‌سازی منابع مشترک lwIP پس از خروج همه‌ی engine‌ها و worker‌ها انجام می‌شود.

۱۱. انتخاب API خاتمه​

وضعیت را دسته‌بندی کنید، نه سطح log را:

وضعیتاقدام
پایان موردانتظار زمان اجرانتیجه را تثبیت کنید، مالکیت محلی را آزاد کنید، requestProgramShutdown(0) را فراخوانی و برگردید.
شکست منظم زمان اجرا با state ساختاری معتبرشکست مؤلفه را منتشر کنید، مالکیت محلی callback را آزاد کنید، requestProgramShutdown(code) را فراخوانی و پشته را به‌طور عادی باز کنید.
شکست مختص lineهمان line را با قرارداد owner آن ببندید؛ بدون الزام صریح محصول آن را سراسری نکنید.
نقض ناوردایی یا state ناامن/خرابlog کنید و abortProgramNow(code) را فراخوانی کنید.
شکست راه‌اندازیوضعیت راه‌اندازی را از پایین به بالا برگردانید/ثبت کنید، منابع محلی و قفل‌ها را آزاد کنید و اجازه دهید مالک بالادستی راه‌اندازی یک بار StartupRollback را انتخاب کند.
شکست فرمان/config پیش از controllerstate متعلق به parser را آزاد و کد خروج را مستقیماً برگردانید.

requestProgramShutdown() را هنگام نگه‌داشتن lock یا buffer موقتی که callback فعلی باید آزاد کند فراخوانی نکنید. منتشرشدن درخواست، callback را پیش‌دستی نمی‌کند. پس از پذیرش درخواست، از جریان کنترل عادی برگردید و کار تازه‌ای فعال نکنید.

API مبهم terminateProgram() وجود ندارد. محل‌های فراخوانی در زمان اجرا از درخواست منظم نوع‌دار یا مسیر بحرانی مستقل و بدون قفل استفاده می‌کنند؛ محل‌های راه‌اندازی وضعیت را به مالک خود منتقل می‌کنند.

مرحله‌های اصلی راه‌اندازی یک ww_startup_result_t برمی‌گردانند. یک مرحله‌ی همگام در سطح بالای راه‌اندازی می‌تواند ww_startup_context_t محلیِ thread را به‌عنوان گردآورنده‌ی پویای خطای کران‌دار نصب کند و دامنه‌های تو‌در‌تو را پیرامون callback‌های تونل با ABI ثابت تاریخی قرار دهد. context پیش از اجرای loop‌های رویداد worker به‌صورت قطعی بازگردانده می‌شود و callback‌های async نباید از آن استفاده کنند. API‌هایی که نوع نتیجه‌ی مفید دارند همچنان باید خطا را صریح برگردانند.

پس از بارگذاری تونل‌های واردشده، parse و نصب config و راه‌اندازی SocketManager، thread اصلی صرف‌نظر از اینکه نتیجه‌ی همان مرحله از قبل شکست بوده، صندوق پیام سیگنال را مصرف می‌کند. مرز مشترک شکست راه‌اندازی از عملیات داوریِ محافظت‌شده با mask در SignalManager که بالاتر توضیح داده شد استفاده می‌کند. بنابراین سیگنال موجود در صندوق پیام، ‏Signal را به‌عنوان منشأ تغییرناپذیر حفظ می‌کند و هر دو علت همچنان StartupRollback را انتخاب می‌کنند؛ سیگنال به‌تعویق‌افتاده پس از گرفتن mask بعداً رسیدگی می‌شود و نمی‌تواند بعداً و به‌صورت عطف‌به‌ماسبق StartupFailure را جایگزین کند. مرحله‌های بعدی آغاز نمی‌شوند.

تعویض رمز AuthenticationServer همان admission و عمر detached job در API را دنبال می‌کند؛ کار queueشده پس از بسته‌شدن admission mutation نمی‌کند و handler در حال اجرا پیش از آزادشدن storage تسویه می‌شود. گم‌شدن پاسخ هنگام shutdown، disconnect یا timeout، commit حافظه را برنمی‌گرداند. خطای node یعنی همان invocation commit نشده؛ delivery نامعلوم ممکن است پس از commit باشد. read تازه token فعلی و state عمومی را می‌دهد، نه اثبات رمز نصب‌شده. caller باید صریحاً تصمیم بگیرد رمز مطلوبِ نگه‌داری‌شده را با token تازه ارسال کند؛ موفقیت ارسال مجدد فقط commit همان transaction را اثبات می‌کند. request ID صرفاً correlation است و retry خودکار یا record پایدار اضافه نمی‌شود.

۱۲. اعتبارسنجی​

اعتبارسنجی متمرکز باید رفتار تغییرکرده را پوشش دهد:

ctest --preset linux-unit-release --output-on-failure \
-R 'line_task_scheduling|worker_context_helpers|shutdown|signal|worker.*quies|wloop_(stop|wakeup)|external_node_abi|quiescence_gate|reader_session|lwip'

# پوشش متمرکز Race روی زنجیره‌ابزارهای پشتیبانی‌شده‌ی Linux
ctest --preset linux-tsan-unit --output-on-failure

fixture‌های مهم موارد زیر را اثبات می‌کنند:

  • line_task_scheduling شکست one-shot را داخل مرز واقعی و fallible eventloopTryZalloc() تزریق می‌کند و settlement مستقیم/خارجی، bufferدار، line مرده، quiescence، teardown و cancellation بازدرون‌شونده را می‌سنجد؛

  • worker_context_helpers نگاشت نتیجه/دلیل worker message و قرارداد مالکیت دقیق callback-or-cleanup را اثبات می‌کند؛

  • پذیرش پایدار با wake ناقص، handleکردن wake اشباع‌شده در POSIX، ‏Coalesceشدن request‌ها، انتخاب reason اول و arbitration نهایی status؛

  • بازگشت callback مربوط به worker 0 پیش از coordination؛

  • ترتیب quiesce، ‏drain، ‏teardown و join برای دو worker؛

  • suppress‌شدن batch ‏callback پس از نخستین درخواست quiesce؛

  • تسویه‌ی دقیق callback یا cleanup برای message‌های queue‌شده و timed؛

  • کامل‌شدن pass‌های request مربوط به component پیش از آغاز هر pass مربوط به wait؛

  • drainشدن source owner در SocketManager پیش از hook‌های worker مربوط به tunnel؛

  • composition واقعی shutdown_production_sequence یک smoke test چندworkerه برای مالکیت source در UDP، مالکیت carrier/child در Mux، کار queue‌شده و خروج پردازه از coordinator است؛ این test اثبات کامل wait برای producer خارجی یا ledger همه‌ی phase‌ها نیست؛

  • unit‌های متمرکز ReverseClient، ‏pair‌های connecting و active را drain می‌کنند، close از re-entrant Init و Finish همسایه پیش از drain را می‌پوشانند و پیش از نابودی timeout index یک owner registry خالی می‌خواهند؛ reverse TCP Bridge همچنان smoke test است؛

  • race میان enter و close برای device/session و میان join و reclamation؛

  • پذیرش ABI فعلی نود خارجی و ردشدن نسخهٔ قبلی یا descriptor گمشده و ناسازگار پیش از node getter؛

  • هیچ leaf در کد تولید phase سراسری shutdown پردازه را نمی‌خواند.

preset ‏linux-tsan-unit، ‏unit‌های متمرکز هم‌زمانی را همراه fixture‌های واقعی production sequence و reverse teardown می‌سازد. اجرای native مربوط به lifetime کنسول و test‌های admission در IOCP بر عهده‌ی CI ویندوز است. تکمیل محلی همچنان باید کوچک‌ترین مجموعه‌ی متمرکزی را اجرا کند که مرز مالکیت تغییرکرده را پوشش می‌دهد. مسیرهای کامل production و unit یک checkpoint مربوط به Release باقی می‌مانند.

پایان عمر writer ممیزی AuthenticationServer​

writer ممیزی فعال باید از تمام submitterهای API و تولیدکننده بیشتر زنده بماند. quiescence سراسری API و تخلیهٔ lineهای owner ابتدا آن‌ها را پایان می‌دهد؛ سپس node admission ممیزی را می‌بندد، writer را بیدار و join می‌کند. writer رکوردهای پذیرفته‌شده را تخلیه و فایل را می‌بندد، آنگاه صف، condition، mutex و state آزاد می‌شوند؛ startup rollback نیز همین ترتیب را رعایت می‌کند. writer از pool worker قرض نمی‌گیرد و callback به event loop نمی‌فرستد. شکست join یک invariant کشنده است و اجازهٔ آزادکردن state قابل‌دسترسی نیست. خطای فایل فقط خرابی همین مؤلفه است؛ حساب را rollback یا commit موفق را شکست اعلام نمی‌کند. تخلیهٔ shutdown بازیابی crash نیست و رکوردهای ازدست‌رفته پس از fault قبلی writer را برنمی‌گرداند.

تسویهٔ shaping در UserController​

quiescence پذیرش و rearm shaping را می‌بندد و timer/continuation روی worker مالک و نوبت‌های مشترک را پیش از نابودی جدول و قفل AuthenticationClient حذف می‌کند. سازندهٔ واقعی normal line را با state و pool زنده drain می‌کند؛ UserController line قرضی را lineDestroy() نمی‌کند. Finish نخست buffer آماده و صف را بدون هزینه دور می‌ریزد، reservation هزینه و ticket و connection/IP را یک بار آزاد می‌کند و فقط دور از فرستنده انتشار می‌یابد. destroy نهایی به inventory و budget خالی نیاز دارد.

cancel continuation فقط notification است؛ ممکن است هم‌زمان، روی thread بیگانه یا بعد از مرگ منطقی اجرا شود و به صف owner-only دست نمی‌زند، timer را rearm نمی‌کند و callback نمی‌فرستد. مسیر inventory روی worker مالک cleanup را انجام می‌دهد. reservation به runtime یکتای قدیمی متصل است تا ID بازساخته‌شده اثر نگیرد. refresh با ID یکسان اعتبار bucket، کسر refill، timestamp و ticket را منتقل می‌کند؛ تغییر نرخ ابتدا نرخ قدیم را تا زمان تغییر settle و سپس ظرفیت جدید را اعمال می‌کند. refresh بدون تغییر burst تازه ندارد. drain اتصال‌های موجود در outage و رد credential از cache ادامه دارد؛ پذیرش جدید gate مستقل دارد. رد timer/continuation لازم فقط line مربوط را می‌بندد.

قراردادهای سرور احراز هویت​

startup مجوز primary و recovery موجود را مستقل از انتخاب candidate دیتابیس محدود می‌کند. primary معتبر نیز به محدودسازی مجوز backup موجود نیاز دارد، اما محتوای backup خوانده یا parse نمی‌شود؛ نبودن backup مجاز است. شکست محدودسازی startup را بدون جایگزینی هیچ‌یک از فایل‌ها متوقف می‌کند. مالک فایل لازم نیست کاربر process باشد؛ محدودسازی نه مالکیت فایل و نه دسترسی process را تغییر می‌دهد، بنابراین اجرا با root دسترسی عادی فایل‌سیستم را حفظ می‌کند.

shutdown عادی سرور save نهایی را force می‌کند ولی StartupRollback نه. periodic clean فقط پس از checkpoint موفق process، primary غیرخالی regular قابل‌استفاده و نبود slot تاریخچهٔ due skip می‌شود. config/stats mutation dirty می‌کند ولی replay دقیق و enrollment نه. AddNewUser با candidate حاضر save synchronous اجباری دارد؛ rollback dirty قبلی را حفظ و موفقیت پس از revision bump clean می‌کند. موفقیت primary حتی با شکست history dirty را پاک می‌کند؛ slot تا retry due می‌ماند. skip نه backup و نه telemetry را عوض می‌کند؛ absence تعمیر و probe error شکست آشکار است.

retry و shutdown همگام‌سازی صفحه‌ای​

streak و deadline monotonic شکست sync متعلق به process است. retry نخست interval تنظیم‌شده و صفر force-queued را حفظ می‌کند؛ شکست‌های بعدی از max(reconnect-interval-ms,1000) دوبرابر تا max(base,60000) و jitter افزوده حداکثر یک‌چهارم دارند. deadline مؤثر بیشینهٔ محدودیت‌ها، شامل کف refusal آمار است. درخواست مستقیم، open، first-use، Resume و timer آن را می‌سنجند؛ wakeup مستقل با interval اختیاری صفر هم فعال است. Session/Pong/صفحهٔ میانی streak را پاک نمی‌کند؛ نصب کامل تأییدشده پاک می‌کند. ترافیک deadline را عقب نمی‌اندازد و در Pause/backoff polling کوتاه نداریم. status فقط دستهٔ خطای ثابت و زمان باقی‌مانده می‌دهد.

traversal مالک staging/range ساعت مستقل از pending page است. هر صفحه و validation مهلت absolute خود را دارد؛ پیشرفت آن را تمدید نمی‌کند. قطع، stale cursor، صفحهٔ نامعتبر، limit یا timeout تنها staging را آزاد و cache و batch/debt را حفظ می‌کند. quiescence پذیرش درخواست را می‌بندد، timer/messageها را cancel و کار صفحه‌ای را متوقف می‌کند. Stop روی worker مالک line نرمال را settle می‌کند؛ cleanup نهایی پس از settlement callback/message، staging باقی‌مانده را آزاد می‌کند. cancellation خارجی به state صفحهٔ owner-only دست نمی‌زند و sync را دوباره شروع نمی‌کند.

پاک‌سازی حافظهٔ متعلق به API​

APIServer پیش از آزادسازی، body و token درخواست، frame و پاسخ نگه‌داشته‌شده و token باینری تنظیمات را پاک می‌کند. بایت‌های ورودی مصرف‌شده یا کنارگذاشته‌شده پیش از بازگشت بافر به pool پاک می‌شوند. درخت کامل JSON در مسیر موفق و رد اعتبارسنجی بعدی پاک می‌شود. dispatcher کپی request/response متعلق به job را در آخرین release، شامل cancellation، پاک می‌کند؛ payload پاسخ برگشتی handler نیز پیش از pool reuse پاک می‌شود. بافر request پس از واگذاری متعلق به handler است و dispatcher بدون برگشت آن دوباره به آن دست نمی‌زند. این قرارداد شامل درخت config، درخت ناقص داخلی parser در خطای syntax، بافر واگذارشده به همسایه، کپی OS یا همهٔ کپی‌های process نیست. پاک‌سازی امن برای بافرهای secret و tokenهای داخل ساختار است؛ metadata عادی فقط reset/release معمولی نیاز دارد. این مرزها مربوط به عملیات مدیریت هستند و به معنی پاک‌سازی همهٔ packetهای VPN نیستند. مقایسهٔ معمولی lookup/index و بررسی constant-time توکن دسترسی نقش جدا دارند؛ پاک‌سازی secret نیازمند تعویض مقایسه‌های نامرتبط نیست.

timer نشست‌های قرضی QUIC​

هر engine، parser و alarm در QuicClient/QuicServer به lineGetWID() و loop همان worker محدود است. clock تازهٔ monotonic برای deadline/RTT و wall time فقط برای اعتبار گواهی و کاربرد صریح پروتکل است. timer loop مالک نزدیک‌ترین alarm یا مهلت آمادگی را با گردکردن امن رو به بالای میلی‌ثانیه و کار محدود دنبال می‌کند. ورودی ناقص مهلت کل handshake و opening را تمدید نمی‌کند.

registry هر worker برای context قرضی است، نه مالکیت line. quiescence admission جدید را می‌بندد و rootهای alarm را روی worker مالک پیش از acknowledgment جدا می‌کند. سپس مالک عادی line در حالی drain می‌کند که chain، state پروتکل و pool زنده‌اند؛ نباید با فرض ترتیب topology برای hookها، زودتر خالی‌بودن registry قرضی را الزام کرد. پیش از آزادشدن config مشترک در destruction نهایی، session، timer و reference فعال driver نباید باقی مانده باشد. Finish بازگشتی فوراً state محلی را جدا می‌کند و حذف engine در driver فعال تا بازگشت امن آن به‌تعویق می‌افتد. callback یا alarm پس از retirement نمی‌ماند و Finish منتظر ACK حاملی با مالک دیگر نمی‌ماند.

timer اولین payload و پایان عمر VmessClient​

مهلت اولین payload از wloopNowLoopRunTime(loop)، timestamp کش‌شده monotonic برحسب میکروثانیه، روی event worker همان line مستقیم TCP یا carrier UDP تحت مالکیت استفاده می‌کند. اولین Est یک deadline مطلق منتشر می‌کند و تکرار Est آن را تمدید نمی‌کند. پیش از اینکه callback timer یک‌باره خروجی بدهد یا association را ببندد، slot مرجع مالک را پاک کنید. بیداری زودهنگام همان deadline را reset می‌کند؛ refusal مربوط به reset، association را بدون نگهداری یا حذف timer متعلق به pending dispatch می‌بندد. خروجی سررسیدشده مستقیم TCP از Pause مشورتی عبور می‌کند؛ header بیکار carrier UDP تا Resume همان carrier منتظر می‌ماند. ورودی محلی خالی/بزرگ تعهد timer را لغو نمی‌کند.

تعهد ارسال درخواست و هر teardown line، timer اولین payload را روی worker مالک لغو می‌کند. backend ساکت timer جداگانه انتظار پاسخ ندارد. بسته‌شدن normal dispatch پس از quiescence ارسال timer و association تازه را منع می‌کند. quiescence عمومی حلقه تخصیص timer معوق را حفظ می‌کند تا source-owner drain بتواند handle منتشرشده را حذف کند. lineهای قرضی برنامه و carrierهای متعلق به node را وقتی state تونل، helperها و poolها زنده‌اند drain کنید. associationها را جدا، صف‌ها را تسویه و کلید نشست را پیش از صفر کردن state پاک کنید. Finish مربوط به carrier تحت مالکیت باید با مرگ منطقی همان line بازگردد. inventory سراسری autonomous یا pump احراز هویت برای کلاینت لازم نیست.