deprecation_collector 0.0.1 → 0.0.4

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: 2342f445e6c89ba4ea74094dd39894d99ad6e546781405fe43a6ecba71396264
4
+ data.tar.gz: d5d6edaa942f57a726f9336a06541dabc3eadae2731d01953443a88d84fc7c2e
5
5
  SHA512:
6
- metadata.gz: c5a10c07173fe8b73003630c2fa0839b07c62f0471b72269aabdcece19935f6693fc6b0a3a3c8634b1731b7cc9db0aae186ff2067a43047692e15f32180d5210
7
- data.tar.gz: b6e598c579eb46756b9a78336fd3b223d4224b01f5a677b55375732279564c9b609dc2e87083a6cb47cb5e86c567c73102465080e275818564d94c1d29b29408
6
+ metadata.gz: 9ac2e881d1687dc3756904840f21f98cc62dc497cac31804fcfeac8e4e01c780b5370497ed52da2d1ec22ca14a68ee3be4dc163c5d48cd47c38b775ef0df7b1c
7
+ data.tar.gz: 5725ea9137d79a83ea8d1df1ae9b458fe0f94479d5e7c9643ac15368ef853f75fac38cce3b7e8543fd4d03df884fa3999019d08902ba437a196f6b5b3c78dd06
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,13 @@
1
+ == 0.0.4
2
+ - added first_timestamp to deprecations (unix timestamp of first occurrence, not accurate because a worker with later timestamp may dump its deprecations earlier)
1
3
 
4
+ == 0.0.3
5
+ - Fixed selective deprecation cleanup (`DeprecationCollector.instance.cleanup { |d| d[:message].include?('foo') }`)
6
+
7
+ == 0.0.2
8
+
9
+ - Reorganized code
2
10
 
3
11
  == 0.0.1
4
12
 
5
- Initial release
13
+ - 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.4)
5
5
  redis (>= 2.0)
6
6
 
7
7
  GEM
@@ -123,7 +123,7 @@ GEM
123
123
  thor (>= 0.20.3, < 2.0)
124
124
  rainbow (3.1.1)
125
125
  rake (13.0.6)
126
- redis (4.6.0)
126
+ redis (4.7.1)
127
127
  regexp_parser (2.5.0)
128
128
  rexml (3.2.5)
129
129
  rspec (3.11.0)
@@ -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,21 +1,39 @@
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.
5
+ Designed to be suitable for use in production under load.
6
+
4
7
  (gem is a work-in-process, documentation will come later)
5
8
 
6
9
  ## Installation
7
10
 
8
11
  Install the gem and add to the application's Gemfile by executing:
9
12
 
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
13
+ ```sh
14
+ bundle add deprecation_collector
15
+ ```
15
16
 
16
17
  ## Usage
17
18
 
18
-
19
+ Add an initializer with configuration, like
20
+
21
+ ```ruby
22
+ Rails.application.config.to_prepare do
23
+ DeprecationCollector.install do |instance|
24
+ instance.redis = Redis.new # default is $redis
25
+ instance.app_revision = ::GIT_REVISION
26
+ instance.count = false
27
+ instance.save_full_backtrace = true
28
+ instance.raise_on_deprecation = false
29
+ instance.write_interval = (::Rails.env.production? && 15.minutes) || 1.minute
30
+ instance.exclude_realms = %i[kernel] if Rails.env.production?
31
+ instance.ignored_messages = [
32
+ "Ignoring db/schema_cache.yml because it has expired"
33
+ ]
34
+ end
35
+ end
36
+ ```
19
37
 
20
38
  ## Development
21
39
 
@@ -25,7 +43,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
25
43
 
26
44
  ## Contributing
27
45
 
28
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/deprecation_collector.
46
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Vasfed/deprecation_collector.
29
47
 
30
48
  ## License
31
49
 
