copy_tuner_client 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fb9ea1caf782db8dac7ded7d77803a61586b5369b221b62d34e9e0290c58b06
4
- data.tar.gz: ad4723a7fe5af378f68af6f786644cc549a1ebbe2523c3424af8fdaba5381433
3
+ metadata.gz: 3b141908e7fa8f6702cf9fccc712b88440af9aa96cc25c8b7875b3269d199825
4
+ data.tar.gz: c7b3f527ba59c387f721364fe0e2689fd849b99b33f4e53557611f23af828794
5
5
  SHA512:
6
- metadata.gz: 7a894e4afd8428064c4fef7dd34db7bdfaab23879b5fa3aaabc5921437a3a80b7cf036fac920f8bf76e7274141f5a522f8364a493797084e7fee879f435d7eb4
7
- data.tar.gz: 34211866166e6491e7488a8f87aadc96810afcea2f3b5069ae2b29b4b9e5f0fe9a620556e3fd55d403ed419c9ab917c34f4fee0eafea90e889a7b6d5653b0540
6
+ metadata.gz: cae2e31ecad418c08b1143f57bd5acf83b574ae5ca4aac1526e594ce911e7d37c98b7098a5a4d1bcaa4cacc133368764712525d80ef63b7dac4390bed5858093
7
+ data.tar.gz: 06e3b11bb99dc6162a0a1110999e1fe2ab4bc3dde42cecfa8ecbe0656a0861b7ea17f659c0ef1791f4df1601cd10e7b465eac755d2ae5305d22e78e0ffe260c4
data/CHANGELOG.md CHANGED
@@ -1,235 +1,2 @@
1
- ## Unreleased
2
-
3
- - **【後方互換性に影響】** `config.exclude_key_regexp` を削除しました。後継の `config.local_first_key_regexp`
4
- を使ってください。両者は対象キーの形式が異なります(`exclude_key_regexp` は locale 付き `ja.views.foo`、
5
- `local_first_key_regexp` は locale を除いた `views.foo`)。正規表現から locale プレフィックスを外して
6
- 移行してください。挙動も異なり、`local_first_key_regexp` は lookup 時に CopyTuner キャッシュをスキップして
7
- ローカル YAML を優先します(完全分離)。
8
- - **【後方互換性に影響】** `config.project_id` を必須にしました。未設定のまま `configure`(`apply`)すると
9
- `ArgumentError: project_id is required` で失敗します。これまで `project_id` 未設定時は `api_key` へ
10
- フォールバックして deprecation 警告を出していましたが、このフォールバックは削除しました。initializer に
11
- `config.project_id = <プロジェクト ID>` を設定してください。
12
- - Copyray オーバーレイのマーカー方式を刷新。訳文への HTML コメント `<!--COPYRAY key-->` 注入をやめ、
13
- 可視トークン `⟦CT:key⟧` を埋め込んだうえで `CopyrayMiddleware` が `data-copyray-key` 属性に変換し、
14
- トークンを HTML から完全に除去するようになりました。最終配信 HTML にコメント・トークンは残りません。
15
- - **【後方互換性に影響】** `config.html_escape` 設定を削除しました。HTML の安全性判定は i18n 標準
16
- (`.html` / `_html` で終わるキーのみ html_safe)に統一され、この設定は参照されなくなっていました(no-op)。
17
- no-op だったため動作には影響しませんが、`config.html_escape = ...` を設定している initializer は
18
- `NoMethodError` になるため、その行を削除してください。`html_escape = false`(全訳文を html_safe 扱いに
19
- する旧互換挙動)に依存していたアプリは、`.html` / `_html` キー命名へ移行してください。
20
- - Copyray オーバーレイは平文・html_safe(`.html` / `_html` キー)どちらの訳文もハイライト対象です。マーカートークンの
21
- 区切り記号は HTML 特殊文字ではないため、平文訳文が ActionView でエスケープされてもトークンは無傷で残り、
22
- `data-copyray-key` 属性へ正しく変換されます。`<head>` 内(title/meta)はトークンを除去するのみでオーバーレイ
23
- 非対象ですが、従来どおりリスト導線(CopyTuner バー)から編集できます。
24
- - **【後方互換性に影響】** `tt` ヘルパーを削除しました。マーカー方式の刷新で `t`(`translate`)が安全にマーカー
25
- 注入できるようになり、`tt` の存在理由は失われました。`tt` の呼び出しは `t` へ置き換えてください。`tt` を使い続けたい
26
- アプリは、ビューヘルパーで `t` に委譲するだけの `tt` を自前で定義してください(gem 撤去後は `t` と実質同義)。
27
-
28
- ## 0.16.1
29
-
30
- - Support for i18n@1.13.0
31
- - キーの相対パス指定とdefaultオプションを組み合わせた場合の不具合修正
32
-
33
- ## 0.16.0
34
-
35
- - Railsエンジン内のviewではオリジナルのtヘルパが呼ばれるように修正
36
-
37
- ## 0.15.1
38
-
39
- - tヘルパーにdefault引数が渡された場合に初期値として登録されない問題を修正
40
-
41
- ## 0.15.0
42
-
43
- - Drop support for ruby 2.7
44
-
45
- ## 0.14.1
46
-
47
- - Fix super call in define_method
48
-
49
- ## 0.14.0
50
-
51
- - Add Support for good_job
52
- - Drop Support for Resque
53
-
54
- ## 0.13.5
55
-
56
- - Rename assets
57
-
58
- ## 0.13.4
59
-
60
- - Fix csp nonce
61
-
62
- ## 0.13.3
63
-
64
- - Add `media="all"` attribute to stylesheet link tag
65
-
66
- ## 0.13.2
67
-
68
- - Add `crossorigin="anonymous"` attribute to script tag
69
-
70
- ## 0.13.1
71
-
72
- - Add `type="module"` attribute to script tag
73
-
74
- ## 0.13.0
75
-
76
- - Drop support for ruby 2.6
77
-
78
- ## 0.12.0
79
-
80
- - Add `config.ignored_keys` and `config.ignored_key_handler`
81
-
82
- ## 0.11.0
83
-
84
- - Remove deprecated rescue_format option
85
- - Fix ruby@2.7 keyword warning
86
-
87
- ## 0.10.0
88
-
89
- - Add copy_tuner:detect_html_incompatible_keys task
90
-
91
- ## 0.9.0
92
-
93
- - Do not upload invalid type keys
94
-
95
- ## 0.8.1
96
-
97
- - Fix bug in `CopyrayMiddleware`
98
-
99
- ## 0.8.0
100
-
101
- - Change the default value of config.upload_disabled_environments
102
-
103
- ## 0.7.0
104
-
105
- - Add config.upload_disabled_environments
106
-
107
- ## 0.6.2
108
-
109
- - Add arguments to export task
110
-
111
- ## 0.6.1
112
-
113
- - Fix ruby@2.7 keyword warning
114
-
115
- ## 0.6.0
116
-
117
- - Drop support for ruby 2.4
118
- - Drop support for rails 5.1
119
-
120
- ## 0.5.2
121
-
122
- - Do not upload invalid keys
123
-
124
- ## 0.5.1
125
-
126
- - Do not upload downloaded keys
127
-
128
- ## 0.5.0
129
-
130
- - Drop support for ruby 2.3
131
- - Add tt helper
132
- - Add copy_tuner:detect_conflict_keys task
133
- - Do not re-upload empty keys
134
- - Fix dual loading tasks
135
- - Remove config.copyray_js_injection_regexp_for_debug
136
- - Remove config.copyray_js_injection_regexp_for_precompiled
137
- - Download translation when initialization
138
-
139
- ## 0.4.11
140
-
141
- - changes
142
- - Fix hide toggle button on mobile device.
143
-
144
- ## 0.4.10
145
-
146
- - changes
147
- - Hide copyray bar on all media.
148
-
149
- ## 0.4.9
150
-
151
- - changes
152
- - Smaller toggle button.
153
- - Hide toggle button on mobile device.
154
-
155
- ## 0.4.8
156
-
157
- - changes
158
- - Support passenger 5.3.x
159
-
160
- ## 0.4.7
161
-
162
- - changes
163
- - Compatibile with bullet gem (rewrap response with ActionDispatch::Response::RackBody)
164
-
165
- ## 0.4.6
166
-
167
- - changes
168
- - Performance imporovement (sync with server asynchronously)
169
- - Add config.middleware_position
170
-
171
- ## 0.4.5
172
-
173
- - changes
174
- - Fix deprecated css.
175
-
176
- ## 0.4.4
177
-
178
- - bug fix
179
- - Don't upload resolved default values.
180
-
181
- ## 0.4.3
182
-
183
- - bug fix
184
- - Start poller thread regardless of puma mode. #39
185
-
186
- ## 0.4.2
187
-
188
- - changes
189
- - span tag is no longer added to translation text.
190
-
191
- ## 0.4.1
192
-
193
- - bug fixes
194
-
195
- - js injection failed if jquery is not used. #33
196
- - Fix some js error. #34
197
- - Wrong key is displayed if scoped option is used. #35
198
-
199
- - deprecation
200
- - config.copyray_js_injection_regexp_for_debug is no longer needed.
201
- - config.copyray_js_injection_regexp_for_precompiled is no longer needed.
202
-
203
- ## 0.4.0
204
-
205
- - Remove jQuery dependency.
206
-
207
- ## 0.3.5
208
-
209
- - Support Rails 5.1
210
-
211
- ## 0.3.4
212
-
213
- - Use Logger to /dev/null as default when rails console
214
-
215
- ## 0.3.3
216
-
217
- - Add config.locales. (#24)
218
- - Fix initialization order bug. (#25)
219
-
220
- ## 0.3.2
221
-
222
- - Support I18n.t :scope option.
223
- - Update copyray_js_injection_regexp_for_debug.
224
-
225
- ## 0.3.1
226
-
227
- - Add search box to copyray bar.
228
- - Add disable_copyray_comment_injection to configuration.
229
-
230
- ## 0.3.0
231
-
232
- - Use https as default.
233
- - Download blurbs from S3.
234
- - Add toolbar.
235
- - "Translations in this page" menu.
1
+ > [!NOTE]
2
+ > 変更履歴は [GitHub Releases](https://github.com/SonicGarden/copy-tuner-ruby-client/releases) に移行済みです。
data/CLAUDE.md CHANGED
@@ -34,6 +34,16 @@ Rails 統合は engine.rb のイニシャライザ経由(ヘルパー/SimpleFo
34
34
  (vite.config.ts が `src/main.ts` → `app/assets/javascripts/copytuner.js` を出力)。
35
35
  - `local_first_key_regexp` — locale を除いたキー対象・lookup 時に作用(ローカル YAML 優先)。
36
36
  local_first キーのアップロード抑止は `Cache#[]=` に集約されている。
37
+ - **poller スレッドの fork 対応は `ForkHook`(`Process._fork` に prepend)に集約する**
38
+ (fork の直前に `Poller#stop` で協調的に停止し、fork 後に親子の両方で張り直す。理由: fork 後の子に残る
39
+ Thread オブジェクトの見え方(`alive?` / `join` の結果)はドキュメント化されていない CRuby の実装依存なので、
40
+ pid を記録して差分を見るような後始末には寄せない。子でも張り直すので、Puma の `fork_worker` のように
41
+ worker が worker を fork する構成でもサーバ固有のフックなしで poller が立つ。
42
+ アプリケーションサーバごとのフック(`ProcessGuard#register_*_hook`)を増やす前にここで足りるか確認する)。
43
+ 起動方法・モードごとにどのプロセスで poller が起動するかは `docs/poller-startup.md` に実測結果がある。
44
+ - **`Poller#poll` は例外をスレッドの外へ漏らさない**
45
+ (`Poller#stop` は fork 経路から呼ばれ、`Thread#join` はスレッドの例外を再送出するため、漏らすと
46
+ poller の失敗がアプリ側の `fork` を壊す)。
37
47
  - **アップロード抑止の新ルールは `Cache#[]=` に足す。`I18nBackend` の書き込み経路(`lookup` / `default` / `store_item`)ごとに個別ガードを足さない**
38
48
  (理由: cache への書き込みは全経路が最終的に `Cache#[]=` を通る単一の関門。経路ごとにガードを足すと付け忘れの穴が生まれ、同じチェックが分散して保守負担になる。実際 local_first の抑止は当初 `default` 個別に足したが穴が残り、`Cache#[]=` への集約に作り直した)。
39
49
 
data/README.md CHANGED
@@ -62,20 +62,57 @@ CopyTuner で一元管理している翻訳を、`views.*` のような単位で
62
62
 
63
63
  アプリ独自の `number.*` キー(例 `number.gift_amount`)は対象外で、従来どおり CopyTuner で管理できます。
64
64
 
65
- ## Claude Code スキル
65
+ ## Middleware の挿入位置
66
66
 
67
- `skills/copy-tuner/` Claude Code 向けのスキルが含まれています。
67
+ CopyTuner は開発環境で `RequestSync` / `CopyrayMiddleware` Rack の middleware スタックに挿入します。`RequestSync` はリクエスト毎に CopyTuner サーバと同期し、`CopyrayMiddleware` はページ内のマーカー(`⟦CT:key⟧`)を除去・変換します。
68
68
 
69
- ### copy-tuner スキル
69
+ 挿入位置は自動で決まるため、通常は設定不要です。Devise(Warden)を使っているアプリでは `Warden::Manager` の直前、それ以外の環境ではスタック末尾に挿入されます。
70
70
 
71
- i18n キーの操作を支援するスキルです。翻訳キーの検索・登録・確認などの依頼に自動的に使用されます。
71
+ Devise 併用時に Warden の直前へ寄せるのは `throw :warden` の挙動に対応するためです。`throw :warden` は `Warden::Manager` の `catch(:warden)` までスタックを巻き戻すため、CopyTuner の middleware が Warden より内側にあると、認証エラー時のレスポンス(`Devise::FailureApp` が返す HTML)を受け取れず、Copyray のマーカーがページに残ってしまいます。
72
+
73
+ 位置を変えたい場合は `config.middleware_position` に `{ before: SomeMiddleware }` または `{ after: SomeMiddleware }` を指定すると、このデフォルトを上書きできます。
74
+
75
+ ```ruby
76
+ CopyTunerClient.configure do |config|
77
+ # ...
78
+ config.middleware_position = { after: Rack::Runtime }
79
+ end
80
+ ```
81
+
82
+ 指定した middleware がスタックに存在しない場合、Rails の起動時に例外(`No such middleware to insert before: ...` / `... insert after: ...`)が発生します。
83
+
84
+ ## Claude Code スキル
85
+
86
+ `skills/` 以下に Claude Code 向けのスキルが含まれています。
72
87
 
73
88
  ```
74
- gh skill install SonicGarden/copy-tuner-ruby-client copy-tuner --scope project
89
+ gh skill install SonicGarden/copy-tuner-ruby-client <スキル名> --scope project
75
90
  ```
76
91
 
92
+ ### copy-tuner スキル
93
+
94
+ i18n キーの操作を支援するスキルです。翻訳キーの検索・登録・確認などの依頼に自動的に使用されます。
95
+
77
96
  詳細: [skills/copy-tuner/SKILL.md](skills/copy-tuner/SKILL.md)
78
97
 
98
+ ### copy-tuner-to-locales-migrate-prefix スキル
99
+
100
+ copy_tuner が集中管理する i18n キーを、prefix(正規表現)単位で `config/locales` のローカル YAML 管理へ移行するスキルです。gem は残したまま特定 prefix だけをローカル化する「部分ローカル化」と、全 prefix を移して完全撤去する「全移行」の両方に使えます。明示的に呼び出したときのみ動作します。
101
+
102
+ 詳細: [skills/copy-tuner-to-locales-migrate-prefix/SKILL.md](skills/copy-tuner-to-locales-migrate-prefix/SKILL.md)
103
+
104
+ ### copy-tuner-to-locales-cleanup スキル
105
+
106
+ `copy-tuner-to-locales-migrate-prefix` で全 prefix の移行が完了した後に、gem・初期化子・CI・deploy・ドキュメント・MCP 設定を一括撤去し、copy_tuner 依存を完全に取り除くスキルです。明示的に呼び出したときのみ動作します。
107
+
108
+ 詳細: [skills/copy-tuner-to-locales-cleanup/SKILL.md](skills/copy-tuner-to-locales-cleanup/SKILL.md)
109
+
110
+ ### copy-tuner-to-t-migrate スキル
111
+
112
+ copy_tuner_client v2.0.0 で削除された独自ヘルパー `tt` の呼び出しを、Rails 標準の `t`(`translate`)へ置換するスキルです。機械的に安全な箇所は一括変換し、文字列加工や `label` の第一引数に渡している箇所は 1 件ずつ確認しながら置換します。破壊的な一括書き換えを含むため、明示的に呼び出したときのみ動作します。
113
+
114
+ 詳細: [skills/copy-tuner-to-t-migrate/SKILL.md](skills/copy-tuner-to-t-migrate/SKILL.md)
115
+
79
116
  Development
80
117
  =================
81
118
 
@@ -0,0 +1,108 @@
1
+ # poller がどのプロセスで起動するか
2
+
3
+ `Poller` はバックグラウンドスレッドで CopyTuner サーバと同期する。**リクエストを処理するプロセスで
4
+ このスレッドが動いていないと翻訳が更新されない**が、動いていなくてもエラーにはならず、翻訳が古いまま
5
+ になるだけなので気づきにくい。アプリケーションサーバの起動方法とモードによって「どのプロセスがアプリを
6
+ ロードするか」が変わるため、ここに実測結果を残す。
7
+
8
+ Puma 以外(Unicorn / Passenger / delayed_job / good_job)については `ProcessGuard` のフック登録
9
+ メソッドを参照。
10
+
11
+ ## 前提: アプリをロードしたプロセスで initializer が走る
12
+
13
+ `CopyTunerClient.configure`(= `Configuration#apply` = `ProcessGuard#start`)は、Rails の
14
+ initializer として **アプリをロードしたプロセスで 1 回だけ**走る。したがって「どのプロセスがアプリを
15
+ ロードするか」が起点になる。
16
+
17
+ ## 実測結果
18
+
19
+ 検証環境: puma 8.0.2 / Rails 8.1.3.1 / Ruby 4.0.2(2026-09-03 実測)
20
+
21
+ | 起動方法 | モード | アプリをロードするプロセス | poller の起動経路 | master に poller |
22
+ | --- | --- | --- | --- | --- |
23
+ | `rails server` | single | そのプロセス | 非 spawner 経路(`start_polling`) | (単一プロセス) |
24
+ | `rails server` | cluster(preload の有無を問わず) | **master のみ** | master で `start_polling` → `ForkHook` が fork 後に worker で張り直す | 立つ |
25
+ | `puma -C` | single | そのプロセス(`Runner#load_and_bind`) | `Puma::Runner#start_server` への prepend | (単一プロセス) |
26
+ | `puma -C` | cluster + preload | master のみ | 各 worker の `start_server` で prepend したフックが発火 | 立たない |
27
+ | `puma -C` | cluster + preload なし | **各 worker** | `$0` の `'cluster worker'` 判定による分岐 | 立たない(master はアプリをロードしない) |
28
+
29
+ ### 判定に使っている値
30
+
31
+ `ProcessGuard#puma_spawner?` は `defined?(Puma::Runner) && $PROGRAM_NAME.include?('puma')` で判定する。
32
+ 実測値は次のとおり。
33
+
34
+ | 起動方法 | `$PROGRAM_NAME` | `defined?(Puma)` | `defined?(Puma::Runner)` | `puma_spawner?` |
35
+ | --- | --- | --- | --- | --- |
36
+ | `rails server` | `"bin/rails"` | yes | **no** | false |
37
+ | `puma -C`(master / single) | `".../bin/puma"` | yes | yes | true |
38
+ | `puma -C`(preload なしの worker) | `"puma: cluster worker 0: <master pid> [app]"` | yes | yes | true |
39
+
40
+ `$PROGRAM_NAME` が master と worker で違うのは Puma 側の実装差による。master は
41
+ `launcher.rb` の `Process.setproctitle`(`$0` を変えない)、worker は `cluster/worker.rb` の
42
+ `$0 = title`(変える)を使っている。
43
+
44
+ ## 各行の背景
45
+
46
+ ### `rails server` は preload 設定に関係なく master でアプリをロードする
47
+
48
+ Rails は Puma を起動する**前に** Rack アプリを組み立てて(`Rails::Server#log_to_stdout` →
49
+ `wrapped_app`)、オブジェクトとして Puma に渡す。そのため `preload_app!` を明示的に切っても worker が
50
+ アプリをロードし直すことはなく、必ず master でロードされる。
51
+
52
+ この構成では `Puma::Runner` が initializer の時点でまだ未定義なので `puma_spawner?` は false になり、
53
+ master で poller が起動する。fork 後の worker には `ForkHook`(`Process._fork` に prepend)が
54
+ 引き継ぐ。master にも poller が 1 本残るが、`puma_spawner?` を無理に真にしようとすると起動方法ごとの
55
+ 判定を増やすことになるため、余分な 1 本を許容している。
56
+
57
+ ### Puma 8 は `workers > 1` のとき preload が既定で有効
58
+
59
+ `Puma::Configuration#set_conditional_default_options` が `preload_app` の既定値を
60
+ `!prune_bundler && workers > 1 && Puma.forkable?` で決めている。非 preload を試すには
61
+ `preload_app!(false)` を明示する必要がある。
62
+
63
+ ### 非 preload では `start_server` の中でアプリがロードされる
64
+
65
+ `Runner#start_server` は `Puma::Server.new(app, ...)` を呼び、`Runner#app` が
66
+ `@app ||= @config.app` で遅延ロードする。つまり非 preload の worker では、initializer が走る時点で
67
+ 既に `start_server` が実行中であり、**そこで `Puma::Runner` に prepend しても間に合わない**。
68
+
69
+ このため `$0` の `'cluster worker'` 判定は最適化ではなく、この構成で poller を起動する唯一の経路になっている。
70
+
71
+ ## 表のとおりにならない例外
72
+
73
+ 上の表は「poller のスレッドが起動する経路」であって、起動した poller が動き続けることまでは
74
+ 保証しない。`Poller` はスレッドの生死とは別にライフサイクルの意図を持っており、次の場合は
75
+ 表の経路を通っても poller が居なくなる。
76
+
77
+ - **`InvalidApiKey`** — API キーが不正だと `poll` が自ら終了し、以後は fork をまたいでも張り直さない
78
+ (張り直しても同じ理由で死ぬだけなので)。ログに `Invalid API key` が出る
79
+ - **起動時に CopyTuner サーバへ到達できない** — `Configuration#apply` の `cache.download` が
80
+ `ConnectionError` を再送出するため、そもそもプロセスが起動に失敗する。Puma の worker では
81
+ `! Unable to start worker` になる
82
+
83
+ 再検証の際は、まずこの 2 つに当たっていないかをログで確かめる。
84
+
85
+ ## 再検証のしかた
86
+
87
+ Puma や Rails を上げたときにこの表が変わっていないか確かめるには、次の観測点を見るのが早い。
88
+
89
+ 1. 最小の Rails アプリを用意し、`config/initializers` で `Process.pid` / `Process.ppid` /
90
+ `$PROGRAM_NAME` / `defined?(Puma::Runner)` を出力する。これで**どのプロセスがアプリをロードしたか**が分かる
91
+ 2. 任意のエンドポイントで `CopyTunerClient.poller` の `@thread` を覗き、`alive?` を返す。
92
+ fork 後の worker が親から継承した dead な Thread を持っていると、ここが `false` になる
93
+ 3. 翻訳を返すだけの偽 CopyTuner サーバを立て(`draft_blurbs.json` を返し、`draft_blurbs` /
94
+ `deploys` の POST を受ける)、`polling_delay` を数秒にする。起動後にサーバ側の翻訳を書き換え、
95
+ worker が拾うかどうかで実際の同期を確認する
96
+ 4. 上の表の 5 構成(`rails server` / `puma -C` × single / cluster+preload / cluster+非 preload)で回す
97
+
98
+ `ProcessGuard` のログ(`Register Puma fork hook` / `Puma would be clustered mode without preload_app` /
99
+ `start poller thread`)をプロセス ID 付きで集計すると、どの経路を通ったかが分かる。
100
+
101
+ ## 関連
102
+
103
+ - `CopyTunerClient::ForkHook` — fork をまたいで poller を引き継ぐ。判定が外れて master で poller が
104
+ 起動してしまった場合の安全網でもある
105
+ - `CopyTunerClient::Poller` — スレッドの生死とは別に `@running`(ポーリングを継続する意図)と
106
+ `@aborted`(張り直しても同じ理由で死ぬ終わり方をしたか)を持つ。`ForkHook` が fork 後に張り直すか
107
+ どうかは `Poller#stop` の戻り値、すなわちこの意図で決まる(スレッドが生きているかではない)
108
+ - [Ruby における fork と Thread の挙動の調査](https://gist.github.com/shunichi/c236b6a85a46a16c60262047ca299608)
@@ -184,6 +184,7 @@ module CopyTunerClient
184
184
  self.local_first_key_regexp = nil
185
185
  self.project_id = nil
186
186
  self.download_cache_dir = Pathname.new(Dir.pwd).join('tmp', 'cache', 'copy_tuner_client')
187
+ self.middleware_position = default_middleware_position
187
188
 
188
189
  @applied = false
189
190
  end
@@ -357,6 +358,20 @@ module CopyTunerClient
357
358
  end
358
359
  end
359
360
 
361
+ # throw :warden は Warden::Manager の catch(:warden) までスタックを巻き戻すため、
362
+ # Warden より内側の middleware は @app.call の戻り値を受け取れず、Devise::FailureApp の
363
+ # 応答がマーカー除去を経ずにブラウザへ届いてしまう。これを避けるため Warden の直前を既定位置にする。
364
+ #
365
+ # 判定に Devise の有無も見るのは、warden を require するだけで Warden::Manager を
366
+ # スタックに積まない gem(authtrail 等)が存在するため。定数の有無だけで決めると
367
+ # そうしたアプリで insert_before が対象を見つけられず起動時例外になる。
368
+ # スタックへ積むのは Devise の railtie(config.app_middleware.use)である。
369
+ def default_middleware_position
370
+ return nil unless defined?(::Warden::Manager) && defined?(::Devise)
371
+
372
+ { before: ::Warden::Manager }
373
+ end
374
+
360
375
  def setup_middleware
361
376
  if enable_middleware?
362
377
  logger.info 'Using copytuner sync middleware'
@@ -372,23 +387,33 @@ module CopyTunerClient
372
387
  logger.info "Available locales: #{locales.join(' ')}"
373
388
  end
374
389
 
375
- def insert_middleware # rubocop:disable Metrics/AbcSize
376
- request_sync_options = {
390
+ def insert_middleware
391
+ # NOTE: 値の nil を除外するのは、{ before: SomeClass if cond } のように条件次第で nil が入る
392
+ # 書き方で従来は末尾 use にフォールバックしていた挙動を保つため(キーの有無だけで分岐すると
393
+ # insert_before(nil) が対象を見つけられず例外になる)。
394
+ case middleware_position
395
+ in { before: target } if target
396
+ middleware.insert_before(target, RequestSync, request_sync_options)
397
+ middleware.insert_before(target, CopyTunerClient::CopyrayMiddleware)
398
+ in { after: target } if target
399
+ # NOTE: insert_after(index, *) は insert(index + 1, *) を呼ぶため、同じ対象へ 2 回挿入すると
400
+ # 後から挿入した方が対象に近い位置に来て順序が反転する。逆順で呼ぶことで
401
+ # 外側→内側が RequestSync → CopyrayMiddleware に揃う。
402
+ middleware.insert_after(target, CopyTunerClient::CopyrayMiddleware)
403
+ middleware.insert_after(target, RequestSync, request_sync_options)
404
+ else
405
+ middleware.use(RequestSync, request_sync_options)
406
+ middleware.use(CopyTunerClient::CopyrayMiddleware)
407
+ end
408
+ end
409
+
410
+ def request_sync_options
411
+ {
377
412
  poller: @poller,
378
413
  cache:,
379
414
  interval: sync_interval,
380
415
  ignore_regex: sync_ignore_path_regex,
381
416
  }
382
- if middleware_position.is_a?(Hash) && middleware_position[:before]
383
- middleware.insert_before(middleware_position[:before], RequestSync, request_sync_options)
384
- middleware.insert_before(middleware_position[:before], CopyTunerClient::CopyrayMiddleware)
385
- elsif middleware_position.is_a?(Hash) && middleware_position[:after]
386
- middleware.insert_after(middleware_position[:after], RequestSync, request_sync_options)
387
- middleware.insert_after(middleware_position[:after], CopyTunerClient::CopyrayMiddleware)
388
- else
389
- middleware.use(RequestSync, request_sync_options)
390
- middleware.use(CopyTunerClient::CopyrayMiddleware)
391
- end
392
417
  end
393
418
 
394
419
  # project_id は必須。未設定なら明示的に失敗させる。
@@ -0,0 +1,53 @@
1
+ module CopyTunerClient
2
+ # fork をまたいで poller スレッドを引き継ぐためのフック。
3
+ # Process._fork に prepend し、fork の直前に poller を協調的に停止して、
4
+ # fork のあと親子それぞれで張り直す。
5
+ module ForkHook
6
+ # Module#prepend は同じモジュールを二重に挿さないので、呼び出し側で登録済みかを見なくてよい
7
+ def self.install
8
+ ::Process.singleton_class.prepend(self)
9
+ end
10
+
11
+ def _fork
12
+ # fork はアプリの任意のタイミングで起きる。Process._fork への prepend は外せないので、
13
+ # configure 前や configuration 差し替え中に NoMethodError でアプリの fork を
14
+ # 壊さないよう nil を許容する
15
+ poller = CopyTunerClient.configuration&.poller
16
+ # スレッドは子プロセスに引き継がれない。fork 後に子へ残る Thread オブジェクトの見え方
17
+ # (alive? / join の結果)はドキュメント化されていない CRuby の実装依存なので、
18
+ # そこに依存した後始末はせず、fork 前に協調的に停止しておく。
19
+ # ここは rescue しない。停止の失敗を握り潰すと、動いている poller ごと fork することになり
20
+ # このフックの目的そのものを裏切る
21
+ restart = poller ? poller.stop : false
22
+
23
+ begin
24
+ super
25
+ ensure
26
+ # ensure は親(super の戻り値 = 子の pid)と子(同 0)の両方を通り、super が失敗した
27
+ # ときも親で復元する。「fork 前に動いていたプロセスは fork 後も動いている」状態を保つ。
28
+ # 子でも張り直すのは、Puma の fork_worker のように worker が worker を fork する
29
+ # 構成でもサーバ固有のフックに頼らず poller を立てるため
30
+ ForkHook.restart_poller(poller) if restart
31
+ end
32
+ end
33
+
34
+ # Process.singleton_class に prepend されるため、インスタンスメソッドとして定義すると
35
+ # Process 自身にメソッドが生えてしまう。フックの補助はモジュール側の特異メソッドに置く
36
+ #
37
+ # fork 後に例外を漏らすと「子プロセスは生成済みなのに親の fork が例外を投げる」状態になり、
38
+ # 呼び出し側が子の pid を受け取れなくなる。起動の失敗はログに落として fork は成立させる
39
+ def self.restart_poller(poller)
40
+ poller.start
41
+ rescue StandardError => e
42
+ log_error("CopyTuner: fork 後の poller 起動に失敗しました: #{e.class}: #{e.message}")
43
+ end
44
+
45
+ # ログ出力自体が失敗しても fork は成立させる。ここで例外を漏らすと、失敗を握るために
46
+ # 置いた rescue が逆に fork を壊す
47
+ def self.log_error(message)
48
+ CopyTunerClient.configuration&.logger&.error(message)
49
+ rescue StandardError
50
+ nil
51
+ end
52
+ end
53
+ end