deprecation_collector 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7936a3e78d550c3940f0abe364171df3b39cc1e19931cfba23b8cfdb060122ca
4
- data.tar.gz: 2a4d06a0baee48c61c027c7791fef7894b5af5d1791fe0e3c1bd3afaf2fa61ab
3
+ metadata.gz: 02cc02dc5d90e6b304ac5dadfc7ee09c1ac684ced0d3e0657095a47ffdc88e82
4
+ data.tar.gz: ce2b0e299aed5c65f302ffb4fb72d49e7ea1f44772a5b6d61eeae1807eecb1fd
5
5
  SHA512:
6
- metadata.gz: c5a10c07173fe8b73003630c2fa0839b07c62f0471b72269aabdcece19935f6693fc6b0a3a3c8634b1731b7cc9db0aae186ff2067a43047692e15f32180d5210
7
- data.tar.gz: b6e598c579eb46756b9a78336fd3b223d4224b01f5a677b55375732279564c9b609dc2e87083a6cb47cb5e86c567c73102465080e275818564d94c1d29b29408
6
+ metadata.gz: b468f3a95e56ced60c3de42996f1d6c4d9249588dfe972c321aaf3dcf80f81cf9048dc040b14c9a604f7c01d780385de2ec4e3ea1cb5dbda47ac541bd20d1a83
7
+ data.tar.gz: a245eb05033c494e514ba10e0ba63888bffb5e7fb86365a9e09cc425674be7c01df17d7c4870af60011c5a26600f07825e3a74971b5b690859762880f35b33e9
data/.rubocop.yml CHANGED
@@ -1,5 +1,7 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
2
+ # spec/ have TargetRubyVersion: 2.6
3
+ TargetRubyVersion: 2.4
4
+ NewCops: enable
3
5
 
4
6
  Style/StringLiterals:
5
7
  Enabled: true
@@ -11,3 +13,8 @@ Style/StringLiteralsInInterpolation:
11
13
 
12
14
  Layout/LineLength:
13
15
  Max: 120
16
+
17
+ Metrics/ClassLength: { Max: 200 }
18
+ Metrics/MethodLength: { Max: 15 }
19
+ Metrics/CyclomaticComplexity: { Max: 9 }
20
+ Metrics/PerceivedComplexity: { Max: 9 }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
+ == unreleased
1
2
 
2
3
 
4
+ == 0.0.2
5
+
6
+ - Reorganized code
7
+
3
8
  == 0.0.1
4
9
 
5
- Initial release
10
+ - Initial release
data/Gemfile CHANGED
@@ -11,6 +11,10 @@ gem "rspec", "~> 3.0"
11
11
  gem "timecop"
12
12
 
13
13
  gem "rubocop", "~> 1.21"
14
+ gem "rubocop-performance"
15
+ gem "rubocop-rails"
16
+ gem "rubocop-rake"
17
+ gem "rubocop-rspec"
14
18
 
15
19
  # TODO: appraisals
16
- gem "rails", '6.0'
20
+ gem "rails", "6.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deprecation_collector (0.0.1)
4
+ deprecation_collector (0.0.2)
5
5
  redis (>= 2.0)
6
6
 
7
7
  GEM
@@ -150,6 +150,17 @@ GEM
150
150
  unicode-display_width (>= 1.4.0, < 3.0)
151
151
  rubocop-ast (1.18.0)
152
152
  parser (>= 3.1.1.0)
153
+ rubocop-performance (1.14.3)
154
+ rubocop (>= 1.7.0, < 2.0)
155
+ rubocop-ast (>= 0.4.0)
156
+ rubocop-rails (2.15.2)
157
+ activesupport (>= 4.2.0)
158
+ rack (>= 1.1)
159
+ rubocop (>= 1.7.0, < 2.0)
160
+ rubocop-rake (0.6.0)
161
+ rubocop (~> 1.0)
162
+ rubocop-rspec (2.11.1)
163
+ rubocop (~> 1.19)
153
164
  ruby-progressbar (1.11.0)
154
165
  sprockets (4.0.3)
