copy_tuner_client 2.1.1 → 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.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/.rubocop.yml +46 -1
- data/CLAUDE.md +1 -0
- data/Gemfile +1 -0
- data/copy_tuner_client.gemspec +3 -3
- data/gemfiles/8.0.gemfile +3 -3
- data/gemfiles/8.1.gemfile +3 -3
- data/gemfiles/main.gemfile +3 -3
- data/lib/copy_tuner_client/cache.rb +34 -27
- data/lib/copy_tuner_client/client.rb +35 -23
- data/lib/copy_tuner_client/configuration.rb +62 -40
- data/lib/copy_tuner_client/copyray/rewriter.rb +7 -4
- data/lib/copy_tuner_client/copyray.rb +1 -0
- data/lib/copy_tuner_client/copyray_middleware.rb +39 -23
- data/lib/copy_tuner_client/dotted_hash.rb +7 -7
- data/lib/copy_tuner_client/engine.rb +4 -6
- data/lib/copy_tuner_client/helper_extension.rb +58 -54
- data/lib/copy_tuner_client/i18n_backend.rb +12 -14
- data/lib/copy_tuner_client/i18n_compat.rb +2 -1
- data/lib/copy_tuner_client/poller.rb +3 -4
- data/lib/copy_tuner_client/prefixed_logger.rb +13 -12
- data/lib/copy_tuner_client/process_guard.rb +29 -26
- data/lib/copy_tuner_client/queue_with_timeout.rb +29 -19
- data/lib/copy_tuner_client/rails.rb +9 -8
- data/lib/copy_tuner_client/request_sync.rb +26 -24
- data/lib/copy_tuner_client/simple_form_extention.rb +10 -6
- data/lib/copy_tuner_client/translation_log.rb +10 -5
- data/lib/copy_tuner_client/version.rb +1 -1
- data/lib/copy_tuner_client.rb +4 -8
- data/lib/tasks/copy_tuner_client_tasks.rake +14 -16
- data/skills/copy-tuner-to-locales-migrate-prefix/scripts/migrate_prefix.rb +14 -7
- data/skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb +19 -17
- data/spec/copy_tuner_client/cache_spec.rb +63 -57
- data/spec/copy_tuner_client/client_spec.rb +82 -81
- data/spec/copy_tuner_client/configuration_spec.rb +223 -222
- data/spec/copy_tuner_client/copyray/marker_spec.rb +2 -1
- data/spec/copy_tuner_client/copyray/rewriter_spec.rb +24 -3
- data/spec/copy_tuner_client/copyray_middleware_spec.rb +42 -5
- data/spec/copy_tuner_client/copyray_spec.rb +8 -8
- data/spec/copy_tuner_client/dotted_hash_spec.rb +33 -33
- data/spec/copy_tuner_client/helper_extension_spec.rb +47 -44
- data/spec/copy_tuner_client/i18n_backend_spec.rb +144 -143
- data/spec/copy_tuner_client/poller_spec.rb +25 -24
- data/spec/copy_tuner_client/prefixed_logger_spec.rb +15 -12
- data/spec/copy_tuner_client/process_guard_spec.rb +27 -27
- data/spec/copy_tuner_client/request_sync_spec.rb +51 -58
- data/spec/copy_tuner_client/translation_log_spec.rb +38 -24
- data/spec/copy_tuner_client_spec.rb +5 -6
- data/spec/support/client_spec_helpers.rb +1 -1
- data/spec/support/defines_constants.rb +3 -34
- data/spec/support/fake_client.rb +1 -3
- data/spec/support/fake_copy_tuner_app.rb +54 -62
- data/spec/support/fake_html_safe_string.rb +2 -3
- data/spec/support/fake_logger.rb +22 -21
- data/spec/support/fake_passenger.rb +4 -6
- data/spec/support/fake_unicorn.rb +2 -4
- data/spec/support/middleware_stack.rb +3 -3
- data/spec/support/writing_cache.rb +4 -4
- metadata +2 -2
|
@@ -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,22 +12,8 @@ module CopyTunerClient
|
|
|
11
12
|
def call(env)
|
|
12
13
|
CopyTunerClient::TranslationLog.clear
|
|
13
14
|
status, headers, response = @app.call(env)
|
|
14
|
-
if
|
|
15
|
-
|
|
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
|
-
body, skipped = CopyTunerClient::Copyray::Rewriter.rewrite(body)
|
|
21
|
-
body = append_js(body, csp_nonce, skipped: skipped)
|
|
22
|
-
content_length = body.bytesize.to_s
|
|
23
|
-
headers['Content-Length'] = content_length
|
|
24
|
-
# maintains compatibility with other middlewares
|
|
25
|
-
if defined?(ActionDispatch::Response::RackBody) && ActionDispatch::Response::RackBody === response
|
|
26
|
-
ActionDispatch::Response.new(status, headers, [body]).to_a
|
|
27
|
-
else
|
|
28
|
-
[status, headers, [body]]
|
|
29
|
-
end
|
|
15
|
+
if rewritable?(status, headers) && (body = response_body(response))
|
|
16
|
+
rewrite_response(env, status, headers, body, response)
|
|
30
17
|
else
|
|
31
18
|
[status, headers, response]
|
|
32
19
|
end
|
|
@@ -34,6 +21,30 @@ module CopyTunerClient
|
|
|
34
21
|
|
|
35
22
|
private
|
|
36
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
|
+
|
|
37
48
|
def helpers
|
|
38
49
|
ActionController::Base.helpers
|
|
39
50
|
end
|
|
@@ -53,7 +64,8 @@ module CopyTunerClient
|
|
|
53
64
|
keysSkipped: #{skipped},
|
|
54
65
|
}
|
|
55
66
|
SCRIPT
|
|
56
|
-
|
|
67
|
+
tag = helpers.javascript_include_tag('copytuner', type: 'module', crossorigin: 'anonymous', nonce: csp_nonce)
|
|
68
|
+
append_to_html_body(html, tag)
|
|
57
69
|
end
|
|
58
70
|
|
|
59
71
|
def append_to_html_body(html, content)
|
|
@@ -61,18 +73,22 @@ module CopyTunerClient
|
|
|
61
73
|
return html unless html.include?('</body>')
|
|
62
74
|
|
|
63
75
|
position = html.rindex('</body>')
|
|
64
|
-
html.insert(position, content
|
|
76
|
+
html.insert(position, "#{content}\n")
|
|
65
77
|
end
|
|
66
78
|
|
|
67
79
|
def file?(headers)
|
|
68
|
-
headers[
|
|
80
|
+
headers['Content-Transfer-Encoding'] == 'binary'
|
|
69
81
|
end
|
|
70
82
|
|
|
71
|
-
def
|
|
83
|
+
def rewritable?(status, headers)
|
|
72
84
|
[200, 422].include?(status) &&
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
headers['Content-Type'] &&
|
|
86
|
+
(headers['Content-Type'].include?('text/html') || turbo_stream?(headers)) &&
|
|
87
|
+
!file?(headers)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def turbo_stream?(headers)
|
|
91
|
+
headers['Content-Type']&.include?('text/vnd.turbo-stream.html')
|
|
76
92
|
end
|
|
77
93
|
|
|
78
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 =
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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, :
|
|
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, :
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
return source
|
|
11
|
-
end
|
|
11
|
+
return source if controller && CopyTunerClient::Rails.controller_of_rails_engine?(controller)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
# NOTE: マーカーは HTML コメントとしてブラウザに無視されつつ Copyray オーバーレイのキー特定に
|
|
21
|
+
# 使われる。HTML 以外の経路(メール本文・render :json・CSV/PDF など)ではコメントが文字列として
|
|
22
|
+
# 出力に混入してしまうため、それらの経路には注入しない。default 引数による初期値登録は維持する
|
|
23
|
+
# 必要があるため、このガードは初期値登録(上の I18n.t 呼び出し)より後に置く。
|
|
24
|
+
return source unless copyray_injectable?
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 = {})
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =>
|
|
63
|
-
logger.error(
|
|
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, &
|
|
11
|
-
log(:info, message, &
|
|
11
|
+
def info(message = nil, &)
|
|
12
|
+
log(:info, message, &)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
def debug(message = nil, &
|
|
15
|
-
log(:debug, message, &
|
|
15
|
+
def debug(message = nil, &)
|
|
16
|
+
log(:debug, message, &)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
def warn(message = nil, &
|
|
19
|
-
log(:warn, message, &
|
|
19
|
+
def warn(message = nil, &)
|
|
20
|
+
log(:warn, message, &)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
def error(message = nil, &
|
|
23
|
-
log(:error, message, &
|
|
23
|
+
def error(message = nil, &)
|
|
24
|
+
log(:error, message, &)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def fatal(message = nil, &
|
|
27
|
-
log(:fatal, message, &
|
|
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, &
|
|
37
|
+
def log(severity, message, &)
|
|
37
38
|
prefixed_message = "#{prefix} #{thread_info} #{message}"
|
|
38
|
-
original_logger.
|
|
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) &&
|
|
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) &&
|
|
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) && $
|
|
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) && $
|
|
53
|
+
defined?(Delayed::Worker) && $PROGRAM_NAME.include?('delayed_job')
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def good_job_spawner?
|
|
56
|
-
$
|
|
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(
|
|
75
|
-
PhusionPassenger.on_event(:starting_worker_process) do |
|
|
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(
|
|
82
|
+
@logger.info('Registered Unicorn fork hook')
|
|
82
83
|
poller = @poller
|
|
83
84
|
Unicorn::HttpServer.class_eval do
|
|
84
|
-
alias_method
|
|
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(
|
|
94
|
+
@logger.info('Registered Delayed::Job start hook')
|
|
94
95
|
poller = @poller
|
|
95
96
|
Delayed::Worker.class_eval do
|
|
96
|
-
alias_method
|
|
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(
|
|
106
|
+
@logger.info('Registered good_job start hook')
|
|
106
107
|
poller = @poller
|
|
107
|
-
hook_module =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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 $
|
|
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 =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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 <<(
|
|
10
|
+
def <<(item)
|
|
11
11
|
@mutex.synchronize do
|
|
12
|
-
@queue <<
|
|
12
|
+
@queue << item
|
|
13
13
|
@received.signal
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def uniq_push(
|
|
17
|
+
def uniq_push(item)
|
|
18
18
|
@mutex.synchronize do
|
|
19
|
-
unless @queue.member?(
|
|
20
|
-
@queue <<
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|