🥍

LaravelでDBへのsticky接続を使う場合は、Jobの処理開始のタイミングで接続をリセットしよう

2024/10/25に公開

LaravelでDatabase接続する際のオプションとして sticky オプションが用意されています。

sticky オプションとは

データベースが read / write 構成 (ここではmaster/slaveやprimary/replicaと同様の意味)の場合に、「書き込みがされた以降は write 接続をし続ける」というオプションです。

sticky option
https://laravel.com/docs/11.x/database#the-sticky-option

read / write 構成の場合、レプリカラグなども考慮する必用が出てきますが、この sticky オプションを利用することで、あまり複雑に考えなくても write 接続が維持されるので、「書き込んだあとに、再取得しようとしたら何故かデータが取得できない」といった不具合の発生を気にする必用がなくなります。

sticky オプションの中身

処理としては以下のように readPdo を取得する際に、 sticky オプションが有効で、かつ、書き込み処理を行った履歴がある場合に、 readPdo を返さず (write)Pdo を返すようになっています。

https://github.com/laravel/framework/blob/v11.29.0/src/Illuminate/Database/Connection.php#L1258-L1261

「書き込み処理を行ったかどうか」については、 $this->recordsModified フラグで管理されており、同 Connection.php 内の statement()affectingStatement() といった書き込み時の処理で $this->recordsHaveBeenModified() を呼び出すことでフラグを立てています。

https://github.com/laravel/framework/blob/v11.29.0/src/Illuminate/Database/Connection.php#L569

HTTPリクエスト時は都度リセットされる

PHPなので、HTTPリクエストの場合は都度この $this->recordsModified はリセットされることになります。

そのため「レコード作成のHTTPリクエストの直後に、レコード取得のHTTPリクエストを行うと何故か取得できない」といったことはおきます。

これ自体は sticky オプションそのものでは対応できないため、セッションなどを活用して一定期間は write 接続が利用されるようにする方法などがあります。
検索すればいくつかのパターンで解決策が出てくると思うので、ここでは割愛します。

弊社では MySqlConnection を拡張する形で解決しています。

Jobの場合はリセットされない

LaravelのJobは、PHPのWorkerプロセスによって処理されます。

これはHTTPリクエストではなく、あくまでPHPコマンドを実行したプロセスを起動し続けているだけなので、「HTTPリクエスト同様にJobを処理する都度メモリがクリアされる」というわけではありません。

supervisor--once 起動オプションを組み合わせることで、「Jobを処理する都度プロセスを再起動することでメモリがクリアされる」ということも可能ではありますが、これは非効率なのでオススメしません。

となると、Jobで sticky 接続を行う場合、 $this->recordsModified フラグが維持され続けることになります。

  1. 1つ目のJobで書き込み処理が行われる
  2. 2つ目のJobは参照しか行わない

このとき、こちらの希望としては、「1つめはwrite接続」「2つめはread接続」を利用してほしいところです。
ところが、前述のとおりJobの場合メモリ空間はクリアされないので、 sticky オプションを利用していたとしても「2つめのJobもwrite接続」することになります。

これはよくないので、なんとかしたい、というのがこの記事になります。

JobProcessing

いくつかリセットしてよいタイミングはあると思いますが、弊社では JobProcessing イベントを利用しています。

https://github.com/laravel/framework/blob/v11.29.0/src/Illuminate/Queue/Worker.php#L427

Jobが処理される直前に実行される raiseBeforeJobEvent() というメソッドの中で発火される Illuminate\Queue\Events\JobProcessing イベントに対するリスナーを追加する形で対応します。

EventServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Queue\Events\JobProcessing;
use App\Listeners\JobProcessingListener;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        JobProcessing::class => [JobProcessingListener::class],
    ];
}
JobProcessingListener.php
<?php
namespace App\Listeners;

class JobProcessingListener
{
    public function handle(): void
    {
        app('db.connection')?->forgetRecordModificationState();
    }
}

このように、 JobProcessing イベントに対する同期的なリスナーとして app('db.connection')?->forgetRecordModificationState(); を実行することで、Jobの処理開始前に、 sticky 接続を write から read に戻すことができます。

標準で対応してくれたらいいのに

Workerのオプションとして提供するなり、 config の設定として提供してくれればよさそうに思えますが、「後方互換性が崩れるからデフォルトの動作を変えることは難しい」ようです。

https://github.com/laravel/framework/issues/37646

TANOMU

Discussion