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 +4 -4
- data/CHANGELOG.md +2 -235
- data/CLAUDE.md +10 -0
- data/README.md +42 -5
- data/docs/poller-startup.md +108 -0
- data/lib/copy_tuner_client/configuration.rb +37 -12
- data/lib/copy_tuner_client/fork_hook.rb +53 -0
- data/lib/copy_tuner_client/poller.rb +87 -23
- data/lib/copy_tuner_client/process_guard.rb +13 -4
- data/lib/copy_tuner_client/queue_with_timeout.rb +8 -2
- data/lib/copy_tuner_client/version.rb +1 -1
- data/skills/copy-tuner-to-locales-cleanup/SKILL.md +5 -5
- data/skills/copy-tuner-to-locales-migrate-prefix/SKILL.md +69 -37
- data/skills/copy-tuner-to-locales-migrate-prefix/references/example-touchpoints.md +31 -10
- data/skills/copy-tuner-to-locales-migrate-prefix/scripts/migrate_prefix.rb +144 -6
- data/spec/copy_tuner_client/configuration_spec.rb +173 -0
- data/spec/copy_tuner_client/fork_hook_spec.rb +171 -0
- data/spec/copy_tuner_client/poller_spec.rb +148 -1
- data/spec/copy_tuner_client/process_guard_spec.rb +8 -0
- data/spec/copy_tuner_client/queue_with_timeout_spec.rb +27 -0
- data/spec/copy_tuner_client/warden_integration_spec.rb +95 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/middleware_stack.rb +39 -5
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b141908e7fa8f6702cf9fccc712b88440af9aa96cc25c8b7875b3269d199825
|
|
4
|
+
data.tar.gz: c7b3f527ba59c387f721364fe0e2689fd849b99b33f4e53557611f23af828794
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cae2e31ecad418c08b1143f57bd5acf83b574ae5ca4aac1526e594ce911e7d37c98b7098a5a4d1bcaa4cacc133368764712525d80ef63b7dac4390bed5858093
|
|
7
|
+
data.tar.gz: 06e3b11bb99dc6162a0a1110999e1fe2ab4bc3dde42cecfa8ecbe0656a0861b7ea17f659c0ef1791f4df1601cd10e7b465eac755d2ae5305d22e78e0ffe260c4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,235 +1,2 @@
|
|
|
1
|
-
|
|
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
|
-
##
|
|
65
|
+
## Middleware の挿入位置
|
|
66
66
|
|
|
67
|
-
`
|
|
67
|
+
CopyTuner は開発環境で `RequestSync` / `CopyrayMiddleware` を Rack の middleware スタックに挿入します。`RequestSync` はリクエスト毎に CopyTuner サーバと同期し、`CopyrayMiddleware` はページ内のマーカー(`⟦CT:key⟧`)を除去・変換します。
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
挿入位置は自動で決まるため、通常は設定不要です。Devise(Warden)を使っているアプリでは `Warden::Manager` の直前、それ以外の環境ではスタック末尾に挿入されます。
|
|
70
70
|
|
|
71
|
-
|
|
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
|
|
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
|
|
376
|
-
|
|
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
|