@@ -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,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DeprecationCollector
4
+ # :nodoc:
5
+ class Deprecation
6
+ attr_reader :message, :realm, :gem_traceline, :app_traceline, :occurences, :first_timestamp, :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
+ @first_timestamp = Time.now.to_i
24
+
25
+ cleanup_prefixes.each do |path|
26
+ @gem_traceline.delete_prefix!(path)
27
+ @message.gsub!(path, "")
28
+ end
29
+
30
+ CLEANUP_REGEXES.each_pair do |regex, replace|
31
+ @gem_traceline&.gsub!(regex, replace)
32
+ @app_traceline&.gsub!(regex, replace)
33
+ end
34
+
35
+ @full_backtrace = backtrace.map(&:to_s) if DeprecationCollector.instance.save_full_backtrace
36
+ end
37
+
38
+ def touch
39
+ @occurences += 1
40
+ end
41
+
42
+ def ignored?
43
+ false
44
+ end
45
+
46
+ def message_for_digest
47
+ # some gems like rest-client put data in warnings, need to aggregate
48
+ # + some bactrace per-worker unique method names may be there
49
+ @message.gsub(/"(?:[^"\\]|\\.)*"/, '""').gsub(/__\d+_\d+/, "___").gsub(/\((pry|irb)\):\d+/, '(\1)')
50
+ end
51
+
52
+ def digest
53
+ @digest ||= Digest::MD5.hexdigest(digest_base)
54
+ end
55
+
56
+ def digest_base
57
+ "1:#{RUBY_VERSION}:#{Rails.version}:#{message_for_digest}:#{gem_traceline}:#{app_traceline}"
58
+ end
59
+
60
+ def as_json(_options = {})
61
+ {
62
+ message: message,
63
+ realm: realm,
64
+ app_traceline: app_traceline,
65
+ gem_traceline: (gem_traceline != app_traceline && gem_traceline) || nil,
66
+ full_backtrace: full_backtrace,
67
+ ruby_version: RUBY_VERSION,
68
+ rails_version: (defined?(Rails) && Rails.version),
69
+ hostname: Socket.gethostname,
70
+ revision: DeprecationCollector.instance.app_revision,
71
+ count: @occurences, # output anyway for frequency estimation (during write_interval inside single process)
72
+ first_timestamp: first_timestamp, # this may not be accurate, a worker with later timestamp may dump earlier
73
+ digest_base: digest_base # for debug purposes
74
+ }.compact
75
+ end
76
+
77
+ protected
78
+
79
+ def find_app_traceline(backtrace)
80
+ app_root = DeprecationCollector.instance.app_root_prefix
81
+ backtrace.find do |line|
82
+ line = line.to_s
83
+ (!line.start_with?("/") || line.start_with?(app_root)) && !line.include?("/gems/")
84
+ end&.to_s&.dup&.delete_prefix(app_root)
85
+ end
86
+
87
+ def find_gem_traceline(backtrace)
88
+ backtrace.find { |line| !line.to_s.include?("kernel_warn") }&.to_s&.dup || backtrace.first.to_s.dup
89
+ end
90
+ end
91
+ 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.4"
5
5
  end
@@ -1,13 +1,14 @@
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"
8
+ require "json"
6
9
 
10
+ # singleton class for collector
7
11
  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
12
  @instance_mutex = Mutex.new
12
13
  @installed = false
13
14
  private_class_method :new
@@ -15,9 +16,13 @@ class DeprecationCollector
15
16
  def self.instance
16
17
  return @instance if defined?(@instance) && @instance
17
18
 
19
+ create_instance
20
+ end
21
+
22
+ def self.create_instance
18
23
  @instance_mutex.synchronize do
19
- # переиспользовать мутекс не обязательно, но он используется ровно один раз
20
- @instance ||= new($redis, mutex: @instance_mutex)
24
+ # no real need to reuse the mutex, but it is used only once here anyway
25
+ @instance ||= new(mutex: @instance_mutex)
21
26
  end
22
27
  @instance
23
28
  end
@@ -28,7 +33,7 @@ class DeprecationCollector
28
33
 
29
34
  # inside dev env may be called multiple times
30
35
  def self.install
31
- instance # to make it created, configuration comes later
36
+ create_instance # to make it created, configuration comes later
32
37
 
33
38
  @instance_mutex.synchronize do