155
166
  concurrent-ruby (~> 1.0)
@@ -178,6 +189,10 @@ DEPENDENCIES
178
189
  rake (~> 13.0)
179
190
  rspec (~> 3.0)
180
191
  rubocop (~> 1.21)
192
+ rubocop-performance
193
+ rubocop-rails
194
+ rubocop-rake
195
+ rubocop-rspec
181
196
  timecop
182
197
 
183
198
  BUNDLED WITH
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # DeprecationCollector
2
+ [![Gem Version](https://badge.fury.io/rb/deprecation_collector.svg)](https://badge.fury.io/rb/deprecation_collector)
2
3
 
3
4
  Collects ruby and rails deprecation warnings.
4
5
  (gem is a work-in-process, documentation will come later)
@@ -7,15 +8,30 @@ Collects ruby and rails deprecation warnings.
7
8
 
8
9
  Install the gem and add to the application's Gemfile by executing:
9
10
 
10
- $ bundle add deprecation_collector
11
-
12
- If bundler is not being used to manage dependencies, install the gem by executing:
13
-
14
- $ gem install deprecation_collector
11
+ ```sh
12
+ bundle add deprecation_collector
13
+ ```
15
14
 
16
15
  ## Usage
17
16
 
18
-
17
+ Add an initializer with configuration, like
18
+
19
+ ```ruby
20
+ DeprecationCollector.create_instance(redis: your_redis_connection)
21
+ Rails.application.config.to_prepare do
22
+ DeprecationCollector.install do |instance|
23
+ instance.app_revision = ::GIT_REVISION
24
+ instance.count = false
25
+ instance.save_full_backtrace = true
26
+ instance.raise_on_deprecation = false
27
+ instance.write_interval = (::Rails.env.production? && 15.minutes) || 1.minute
28
+ instance.exclude_realms = %i[kernel] if Rails.env.production?
29
+ instance.ignored_messages = [
30
+ "Ignoring db/schema_cache.yml because it has expired"
31
+ ]
32
+ end
33
+ end
34
+ ```
19
35
 
20
36
  ## Development
21
37
 
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ class DeprecationCollector
5
+ class << self
6
+ protected
7
+
8
+ def install_collectors
9
+ tap_activesupport if defined?(ActiveSupport::Deprecation)
10
+ tap_kernel
11
+ tap_warning_class
12
+ end
13
+
14
+ def tap_warning_class
15
+ Warning.singleton_class.prepend(DeprecationCollector::WarningCollector)
16
+ Warning[:deprecated] = true if Warning.respond_to?(:[]=) # turn on ruby 2.7 deprecations
17
+ end
18
+
19
+ def tap_activesupport
20
+ # TODO: a more polite hook
21
+ ActiveSupport::Deprecation.behavior = lambda do |message, callstack, deprecation_horizon, gem_name|
22
+ # not polite to turn off all other possible behaviors, but otherwise may get duplicate calls
23
+ DeprecationCollector.collect(message, callstack, :rails)
24
+ ActiveSupport::Deprecation::DEFAULT_BEHAVIORS[stock_activesupport_behavior].call(
25
+ message, callstack, deprecation_horizon, gem_name
26
+ )
27
+ end
28
+ end
29
+
30
+ def stock_activesupport_behavior
31
+ Rails.application&.config&.active_support&.deprecation || :log
32
+ end
33
+
34
+ def tap_kernel
35
+ Kernel.class_eval do
36
+ # module is included in others thus prepend does not work
37
+ remove_method :warn
38
+ class << self
39
+ remove_method :warn
40
+ end
41
+ module_function(define_method(:warn) do |*messages, **kwargs|
42
+ KernelWarningCollector.warn(*messages, backtrace: caller, **kwargs)
43
+ end)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Ruby sometimes has two warnings for one actual occurence
49
+ # Example:
50
+ # caller.rb:1: warning: Passing the keyword argument as the last hash parameter is deprecated
51
+ # calleee.rb:1: warning: The called method `method_name' is defined here
52
+ module MultipartWarningJoiner
53
+ module_function
54
+
55
+ def two_part_warning?(str)
56
+ # see ruby src - `rb_warn`, `rb_compile_warn`
57
+ str.end_with?(
58
+ "uses the deprecated method signature, which takes one parameter\n", # respond_to?
59
+ # 2.7 kwargs:
60
+ "maybe ** should be added to the call\n",
61
+ "Passing the keyword argument as the last hash parameter is deprecated\n", # бывает и не двойной
62
+ "Splitting the last argument into positional and keyword parameters is deprecated\n"
63
+ ) ||
64
+ str.include?("warning: already initialized constant") ||
65
+ str.include?("warning: method redefined; discarding old")
66
+ end
67
+
68
+ def handle(new_str)
69
+ old_str = Thread.current[:multipart_warning_str]
70
+ Thread.current[:multipart_warning_str] = nil
71
+ if old_str
72
+ return yield(old_str + new_str) if new_str.include?("is defined here") || new_str.include?(" was here")
73
+
74
+ yield(old_str)
75
+ end
76
+
77
+ return (Thread.current[:multipart_warning_str] = new_str) if two_part_warning?(new_str)
78
+
79
+ yield(new_str)
80
+ end
81
+ end
82
+
83
+ # taps into ruby core Warning#warn
84
+ module WarningCollector
85
+ def warn(str)
86
+ backtrace = caller
87
+ MultipartWarningJoiner.handle(str) do |multi_str|
88
+ DeprecationCollector.collect(multi_str, backtrace, :warning)
89
+ end
90
+ end
91
+ end
92
+
93
+ # for tapping into Kernel#warn
94
+ module KernelWarningCollector
95
+ module_function
96
+
97
+ def warn(*messages, backtrace: nil, **_kwargs)
98
+ backtrace ||= caller
99
+ str = messages.map(&:to_s).join("\n").strip
100
+ DeprecationCollector.collect(str, backtrace, :kernel)
101
+ # not passing to `super` - it will pass to Warning#warn, we do not want that
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DeprecationCollector
4
+ # :nodoc:
5
+ class Deprecation
6
+ attr_reader :message, :realm, :gem_traceline, :app_traceline, :occurences, :full_backtrace
7
+
8
+ CLEANUP_REGEXES = {
9
+ # rails views generated methods names are unique per-worker
10
+ /_app_views_(\w+)__(\d+)_(\d+)/ => "_app_views_\\1__",
11
+
12
+ # repl line numbers are not important, may be ignore all repl at all
13
+ /\A\((pry|irb)\):\d+/ => '(\1)'
14
+ }.freeze
15
+
16
+ def initialize(message, realm = nil, backtrace = [], cleanup_prefixes = [])
17
+ # backtrace is Thread::Backtrace::Location or array of strings for other realms
18
+ @message = message.dup
19
+ @realm = realm
20
+ @occurences = 0
21
+ @gem_traceline = find_gem_traceline(backtrace)
22
+ @app_traceline = find_app_traceline(backtrace)
23
+
24
+ cleanup_prefixes.each do |path|
25
+ @gem_traceline.delete_prefix!(path)
26
+ @message.gsub!(path, "")
27
+ end
28
+
29
+ CLEANUP_REGEXES.each_pair do |regex, replace|
30
+ @gem_traceline&.gsub!(regex, replace)
31
+ @app_traceline&.gsub!(regex, replace)
32
+ end
33
+
34
+ @full_backtrace = backtrace.map(&:to_s) if DeprecationCollector.instance.save_full_backtrace
35
+ end
36
+
37
+ def touch
38
+ @occurences += 1
39
+ end
40
+
41
+ def ignored?
42
+ false
43
+ end
44
+
45
+ def message_for_digest
46
+ # some gems like rest-client put data in warnings, need to aggregate
47
+ # + some bactrace per-worker unique method names may be there
48
+ @message.gsub(/"(?:[^"\\]|\\.)*"/, '""').gsub(/__\d+_\d+/, "___").gsub(/\((pry|irb)\):\d+/, '(\1)')
49
+ end
50
+
51
+ def digest
52
+ @digest ||= Digest::MD5.hexdigest(digest_base)
53
+ end
54
+
55
+ def digest_base
56
+ "1:#{RUBY_VERSION}:#{Rails.version}:#{message_for_digest}:#{gem_traceline}:#{app_traceline}"
57
+ end
58
+
59
+ def as_json(_options = {})
60
+ {
61
+ message: message,
62
+ realm: realm,
63
+ app_traceline: app_traceline,
64
+ gem_traceline: (gem_traceline != app_traceline && gem_traceline) || nil,
65
+ full_backtrace: full_backtrace,
66
+ ruby_version: RUBY_VERSION,
67
+ rails_version: (defined?(Rails) && Rails.version),
68
+ hostname: Socket.gethostname,
69
+ revision: DeprecationCollector.instance.app_revision,
70
+ count: @occurences, # output anyway for frequency estimation (during write_interval inside single process)
71
+ digest_base: digest_base # for debug purposes
72
+ }.compact
73
+ end
74
+
75
+ protected
76
+
77
+ def find_app_traceline(backtrace)
78
+ app_root = DeprecationCollector.instance.app_root_prefix
79
+ backtrace.find do |line|
80
+ line = line.to_s
81
+ (!line.start_with?("/") || line.start_with?(app_root)) && !line.include?("/gems/")
82
+ end&.to_s&.dup&.delete_prefix(app_root)
83
+ end
84
+
85
+ def find_gem_traceline(backtrace)
86
+ backtrace.find { |line| !line.to_s.include?("kernel_warn") }&.to_s&.dup || backtrace.first.to_s.dup
87
+ end
88
+ end
89
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeprecationCollector
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "deprecation_collector/version"
4
- require 'time'
5
- require 'redis'
4
+ require_relative "deprecation_collector/deprecation"
5
+ require_relative "deprecation_collector/collectors"
6
+ require "time"
7
+ require "redis"
6
8
 
9
+ # singleton class for collector
7
10
  class DeprecationCollector
8
- # NB: in production with hugreds of workers may easily overload redis with writes, so more delay needed:
9
- FLUSH_INTERVAL = (::Rails.env.production? && 15.minutes) || 1.minute
10
-
11
11
  @instance_mutex = Mutex.new
12
12
  @installed = false
13
13
  private_class_method :new
@@ -15,9 +15,13 @@ class DeprecationCollector
15
15
  def self.instance
16
16
  return @instance if defined?(@instance) && @instance
17
17
 
18
+ create_instance
19
+ end
20
+
21
+ def self.create_instance
18
22
  @instance_mutex.synchronize do
19
- # переиспользовать мутекс не обязательно, но он используется ровно один раз
20
- @instance ||= new($redis, mutex: @instance_mutex)
23
+ # no real need to reuse the mutex, but it is used only once here anyway
24
+ @instance ||= new(mutex: @instance_mutex)
21
25
  end
22
26
  @instance
23
27
  end
@@ -28,7 +32,7 @@ class DeprecationCollector
28
32
 
29
33
  # inside dev env may be called multiple times
30
34
  def self.install
31
- instance # to make it created, configuration comes later
35
+ create_instance # to make it created, configuration comes later
32
36
 
33
37
  @instance_mutex.synchronize do
34
38
  unless @installed
@@ -37,213 +41,63 @@ class DeprecationCollector
37
41
  end
38
42
 
39
43
  yield instance if block_given?
44
+ instance.fetch_known_digests
40
45
 
41
- # TODO: a more polite hook
42
- ActiveSupport::Deprecation.behavior = lambda do |message, callstack, deprecation_horizon, gem_name|
43
- # not polite to turn off all other possible behaviors, but otherwise may get duplicate calls
44
- DeprecationCollector.collect(message, callstack, :rails)
45
- ActiveSupport::Deprecation::DEFAULT_BEHAVIORS[Rails.application&.config&.active_support&.deprecation || :log].call(
46
- message, callstack, deprecation_horizon, gem_name
47
- )
48
- end
49
-
50
- Kernel.class_eval do
51
- # module is included in others thus prepend does not work
52
- remove_method :warn
53
- class << self
54
- remove_method :warn
55
- end
56
- module_function(define_method(:warn) do |*messages, **kwargs|
57
- KernelWarningCollector.warn(*messages, backtrace: caller, **kwargs)
58
- end)
59
- end
60
-
61
- Warning.singleton_class.prepend(DeprecationCollector::WarningCollector)
62
- Warning[:deprecated] = true if Warning.respond_to?(:[]=) # turn on ruby 2.7 deprecations
46
+ install_collectors
63
47
  end
64
48
 
65
49
  @instance
66
50
  end
67
51
 
68
- module MultipartWarningJoiner
69
- module_function
70
-
71
- # Ruby sometimes has two warnings for one actual occurence
72
- # Example:
73
- # caller.rb:1: warning: Passing the keyword argument as the last hash parameter is deprecated
74
- # calleee.rb:1: warning: The called method `method_name' is defined here
75
- def two_part_warning?(str)
76
- # see ruby src - `rb_warn`, `rb_compile_warn`
77
- str.end_with?(
78
- "uses the deprecated method signature, which takes one parameter\n", # respond_to?
79
- # 2.7 kwargs:
80
- "maybe ** should be added to the call\n",
81
- "Passing the keyword argument as the last hash parameter is deprecated\n", # бывает и не двойной
82
- "Splitting the last argument into positional and keyword parameters is deprecated\n"
83
- ) ||
84
- str.include?("warning: already initialized constant") ||
85
- str.include?("warning: method redefined; discarding old")
86
- end
87
-
88
- def handle(new_str)
89
- old_str = Thread.current[:multipart_warning_str]
90
- Thread.current[:multipart_warning_str] = nil
91
- if old_str
92
- return yield(old_str + new_str) if new_str.include?('is defined here') || new_str.include?(' was here')
93
- yield(old_str)
94
- end
95
-
96
- if two_part_warning?(new_str)
97
- Thread.current[:multipart_warning_str] = new_str
98
- return
99
- end
100
-
101
- yield(new_str)
102
- end
103
- end
104
-
105
- # taps into ruby core Warning#warn
106
- module WarningCollector
107
- def warn(str)
108
- backtrace = caller
109
- MultipartWarningJoiner.handle(str) do |multi_str|
110
- DeprecationCollector.collect(multi_str, backtrace, :warning)
111
- end
112
- end
113
- end
114
-
115
- module KernelWarningCollector
116
- module_function
117
-
118
- def warn(*messages, backtrace: nil, **_kwargs)
119
- backtrace ||= caller
120
- str = messages.map(&:to_s).join("\n").strip
121
- DeprecationCollector.collect(str, backtrace, :kernel)
122
- # not passing to `super` - it will pass to Warning#warn, we do not want that
123
- end
124
- end
125
-
126
- class Deprecation
127
- attr_reader :message, :realm, :gem_traceline, :app_traceline, :occurences, :full_backtrace
128
-
129
- def initialize(message, realm = nil, backtrace = [], cleanup_prefixes = [])
130
- # backtrace is Thread::Backtrace::Location or array of strings for other realms
131
- @message = message.dup
132
- @realm = realm
133
- @occurences = 0
134
- @gem_traceline = backtrace.find { |line| !line.to_s.include?('kernel_warn') }&.to_s&.dup ||
135
- backtrace.first.to_s.dup
136
- cleanup_prefixes.each do |path|
137
- @gem_traceline.delete_prefix!(path)
138
- @message.gsub!(path, '')
139
- end
140
-
141
- app_root = "#{DeprecationCollector.instance.app_root}/" # rubocop:disable Rails/FilePath
142
- @app_traceline = backtrace.find do |line|
143
- line = line.to_s
144
- (!line.start_with?('/') || line.start_with?(app_root)) && !line.include?('/gems/')
145
- end&.to_s&.dup&.delete_prefix(app_root)
146
-
147
- # rails views generated methods names are unique per-worker
148
- @gem_traceline&.gsub!(/_app_views_(\w+)__(\d+)_(\d+)/, "_app_views_\\1__")
149
- @app_traceline&.gsub!(/_app_views_(\w+)__(\d+)_(\d+)/, "_app_views_\\1__")
150
-
151
- # repl line numbers are not important, may be ignore all repl at all
152
- @app_traceline&.gsub!(/\A\((pry|irb)\):\d+/, '(\1)')
153
- @gem_traceline&.gsub!(/\A\((pry|irb)\):\d+/, '(\1)') # may contain app traceline, so filter too
154
-
155
- @full_backtrace = backtrace.map(&:to_s) if DeprecationCollector.instance.save_full_backtrace
156
- end
157
-
158
- def touch
159
- @occurences += 1
160
- end
161
-
162
- def ignored?
163
- false
164
- end
165
-
166
- def message_for_digest
167
- # some gems like rest-client put data in warnings, need to aggregate
168
- # + some bactrace per-worker unique method names may be there
169
- @message.gsub(/"(?:[^"\\]|\\.)*"/, '""').gsub(/__\d+_\d+/, '___').gsub(/\((pry|irb)\):\d+/, '(\1)')
170
- end
171
-
172
- def digest
173
- @digest ||= Digest::MD5.hexdigest(digest_base)
174
- end
175
-
176
- def digest_base
177
- "1:#{RUBY_VERSION}:#{Rails.version}:#{message_for_digest}:#{gem_traceline}:#{app_traceline}"
178
- end
179
-
180
- def as_json(_options = {})
181
- {
182
- message: message,
183
- realm: realm,
184
- app_traceline: app_traceline,
185
- gem_traceline: (gem_traceline != app_traceline && gem_traceline) || nil,
186
- full_backtrace: full_backtrace,
187
- ruby_version: RUBY_VERSION,
188
- rails_version: Rails.version,
189
- hostname: Socket.gethostname,
190
- revision: DeprecationCollector.instance.app_revision,
191
- count: @occurences, # output anyway for frequency estimation (during write_interval inside single process)
192
- digest_base: digest_base # for debug purposes
193
- }.compact
194
- end
195
- end
196
-
52
+ # NB: count is expensive in production env (but possible if needed) - produces a lot of redis writes
197
53
  attr_accessor :count, :raise_on_deprecation, :save_full_backtrace,
