deprecation_collector 0.0.1 → 0.0.2

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 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