copy_tuner_client 2.1.2 → 2.2.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/.rubocop.yml +46 -1
  4. data/CLAUDE.md +1 -0
  5. data/Gemfile +1 -0
  6. data/copy_tuner_client.gemspec +3 -3
  7. data/gemfiles/8.0.gemfile +3 -3
  8. data/gemfiles/8.1.gemfile +3 -3
  9. data/gemfiles/main.gemfile +3 -3
  10. data/lib/copy_tuner_client/cache.rb +34 -27
  11. data/lib/copy_tuner_client/client.rb +35 -23
  12. data/lib/copy_tuner_client/configuration.rb +62 -40
  13. data/lib/copy_tuner_client/copyray/rewriter.rb +1 -1
  14. data/lib/copy_tuner_client/copyray.rb +1 -0
  15. data/lib/copy_tuner_client/copyray_middleware.rb +31 -24
  16. data/lib/copy_tuner_client/dotted_hash.rb +7 -7
  17. data/lib/copy_tuner_client/engine.rb +4 -6
  18. data/lib/copy_tuner_client/helper_extension.rb +58 -54
  19. data/lib/copy_tuner_client/i18n_backend.rb +12 -14
  20. data/lib/copy_tuner_client/i18n_compat.rb +2 -1
  21. data/lib/copy_tuner_client/poller.rb +3 -4
  22. data/lib/copy_tuner_client/prefixed_logger.rb +13 -12
  23. data/lib/copy_tuner_client/process_guard.rb +29 -26
  24. data/lib/copy_tuner_client/queue_with_timeout.rb +29 -19
  25. data/lib/copy_tuner_client/rails.rb +9 -8
  26. data/lib/copy_tuner_client/request_sync.rb +26 -24
  27. data/lib/copy_tuner_client/simple_form_extention.rb +10 -6
  28. data/lib/copy_tuner_client/translation_log.rb +10 -5
  29. data/lib/copy_tuner_client/version.rb +1 -1
  30. data/lib/copy_tuner_client.rb +4 -8
  31. data/lib/tasks/copy_tuner_client_tasks.rake +14 -16
  32. data/skills/copy-tuner-to-locales-migrate-prefix/scripts/migrate_prefix.rb +14 -7
  33. data/skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb +19 -17
  34. data/spec/copy_tuner_client/cache_spec.rb +63 -57
  35. data/spec/copy_tuner_client/client_spec.rb +82 -81
  36. data/spec/copy_tuner_client/configuration_spec.rb +223 -222
  37. data/spec/copy_tuner_client/copyray/marker_spec.rb +2 -1
  38. data/spec/copy_tuner_client/copyray/rewriter_spec.rb +15 -10
  39. data/spec/copy_tuner_client/copyray_middleware_spec.rb +9 -6
  40. data/spec/copy_tuner_client/copyray_spec.rb +8 -8
  41. data/spec/copy_tuner_client/dotted_hash_spec.rb +33 -33
  42. data/spec/copy_tuner_client/helper_extension_spec.rb +47 -44
  43. data/spec/copy_tuner_client/i18n_backend_spec.rb +144 -143
  44. data/spec/copy_tuner_client/poller_spec.rb +25 -24
  45. data/spec/copy_tuner_client/prefixed_logger_spec.rb +15 -12
  46. data/spec/copy_tuner_client/process_guard_spec.rb +27 -27
  47. data/spec/copy_tuner_client/request_sync_spec.rb +51 -58
  48. data/spec/copy_tuner_client/translation_log_spec.rb +38 -24
  49. data/spec/copy_tuner_client_spec.rb +5 -6
  50. data/spec/support/client_spec_helpers.rb +1 -1
  51. data/spec/support/defines_constants.rb +3 -34
  52. data/spec/support/fake_client.rb +1 -3
  53. data/spec/support/fake_copy_tuner_app.rb +54 -62
  54. data/spec/support/fake_html_safe_string.rb +2 -3
  55. data/spec/support/fake_logger.rb +22 -21
  56. data/spec/support/fake_passenger.rb +4 -6
  57. data/spec/support/fake_unicorn.rb +2 -4
  58. data/spec/support/middleware_stack.rb +3 -3
  59. data/spec/support/writing_cache.rb +4 -4
  60. metadata +1 -1
@@ -3,6 +3,7 @@
3
3
  require 'copy_tuner_client/copyray/rewriter'
4
4
 
5
5
  module CopyTunerClient
6
+ # レスポンス HTML を Rewriter に通し、マーカートークンを data-copyray-key 属性へ変換する Rack middleware
6
7
  class CopyrayMiddleware