198
54
  :exclude_realms,
199
55
  :write_interval, :write_interval_jitter,
200
56
  :app_revision, :app_root
57
+ attr_writer :redis
201
58
 
202
- def initialize(redis, mutex: nil)
203
- @redis = redis
59
+ def initialize(mutex: nil)
204
60
  # on cruby hash itself is threadsafe, but we need to prevent races
205
61
  @deprecations_mutex = mutex || Mutex.new
206
62
  @deprecations = {}
207
63
  @known_digests = Set.new
208
64
  @last_write_time = current_time
65
+ @enabled = true
66
+
67
+ load_default_config
68
+ end
209
69
 
210
- # default config:
70
+ def load_default_config
71
+ @redis = defined?($redis) && $redis # rubocop:disable Style/GlobalVars
211
72
  @count = false
212
73
  @raise_on_deprecation = false
213
74
  @exclude_realms = []
214
- @write_interval = FLUSH_INTERVAL
215
- @write_interval_jitter = 60
216
- @enabled = true
217
75
  @ignore_message_regexp = nil
218
- @app_root = defined?(Rails) && Rails.root.present? || Dir.pwd
219
-
220
- fetch_known_digests # prevent fresh process from wiring frequent already known messages
76
+ @app_root = (defined?(Rails) && Rails.root.present? && Rails.root) || Dir.pwd
77
+ # NB: in production with hugreds of workers may easily overload redis with writes, so more delay needed:
78
+ @write_interval = 900 # 15.minutes
79
+ @write_interval_jitter = 60
221
80
  end
