# Counts エクスポートワーカー Counts の画面からのエクスポートは永続ファイルキューへ登録する。 Active Job の既定のプロセス内 async アダプターは使用しない。 追加 gem、外部キュー、DB マイグレーションは不要。 ## 起動・配置 backend で `bundle exec rake counts:export_worker` を、systemd 等の監視対象として 別プロセスで起動する。Web プロセス内では起動しない。 1監視プロセスは同時に1ジョブを専用の子プロセスで生成し、全体の実行枠は 既存の2枠を共有する。fork 前に DB 接続を閉じ、子が独立した接続を取得する。 Web 内のスレッドや Active Job async では実行しない。Linux / POSIX の fork が必要。 監視側は SQL 中も1秒間隔でジョブとワーカーの heartbeat を更新する。 `docs/systemd/btrc-counts-exports.service` を配備用 unit として追加している。 Restart=always、KillMode=control-group、停止猶予150秒を指定している。 User、WorkingDirectory、bundle が見つかる PATH を実際の配備先へ合わせる。 `/etc/btrc-hub/counts-exports.env` に Web と同じ Rails secret・接続設定・ COUNTS_EXPORT_DIRECTORY を設定し、同じ Unix ユーザーで読み書きできるようにする。 OS 起動時の有効化・サービス起動は運用側で行う必要がある。 今回 unit のインストール、有効化、本番での起動・再起動は行っていない。 Web とワーカーは同一リリース、Rails secret、アプリケーション環境、DB 接続先を 使用する。DB コネクションはワーカープロセスごとに必要。 `COUNTS_EXPORT_DIRECTORY` を同じ永続 POSIX 共有ディレクトリに設定する。 省略時は `backend/tmp/counts_exports`。 全プロセスで同一の所有者とアクセス権を使用する。 複数ホストでは、flock・同一ファイルシステム内の atomic rename・fsync が正しく 機能する共有ボリュームが必須。オブジェクトストレージや各コンテナ独立の tmp は不可。 ジョブ JSON と完成ファイルが格納されるため、このディレクトリを静的公開しない。 ## API と進捗 - POST `/counts/export/jobs`:要求検証・実行時点の固定・署名付きジョブトークンの発行。 - GET `/counts/export/jobs/:token`:状態、実際の処理済みレコード数、判明している総数。 - DELETE `/counts/export/jobs/:token`:取消を永続記録。 - POST `/counts/export/jobs/:token/download`:セッション所有者の開始確認要求を登録。 - GET `/counts/export/files/:token?job=...&attempt=...`:実 GET の認可・送出開始の確認。 - GET `/counts/export/files/:token`:既存クライアント向けの認可付きダウンロード。 ジョブ・ファイルのトークンは作成元のセッションに紐付く。 既存の BAN・CSRF 制御を使用する。フロントは約800ms間隔で状態を取得する。 明細の総数は同じ読み取りトランザクション内の SQL 件数を使用する。 queued と running に加え、running 内の phase で aggregating(集計中)と writing(出力中)を区別する。最初の SQL より前に aggregating を記録する。 heartbeat_at は監視側の生存時刻、updated_at は生成処理が状態を書いた時刻であり、 処理件数と分離する。SQL 中に件数は増やさない。 集計の総出力数は生成終了まで未確定とし、不確定進捗として扱う。 processed は CSV では実際に生成したデータ行数、JSON のクロス集計では 実際に生成した pivot_cell 数。ヘッダーは含めず、小計・合計の物理レコードは含む。 通常の集計行だけを数える logical_count_unit=regular_result_rows と区別する。 出力中は約250ms間隔と最終レコードで実測件数を保存する。 完成状態はファイルの生成完了であり、ブラウザの保存完了を意味しない。 ## 取消・障害・制限 通常は約250ms間隔の生成チェックポイントで取消を検知する。 監視側も取消を確認し、子へ TERM、5秒後も終了していなければ KILL を送る。 子の回収後に一時ファイルを削除する。DB 側の実行停止は切断検知等に依存するため、 取消 API の受理と DB の即時停止は同義ではない。 取消済みへの遷移は生成処理の終了と一時ファイル削除後に行う。 既に始まった HTTP 転送やブラウザ側の保存は取り消せない。 ワーカーが異常終了したジョブは、次のワーカーが実行ロックを取得した際に失敗状態にし、 そのジョブの残存生成ファイルを削除する。別の読み取り状態で自動再実行しない。 原子的に書き換える状態ファイルとプロセス間ロックにより、二重処理・取消との競合を防ぐ。 既存の同時実行2、1ファイル128MiB、保存領域512MiB、生成120秒、期限10分を維持する。 生成120秒は監視側でも強制する。ワーカー未起動なら登録 API は503を返す。 待機中のワーカー消失、15秒以上の heartbeat 停止、150秒以上の待機は失敗とする。 ネットワーク切断時も UI が無期限に処理中にならないよう、状態取得は1回15秒で中断し、 30秒以上状態を取得できなければ失敗表示へ移る。処理件数を捏造しない。 画面からの通信断だけではサーバーの取消完了を保証しない。サーバー側の期限は独立して働く。 期限切れのジョブ・ファイル・残存状態書込ファイルは登録時またはワーカーの巡回時に削除する。 ワーカーと Web の両方を停止した間は巡回削除も停止する。 これらの処理は静的確認のみで、実環境のキュー処理・取消・転送・共有ボリュームでの 排他動作は未確認。 ## Counts の件数と性能 通常の Counts 応答では、使用していない全履歴の population_count は計算しない。 キーは null、population_count_computed は false とし、0に置き換えない。 summary は非公開履歴を含む同じ母集団を集計する。非公開の軸はサーバーの CASE 式で 単一の予約値へまとめてから GROUP BY し、output_count / visible_output_count はその 安全化した通常行数を表す。excluded_count は返さない。details は従来どおり output_count が権限適用前、visible_output_count が適用後の明細行数である。 小計・合計の物理行は行数に含めない。 CSV の物理行と JSON の pivot_cell には集計レコードを含むため、両者を混同しない。 除票境界・参照禁止集合は GROUP BY で識別キー単位にまとめる。 投稿の参照判定は、全除票履歴を JSON_CONTAINS / FIND_IN_SET で反復照合せず、 保存済みスナップショットから参照IDを JSON_TABLE で展開して照合する。 権限条件付きの母集団件数は同一 export インスタンス内で再利用する。 多段小計は、全軸の JOIN へ実際に寄与するレコードを CTE に保持し、各枝で同じ 母集合を使う。changed_field には直前版がある実変更だけが寄与する。タグが行軸に ない場合は DISTINCT 投稿版を数える。タグが行軸にある場合は投稿版×タグを数え、 全行軸の葉単位から小計・合計を求める。変更項目も行軸ならその寄与も保持する。 private のタグ・変更項目をまとめても、内部の寄与は捨てず、ラベルだけを保護する。 スキーマ上、以下の追加インデックスが候補となる。migration は作成・実行していない。 - post_versions: (event_type, post_id, version_no) - tag_versions / nico_tag_versions: (event_type, tag_id, version_no) - tag_description_versions: (event_type, tag_id, language_code, version_no) - tag_group_versions: (event_type, tag_group_id, version_no) - material_versions: (event_type, material_id, version_no) - 各履歴テーブル: (event_type, id)。ファイル認可の policy revision 用 MAX(id) の候補。 - post_versions 等で未整備の (created_at)。履歴の異動期間を絞る場合の候補。 - post_versions: (post_id, created_at, version_no)。日時指定の最新版探索の候補。 追加は許可後の新規 add_index migration として行い、既存 migration は変更しない。 実DBでのインデックス容量・作成時ロック・実行計画・時間は未確認。 索引追加なしでの最大データ量に対する応答時間は保証しない。 ## GET 応答開始の確認(静的実装・未実測) 進捗画面は ready 後に owner/job に紐づく download_attempt を登録し、同じ attempt を native iframe の実 GET に渡す。サーバーは署名・所有者・権限 revision・期限・ファイルを 再検証し、64KiB の最初のチャンクを Rack 出力側へ渡した後に started を記録する。 HEAD や受付 POST では started にしない。確認には30秒の独立した期限がある。 サーバーの出力開始はブラウザー受信・保存完了を保証しない。途中の切断もあり得る。 開始確認前の失敗はダイアログに残る。再実行はダイアログを閉じてエクスポートをやり直す。 Web と worker は同じ COUNTS_EXPORT_DIRECTORY、Rails secret、DB、実行ユーザーで 共有 POSIX filesystem を使うこと。別ホストではローカル tmp を共有できない。 systemd の EnvironmentFile と Web の環境を照合し、directory の owner と0700相当の アクセス制限、容量、期限切れ削除、worker heartbeat と restart を公開前に確認する。 この作業では systemctl・worker 起動・本番環境への適用を行っていない。 これらのコード対策と回帰テストを追加しているが、テスト・型チェック・build は未実行。 middleware/proxy 経由の転送は別途実環境で確認する。 ## 開始確認の競合と権限 開始受付は pending、実 GET の単独取得は streaming、最初の Rack yield 後は started。 同じ job の重複受付は既存 attempt を返す。streaming への遷移はプロセス間ロックで 単独取得し、同一 attempt の二重 GET を拒否する。開始前の attempt は45秒で失効する。 Frontend は30秒の独立タイマーで確認を打ち切る。一時的なポーリング通信断は同じ attempt で再取得するが、別の GET は始めない。取消失敗後の再試行は利用者の再操作で行う。 ファイル・ジョブは owner、署名、期限、現在の権限 revision 及び artifact_policy を 検証する。最初の送出前にも現ユーザー・BAN・ファイル権限を再確認する。旧ポリシーの 生成物は再利用しない。送出中は各チャンクで取消状態を確認し、例外時はログと failed 状態を記録して body を閉じる。コミット後に head で別レスポンスを書かない。 ResponseBody に to_path を提供せず、sendfile middleware による ACK の迂回を避ける。 Last-Modified と X-Accel-Buffering: no を設定するが、プロキシの動作まで保証しない。 サーバーの最初の Rack yield は、クライアントの受信開始の厳密な証明ではない。 非管理者の summary では、公開集計属性は操作日時・異動区分。その他の属性に対する 抽出条件・補助タグ検索は拒否する。measures / column_order はマスク後の式だけと 比較するため、推測した private ID は一致しない。予約値そのものの条件指定は拒否する。 private ID の正誤に応じて許可可否を変えないため、条件の一致探索を避けられる。 非公開列を含む列順変更は API の column_values_editable でも不可と通知する。 公開列の値を使うソートは引き続き利用できる。 明細には元の認可を適用し、抽出条件自体や取得済みの全言語値を表示設定で改変しない。 外部タグ履歴の linked_tags は名前しか記録していないため、非公開タグの記録名を含む スナップショットを保守的に非公開とする。名前全体を空白区切りの境界で照合する。 区切りが曖昧な旧データでは過剰な秘匿の可能性はあるが、参照 ID がない旧形式から 名前の公開可否を楽観的に推定しない。