7
8
  def initialize(app)
8
9
  @app = app
@@ -11,26 +12,8 @@ module CopyTunerClient
11
12
  def call(env)
12
13
  CopyTunerClient::TranslationLog.clear
13
14
  status, headers, response = @app.call(env)
14
- if rewritable?(status, headers) && body = response_body(response)
15
- csp_nonce = env['action_dispatch.content_security_policy_nonce'] || env['secure_headers_content_security_policy_nonce']
16
- # NOTE: CSS/JS 挿入の前に Rewriter を通す。serialize 後も </body> は必ず出力されるので
17
- # append_to_html_body の rindex は機能し、CSS/JS タグはトークン非含有なので二重処理も起きない。
18
- # NOTE: skipped は data-copyray-key を付与できなかったこと(巨大DOM/Nokogiri例外)を表す。
19
- # JS にこれを伝え、オーバーレイ非対応である旨をツールバーで案内させる。
20
- # NOTE: turbo stream はページ断片なので fragment パーサで走査する(html/body ラッパを付けない)。
21
- turbo_stream = turbo_stream?(headers)
22
- body, skipped = CopyTunerClient::Copyray::Rewriter.rewrite(body, fragment: turbo_stream)
23
- # NOTE: ブートストラップ JS はフルページ読み込み時に一度だけ挿入すればよい。turbo stream 断片には
24
- # 挿入先の </body> も無く、既に初期化済みのページへマージされるだけなので挿入しない。
25
- body = append_js(body, csp_nonce, skipped: skipped) unless turbo_stream
26
- content_length = body.bytesize.to_s
27
- headers['Content-Length'] = content_length
28
- # maintains compatibility with other middlewares
29
- if defined?(ActionDispatch::Response::RackBody) && response.is_a?(ActionDispatch::Response::RackBody)
30
- ActionDispatch::Response.new(status, headers, [body]).to_a
31
- else
32
- [status, headers, [body]]
33
- end
15
+ if rewritable?(status, headers) && (body = response_body(response))
16
+ rewrite_response(env, status, headers, body, response)
34
17
  else
35
18
  [status, headers, response]
36
19
  end
@@ -38,6 +21,30 @@ module CopyTunerClient
38
21
 
39
22
  private
40
23
 
24
+ def rewrite_response(env, status, headers, body, response)
25
+ csp_nonce =
26
+ env.fetch('action_dispatch.content_security_policy_nonce') do
27
+ env['secure_headers_content_security_policy_nonce']
28
+ end
29
+ # NOTE: CSS/JS 挿入の前に Rewriter を通す。serialize 後も </body> は必ず出力されるので
30
+ # append_to_html_body の rindex は機能し、CSS/JS タグはトークン非含有なので二重処理も起きない。
31
+ # NOTE: skipped は data-copyray-key を付与できなかったこと(巨大DOM/Nokogiri例外)を表す。
32
+ # JS にこれを伝え、オーバーレイ非対応である旨をツールバーで案内させる。
33
+ # NOTE: turbo stream はページ断片なので fragment パーサで走査する(html/body ラッパを付けない)。
34
+ turbo_stream = turbo_stream?(headers)
35
+ body, skipped = CopyTunerClient::Copyray::Rewriter.rewrite(body, fragment: turbo_stream)
36
+ # NOTE: ブートストラップ JS はフルページ読み込み時に一度だけ挿入すればよい。turbo stream 断片には
37
+ # 挿入先の </body> も無く、既に初期化済みのページへマージされるだけなので挿入しない。
38
+ body = append_js(body, csp_nonce, skipped:) unless turbo_stream
39
+ headers['Content-Length'] = body.bytesize.to_s
40
+ # maintains compatibility with other middlewares
41
+ if defined?(ActionDispatch::Response::RackBody) && response.is_a?(ActionDispatch::Response::RackBody)
42
+ ActionDispatch::Response.new(status, headers, [body]).to_a
43
+ else
44
+ [status, headers, [body]]
45
+ end
46
+ end
47
+
41
48
  def helpers
42
49
  ActionController::Base.helpers
43
50
  end
@@ -57,7 +64,8 @@ module CopyTunerClient
57
64
  keysSkipped: #{skipped},
58
65
  }
59
66
  SCRIPT
60
- append_to_html_body(html, helpers.javascript_include_tag('copytuner', type: 'module', crossorigin: 'anonymous', nonce: csp_nonce))
67
+ tag = helpers.javascript_include_tag('copytuner', type: 'module', crossorigin: 'anonymous', nonce: csp_nonce)
68
+ append_to_html_body(html, tag)
61
69
  end