222
81
 
223
82
  def ignored_messages=(val)
224
83
  @ignore_message_regexp = (val && Regexp.union(val)) || nil
225
84
  end
226
85
 
86
+ def app_root_prefix
87
+ "#{app_root}/"
88
+ end
89
+
227
90
  def cleanup_prefixes
228
- @cleanup_prefixes ||= Gem.path + ["#{app_root}/"] # rubocop:disable Rails/FilePath
91
+ @cleanup_prefixes ||= Gem.path + [app_root_prefix]
229
92
  end
230
93
 
231
94
  def collect(message, backtrace, realm = :unknown)
232
- return unless @enabled
233
- return if exclude_realms.include?(realm)
234
- return if @ignore_message_regexp&.match?(message)
95
+ return if !@enabled || exclude_realms.include?(realm) || @ignore_message_regexp&.match?(message)
235
96
  raise "Deprecation: #{message}" if @raise_on_deprecation
236
97
 
237
98
  deprecation = Deprecation.new(message, realm, backtrace, cleanup_prefixes)
238
- return if deprecation.ignored?
239
-
240
- @deprecations_mutex.synchronize do
241
- (@deprecations[deprecation.digest] ||= deprecation).touch
242
- end
243
-
244
- write_to_redis if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
245
-
246
- $stderr.puts(message) if Rails.env.development? # rubocop:disable Style/StderrPuts
99
+ store_deprecation(deprecation)
100
+ log_deprecation_if_needed(deprecation)
247
101
  end