34
39
  unless @installed
@@ -37,213 +42,63 @@ class DeprecationCollector
37
42
  end
38
43
 
39
44
  yield instance if block_given?
45
+ instance.fetch_known_digests
40
46
 
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
47
+ install_collectors
63
48
  end
64
49
 
65
50
  @instance
66
51
  end
67
52
 
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
-
53
+ # NB: count is expensive in production env (but possible if needed) - produces a lot of redis writes
197
54
  attr_accessor :count, :raise_on_deprecation, :save_full_backtrace,
198
55
  :exclude_realms,
199
56
  :write_interval, :write_interval_jitter,
200
57
  :app_revision, :app_root
58
+ attr_writer :redis
201
59
 
202
- def initialize(redis, mutex: nil)
203
- @redis = redis
60
+ def initialize(mutex: nil)
204
61
  # on cruby hash itself is threadsafe, but we need to prevent races
205
62
  @deprecations_mutex = mutex || Mutex.new
206
63
  @deprecations = {}
207
64
  @known_digests = Set.new
208
65
  @last_write_time = current_time
66
+ @enabled = true
209
67
 
210
- # default config:
68
+ load_default_config
69
+ end
70
+
71
+ def load_default_config
72
+ @redis = defined?($redis) && $redis # rubocop:disable Style/GlobalVars
211
73
  @count = false
212
74
  @raise_on_deprecation = false
213
75
  @exclude_realms = []
214
- @write_interval = FLUSH_INTERVAL
215
- @write_interval_jitter = 60
216
- @enabled = true
217
76
  @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
77
+ @app_root = (defined?(Rails) && Rails.root.present? && Rails.root) || Dir.pwd
78
+ # NB: in production with hugreds of workers may easily overload redis with writes, so more delay needed:
79
+ @write_interval = 900 # 15.minutes
80
+ @write_interval_jitter = 60
221
81
  end
222
82
 
223
83
  def ignored_messages=(val)
224
84
  @ignore_message_regexp = (val && Regexp.union(val)) || nil
225
85
  end
226
86
 
87
+ def app_root_prefix
88
+ "#{app_root}/"
89
+ end
90
+
227
91
  def cleanup_prefixes
228
- @cleanup_prefixes ||= Gem.path + ["#{app_root}/"] # rubocop:disable Rails/FilePath
92
+ @cleanup_prefixes ||= Gem.path + [app_root_prefix]
229
93
  end
230
94
 
231
95
  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)
96
+ return if !@enabled || exclude_realms.include?(realm) || @ignore_message_regexp&.match?(message)
235
97
  raise "Deprecation: #{message}" if @raise_on_deprecation
236
98
 
237
99
  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
100
+ store_deprecation(deprecation)
101
+ log_deprecation_if_needed(deprecation)
247
102
  end
248
103
 
249
104
  def unsent_data?
@@ -254,40 +109,27 @@ class DeprecationCollector
254
109
  @count
255
110
  end
256
111
 
257
- def fetch_known_digests
258
- @known_digests.merge(@redis.hkeys('deprecations:data'))
112
+ def redis
113
+ raise "DeprecationCollector#redis is not set" unless @redis
114
+
115
+ @redis
259
116
  end
260
117
 
261
- def write_to_redis(force: false)
262
- return unless @enabled || force
118
+ def write_to_redis(force: false) # rubocop:disable Metrics/AbcSize
119
+ return unless force || (@enabled && (current_time > @last_write_time + @write_interval))
263
120
 
264
121
  deprecations_to_flush = nil
265
122
  @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
123
  deprecations_to_flush = @deprecations
276
124
  @deprecations = {}
277
125
  @last_write_time = current_time
126
+ # checking in this section to prevent multiple parallel check requests
127
+ return (@enabled = false) unless enabled_in_redis?
278
128
  end
279
129
 
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
130
+ write_count_to_redis(deprecations_to_flush) if count?
288
131
 
289
132
  # make as few writes as possible, other workers may already have reported our warning
290
- # TODO: at some point turn off writes?
291
133
  fetch_known_digests