62
70
 
63
71
  def append_to_html_body(html, content)
@@ -65,7 +73,7 @@ module CopyTunerClient
65
73
  return html unless html.include?('</body>')
66
74
 
67
75
  position = html.rindex('</body>')
68
- html.insert(position, content + "\n")
76
+ html.insert(position, "#{content}\n")
69
77
  end
70
78
 
71
79
  def file?(headers)
@@ -80,8 +88,7 @@ module CopyTunerClient
80
88
  end
81
89
 
82
90
  def turbo_stream?(headers)
83
- headers['Content-Type'] &&
84
- headers['Content-Type'].include?('text/vnd.turbo-stream.html')
91
+ headers['Content-Type']&.include?('text/vnd.turbo-stream.html')
85
92
  end
86
93
 
87
94
  def response_body(response)
@@ -1,4 +1,5 @@
1
1
  module CopyTunerClient
2
+ # `a.b.c` 形式のドット区切りキーを持つ Hash とネストした Hash を相互変換する
2
3
  module DottedHash
3
4
  def to_h(dotted_hash)
4
5
  hash = {}
@@ -15,18 +16,17 @@ module CopyTunerClient
15
16
 
16
17
  all_keys.each_with_index do |key, index|
17
18
  prefix = "#{key}."
18
- conflict_keys = ((index + 1)..Float::INFINITY)
19
- .take_while { |i| all_keys[i]&.start_with?(prefix) }
20
- .map { |i| all_keys[i] }
19
+ conflict_keys =
20
+ ((index + 1)..Float::INFINITY)
21
+ .take_while { |i| all_keys[i]&.start_with?(prefix) }
22
+ .map { |i| all_keys[i] }
21
23
 
22
- if conflict_keys.present?
23
- results[key] = conflict_keys
24
- end
24
+ results[key] = conflict_keys if conflict_keys.present?
25
25
  end
26
26
 
27
27
  results
28
28
  end
29
29
 
30
- module_function :to_h, :conflict_keys # rubocop:disable Style/AccessModifierDeclarations
30
+ module_function :to_h, :conflict_keys
31
31
  end
32
32
  end
@@ -5,11 +5,11 @@ require 'copy_tuner_client/helper_extension'
5
5
  module CopyTunerClient
6
6
  # Connects to integration points for Rails 3 applications
7
7
  class Engine < ::Rails::Engine
8
- initializer :initialize_copy_tuner_rails, :before => :load_config_initializers do |app|
8
+ initializer :initialize_copy_tuner_rails, before: :load_config_initializers do |_app|
9
9
  CopyTunerClient::Rails.initialize
10
10
  end
11
11
 
12
- initializer :initialize_copy_tuner_hook_methods, :after => :load_config_initializers do |app|
12
+ initializer :initialize_copy_tuner_hook_methods, after: :load_config_initializers do |_app|
13
13
  ActiveSupport.on_load(:action_view) do
14
14
  CopyTunerClient::HelperExtension.hook_translation_helper(
15
15
  ActionView::Helpers::TranslationHelper,
@@ -17,14 +17,12 @@ module CopyTunerClient
17
17
  )
18
18
  end
19
19
 
20
- if CopyTunerClient.configuration.enable_middleware?
21
- CopyTunerClient::TranslationLog.install_hook
22
- end
20
+ CopyTunerClient::TranslationLog.install_hook if CopyTunerClient.configuration.enable_middleware?
23
21
 
24
22
  require 'copy_tuner_client/simple_form_extention'
25
23
  end
26
24
 
27
- initializer "copy_tuner.assets.precompile", group: :all do |app|
25
+ initializer 'copy_tuner.assets.precompile', group: :all do |app|
28
26
  app.config.assets.precompile += ['copytuner.js']
29
27
  end
30
28
  end
@@ -1,67 +1,71 @@
1
1
  module CopyTunerClient
2
+ # ActionView の translate/t を alias_method で差し替え、Copyray のオーバーレイマーカーを注入する
2
3
  module HelperExtension
3
- class << self
4
- def hook_translation_helper(mod, middleware_enabled:)
5
- mod.class_eval do
6
- def translate_with_copyray_comment(key, **options)
7
- source = translate_without_copyray_comment(key, **options)
4
+ # NOTE: class_eval ブロック内で def すると、ブロック内メソッドの複雑さが
5
+ # hook_translation_helper 自体の Metrics/AbcSize としてカウントされてしまうため、
6
+ # メソッド本体は独立したモジュールに切り出して include する。
7
+ module CopyrayCommentInjection
8
+ def translate_with_copyray_comment(key, **options)
9
+ source = translate_without_copyray_comment(key, **options)
8
10
 