248
102
 
249
103
  def unsent_data?
@@ -254,40 +108,27 @@ class DeprecationCollector
254
108
  @count
255
109
  end
256
110
 
257
- def fetch_known_digests
258
- @known_digests.merge(@redis.hkeys('deprecations:data'))
111
+ def redis
112
+ raise "DeprecationCollector#redis is not set" unless @redis
113
+
114
+ @redis
259
115
  end
260
116
 
261
- def write_to_redis(force: false)
262
- return unless @enabled || force
117
+ def write_to_redis(force: false) # rubocop:disable Metrics/AbcSize
118
+ return unless force || (@enabled && (current_time > @last_write_time + @write_interval))
263
119
 
264
120
  deprecations_to_flush = nil
265
121
  @deprecations_mutex.synchronize do
266
- # check in this section to prevent multiple check requests
267
- unless enabled_in_redis?
268
- @enabled = false
269
- @deprecations = {}
270
- return
271
- end
272
-
273
- return unless force || current_time > @last_write_time + @write_interval
274
-
275
122
  deprecations_to_flush = @deprecations
276
123
  @deprecations = {}
277
124
  @last_write_time = current_time
125
+ # checking in this section to prevent multiple parallel check requests
126
+ return (@enabled = false) unless enabled_in_redis?
278
127
  end
