사이드키크 항등 작업

작업이 실패할 수 있는 이유는 여러 가지가 알려져 있습니다. 예를 들어, 네트워크 중단이나 버그가 있습니다.

이를 해결하기 위해 사이드키크는 GitLab 내의 대부분의 작업자에 의해 기본적으로 사용되는 내장 재시도 메커니즘을 가지고 있습니다.

작업은 실패 후 애플리케이션이나 사용자에게 큰 부작용 없이 다시 실행될 수 있다고 기대됩니다. 그렇기 때문에 사이드키크는 작업이 항등성과 트랜잭셔널을 지녀야 한다고 권장합니다.

일반적으로 작업자는 다음 조건을 충족할 경우 항등적이라고 간주될 수 있습니다:

  • 동일한 인수로 안전하게 여러 번 실행할 수 있습니다.
  • 애플리케이션 부작용은 한 번만 발생해야 한다고 예상됩니다 (또는 두 번째 실행의 부작용이 영향을 미치지 않습니다).

좋은 예로는 캐시 만료 작업자가 있습니다.

항등 작업자로 예약된 작업은 중복 제거됩니다. 동일한 인수를 가진 시작되지 않은 작업이 이미 대기열에 있는 경우입니다.

작업자가 항등성을 보장하기

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

it_behaves_like 'an idempotent worker'

공유 예제는 job_args가 정의되어야 합니다. 주어지지 않으면 인수 없이 작업을 호출합니다.

공유 예제가 실행될 때, 작업의 부작용을 피하는 모킹이 없어야 합니다. 예를 들어, 작업자가 서비스 호출 시 실행 메서드를 스텁하지 않도록 허용합니다. 이 방식으로, 작업이 진정으로 항등적임을 확정할 수 있습니다.

공유 예제에는 몇 가지 기본 테스트가 포함되어 있습니다. 공유 예제 블록에 작업자에 특화된 추가 항등성 테스트를 추가할 수 있습니다.

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

더 많은 중복 제거 전략이 제안되었습니다. 다른 전략의 이점을 얻을 수 있는 작업자를 구현하는 경우, 이슈에 댓글을 남겨주세요.

실행 전까지

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

예를 들어, 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 옵션을 전달하여 현재 실행 중인 작업이 완료된 후 한 번 재실행하도록 작업을 설정할 수 있습니다. 그리고 중복 방지가 최소한 한 번 발생했음을 보장합니다. 이렇게 하면 경합 조건이 발생하더라도 항상 최신 결과가 생성됩니다. 자세한 내용은 이 문제를 참조하세요.

미래에 작업 예약하기

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에 도달하면 중복 작업이 발생할 수 있으므로, 일부 중복을 처리할 수 있는 작업에 대해서만 이 값을 낮추는 것이 좋습니다.

idempotent 작업을 위한 최신 WAL 위치 유지

중복 방지는 항상 최신 바이너리 복제 포인터를 고려합니다. 이는 두 번째로 예약된 동일한 작업이 삭제되고 Write-Ahead Log (WAL)가 손실되기 때문입니다. 이로 인해 오래된 WAL 위치를 비교하고 오래된 복제본에서 읽을 수 있습니다.

중복 방지와 데이터 일관성을 유지하기 위해 부하 분산과 함께, Redis에서 idempotent 작업에 대한 최신 WAL 위치를 유지하고 있습니다.

이렇게 하여 최신 바이너리 복제 포인터를 항상 비교하고, 완전히 동기화된 복제본에서 읽도록 보장합니다.