9
- if controller && CopyTunerClient::Rails.controller_of_rails_engine?(controller)
10
- return source
11
- end
11
+ return source if controller && CopyTunerClient::Rails.controller_of_rails_engine?(controller)
12
12
 
13
- # TODO: test
14
- # NOTE: default引数が設定されている場合は、copytunerキャッシュの値をI18n.t呼び出しにより上書きしている
15
- # SEE: https://github.com/rails/rails/blob/6c43ebc220428ce9fc9569c2e5df90a38a4fc4e4/actionview/lib/action_view/helpers/translation_helper.rb#L82
16
- if options.key?(:default)
17
- I18n.t(key.to_s.first == '.' ? scope_key_by_partial(key) : key, **options)
18
- end
13
+ # TODO: test
14
+ # NOTE: default引数が設定されている場合は、copytunerキャッシュの値をI18n.t呼び出しにより上書きしている
15
+ # SEE: https://github.com/rails/rails/blob/6c43ebc220428ce9fc9569c2e5df90a38a4fc4e4/actionview/lib/action_view/helpers/translation_helper.rb#L82
16
+ if options.key?(:default)
17
+ I18n.t(key.to_s.first == '.' ? scope_key_by_partial(key) : key, **options)
18
+ end
19
19
 
20
- # NOTE: マーカーは HTML コメントとしてブラウザに無視されつつ Copyray オーバーレイのキー特定に
21
- # 使われる。HTML 以外の経路(メール本文・render :json・CSV/PDF など)ではコメントが文字列として
22
- # 出力に混入してしまうため、それらの経路には注入しない。default 引数による初期値登録は維持する
23
- # 必要があるため、このガードは初期値登録(上の I18n.t 呼び出し)より後に置く。
24
- return source unless copyray_injectable?
20
+ # NOTE: マーカーは HTML コメントとしてブラウザに無視されつつ Copyray オーバーレイのキー特定に
21
+ # 使われる。HTML 以外の経路(メール本文・render :json・CSV/PDF など)ではコメントが文字列として
22
+ # 出力に混入してしまうため、それらの経路には注入しない。default 引数による初期値登録は維持する
23
+ # 必要があるため、このガードは初期値登録(上の I18n.t 呼び出し)より後に置く。
24
+ return source unless copyray_injectable?
25
25
 
26
- if CopyTunerClient.configuration.disable_copyray_comment_injection
27
- source
28
- else
29
- separator = options[:separator] || I18n.default_separator
30
- scope = options[:scope]
31
- scope_key =
32
- if key.to_s.first == '.'
33
- scope_key_by_partial(key)
34
- else
35
- # NOTE: locale prefix無しのkeyが必要のためこうしている
36
- I18n.normalize_keys(nil, key, scope, separator).compact.join(separator)
37
- end
38
- CopyTunerClient::Copyray.augment_template(source, scope_key)
39
- end
40
- end
26
+ if CopyTunerClient.configuration.disable_copyray_comment_injection
27
+ source
28
+ else
29
+ CopyTunerClient::Copyray.augment_template(source, copyray_scope_key(key, options))
30
+ end
31
+ end
41
32
 
42
- # NOTE: HTML 以外の経路(メール本文・render :json・CSV/PDF など)ではマーカーが文字列として
43
- # 出力に混入するため注入しない。判定には controller.request.format を使い、@current_template.format /
44
- # lookup_context.formats のような ActionView の内部実装には依存させない(Rails バージョン間で壊れうるため)。
45
- def copyray_injectable?
46
- current_controller = controller
47
- return false if current_controller.nil?
33
+ def copyray_scope_key(key, options)
34
+ return scope_key_by_partial(key) if key.to_s.first == '.'
48
35
 
49
- # NOTE: mailer は request を持たず request.format で判定できない。かつメール本文への
50
- # マーカー混入は実害が大きいため、controller の型で明示除外する。
51
- return false if defined?(ActionMailer::Base) && current_controller.is_a?(ActionMailer::Base)
36
+ separator = options.fetch(:separator, I18n.default_separator)
37
+ # NOTE: locale prefix無しのkeyが必要のためこうしている
38
+ I18n.normalize_keys(nil, key, options[:scope], separator).compact.join(separator)
39
+ end
40
+ private :copyray_scope_key
52
41
 
