Sidekiq偶発ジョブ

ジョブが複数の理由で失敗することはよく知られています。例えば、ネットワーク障害やバグなどです。このアドレスに対応するため、Sidekiqには組み込みのリトライメカニズムがあり、GitLab内のほとんどのワーカーでデフォルトで使用されています。

Sidekiqがジョブをidempotentかつtransactionalにすることを推奨しているのはそのためです。

一般的なルールとして、ワーカーは以下の場合にべき等とみなされます:

  • 同じ引数で複数回安全に実行できること。
  • アプリケーションの副作用は一度しか起きないと予想されます(あるいは、2回目の実行による副作用は影響しません)。

その良い例がキャッシュ期限切れワーカーです。

idempotentワーカーにスケジュールされたジョブは、同じ引数を持つ未開始のジョブが既にキューにある場合に、重複排除されます。

ワーカーが冪等であることの保証

ワーカーのテストが合格することを、次の共有例を使って確認してください:

include_examples 'an idempotent worker' do
  it 'marks the MR as merged' do
    # Using subject inside this block will process the job multiple times
    subject

    expect(merge_request.state).to eq('merged')
  end
end

job.perform の代わりにperform_multiple メソッドを直接使ってください (このヘルパーメソッドはワーカーに自動的に組み込まれます)。

ワーカーを冪等であると宣言する方法

class IdempotentWorker
  include ApplicationWorker

  # Declares a worker is idempotent and can
  # safely run multiple times.
  idempotent!

  # ...
end

perform メソッドが他のクラスやモジュールで定義されている場合でも、idempotent! 呼び出しは一番上のワーカークラスでのみ行うことを推奨します。

ワーカークラスが冪等であるとマークされていない場合、cop は失敗します。ジョブを安全に複数回実行できる自信がない場合は、cop をスキップすることを検討してください。

重複排除

あるidempotent workerのジョブがキューに入っている間に別の未着手のジョブがキューに入ると、GitLabは2番目のジョブを削除します。なぜなら、同じ作業は最初にスケジュールされたジョブが行うからです。2番目のジョブが実行される頃には、1番目のジョブは何もしていないでしょう。

戦略

GitLabは2つの重複排除ストラテジーをサポートしています:

  • until_executingこれはデフォルトの戦略です。
  • until_executed

より多くの重複排除戦略が提案されています。もし別の戦略が有効なワーカーを実装している場合は、イシューにコメントしてください。

実行まで

このストラテジーは、ジョブがキューに追加されたときにロックを取り、ジョブが開始する前にそのロックを削除します。

例えば、AuthorizedProjectsWorker はユーザー ID を取ります。ワーカーが実行すると、ユーザーの作成者を再計算します。GitLab は、あるアクションがユーザーの権限を変更する可能性があるたびにこのジョブをスケジュールします。同じユーザーが同時に二つのプロジェクトに追加された場合、最初のジョブが始まっていなければ二番目のジョブはスキップすることができます。

module AuthorizedProjectUpdate
  class UserRefreshOverUserRangeWorker
    include ApplicationWorker

    deduplicate :until_executing
    idempotent!

    # ...
  end
end

実行されるまで

このストラテジーは、ジョブがキューに追加されたときにロックを取り、ジョブが終了した後にそのロックを削除します。これは、ジョブが同時に複数回実行されるのを防ぐために使用できます。

module Ci
  class BuildTraceChunkFlushWorker
    include ApplicationWorker

    deduplicate :until_executed
    idempotent!

    # ...
  end
end

また、if_deduplicated: :reschedule_once オプションを渡すと、現在実行中のジョブが終了し、重複排除が少なくとも1回起こった後に、ジョブを1回再実行することができます。これにより、レースコンディションが発生しても、常に最新の結果が生成されます。詳細はこのイシューを参照してください。

将来のジョブのスケジューリング

GitLabは未来にスケジューリングされたジョブをスキップしません。ジョブの実行がスケジューリングされるまでに状態が変化していることを想定しているからです。未来にスケジュールされたジョブの重複排除は、until_executeduntil_executing 戦略の両方で可能です。

将来スケジュールされるジョブを重複排除したい場合は、重複排除ストラテジーを定義する際にincluding_scheduled: true 引数を渡すことで、ワーカー側で指定することができます:

module AuthorizedProjectUpdate
  class UserRefreshOverUserRangeWorker
    include ApplicationWorker

    deduplicate :until_executing, including_scheduled: true
    idempotent!

    # ...
  end
end

重複排除のタイムトゥライブの設定(TTL)

重複排除はRedisに保存されるidempotentキーに依存します。これは通常、設定された重複排除ストラテジーによってクリアされます。

しかし、以下のような特定のケースでは、キーがTTLまで残ることがあります:

  1. until_executing が使用されているにもかかわらず、Sidekiqクライアントミドルウェアが実行された後、ジョブがエンキューされることも実行されることもありませんでした。

  2. until_executed が使用されているが、リトライの枯渇によりジョブが終了しなかった、最大回数中断された、または失われた場合。

デフォルト値は6時間です。この時間内は、最初のジョブが実行されなかったり終了しなかったりしても、ジョブはキューに入れられません。

TTLは以下で設定できます:

class ProjectImportScheduleWorker
  include ApplicationWorker

  idempotent!
  deduplicate :until_executing, ttl: 5.minutes
end

TTLに達するとジョブの重複が発生する可能性があるため、多少の重複を許容できるジョブにのみTTLを下げてください。

べき乗ジョブの最新WALロケーションを保持

重複排除は常に、最初のポインタではなく最新のバイナリレプリケーションポインタを考慮します。これは、2回目にスケジュールされた同じジョブをドロップし、Write-Ahead Log(WAL) が失われたために起こります。これは、古いWALの場所を比較し、古いレプリカから読み取ることにつながる可能性があります。

重複排除とロードバランシングによるデータの一貫性維持の両方をサポートするために、Redisではidempotentジョブの最新のWALロケーションを保存しています。こうすることで、常に最新のバイナリレプリケーションポインタを比較し、完全に追いついているレプリカから読み込むようにしています。