279
128
 
280
- # count is expensive in production env (but possible if needed) - a lot of redis writes
281
- if count?
282
- @redis.pipelined do
283
- deprecations_to_flush.each_pair do |digest, deprecation|
284
- @redis.hincrby("deprecations:counter", digest, deprecation.occurences)
285
- end
286
- end
287
- end
129
+ write_count_to_redis(deprecations_to_flush) if count?
288
130
 
289
131
  # make as few writes as possible, other workers may already have reported our warning
290
- # TODO: at some point turn off writes?
291
132
  fetch_known_digests
292
133
  deprecations_to_flush.reject! { |digest, _val| @known_digests.include?(digest) }
293
134
  return unless deprecations_to_flush.any?
@@ -296,6 +137,11 @@ class DeprecationCollector
296
137
  @redis.mapped_hmset("deprecations:data", deprecations_to_flush.transform_values(&:to_json))
297
138
  end
298
139
 
140
+ # prevent fresh process from wiring frequent already known messages
141
+ def fetch_known_digests
142
+ @known_digests.merge(@redis.hkeys("deprecations:data"))
143
+ end
144
+
299
145
  def flush_redis(enable: false)
300
146
  @redis.del("deprecations:data", "deprecations:counter", "deprecations:notes")
301
147
  @redis.del("deprecations:enabled") if enable