53
- # NOTE: request が無い/format html でない経路には注入しない。&. と || false で
54
- # request 不在時も安全に false を返す。
55
- current_controller.request&.format&.html? || false
56
- end
57
- private :copyray_injectable?
42
+ # NOTE: HTML 以外の経路(メール本文・render :json・CSV/PDF など)ではマーカーが文字列として
43
+ # 出力に混入するため注入しない。判定には controller.request.format を使い、@current_template.format /
44
+ # lookup_context.formats のような ActionView の内部実装には依存させない(Rails バージョン間で壊れうるため)。
45
+ def copyray_injectable?
46
+ current_controller = controller
47
+ return false if current_controller.nil?
58
48
 
59
- if middleware_enabled
60
- alias_method :translate_without_copyray_comment, :translate
61
- alias_method :translate, :translate_with_copyray_comment
62
- alias :t :translate
63
- end
64
- end
49
+ # NOTE: mailer は request を持たず request.format で判定できない。かつメール本文への
50
+ # マーカー混入は実害が大きいため、controller の型で明示除外する。
51
+ return false if defined?(ActionMailer::Base) && current_controller.is_a?(ActionMailer::Base)
52
+
53
+ # NOTE: request が無い/format が html でない経路には注入しない。&. と || false で
54
+ # request 不在時も安全に false を返す。
55
+ current_controller.request&.format&.html? || false
56
+ end
57
+ private :copyray_injectable?
58
+ end
59
+ private_constant :CopyrayCommentInjection
60
+
61
+ def self.hook_translation_helper(mod, middleware_enabled:)
62
+ return unless middleware_enabled
63
+
64
+ mod.include(CopyrayCommentInjection)
65
+ mod.class_eval do
66
+ alias_method :translate_without_copyray_comment, :translate
67
+ alias_method :translate, :translate_with_copyray_comment
68
+ alias_method :t, :translate
65
69
  end
66
70
  end
67
71
  end
@@ -57,7 +57,7 @@ module CopyTunerClient
57
57
 
58
58
  private
59
59
 
60
- def lookup(locale, key, scope = [], options = {}) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
60
+ def lookup(locale, key, scope = [], options = {})
61
61
  return nil if !key.is_a?(String) && !key.is_a?(Symbol)
62
62
 
63
63
  parts = I18n.normalize_keys(locale, key, scope, options[:separator])
@@ -68,21 +68,14 @@ module CopyTunerClient
68
68
  # ローカル config/locales(I18n::Backend::Simple)を優先する。段階的にローカルへ移行するための仕組み。
69
69
  # ローカルに無い場合は nil(未訳)のまま返し、copy_tuner へのフォールバックも空キー登録も行わない(完全分離)。
70
70
  # ignored_keys より先に評価することで、両方にマッチするキーでも確実にローカルへ委譲する。
71
- if local_first_key?(key_without_locale)
72
- return super
73
- end
71
+ return super if local_first_key?(key_without_locale)
74
72
 
75
- config = CopyTunerClient.configuration
76
- if config.ignored_keys.include?(key_without_locale)
77
- config.ignored_key_handler.call(IgnoredKey.new("Ignored key: #{key_without_locale}"))
78
- end
73
+ handle_ignored_key(key_without_locale)
79
74
 
80
75
  # NOTE: ハッシュ化した場合に削除されるキーに対応するため、最初に完全一致をチェック(旧クライアントの動作を維持)
81
76
  # 例: `en.test.key` が `en.test.key.conflict` のように別のキーで上書きされている場合の対応
82
77
  exact_match = cache[key_with_locale]
83
- if exact_match
84
- return exact_match
85
- end
78
+ return exact_match if exact_match
86
79
 
87
80
  ensure_tree_cache_current
88
81
  tree_result = lookup_in_tree_cache(parts)
@@ -90,13 +83,18 @@ module CopyTunerClient
90
83
 
91
84
  content = super
92
85
 
93
- if content.nil?
94
- cache[key_with_locale] = nil
95
- end
86
+ cache[key_with_locale] = nil if content.nil?
96
87
 
97
88
  content
98
89
  end
99
90
 
91
+ def handle_ignored_key(key_without_locale)
92
+ config = CopyTunerClient.configuration
93
+ return unless config.ignored_keys.include?(key_without_locale)
94
+
95
+ config.ignored_key_handler.call(IgnoredKey.new("Ignored key: #{key_without_locale}"))
96
+ end
97
+
100
98
  def ensure_tree_cache_current
