Module: CopyTunerClient::ForkHook

Defined in:
lib/copy_tuner_client/fork_hook.rb

Overview

fork をまたいで poller スレッドを引き継ぐためのフック。 Process._fork に prepend し、fork の直前に poller を協調的に停止して、 fork のあと親子それぞれで張り直す。

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.installObject

Module#prepend は同じモジュールを二重に挿さないので、呼び出し側で登録済みかを見なくてよい



7
8
9
# File 'lib/copy_tuner_client/fork_hook.rb', line 7

def self.install
  ::Process.singleton_class.prepend(self)
end

.log_error(message) ⇒ Object

ログ出力自体が失敗しても fork は成立させる。ここで例外を漏らすと、失敗を握るために 置いた rescue が逆に fork を壊す



47
48
49
50
51
# File 'lib/copy_tuner_client/fork_hook.rb', line 47

def self.log_error(message)
  CopyTunerClient.configuration&.logger&.error(message)
rescue StandardError
  nil
end

.restart_poller(poller) ⇒ Object

Process.singleton_class に prepend されるため、インスタンスメソッドとして定義すると Process 自身にメソッドが生えてしまう。フックの補助はモジュール側の特異メソッドに置く

fork 後に例外を漏らすと「子プロセスは生成済みなのに親の fork が例外を投げる」状態になり、 呼び出し側が子の pid を受け取れなくなる。起動の失敗はログに落として fork は成立させる



39
40
41
42
43
# File 'lib/copy_tuner_client/fork_hook.rb', line 39

def self.restart_poller(poller)
  poller.start
rescue StandardError => e
  log_error("CopyTuner: fork 後の poller 起動に失敗しました: #{e.class}: #{e.message}")
end

Instance Method Details

#_forkObject



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/copy_tuner_client/fork_hook.rb', line 11

def _fork
  # fork はアプリの任意のタイミングで起きる。Process._fork への prepend は外せないので、
  # configure 前や configuration 差し替え中に NoMethodError でアプリの fork を
  # 壊さないよう nil を許容する
  poller = CopyTunerClient.configuration&.poller
  # スレッドは子プロセスに引き継がれない。fork 後に子へ残る Thread オブジェクトの見え方
  # (alive? / join の結果)はドキュメント化されていない CRuby の実装依存なので、
  # そこに依存した後始末はせず、fork 前に協調的に停止しておく。
  # ここは rescue しない。停止の失敗を握り潰すと、動いている poller ごと fork することになり
  # このフックの目的そのものを裏切る
  restart = poller ? poller.stop : false

  begin
    super
  ensure
    # ensure は親(super の戻り値 = 子の pid)と子(同 0)の両方を通り、super が失敗した
    # ときも親で復元する。「fork 前に動いていたプロセスは fork 後も動いている」状態を保つ。
    # 子でも張り直すのは、Puma の fork_worker のように worker が worker を fork する
    # 構成でもサーバ固有のフックに頼らず poller を立てるため
    ForkHook.restart_poller(poller) if restart
  end
end