@@ -339,49 +185,59 @@ class DeprecationCollector
339
185
  def read_one(digest)
340
186
  decode_deprecation(
341
187
  digest,
342
- @redis.hget("deprecations:data", digest),
343
- @redis.hget("deprecations:counter", digest),
344
- @redis.hget("deprecations:notes", digest)
188
+ *@redis.pipelined do
189
+ @redis.hget("deprecations:data", digest)
190
+ @redis.hget("deprecations:counter", digest)
191
+ @redis.hget("deprecations:notes", digest)
192
+ end
345
193
  )
346
194
  end
347
195
 
348
196
  def delete_deprecations(remove_digests)
197
+ return 0 unless remove_digests.any?
198
+
349
199
  @redis.pipelined do
350
200
  @redis.hdel("deprecations:data", *remove_digests)
351
201
  @redis.hdel("deprecations:notes", *remove_digests)
352
202
  @redis.hdel("deprecations:counter", *remove_digests) if @count
353
- end
203
+ end.first
354
204
  end
355
205
 
356
206
  def cleanup
357
207
  cursor = 0
358
- removed = 0
359
- total = 0
208
+ removed = total = 0
360
209
  loop do
361
- cursor, data_pairs = @redis.hscan("deprecations:data", cursor)
362
-
363
- if data_pairs.any?
364
- remove_digests = []
365
- total += data_pairs.size
366
- data_pairs.each do |(digest, data)|
367
- data = JSON.parse(data, symbolize_names: true)
368
- remove_digests << digest if !block_given? || yield(data)
369
- end
370
-
371
- if remove_digests.any?
372
- delete_deprecations(remove_digests)
373
- removed += remove_digests.size
374
- end
375
- end
210
+ cursor, data_pairs = @redis.hscan("deprecations:data", cursor) # NB: some pages may be empty
211
+ total += data_pairs.size
212
+ removed += delete_deprecations(
213
+ data_pairs.select { |_digest, data| !block_given? || yield(JSON.parse(data, symbolize_names: true)) }.keys
214
+ )
376
215
  break if cursor == "0"