101
99
  current_version = cache.version
102
100
  # ETag が nil の場合(初回ダウンロード前)や変更があった場合のみ更新
@@ -1,10 +1,11 @@
1
1
  require 'nokogiri'
2
2
 
3
3
  module CopyTunerClient
4
+ # `_html` サフィックス無しに HTML タグを含む訳文(i18n の HTML 安全規約違反)を検出する
4
5
  module I18nCompat
5
6
  def select_html_incompatible_blurbs(blurbs)
6
7
  non_html_key_blurbs = blurbs.reject { |key| key.ends_with?('.html') || key.ends_with?('_html') }
7
- html_blurbs = non_html_key_blurbs.select do |key, content|
8
+ non_html_key_blurbs.select do |_key, content|
8
9
  Nokogiri::HTML.fragment(content).children.any? { |node| node.name != 'text' }
9
10
  end
10
11
  end
@@ -1,4 +1,3 @@
1
- require 'thread'
2
1
  require 'copy_tuner_client/cache'
3
2
  require 'copy_tuner_client/queue_with_timeout'
4
3
 
@@ -30,7 +29,7 @@ module CopyTunerClient
30
29
  def stop
31
30
  @mutex.synchronize do
32
31
  @command_queue.uniq_push(:stop)
33
- @thread.join if @thread
32
+ @thread&.join
34
33
  @thread = nil
35
34
  end
36
35
  end
@@ -59,8 +58,8 @@ module CopyTunerClient
59
58
  end
60
59
  end
61
60
  @logger.info 'stop poller thread'
62
- rescue InvalidApiKey => error
63
- logger.error(error.message)
61
+ rescue InvalidApiKey => e
62
+ logger.error(e.message)
64
63
  end
65
64
  end
66
65
  end
@@ -1,4 +1,5 @@
1
1
  module CopyTunerClient
2
+ # ログ出力にプレフィックスとプロセス/スレッド情報を付加する Logger ラッパー
2
3
  class PrefixedLogger
3
4
  attr_reader :prefix, :original_logger
4
5
 
@@ -7,24 +8,24 @@ module CopyTunerClient
7
8
  @original_logger = logger
8
9
  end
9
10
 
10
- def info(message = nil, &block)
11
- log(:info, message, &block)
11
+ def info(message = nil, &)
12
+ log(:info, message, &)
12
13
  end
13
14
 
14
- def debug(message = nil, &block)
15
- log(:debug, message, &block)
15
+ def debug(message = nil, &)
16
+ log(:debug, message, &)
16
17
  end
17
18
 
18
- def warn(message = nil, &block)
19
- log(:warn, message, &block)
19
+ def warn(message = nil, &)
20
+ log(:warn, message, &)
20
21
  end
21
22
 
22
- def error(message = nil, &block)
23
- log(:error, message, &block)
23
+ def error(message = nil, &)
24
+ log(:error, message, &)
24
25
  end
25
26
 
26
- def fatal(message = nil, &block)
27
- log(:fatal, message, &block)
27
+ def fatal(message = nil, &)
28
+ log(:fatal, message, &)
28
29
  end
29
30
 
30
31
  def flush
@@ -33,9 +34,9 @@ module CopyTunerClient
33
34
 
34
35
  private
35
36
 
36
- def log(severity, message, &block)
37
+ def log(severity, message, &)
37
38
  prefixed_message = "#{prefix} #{thread_info} #{message}"
38
- original_logger.send(severity, prefixed_message, &block)
39
+ original_logger.public_send(severity, prefixed_message, &)
39
40
  end
40
41
 
41
42
  def thread_info
@@ -3,7 +3,7 @@ module CopyTunerClient
3
3
  # process (such as in Unicorn or Passenger). Also registers hooks for exiting
4
4
  # processes and completing background jobs. Applications using the client
5
5
  # will not need to interact with this class directly.
6
- class ProcessGuard
6
+ class ProcessGuard # rubocop:disable Metrics/ClassLength
7
7
  # @param options [Hash]
8
8
  # @option options [Logger] :logger where errors should be logged
9
9
  def initialize(cache, poller, options)
@@ -33,15 +33,16 @@ module CopyTunerClient
33
33
  end
34
34
 
35
35
  def passenger_spawner?
36
- defined?(PhusionPassenger) && ($0.include?("Passenger AppPreloader") || $0.include?("ApplicationSpawner") || $0.include?("rack-preloader"))
36
+ defined?(PhusionPassenger) &&
37
+ ['Passenger AppPreloader', 'ApplicationSpawner', 'rack-preloader'].any? { |name| $PROGRAM_NAME.include?(name) }
37
38
  end
