copy_tuner_client 2.2.1 → 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/CLAUDE.md +10 -0
- data/docs/poller-startup.md +108 -0
- 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/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/spec_helper.rb +6 -0
- metadata +5 -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/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
|
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -9,28 +9,60 @@ module CopyTunerClient
|
|
|
9
9
|
# @option options [Logger] :logger where errors should be logged
|
|
10
10
|
# @option options [Fixnum] :polling_delay how long to wait in between requests
|
|
11
11
|
def initialize(cache, options)
|
|
12
|
-
@cache
|
|
13
|
-
@polling_delay
|
|
14
|
-
@logger
|
|
15
|
-
@command_queue
|
|
16
|
-
@mutex
|
|
17
|
-
@thread
|
|
12
|
+
@cache = cache
|
|
13
|
+
@polling_delay = options[:polling_delay]
|
|
14
|
+
@logger = options[:logger]
|
|
15
|
+
@command_queue = CopyTunerClient::QueueWithTimeout.new
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@thread = nil
|
|
18
|
+
@last_synced_at = nil
|
|
19
|
+
# スレッドの生死とは別に、ライフサイクルの意図を持つ。
|
|
20
|
+
# @running: ポーリングを継続する意図があるか(start で true / stop で false)
|
|
21
|
+
# @aborted: 張り直しても同じ理由で死ぬと分かっている終わり方をしたか
|
|
22
|
+
@running = false
|
|
23
|
+
@aborted = false
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def start
|
|
21
27
|
@mutex.synchronize do
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
@running = true
|
|
29
|
+
# 回復の見込みがない理由で終了しているなら張り直さない。fork のたびに同じ例外で
|
|
30
|
+
# 死ぬスレッドを作り直してログを埋めるだけになる
|
|
31
|
+
next if @aborted
|
|
32
|
+
# fork 後の子は親から dead な Thread オブジェクトを継承するため、nil かどうかだけでは
|
|
33
|
+
# 「動いていない」を判定できない。死んでいるスレッドは張り直す
|
|
34
|
+
next if @thread&.alive?
|
|
35
|
+
|
|
36
|
+
# コマンドキューは世代ごとに作り直し、スレッドに自分のキューを渡す。前の世代宛に
|
|
37
|
+
# 積まれたまま未消費で残った :stop を次の世代が 1 周目で拾って自殺するのを、
|
|
38
|
+
# 「1 つのキューは 1 本のスレッドだけのもの」という不変条件で構造的に防ぐ
|
|
39
|
+
queue = CopyTunerClient::QueueWithTimeout.new
|
|
40
|
+
@command_queue = queue
|
|
41
|
+
@logger.info 'start poller thread'
|
|
42
|
+
@thread = Thread.new { poll(queue) } or logger.error("Couldn't start poller thread")
|
|
26
43
|
end
|
|
27
44
|
end
|
|
28
45
|
|
|
46
|
+
# 戻り値は ForkHook が「fork 後に張り直すか」を決めるのに使う。スレッドの生死ではなく
|
|
47
|
+
# ポーリングを継続する意図があったかを返す。想定外の例外で死んだだけのスレッドは
|
|
48
|
+
# fork 後に張り直したいが、生死で判定すると張り直せなくなる
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean] ポーリング継続の意図があった(= fork 後に張り直すべき)なら +true+
|
|
29
51
|
def stop
|
|
30
52
|
@mutex.synchronize do
|
|
31
|
-
@
|
|
32
|
-
@
|
|
53
|
+
resumable = @running && !@aborted
|
|
54
|
+
@running = false
|
|
55
|
+
|
|
56
|
+
thread = @thread
|
|
33
57
|
@thread = nil
|
|
58
|
+
# 例外で終わったスレッドは非 nil のまま dead で残る。それに :stop を積んでも誰も
|
|
59
|
+
# pop しないので、生きているときだけ積んで待つ
|
|
60
|
+
if thread&.alive?
|
|
61
|
+
@command_queue.uniq_push(:stop)
|
|
62
|
+
thread.join
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
resumable
|
|
34
66
|
end
|
|
35
67
|
end
|
|
36
68
|
|
|
@@ -46,20 +78,52 @@ module CopyTunerClient
|
|
|
46
78
|
|
|
47
79
|
attr_reader :cache, :logger, :polling_delay
|
|
48
80
|
|
|
49
|
-
def poll
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
command = @command_queue.pop_with_timeout(polling_delay)
|
|
55
|
-
break if command == :stop
|
|
56
|
-
rescue ThreadError
|
|
57
|
-
# timeout
|
|
58
|
-
end
|
|
81
|
+
def poll(queue)
|
|
82
|
+
timeout = remaining_delay
|
|
83
|
+
until wait_for_command(queue, timeout) == :stop
|
|
84
|
+
sync
|
|
85
|
+
timeout = polling_delay
|
|
59
86
|
end
|
|
60
|
-
|
|
87
|
+
logger.info 'stop poller thread'
|
|
61
88
|
rescue InvalidApiKey => e
|
|
89
|
+
# キーが不正なら張り直しても同じ結果になるので、以後の再開を止める
|
|
90
|
+
@aborted = true
|
|
62
91
|
logger.error(e.message)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
# 例外はスレッドの外へ漏らさない。stop の join は fork の直前にも呼ばれるため、
|
|
94
|
+
# 漏らすと poller の失敗がアプリ側の fork まで巻き添えにする。
|
|
95
|
+
# ここで握ると report_on_exception による stderr 出力も消えるので backtrace を残す
|
|
96
|
+
logger.error("poller thread aborted: #{e.class}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def sync
|
|
100
|
+
cache.sync
|
|
101
|
+
@last_synced_at = monotonic_now
|
|
102
|
+
logger.flush if logger.respond_to?(:flush)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# 最初の待ち時間を「前回 sync からの残り」にすることで、stop / start を繰り返しても
|
|
106
|
+
# sync の間隔を保つ。fork のたびに stop / start されるので、これが無いと worker を
|
|
107
|
+
# fork する数だけ親プロセスで sync(HTTP 往復)が走り直し、その分 join も待たされる。
|
|
108
|
+
# 基点には NTP や手動の時刻変更で巻き戻らない CLOCK_MONOTONIC を使う
|
|
109
|
+
# (Time.now だと時計が後ろに飛んだぶんだけ待ち時間が伸びて同期が止まる)
|
|
110
|
+
def remaining_delay
|
|
111
|
+
return 0 if @last_synced_at.nil?
|
|
112
|
+
|
|
113
|
+
remaining = polling_delay - (monotonic_now - @last_synced_at)
|
|
114
|
+
remaining.negative? ? 0 : remaining
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# 自分の世代のキューを受け取る。@command_queue は start のたびに差し替わるため、
|
|
118
|
+
# 終了処理中の古いスレッドが新しい世代のキューを覗いてしまわないようにする
|
|
119
|
+
def wait_for_command(queue, timeout)
|
|
120
|
+
queue.pop_with_timeout(timeout)
|
|
121
|
+
rescue ThreadError
|
|
122
|
+
nil # timeout
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def monotonic_now
|
|
126
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
63
127
|
end
|
|
64
128
|
end
|
|
65
129
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'copy_tuner_client/fork_hook'
|
|
2
|
+
|
|
1
3
|
module CopyTunerClient
|
|
2
4
|
# Starts the poller from a worker process, or register hooks for a spawner
|
|
3
5
|
# process (such as in Unicorn or Passenger). Also registers hooks for exiting
|
|
@@ -14,6 +16,9 @@ module CopyTunerClient
|
|
|
14
16
|
|
|
15
17
|
# Starts the poller or registers hooks
|
|
16
18
|
def start
|
|
19
|
+
# fork の前後で poller を張り直すフックは、どのプロセスでも必ず登録する
|
|
20
|
+
ForkHook.install
|
|
21
|
+
|
|
17
22
|
if spawner?
|
|
18
23
|
register_spawn_hooks
|
|
19
24
|
else
|
|
@@ -115,9 +120,12 @@ module CopyTunerClient
|
|
|
115
120
|
::Process.singleton_class.prepend(hook_module)
|
|
116
121
|
end
|
|
117
122
|
|
|
123
|
+
# 起動方法(rails server / puma -C)とモード(single / cluster / preload の有無)ごとに
|
|
124
|
+
# どのプロセスで poller が起動するかは docs/poller-startup.md にまとめてある
|
|
118
125
|
def register_puma_hook
|
|
119
|
-
#
|
|
120
|
-
#
|
|
126
|
+
# 非 preload の cluster worker はここに来る。この構成ではアプリのロードが
|
|
127
|
+
# Puma::Runner#start_server の中で起きるため、今から prepend しても実行中の呼び出しには
|
|
128
|
+
# 間に合わない。つまりこの分岐が poller を起動する唯一の経路で、最適化ではなく必須
|
|
121
129
|
if $PROGRAM_NAME.include?('cluster worker')
|
|
122
130
|
@logger.info('Puma would be clustered mode without preload_app')
|
|
123
131
|
@poller.start
|
|
@@ -125,8 +133,9 @@ module CopyTunerClient
|
|
|
125
133
|
end
|
|
126
134
|
|
|
127
135
|
@logger.info('Register Puma fork hook')
|
|
128
|
-
#
|
|
129
|
-
#
|
|
136
|
+
# preload ありの cluster ではこのメソッドが fork 前の master で走る。master は
|
|
137
|
+
# リクエストを捌かないのでここでは起動せず、worker 側で呼ばれる
|
|
138
|
+
# Puma::Runner#start_server まで遅らせる
|
|
130
139
|
poller = @poller
|
|
131
140
|
hook_module =
|
|
132
141
|
Module.new do
|
|
@@ -51,10 +51,16 @@ module CopyTunerClient
|
|
|
51
51
|
|
|
52
52
|
def wait_with_timeout(timeout)
|
|
53
53
|
# wait for element or timeout
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
# NTP の step 補正や手動の時刻変更で巻き戻らない CLOCK_MONOTONIC で締め切りを測る。
|
|
55
|
+
# ウォールクロックだと時計が後ろへ飛んだぶんだけ待ち時間が伸びる
|
|
56
|
+
deadline = monotonic_now + timeout
|
|
57
|
+
while @queue.empty? && (remaining_time = deadline - monotonic_now).positive?
|
|
56
58
|
@received.wait(@mutex, remaining_time)
|
|
57
59
|
end
|
|
58
60
|
end
|
|
61
|
+
|
|
62
|
+
def monotonic_now
|
|
63
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
64
|
+
end
|
|
59
65
|
end
|
|
60
66
|
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe CopyTunerClient::ForkHook do
|
|
4
|
+
let(:client) { FakeClient.new }
|
|
5
|
+
let(:cache) { CopyTunerClient::Cache.new(client, logger: FakeLogger.new) }
|
|
6
|
+
let(:poller) do
|
|
7
|
+
config = CopyTunerClient::Configuration.new.to_hash
|
|
8
|
+
CopyTunerClient::Poller.new(cache, config.update(logger: FakeLogger.new, polling_delay:))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def polling_delay
|
|
12
|
+
0.5
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
before do
|
|
16
|
+
described_class.install
|
|
17
|
+
# フックは CopyTunerClient.configuration&.poller を見るので、実際の設定に載せる
|
|
18
|
+
CopyTunerClient.configuration.poller = poller
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
after do
|
|
22
|
+
poller.stop
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe '停止と再開の順序' do
|
|
26
|
+
# 実際に fork してしまうと「fork の瞬間にスレッドが止まっているか」を観測できないため、
|
|
27
|
+
# _fork の代わりに記録だけするオブジェクトに prepend して順序を見る
|
|
28
|
+
def build_forkable(events, &fork_body)
|
|
29
|
+
forkable = Object.new
|
|
30
|
+
forkable.define_singleton_method(:_fork) do
|
|
31
|
+
events << :fork
|
|
32
|
+
fork_body ? fork_body.call : 0
|
|
33
|
+
end
|
|
34
|
+
forkable.singleton_class.prepend(described_class)
|
|
35
|
+
forkable
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def stub_poller(events, stopped:)
|
|
39
|
+
allow(poller).to receive(:stop) do
|
|
40
|
+
events << :stop
|
|
41
|
+
stopped
|
|
42
|
+
end
|
|
43
|
+
allow(poller).to receive(:start) { events << :start }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'fork の前に poller を停止し、fork の後に再開する' do
|
|
47
|
+
events = []
|
|
48
|
+
stub_poller(events, stopped: true)
|
|
49
|
+
|
|
50
|
+
build_forkable(events)._fork
|
|
51
|
+
|
|
52
|
+
expect(events).to eq(%i[stop fork start])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'fork 前に poller が動いていなければ再開しない' do
|
|
56
|
+
events = []
|
|
57
|
+
stub_poller(events, stopped: false)
|
|
58
|
+
|
|
59
|
+
build_forkable(events)._fork
|
|
60
|
+
|
|
61
|
+
expect(events).to eq(%i[stop fork])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'fork が失敗しても poller を再開する' do
|
|
65
|
+
events = []
|
|
66
|
+
stub_poller(events, stopped: true)
|
|
67
|
+
|
|
68
|
+
forkable = build_forkable(events) { raise Errno::EAGAIN }
|
|
69
|
+
|
|
70
|
+
expect { forkable._fork }.to raise_error(Errno::EAGAIN)
|
|
71
|
+
expect(events).to eq(%i[stop fork start])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe 'poller を取得できないとき' do
|
|
76
|
+
it 'configuration が nil でも fork を壊さない' do
|
|
77
|
+
# Process._fork への prepend は外せないので、ここで例外を漏らすとアプリの
|
|
78
|
+
# すべての fork が失敗する
|
|
79
|
+
CopyTunerClient.configuration = nil
|
|
80
|
+
|
|
81
|
+
expect { Process.waitpid(fork { exit!(0) }) }.not_to raise_error
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'ログ出力自体が失敗しても fork は成立する' do
|
|
85
|
+
allow(poller).to receive(:stop).and_return(true)
|
|
86
|
+
allow(poller).to receive(:start).and_raise(ThreadError, 'cannot create thread')
|
|
87
|
+
allow(CopyTunerClient.configuration.logger).to receive(:error).and_raise('logger is broken')
|
|
88
|
+
|
|
89
|
+
expect { Process.waitpid(fork { exit!(0) }) }.not_to raise_error
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'fork 後の poller 起動に失敗しても fork 自体は成立する' do
|
|
93
|
+
allow(poller).to receive(:stop).and_return(true)
|
|
94
|
+
allow(poller).to receive(:start).and_raise(ThreadError, 'cannot create thread')
|
|
95
|
+
|
|
96
|
+
expect { Process.waitpid(fork { exit!(0) }) }.not_to raise_error
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe '実際に fork したとき' do
|
|
101
|
+
it 'fork 前に poller が例外で死んでいても親子とも張り直す' do
|
|
102
|
+
# rails server の cluster では ForkHook が唯一の再開経路なので、ここで張り直さないと
|
|
103
|
+
# 親子とも poller を失う
|
|
104
|
+
fail_once = true
|
|
105
|
+
allow(cache).to receive(:sync).and_wrap_original do |original, *args|
|
|
106
|
+
if fail_once
|
|
107
|
+
fail_once = false
|
|
108
|
+
raise 'boom'
|
|
109
|
+
end
|
|
110
|
+
original.call(*args)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
poller.start
|
|
114
|
+
sleep(polling_delay * 0.4) # スレッドが例外で終わる
|
|
115
|
+
|
|
116
|
+
reader, writer = IO.pipe
|
|
117
|
+
pid =
|
|
118
|
+
fork do
|
|
119
|
+
reader.close
|
|
120
|
+
client['child.key'] = 'value'
|
|
121
|
+
sleep(polling_delay * 3)
|
|
122
|
+
writer.write(cache['child.key'].to_s)
|
|
123
|
+
writer.close
|
|
124
|
+
exit!(0)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
writer.close
|
|
128
|
+
child_result = reader.read
|
|
129
|
+
Process.waitpid(pid)
|
|
130
|
+
|
|
131
|
+
client['parent.key'] = 'value'
|
|
132
|
+
sleep(polling_delay * 3)
|
|
133
|
+
|
|
134
|
+
expect(child_result).to eq('value')
|
|
135
|
+
expect(cache['parent.key']).to eq('value')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it '子プロセスでも poller がポーリングを続ける' do
|
|
139
|
+
poller.start
|
|
140
|
+
reader, writer = IO.pipe
|
|
141
|
+
|
|
142
|
+
pid =
|
|
143
|
+
fork do
|
|
144
|
+
reader.close
|
|
145
|
+
client['test.key'] = 'value'
|
|
146
|
+
sleep(polling_delay * 3)
|
|
147
|
+
writer.write(cache['test.key'].to_s)
|
|
148
|
+
writer.close
|
|
149
|
+
exit!(0)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
writer.close
|
|
153
|
+
result = reader.read
|
|
154
|
+
Process.waitpid(pid)
|
|
155
|
+
|
|
156
|
+
expect(result).to eq('value')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it '親プロセスでも poller がポーリングを続ける' do
|
|
160
|
+
poller.start
|
|
161
|
+
sleep(polling_delay * 0.2)
|
|
162
|
+
|
|
163
|
+
Process.waitpid(fork { exit!(0) })
|
|
164
|
+
|
|
165
|
+
client['test.key'] = 'value'
|
|
166
|
+
sleep(polling_delay * 3)
|
|
167
|
+
|
|
168
|
+
expect(cache['test.key']).to eq('value')
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -11,7 +11,7 @@ describe CopyTunerClient::Poller do
|
|
|
11
11
|
|
|
12
12
|
def build_poller(config = {})
|
|
13
13
|
config[:logger] ||= FakeLogger.new
|
|
14
|
-
config[:polling_delay]
|
|
14
|
+
config[:polling_delay] ||= polling_delay
|
|
15
15
|
default_config = CopyTunerClient::Configuration.new.to_hash
|
|
16
16
|
poller = CopyTunerClient::Poller.new(cache, default_config.update(config))
|
|
17
17
|
pollers << poller
|
|
@@ -95,4 +95,151 @@ describe CopyTunerClient::Poller do
|
|
|
95
95
|
|
|
96
96
|
expect(logger).to have_received(:flush).at_least(:once)
|
|
97
97
|
end
|
|
98
|
+
|
|
99
|
+
describe '#stop' do
|
|
100
|
+
it 'スレッドを停止したときは true を返す' do
|
|
101
|
+
poller = build_poller
|
|
102
|
+
poller.start
|
|
103
|
+
|
|
104
|
+
expect(poller.stop).to be true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'スレッドが動いていないときは false を返す' do
|
|
108
|
+
poller = build_poller
|
|
109
|
+
|
|
110
|
+
expect(poller.stop).to be false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'スレッドが例外で終わっていても例外を再送出しない' do
|
|
114
|
+
# stop は fork の直前にも呼ばれる。ここで join が poller の例外を再送出すると
|
|
115
|
+
# アプリ側の fork まで巻き添えになる
|
|
116
|
+
logger = FakeLogger.new
|
|
117
|
+
allow(cache).to receive(:sync).and_raise('boom')
|
|
118
|
+
poller = build_poller(logger:)
|
|
119
|
+
poller.start
|
|
120
|
+
sleep(polling_delay * 0.2)
|
|
121
|
+
|
|
122
|
+
expect { poller.stop }.not_to raise_error
|
|
123
|
+
expect(logger).to have_entry(:error, 'boom')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'start していないときに stop してもキューに :stop を残さない' do
|
|
127
|
+
poller = build_poller
|
|
128
|
+
poller.stop
|
|
129
|
+
|
|
130
|
+
poller.start
|
|
131
|
+
|
|
132
|
+
# 1 周目の sync だけでは「:stop がキューに残っている」状態と区別できないため、
|
|
133
|
+
# 2 周目以降も同期が続くことを確かめる
|
|
134
|
+
wait_for_next_sync
|
|
135
|
+
client['test.key'] = 'value'
|
|
136
|
+
wait_for_next_sync
|
|
137
|
+
|
|
138
|
+
expect(cache['test.key']).to eq('value')
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe '世代をまたいだコマンドの混入' do
|
|
143
|
+
# :stop は pop されるまでキューに残る。前の世代のスレッド宛に積まれた :stop を
|
|
144
|
+
# 次の世代のスレッドが拾うと、起動直後に自分を止めてしまう
|
|
145
|
+
it 'スレッドが例外で死んだ後に stop → start してもポーリングが続く' do
|
|
146
|
+
poller = build_poller
|
|
147
|
+
allow(cache).to receive(:sync).and_raise('boom')
|
|
148
|
+
poller.start
|
|
149
|
+
sleep(polling_delay * 0.3) # スレッドが例外で終わるのを待つ
|
|
150
|
+
|
|
151
|
+
allow(cache).to receive(:sync).and_call_original
|
|
152
|
+
poller.stop
|
|
153
|
+
poller.start
|
|
154
|
+
|
|
155
|
+
wait_for_next_sync
|
|
156
|
+
client['test.key'] = 'value'
|
|
157
|
+
wait_for_next_sync
|
|
158
|
+
|
|
159
|
+
expect(cache['test.key']).to eq('value')
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'stop の直後に sync が例外で終わっても、次の start でポーリングが続く' do
|
|
163
|
+
poller = build_poller
|
|
164
|
+
# stop が :stop を積んだ後にスレッドが例外で終わる順序を作る。
|
|
165
|
+
# sync に入ったところで stop を待たせ、:stop を積み終えてから例外にする
|
|
166
|
+
syncing = Queue.new
|
|
167
|
+
resume = Queue.new
|
|
168
|
+
allow(cache).to receive(:sync) do
|
|
169
|
+
syncing << true
|
|
170
|
+
resume.pop
|
|
171
|
+
raise 'boom'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
poller.start
|
|
175
|
+
syncing.pop # スレッドが sync に入った
|
|
176
|
+
|
|
177
|
+
stopper = Thread.new { poller.stop }
|
|
178
|
+
sleep(0.1) # :stop がキューに積まれるのを待つ
|
|
179
|
+
resume << true # ここで sync が例外になり、:stop は消費されない
|
|
180
|
+
stopper.join
|
|
181
|
+
|
|
182
|
+
allow(cache).to receive(:sync).and_call_original
|
|
183
|
+
poller.start
|
|
184
|
+
|
|
185
|
+
wait_for_next_sync
|
|
186
|
+
client['test.key'] = 'value'
|
|
187
|
+
wait_for_next_sync
|
|
188
|
+
|
|
189
|
+
expect(cache['test.key']).to eq('value')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# stop の戻り値は ForkHook の「fork 後に張り直すか」の判断に使われる。スレッドの生死ではなく
|
|
193
|
+
# 「ポーリングを継続する意図があるか」を返す必要がある
|
|
194
|
+
it 'スレッドが例外で死んでいても、起動中だったなら stop は true を返す' do
|
|
195
|
+
poller = build_poller
|
|
196
|
+
allow(cache).to receive(:sync).and_raise('boom')
|
|
197
|
+
poller.start
|
|
198
|
+
sleep(polling_delay * 0.3)
|
|
199
|
+
|
|
200
|
+
expect(poller.stop).to be true
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it '回復の見込みがない理由で終了した後の stop は false を返す' do
|
|
204
|
+
poller = build_poller
|
|
205
|
+
allow(cache).to receive(:sync).and_raise(CopyTunerClient::InvalidApiKey, 'Invalid API key')
|
|
206
|
+
poller.start
|
|
207
|
+
sleep(polling_delay * 0.3)
|
|
208
|
+
|
|
209
|
+
# API キーが不正なら fork のたびに張り直しても同じ理由で死ぬだけなので再開させない
|
|
210
|
+
expect(poller.stop).to be false
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
describe '再開時の sync 間隔' do
|
|
215
|
+
# 経過時間で判定するため、CI の負荷や GC で sleep が伸びても落ちないよう
|
|
216
|
+
# 他のテストより長い間隔を使ってマージンを稼ぐ
|
|
217
|
+
let(:slow_delay) { 2.0 }
|
|
218
|
+
|
|
219
|
+
it '初回の start では待たずに sync する' do
|
|
220
|
+
poller = build_poller(polling_delay: slow_delay)
|
|
221
|
+
|
|
222
|
+
poller.start
|
|
223
|
+
sleep(slow_delay * 0.2)
|
|
224
|
+
|
|
225
|
+
expect(client.downloads).to eq(1)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it '停止直後に再開したときは前回 sync からの残り時間を待ってから sync する' do
|
|
229
|
+
poller = build_poller(polling_delay: slow_delay)
|
|
230
|
+
poller.start
|
|
231
|
+
sleep(slow_delay * 0.2)
|
|
232
|
+
poller.stop
|
|
233
|
+
|
|
234
|
+
poller.start
|
|
235
|
+
|
|
236
|
+
# 前回 sync から polling_delay 経つまでは sync しない
|
|
237
|
+
sleep(slow_delay * 0.4)
|
|
238
|
+
expect(client.downloads).to eq(1)
|
|
239
|
+
|
|
240
|
+
# 残り時間が過ぎれば sync が再開する
|
|
241
|
+
sleep(slow_delay * 0.8)
|
|
242
|
+
expect(client.downloads).to eq(2)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
98
245
|
end
|
|
@@ -20,6 +20,14 @@ describe CopyTunerClient::ProcessGuard do
|
|
|
20
20
|
process_guard
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
it 'fork の前後で poller を張り直すフックを登録する' do
|
|
24
|
+
allow(CopyTunerClient::ForkHook).to receive(:install)
|
|
25
|
+
|
|
26
|
+
build_process_guard.start
|
|
27
|
+
|
|
28
|
+
expect(CopyTunerClient::ForkHook).to have_received(:install)
|
|
29
|
+
end
|
|
30
|
+
|
|
23
31
|
it 'starts polling from a worker process' do
|
|
24
32
|
process_guard = build_process_guard
|
|
25
33
|
process_guard.start
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
|
|
4
|
+
describe CopyTunerClient::QueueWithTimeout do
|
|
5
|
+
subject(:queue) { described_class.new }
|
|
6
|
+
|
|
7
|
+
describe '#pop_with_timeout' do
|
|
8
|
+
it 'キューに要素があれば取り出す' do
|
|
9
|
+
queue << :sync
|
|
10
|
+
|
|
11
|
+
expect(queue.pop_with_timeout(0)).to eq(:sync)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it '要素が無いままタイムアウトしたら ThreadError を投げる' do
|
|
15
|
+
expect { queue.pop_with_timeout(0.1) }.to raise_error(ThreadError)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'ウォールクロックが巻き戻ってもタイムアウトが伸びない' do
|
|
19
|
+
# 待っている最中に NTP の step 補正や手動の時刻変更で時計が後ろへ飛ぶ状況を模す。
|
|
20
|
+
# 経過時間をウォールクロックで測っていると、飛んだ幅がそのまま待ち時間に乗る
|
|
21
|
+
base = Time.now
|
|
22
|
+
allow(Time).to receive(:now).and_return(base, base - 3600)
|
|
23
|
+
|
|
24
|
+
expect { Timeout.timeout(2) { queue.pop_with_timeout(0.1) } }.to raise_error(ThreadError)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -26,4 +26,10 @@ RSpec.configure do |config|
|
|
|
26
26
|
FakeCopyTunerApp.reset
|
|
27
27
|
reset_config
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# apply を通す spec は本物の poller スレッドを起動するので、example ごとに止める。
|
|
31
|
+
# 放置するとスレッドがスイート終了まで積み上がり、実際に HTTP を叩き続ける
|
|
32
|
+
config.after do
|
|
33
|
+
CopyTunerClient.configuration&.poller&.stop
|
|
34
|
+
end
|
|
29
35
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: copy_tuner_client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SonicGarden
|
|
@@ -200,6 +200,7 @@ files:
|
|
|
200
200
|
- app/assets/javascripts/copytuner.js
|
|
201
201
|
- biome.json
|
|
202
202
|
- copy_tuner_client.gemspec
|
|
203
|
+
- docs/poller-startup.md
|
|
203
204
|
- gemfiles/8.0.gemfile
|
|
204
205
|
- gemfiles/8.1.gemfile
|
|
205
206
|
- gemfiles/main.gemfile
|
|
@@ -215,6 +216,7 @@ files:
|
|
|
215
216
|
- lib/copy_tuner_client/dotted_hash.rb
|
|
216
217
|
- lib/copy_tuner_client/engine.rb
|
|
217
218
|
- lib/copy_tuner_client/errors.rb
|
|
219
|
+
- lib/copy_tuner_client/fork_hook.rb
|
|
218
220
|
- lib/copy_tuner_client/helper_extension.rb
|
|
219
221
|
- lib/copy_tuner_client/i18n_backend.rb
|
|
220
222
|
- lib/copy_tuner_client/i18n_compat.rb
|
|
@@ -251,11 +253,13 @@ files:
|
|
|
251
253
|
- spec/copy_tuner_client/copyray_middleware_spec.rb
|
|
252
254
|
- spec/copy_tuner_client/copyray_spec.rb
|
|
253
255
|
- spec/copy_tuner_client/dotted_hash_spec.rb
|
|
256
|
+
- spec/copy_tuner_client/fork_hook_spec.rb
|
|
254
257
|
- spec/copy_tuner_client/helper_extension_spec.rb
|
|
255
258
|
- spec/copy_tuner_client/i18n_backend_spec.rb
|
|
256
259
|
- spec/copy_tuner_client/poller_spec.rb
|
|
257
260
|
- spec/copy_tuner_client/prefixed_logger_spec.rb
|
|
258
261
|
- spec/copy_tuner_client/process_guard_spec.rb
|
|
262
|
+
- spec/copy_tuner_client/queue_with_timeout_spec.rb
|
|
259
263
|
- spec/copy_tuner_client/request_sync_spec.rb
|
|
260
264
|
- spec/copy_tuner_client/translation_log_spec.rb
|
|
261
265
|
- spec/copy_tuner_client/warden_integration_spec.rb
|