377
216
  end
378
217
  "#{removed} removed, #{total - removed} left"
379
218
  end
380
219
 
381
- private
220
+ protected
221
+
222
+ def store_deprecation(deprecation)
223
+ return if deprecation.ignored?
224
+
225
+ @deprecations_mutex.synchronize do
226
+ (@deprecations[deprecation.digest] ||= deprecation).touch
227
+ end
228
+
229
+ write_to_redis if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
230
+ end
231
+
232
+ def log_deprecation_if_needed(deprecation)
233
+ return unless defined?(Rails) && Rails.env.development? && !deprecation.ignored?
234
+
235
+ $stderr.puts(deprecation.message) # rubocop:disable Style/StderrPuts
236
+ end
382
237
 
383
238
  def current_time
384
239
  return Time.zone.now if Time.respond_to?(:zone) && Time.zone
240
+
385
241
  Time.now
386
242
  end
387
243
 
@@ -392,4 +248,12 @@ class DeprecationCollector
392
248
  data[:count] = count.to_i if count
393
249
  data
394
250
  end
251
+
252
+ def write_count_to_redis(deprecations_to_flush)
253
+ @redis.pipelined do
254
+ deprecations_to_flush.each_pair do |digest, deprecation|
255
+ @redis.hincrby("deprecations:counter", digest, deprecation.occurences)
256
+ end
257
+ end
258
+ end
395
259
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deprecation_collector
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vasily Fedoseyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-29 00:00:00.000000000 Z
11
+ date: 2022-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -43,6 +43,8 @@ files:
43
43
  - Rakefile
44
44
  - deprecation_collector.gemspec
45
45
  - lib/deprecation_collector.rb
46
+ - lib/deprecation_collector/collectors.rb
47
+ - lib/deprecation_collector/deprecation.rb
46
48
  - lib/deprecation_collector/version.rb
47
49
  - sig/deprecation_collector.rbs
48
50
  homepage: https://github.com/Vasfed/deprecation_collector