292
134
  deprecations_to_flush.reject! { |digest, _val| @known_digests.include?(digest) }
293
135
  return unless deprecations_to_flush.any?
@@ -296,6 +138,11 @@ class DeprecationCollector
296
138
  @redis.mapped_hmset("deprecations:data", deprecations_to_flush.transform_values(&:to_json))
297
139
  end
298
140
 
141
+ # prevent fresh process from wiring frequent already known messages
142
+ def fetch_known_digests
143
+ @known_digests.merge(@redis.hkeys("deprecations:data"))
144
+ end
145
+
299
146
  def flush_redis(enable: false)
300
147
  @redis.del("deprecations:data", "deprecations:counter", "deprecations:notes")
301
148
  @redis.del("deprecations:enabled") if enable
@@ -317,6 +164,10 @@ class DeprecationCollector
317
164
  @redis.set("deprecations:enabled", "false")
318
165
  end
319
166
 
167
+ def dump
168
+ read_each.to_a.to_json
169
+ end
170
+
320
171
  def read_each
321
172
  return to_enum(:read_each) unless block_given?
322
173
 
@@ -339,49 +190,59 @@ class DeprecationCollector
339
190
  def read_one(digest)
340
191
  decode_deprecation(
341
192
  digest,
342
- @redis.hget("deprecations:data", digest),
343
- @redis.hget("deprecations:counter", digest),
344
- @redis.hget("deprecations:notes", digest)
193
+ *@redis.pipelined do
194
+ @redis.hget("deprecations:data", digest)
195
+ @redis.hget("deprecations:counter", digest)
196
+ @redis.hget("deprecations:notes", digest)
197
+ end
345
198
  )
346
199
  end
347
200
 
348
201
  def delete_deprecations(remove_digests)
202
+ return 0 unless remove_digests.any?
203
+
349
204
  @redis.pipelined do
350
205
  @redis.hdel("deprecations:data", *remove_digests)
351
206
  @redis.hdel("deprecations:notes", *remove_digests)
352
207
  @redis.hdel("deprecations:counter", *remove_digests) if @count
353
- end
208
+ end.first
354
209
  end
355
210
 
356
211
  def cleanup
357
212
  cursor = 0
358
- removed = 0
359
- total = 0
213
+ removed = total = 0
360
214
  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
215
+ cursor, data_pairs = @redis.hscan("deprecations:data", cursor) # NB: some pages may be empty
216
+ total += data_pairs.size
217
+ removed += delete_deprecations(
218
+ data_pairs.to_h.select { |_digest, data| !block_given? || yield(JSON.parse(data, symbolize_names: true)) }.keys
219
+ )
376
220
  break if cursor == "0"
377
221
  end
378
222
  "#{removed} removed, #{total - removed} left"
379
223
  end
380
224
 
381
- private
225
+ protected
226
+
227
+ def store_deprecation(deprecation)
228
+ return if deprecation.ignored?
229
+
230
+ @deprecations_mutex.synchronize do
231
+ (@deprecations[deprecation.digest] ||= deprecation).touch
232
+ end
233
+
234
+ write_to_redis if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
235
+ end
236
+
237
+ def log_deprecation_if_needed(deprecation)
238
+ return unless defined?(Rails) && Rails.env.development? && !deprecation.ignored?
239
+
240
+ $stderr.puts(deprecation.message) # rubocop:disable Style/StderrPuts
241
+ end
382
242
 
383
243
  def current_time
384
244
  return Time.zone.now if Time.respond_to?(:zone) && Time.zone
245
+
385
246
  Time.now
386
247
  end
387
248
 
@@ -392,4 +253,12 @@ class DeprecationCollector
392
253
  data[:count] = count.to_i if count
393
254
  data
394
255
  end
256
+
257
+ def write_count_to_redis(deprecations_to_flush)
258
+ @redis.pipelined do
259
+ deprecations_to_flush.each_pair do |digest, deprecation|
260
+ @redis.hincrby("deprecations:counter", digest, deprecation.occurences)
261
+ end
262
+ end
263
+ end
395
264
  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.4
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-29 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