رفتن به متن راهنما
همه‌ی راهنماها

حصار برای نوشتن در مقصد

اجارهٔ slot ممکن است تمام شود، در حالی که callback اول هنوز کار می‌کند. بگذارید مقصد نوشتن، نوشتن دیرهنگام را با شمارهٔ حصار رد کند.

برنامه Reqtide PHP 0.1.0آخرین بررسی زمان خواندن 5 دقیقه

در این صفحه

همهٔ بلوک‌های کد از examples/12-fencing-downstream.php (opens in a new tab) گرفته شده‌اند. این مثال به افزونهٔ pdo_sqlite نیاز دارد.

مسئله

هر slot در سقف اجرای هم‌زمان (Bulkhead) یک اجاره (lease) دارد. اگر یک worker از مدت اجاره بیشتر متوقف بماند، slot آزاد می‌شود و worker دیگری کار را برمی‌دارد. اولی متوقف نمی‌شود. وقتی بیدار شود، هنوز callback خود را دارد و ممکن است نتیجه را بنویسد. آن‌وقت دو worker خود را مالک کار می‌دانند. سقف اجرای هم‌زمان و دورهٔ انتظار را ببینید.

پیش از هر ارسال، Reqtide بررسی می‌کند که اجاره هنوز بیشتر از تلاش دوام داشته باشد. این بررسی توقف بعد از ارسال را پوشش نمی‌دهد. توکن حصار (fence) هم در Reqtide وجود ندارد که به شما بدهد. توکن فقط وقتی کمک می‌کند که سامانهٔ دریافت‌کنندهٔ نوشتن آن را بررسی کند، پس بررسی باید همان‌جا باشد.

ایده

کنار داده‌ای که می‌خواهید محافظت کنید، یک عدد نگه دارید و اسم آن را حصار بگذارید. این عدد فقط بالا می‌رود و فقط مقصد آن را تغییر می‌دهد.

  1. ابتدا worker کار را claim می‌کند. مقصد یک واحد به حصار اضافه می‌کند و مقدار تازه را برمی‌گرداند.
  2. سپس worker کار را انجام می‌دهد و نتیجه را همراه با حصاری که گرفته می‌فرستد.
  3. مقصد نوشتن را فقط وقتی می‌پذیرد که حصار موجود در درخواست هنوز تازه‌ترین باشد.

اگر worker جایگاه slot خود را از دست داده باشد، حصار او قدیمی است. نوشتن او عدد تازه‌تری را می‌بیند و رد می‌شود.

برداشتن کار (claim) و نوشتن

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

PHP
$store = new \PDO('sqlite::memory:', null, null, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
$store->exec('CREATE TABLE jobs (id TEXT PRIMARY KEY, fence INTEGER NOT NULL, result TEXT)');
$store->exec("INSERT INTO jobs (id, fence, result) VALUES ('invoice-7', 0, NULL)");

/** Takes the job and returns its new fence. The number only goes up, and only the store hands it out. */
$claim = static function (string $job) use ($store): int {
    $store->beginTransaction();
    $store->prepare('UPDATE jobs SET fence = fence + 1 WHERE id = ?')->execute([$job]);
    $select = $store->prepare('SELECT fence FROM jobs WHERE id = ?');
    $select->execute([$job]);
    $fence = (int) $select->fetchColumn();
    $store->commit();

    return $fence;
};

/** Writes the result only if the fence is still the newest one. Returns false for a stale holder. */
$write = static function (string $job, int $fence, string $result) use ($store): bool {
    $update = $store->prepare('UPDATE jobs SET result = ? WHERE id = ? AND fence = ?');
    $update->execute([$result, $job, $fence]);

    return $update->rowCount() === 1;
};

همین ایده را با هر مقصدی که بتواند نوشتن شرطی انجام دهد می‌توان به کار برد: ستون نسخه در SQL، compare-and-set در یک مخزن کلید و مقدار، یا هدر If-Match در یک API مبتنی بر HTTP.

استفاده درون یک پایپ‌لاین

حصار را داخل callable بگیرید تا هر تلاش حصار خودش را داشته باشد. آن را به هر نوشتنی که تلاش انجام می‌دهد بدهید.

PHP
$pipeline = PipelineBuilder::profile('invoices', 'v1')
    ->partition('shop', 'prod', 'invoice-writer')
    ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative)
    ->bulkhead(1, Duration::seconds(10))
    ->compileInMemory(ExceptionClassifier::create());

مثال هر دو worker را بازی می‌کند. ابتدا worker A کار را claim می‌کند و متوقف می‌شود. بعد worker B از پایپ‌لاین می‌گذرد و می‌نویسد.

PHP
// Worker A takes the job and gets fence 1. Then it stalls: a slow call, a paused process, a long garbage collection.
$staleFence = $claim('invoice-7');

echo "-- 1. The current holder writes\n";
$outcome = $pipeline->execute(
    static function (AttemptContext $attempt) use ($claim, $write): string {
        $fence = $claim('invoice-7');

        return $write('invoice-7', $fence, 'written by ' . $attempt->operationId->value) ? "accepted with fence {$fence}" : "refused with fence {$fence}";
    },
    $pipeline->newContext(Duration::seconds(30), operationId: 'worker-b'),
);
echo $outcome, "\n";

سپس worker A بیدار می‌شود و با حصار 1 می‌خواهد بنویسد.

PHP
// Worker A wakes up. Its lease ran out long ago, and worker B has taken the job with a newer fence.
echo $write('invoice-7', $staleFence, 'written by worker-a') ? "accepted with fence {$staleFence}" : "refused with fence {$staleFence}", "\n";

خروجی:

Text
-- 1. The current holder writes
accepted with fence 2
-- 2. A holder that lost its slot writes late
refused with fence 1
stored: fence 2, written by worker-b

آنچه این روش پوشش نمی‌دهد

  • حصار از داده‌ای در مقصدی که آن را صادر می‌کند محافظت می‌کند. اثر جانبی در جای دیگر، مثل ایمیل یا تماس با شخص ثالث، پوشش داده نمی‌شود. برای آن تماس‌ها کلید idempotency بدهید.
  • نوشتنی که حصار را همراه نداشته باشد، بررسی نمی‌شود. حصار را با هر نوشتن همان تلاش بفرستید.
  • ممکن است worker جایگزین‌شده تا وقتی متوجه شود، هنوز زمان و اتصال مصرف کند. حصار فقط نتیجهٔ او را از داده بیرون نگه می‌دارد.