Sidekiq idempotent jobs

작업은 여러 이유로 실패할 수 있다는 것이 알려져 있습니다. 예를 들어 네트워크 장애 또는 버그가 그 예입니다. 이에 대응하기 위해 Sidekiq에는 기본적으로 내장된 재시도 메커니즘이 있습니다. 이는 GitLab 내 대부분의 워커들에게 기본적으로 사용됩니다.

작업이 실패한 후에도 프로그램이나 사용자에게 주요한 부작용이 없이 작업이 다시 실행될 수 있다고 기대됩니다. 따라서 Sidekiq는 작업을 멱등(idempotent)하고 트랜잭션적(transactional)으로 만들 것을 권장합니다.

일반적으로 워커가 멱등하다고 볼 수 있는 경우는 다음과 같습니다:

  • 동일한 매개변수로 안전하게 여러 번 실행할 수 있는 경우
  • 애플리케이션의 부작용이 한 번만 발생할 것으로 예상됨 (또는 두 번째 실행의 부작용에는 영향이 없음).

한 예로는 캐시 만료 작업이 있습니다.

멱등한 워커에 대한 작업은 중복 제거(deduplicated)됩니다. 이미 대기열에 동일한 매개변수로 시작되지 않은 작업이 있을 때 중복되지 않습니다.

워커가 멱등한지 확인하기

작업을 두 번 실행하는 경우 효과를 보기 위해 다음의 공유 예시를 사용하세요.

it_behaves_like 'an idempotent worker'

공유 예시에는 job_args가 정의되어 있어야 합니다. 제공되지 않으면 매개변수 없이 작업을 호출합니다.

공유 예시를 실행할 때 작업의 부작용을 피하기 위해 목업(mocking)이 없어야 합니다. 예를 들어, 워커가 execute 메소드를 스텁하지 않고 서비스를 호출하게 하세요. 이렇게 함으로써 작업이 정말로 멱등한지 확인할 수 있습니다.

공유 예시에는 일부 기본적인 테스트가 포함되어 있습니다. 공유 예시 블록에서 워커에 특정한 멱등성 테스트를 추가할 수 있습니다.

it_behaves_like 'an idempotent worker' do
  it 'multiple calls에 대한 부작용을 확인합니다' do
    # `subject`는 작업의 perform 메소드를 2회 호출합니다
    subject

    expect(model.state).to eq('state')
  end
end

워커를 멱등하다고 선언하기

class IdempotentWorker
  include ApplicationWorker

  # 워커가 멱등하다고 선언하고
  # 여러 번 안전하게 실행할 수 있음을 명시합니다.
  idempotent!

  # ...
end

워커 클래스가 멱등하다고 표시되어 있을 때에도, perform 메소드가 다른 클래스나 모듈에서 정의되어 있더라도 최상위 워커 클래스에만 idempotent! 호출을 두는 것이 좋습니다.

만약 워커 클래스가 멱등하다고 표시되어 있지 않다면, 검사가 실패합니다. 작업이 안전하게 여러 번 실행될 수 있다고 확신할 수 없다면 검사를 건너뛰는 것을 고려하세요.

중복 제거

멱등한 워커의 작업이 대기열에 들어갈 때 이미 동일한 매개변수로 시작되지 않은 다른 작업이 이미 대기열에 있는 경우, GitLab은 두 번째 작업을 삭제합니다. 첫 번째 작업이 예정된 시점에 작업을 수행할 것이기 때문에 두 번째 작업은 스킵됩니다.

전략

GitLab은 두 가지 중복 제거 전략을 지원합니다:

  • until_executing, 기본 전략
  • until_executed

더 많은 중복 제거 전략이 제안되었습니다. 다른 전략이 필요한 워커를 구현 중이라면 해당 이슈에 의견을 남기세요.

Until Executing

이 전략은 작업이 대기열에 추가될 때 잠금을 가지고, 작업을 시작하기 전에 해당 잠금을 제거합니다.