38
39
 
39
40
  def unicorn_spawner?
40
- defined?(Unicorn::HttpServer) && ($0.include?("unicorn") && !caller.any? { |line| line.include?("worker_loop") })
41
+ defined?(Unicorn::HttpServer) && $PROGRAM_NAME.include?('unicorn') && caller.none? { |line| line.include?('worker_loop') }
41
42
  end
42
43
 
43
44
  def puma_spawner?
44
- defined?(Puma::Runner) && $0.include?('puma')
45
+ defined?(Puma::Runner) && $PROGRAM_NAME.include?('puma')
45
46
  end
46
47
 
47
48
  def delayed_job_spawner?
@@ -49,11 +50,11 @@ module CopyTunerClient
49
50
  # - bin/delayed_job start
50
51
  # - bin/rake jobs:work
51
52
  # 前者の呼び出しでのみジョブ処理用の子プロセスが作られるため、 poller を作るフックを仕込む必要がある。
52
- defined?(Delayed::Worker) && $0.include?('delayed_job')
53
+ defined?(Delayed::Worker) && $PROGRAM_NAME.include?('delayed_job')
53
54
  end
54
55
 
55
56
  def good_job_spawner?
56
- $0.include?('good_job') && defined?(GoodJob) && ARGV.include?('--daemonize')
57
+ $PROGRAM_NAME.include?('good_job') && defined?(GoodJob) && ARGV.include?('--daemonize')
57
58
  end
58
59
 
59
60
  def register_spawn_hooks
@@ -71,17 +72,17 @@ module CopyTunerClient
71
72
  end
72
73
 
73
74
  def register_passenger_hook
74
- @logger.info("Registered Phusion Passenger fork hook")
75
- PhusionPassenger.on_event(:starting_worker_process) do |forked|
75
+ @logger.info('Registered Phusion Passenger fork hook')
76
+ PhusionPassenger.on_event(:starting_worker_process) do |_forked|
76
77
  start_polling
77
78
  end
78
79
  end
79
80
 
80
81
  def register_unicorn_hook
81
- @logger.info("Registered Unicorn fork hook")
82
+ @logger.info('Registered Unicorn fork hook')
82
83
  poller = @poller
83
84
  Unicorn::HttpServer.class_eval do
84
- alias_method :worker_loop_without_copy_tuner, :worker_loop
85
+ alias_method(:worker_loop_without_copy_tuner, :worker_loop)
85
86
  define_method :worker_loop do |worker|
86
87
  poller.start
87
88
  worker_loop_without_copy_tuner(worker)
@@ -90,10 +91,10 @@ module CopyTunerClient
90
91
  end
91
92
 
92
93
  def register_delayed_hook
93
- @logger.info("Registered Delayed::Job start hook")
94
+ @logger.info('Registered Delayed::Job start hook')
94
95
  poller = @poller
95
96
  Delayed::Worker.class_eval do
96
- alias_method :start_without_copy_tuner, :start
97
+ alias_method(:start_without_copy_tuner, :start)
97
98
  define_method :start do
98
99
  poller.start
99
100
  start_without_copy_tuner
@@ -102,21 +103,22 @@ module CopyTunerClient
102
103
  end
103
104
 
104
105
  def register_good_job_hook
105
- @logger.info("Registered good_job start hook")
106
+ @logger.info('Registered good_job start hook')
106
107
  poller = @poller
107
- hook_module = Module.new do
108
- define_method :daemon do
109
- super() # NOTE: define_method 内で super を呼ぶ場合は引数を明示的に指定する必要があるので注意
110
- poller.start
108
+ hook_module =
109
+ Module.new do
110
+ define_method :daemon do
111
+ super() # NOTE: define_method 内で super を呼ぶ場合は引数を明示的に指定する必要があるので注意
112
+ poller.start
113
+ end
111
114
  end
112
- end
113
- ::Process.singleton_class.prepend hook_module
115
+ ::Process.singleton_class.prepend(hook_module)
114
116
  end
115
117
 
116
118
  def register_puma_hook
117
119
  # If Puma is clustered mode without preload_app, this method is called on worker process.
118
120
  # Just start poller and return.
119
- if $0.include?('cluster worker')
121
+ if $PROGRAM_NAME.include?('cluster worker')
120
122
  @logger.info('Puma would be clustered mode without preload_app')
121
123
  @poller.start
122
124
  return
