このコミットが含まれているのは:
2026-10-28 19:28:11 +09:00
コミット 8ea3d211dc
49個のファイルの変更、967行の追加、356行の削除
+59 -5
ファイルの表示
@@ -8,8 +8,18 @@ Active Job の既定のプロセス内 async アダプターは使用しない
backend で `bundle exec rake counts:export_worker` を、systemd 等の監視対象として
別プロセスで起動する。Web プロセス内では起動しない。
ワーカー未起動の間は待機状態となる。1 プロセスは同時に1ジョブを処理し、
全体の実行枠は既存の2枠を共有する。ワーカーの起動・配備は実施していない。
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 コネクションはワーカープロセスごとに必要。
@@ -30,13 +40,23 @@ Web とワーカーは同一リリース、Rails secret、アプリケーショ
ジョブ・ファイルのトークンは作成元のセッションに紐付く。
既存の 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間隔の生成チェックポイントで取消を検知する。
DB 問い合わせ・ファイル I/O がブロックしている間は即時に中断できない。
監視側も取消を確認し、子へ TERM、5秒後も終了していなければ KILL を送る。
子の回収後に一時ファイルを削除する。DB 側の実行停止は切断検知等に依存するため、
取消 API の受理と DB の即時停止は同義ではない。
取消済みへの遷移は生成処理の終了と一時ファイル削除後に行う。
既に始まった HTTP 転送やブラウザ側の保存は取り消せない。
@@ -45,9 +65,43 @@ DB 問い合わせ・ファイル I/O がブロックしている間は即時に
原子的に書き換える状態ファイルとプロセス間ロックにより、二重処理・取消との競合を防ぐ。
既存の同時実行2、1ファイル128MiB、保存領域512MiB、生成120秒、期限10分を維持する。
生成時間制限もチェックポイント方式であり、実行中 SQL の強制停止ではない。
生成120秒は監視側でも強制する。ワーカー未起動なら登録 API は503を返す。
待機中のワーカー消失、15秒以上の heartbeat 停止、150秒以上の待機は失敗とする。
ネットワーク切断時も UI が無期限に処理中にならないよう、状態取得は1回15秒で中断し、
30秒以上状態を取得できなければ失敗表示へ移る。処理件数を捏造しない。
画面からの通信断だけではサーバーの取消完了を保証しない。サーバー側の期限は独立して働く。
期限切れのジョブ・ファイル・残存状態書込ファイルは登録時またはワーカーの巡回時に削除する。
ワーカーと Web の両方を停止した間は巡回削除も停止する。
これらの処理は静的確認のみで、実環境のキュー処理・取消・転送・共有ボリュームでの
排他動作は未確認。
## Counts の件数と性能
通常の Counts 応答では、使用していない全履歴の population_count は計算しない。
キーは null、population_count_computed は false とし、0に置き換えない。
現在条件の output_count は権限適用前、visible_output_count は権限適用後の
通常の集計行数(明細では明細行数)。小計・合計の物理行は含めない。
CSV の物理行と JSON の pivot_cell には集計レコードを含むため、両者を混同しない。
除票境界・参照禁止集合は GROUP BY で識別キー単位にまとめる。
投稿の参照判定は、全除票履歴を JSON_CONTAINS / FIND_IN_SET で反復照合せず、
保存済みスナップショットから参照IDを JSON_TABLE で展開して照合する。
権限条件付きの母集団件数は同一 export インスタンス内で再利用する。
多段小計は、認可済み母集団の CTE を MySQL の無制限 LIMIT で実体化させ、
各枝が同じ認可判定をやり直すことを避ける。計数は COUNT(DISTINCT v.id) のまま。
スキーマ上、以下の追加インデックスが候補となる。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でのインデックス容量・作成時ロック・実行計画・時間は未確認。
索引追加なしでの最大データ量に対する応答時間は保証しない。
+21
ファイルの表示
@@ -0,0 +1,21 @@
[Unit]
Description=BTRC Hub Counts export supervisor
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=miteruzo
WorkingDirectory=/home/miteruzo/projects/btrc-hub/backend
Environment=RAILS_ENV=production
EnvironmentFile=/etc/btrc-hub/counts-exports.env
ExecStart=/usr/bin/env bundle exec rake counts:export_worker
Restart=always
RestartSec=3
KillMode=control-group
TimeoutStopSec=150
UMask=0077
[Install]
WantedBy=multi-user.target