예를 들어, AuthorizedProjectsWorker는 사용자 ID를 사용합니다. 워커가 실행되면 사용자의 권한을 다시 계산합니다. GitLab은 사용자의 권한을 변경하는 모든 작업에서 이 작업을 예약합니다. 동시에 동일한 사용자가 두 개의 프로젝트에 추가되면, 첫 번째 작업이 실행되기 전에는 두 번째 작업이 스킵될 수 있습니다.

module AuthorizedProjectUpdate
  class UserRefreshOverUserRangeWorker
    include ApplicationWorker

    deduplicate :until_executing
    idempotent!

    # ...
  end
end

Until Executed

이 전략은 작업이 대기열에 추가될 때 잠금을 가지고, 작업이 끝난 후 해당 잠금을 제거합니다. 이는 여러 번 동시에 작업이 실행되는 것을 방지하는 데 사용될 수 있습니다.

module Ci
  class BuildTraceChunkFlushWorker
    include ApplicationWorker

    deduplicate :until_executed
    idempotent!

    # ...
  end
end

또한, if_deduplicated: :reschedule_once 옵션을 전달하여 현재 실행 중인 작업이 중복 제거된 후 최대 한 번 다시 실행하도록 할 수 있습니다. 이는 경합 조건이 발생하더라도 항상 최신 결과가 생성되도록 보장합니다. 자세한 내용은 이 이슈를 참조하세요.

미래에 작업 예약하기

GitLab은 작업이 미래에 예약되어 있는 경우에는 스킵하지 않습니다. 작업이 실행될 시점에는 상태가 변경된 것으로 가정합니다. until_executeduntil_executing 전략 모두 미래에 예약된 작업에 대한 중복 제거가 가능합니다.

미래에 예약된 작업에 대해 중복 제거를하려면, 중복 제거 전략 정의시 including_scheduled: true 인수를 전달하여 해당 워커에 지정할 수 있습니다.

module AuthorizedProjectUpdate
  class UserRefreshOverUserRangeWorker
    include ApplicationWorker

    deduplicate :until_executing, including_scheduled: true
    idempotent!

    # ...
  end
end

중복 제거 TTL 설정하기

중복 제거는 Redis에 저장된 멱등한 키에 따라 달라집니다. 이는 보통 구성된 중복 제거 전략에 의해 지워집니다.

그러나, 특정 경우에는 키가 지워지지 않을 수 있습니다:

  1. until_executing가 사용되지만 Sidekiq 클라이언트 미들웨어가 실행된 후 작업이 대기열에 추가되거나 수행되지 않은 경우
  2. until_executed가 사용되지만 작업이 다시 시도한 후에 완료되지 못하거나, 최대 반복 회수를 초과하여 작업이 중단된 경우

기본값은 6시간입니다. 이 기간 동안, 첫 번째 작업이 실행되지 않거나 완료되지 않아도 작업이 대기열에 추가되지 않습니다.

TTL은 다음과 같이 구성 가능합니다:

class ProjectImportScheduleWorker
  include ApplicationWorker

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

TTL이 도달할 경우 중복된 작업이 발생할 수 있으므로, 이 기간을 수용할 수 있는 작업에 대해서만 이를 낮추도록 하세요.

idempotent 작업을 위한 최신 WAL 위치 보존

중복 제거는 항상 최신 이진 복제 포인터를 고려하며 처음 것이 아닙니다. 두 번째로 예약된 동일한 작업을 삭제하고 Write-Ahead Log (WAL)이 손실되기 때문에 이렇게 발생합니다. 이로 인해 이전 WAL 위치를 비교하고 오래된 레플리카에서 읽을 수 있습니다.

중복 제거와 로드 밸런싱을 통한 데이터 일관성 유지를 지원하기 위해, 우리는 idempotent 작업을 위한 최신 WAL 위치를 Redis에 유지합니다. 이렇게 하면 항상 최신 이진 복제 포인터를 비교하여 레플리카가 완전히 따라잡혔는지 확인합니다.