MirrorService#mirror の exist? チェックとアップロードを並列化

rails/rails

MirrorService#mirror における exist? チェックとアップロードが、既存のスレッドプールを活用して並列実行されるようになりました。これにより、N台のミラーに対するwall timeがO(N)からO(1)のネットワークラウンドトリップに短縮されます。

背景

MirrorService#mirror は、ファイルをプライマリサービスから各ミラーへコピーする処理を順次(O(N))実行していました。一方、deletedelete_prefixed の処理では @executor スレッドプールがすでに活用されていましたが、mirror メソッドはこのプールを一切使用していませんでした。

PRの説明に示されたベンチマーク(クラウドレイテンシ50ms/callを模擬、5回の中央値)によると、逐次実行と並列実行の差は以下のとおりです:

ミラー数 逐次実行 並列実行 高速化
2 100ms 50ms 約2倍
3 150ms 50ms 約3倍
5 250ms 50ms 約5倍

ボトルネックはネットワークのラウンドトリップであり、ファイルサイズ(50KB〜20MB)は影響しないことも確認されています。

技術的な変更

mirror メソッドが Concurrent::Promise を用いた2段階の並列処理に刷新され、exist? チェックとアップロードの両フェーズが並列実行されます。

変更前:

def mirror(key, checksum:)
  instrument :mirror, key: key, checksum: checksum do
    if (mirrors_in_need_of_mirroring = mirrors.select { |service| !service.exist?(key) }).any?
      primary.open(key, checksum: checksum, verify: checksum.present?) do |io|
        mirrors_in_need_of_mirroring.each do |service|
          io.rewind
          service.upload key, io, checksum: checksum
        end
      end
    end
  end
end

変更後:

def mirror(key, checksum:)
  instrument :mirror, key: key, checksum: checksum do
    mirrors_in_need_of_mirroring = mirrors_needing_mirroring(key)
    if mirrors_in_need_of_mirroring.any?
      primary.open(key, checksum: checksum, verify: checksum.present?) do |io|
        io.rewind
        content = io.read.freeze
        tasks = mirrors_in_need_of_mirroring.map do |service|
          Concurrent::Promise.execute(executor: @executor) do
            service.upload key, StringIO.new(content), checksum: checksum
          end
        end
        tasks.each(&:value!)
      end
    end
  end
end

private
  def mirrors_needing_mirroring(key)
    tasks = mirrors.map do |service|
      [ service, Concurrent::Promise.execute(executor: @executor) { service.exist?(key) } ]
    end
    tasks.reject { |_, promise| promise.value! }.map(&:first)
  end

スレッドセーフ性の確保には2つの工夫が施されています。第一に、ファイルの内容を io.read.freeze で一度だけ読み込んで凍結した文字列として保持し、各ミラーへのアップロード時には StringIO.new(content) で個別のIOオブジェクトを生成します。これにより、複数スレッドが同一のIOポインタを競合して操作する問題を回避しています。第二に、primary.open のブロック冒頭で io.rewind を呼び出すことで、Tempfileが EOF 状態で yield された場合でも正しくファイル先頭から読み込めるよう保護しています。

mirrors_needing_mirroring ヘルパーメソッドの抽出により、exist? チェックフェーズも @executor プールで並列化されています。promise.value! は処理の完了を待機するとともに、いずれかのPromiseが例外で終了した場合にその例外を呼び出し元へ伝播させます。

設計判断

ファイル内容を一度だけ読んで凍結文字列として共有する方式が採用されました。

並列アップロードの実装として、各スレッドが独自にプライマリストレージを読み直す方式も考えられますが、それではネットワークI/Oが増加します。本PRでは内容を1回だけ読み込んで freeze した文字列を共有し、各スレッドへは StringIO.new(content) で独立したIOオブジェクトを渡すことで、読み込みコストを抑えつつスレッドセーフを実現しています。

exist? チェックとアップロードの2フェーズを明確に分離したことも、設計上の判断として読み取れます。exist? チェックの結果をすべて待機してからアップロードを開始することで、不要なアップロードを確実に防ぎつつ、各フェーズ内では並列実行を最大化しています。また、@executor はすでに mirrors.size にサイズされているため、スレッドプールの追加設定なしに適切な並列度が確保されます。

まとめ

本PRは、MirrorService に既存のスレッドプールが未活用だった mirror メソッドへの並列処理を導入した変更です。スレッドセーフのための凍結文字列と独立したStringIOという設計により、ミラー数に比例して増加していたレイテンシをO(1)に抑え、クラウドストレージのマルチミラー構成における実用的な高速化を実現しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
155b9560

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的な変更、設計判断、まとめという「総論→各論→結論」の構成が明確であり、各セクションが必要な情報を過不足なく含んでいます。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きのシンタックスハイライト(```ruby:path/to/file.rb)とPR番号のリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Concurrent::Promiseやスレッドセーフティといった概念を前提としており、専門知識を持つエンジニアという対象読者に適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフが「総論→各論」の構造で書かれており、トピックセンテンスが明確です。1段落1トピックの原則も守られており、可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内の「変更前」および「変更後」のコードブロックは、提供されたDiffの内容を正確に反映しています。ファイルパスも一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「Concurrent::Promise」「StringIO」「io.read.freeze」などの技術用語が、PRの文脈と一般的に受け入れられている意味の両方で正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

スレッドセーフ性を確保するための手法(凍結文字列とStringIOの利用)や、io.rewindの役割に関する説明は、PRの意図と技術的に完全に一致しており、正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescriptionやDiffで裏付けられています。特にベンチマークの数値やO(N)からO(1)への改善といった記述はPR情報と完全に一致しており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#57171)、ベンチマークの数値(100ms, 50msなど)、ファイルサイズ(50KB〜20MB)といったすべての数値・固有名詞が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「MirrorService#mirror の exist? チェックとアップロードを並列化」は、PRのタイトル「Parallelize exist? checks and uploads in MirrorService#mirror」を忠実に反映しており、内容と一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事はPRで提供された情報のみに基づいており、サポート状況やリリース予定など、PRに記載のない外部知識の創作はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「すでに活用されていました」といった時間表現が、PR Descriptionの「already ... used」と正確に対応しており、時間的な文脈の歪曲はありません。