@@ -126,13 +128,14 @@ module CopyTunerClient
126
128
  # If Puma is clustered mode with preload_app, this method is called before fork.
127
129
  # Delay poller start until Puma::Runner#start_server which is called on worker process.
128
130
  poller = @poller
129
- hook_module = Module.new do
130
- define_method :start_server do
131
- poller.start
132
- super() # NOTE: define_method 内で super を呼ぶ場合は引数を明示的に指定する必要があるので注意
131
+ hook_module =
132
+ Module.new do
133
+ define_method :start_server do
134
+ poller.start
135
+ super() # NOTE: define_method 内で super を呼ぶ場合は引数を明示的に指定する必要があるので注意
136
+ end
133
137
  end
134
- end
135
- Puma::Runner.prepend hook_module
138
+ Puma::Runner.prepend(hook_module)
136
139
  end
137
140
 
138
141
  def register_exit_hooks
@@ -7,17 +7,17 @@ module CopyTunerClient
7
7
  @received = ConditionVariable.new
8
8
  end
9
9
 
10
- def <<(x)
10
+ def <<(item)
11
11
  @mutex.synchronize do
12
- @queue << x
12
+ @queue << item
13
13
  @received.signal
14
14
  end
15
15
  end
16
16
 
17
- def uniq_push(x)
17
+ def uniq_push(item)
18
18
  @mutex.synchronize do
19
- unless @queue.member?(x)
20
- @queue << x
19
+ unless @queue.member?(item)
20
+ @queue << item
21
21
  @received.signal
22
22
  end
23
23
  end
@@ -29,22 +29,32 @@ module CopyTunerClient
29
29
 
30
30
  def pop_with_timeout(timeout = nil)
31
31
  @mutex.synchronize do
32
- if timeout.nil?
33
- # wait indefinitely until there is an element in the queue
34
- while @queue.empty?
35
- @received.wait(@mutex)
36
- end
37
- elsif @queue.empty? && timeout != 0
38
- # wait for element or timeout
39
- timeout_time = timeout + Time.now.to_f
40
- while @queue.empty? && (remaining_time = timeout_time - Time.now.to_f) > 0
41
- @received.wait(@mutex, remaining_time)
42
- end
43
- end
44
- #if we're still empty after the timeout, raise exception
45
- raise ThreadError, "queue empty" if @queue.empty?
32
+ wait_for_item(timeout)
33
+
34
+ # if we're still empty after the timeout, raise exception
35
+ raise ThreadError, 'queue empty' if @queue.empty?
36
+
46
37
  @queue.shift
47
38
  end
48
39
  end
40
+
41
+ private
42
+
43
+ def wait_for_item(timeout)
44
+ if timeout.nil?
45
+ # wait indefinitely until there is an element in the queue
46
+ @received.wait(@mutex) while @queue.empty?
47
+ elsif @queue.empty? && timeout != 0
48
+ wait_with_timeout(timeout)
49
+ end
50
+ end
51
+
52
+ def wait_with_timeout(timeout)
53
+ # wait for element or timeout
54
+ timeout_time = timeout + Time.now.to_f
55
+ while @queue.empty? && (remaining_time = timeout_time - Time.now.to_f).positive?
56
+ @received.wait(@mutex, remaining_time)
57
+ end
58
+ end
49
59
  end
50
60
  end
@@ -4,15 +4,16 @@ module CopyTunerClient
4
4
  # Sets up the logger, environment, name, project root, and framework name
5
5
  # for Rails applications. Must be called after framework initialization.
6
6
  def self.initialize
7
- CopyTunerClient.configure(false) do |config|
7
+ CopyTunerClient.configure(apply: false) do |config|
8
8
  config.environment_name = ::Rails.env
9
- config.logger = if defined?(::Rails::Console)
10
- Logger.new('/dev/null')
11
- elsif defined?(::Rails) && ::Rails.env.development?
12
- Logger.new('log/copy_tuner.log')
13
- else
14
- ::Rails.logger
15
- end
9
+ config.logger =
10
+ if defined?(::Rails::Console)
11
+ Logger.new(File::NULL)
12
+ elsif defined?(::Rails) && ::Rails.env.development?
13
+ Logger.new('log/copy_tuner.log')
14
+ else
15
+ ::Rails.logger
16
+ end
16
17
  config.framework = "Rails: #{::Rails::VERSION::STRING}"
17
18
  config.middleware = ::Rails.configuration.middleware
18
19
  config.download_cache_dir = ::Rails.root.join('tmp', 'cache', 'copy_tuner_client')