deprecation_collector 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,8 +3,8 @@
3
3
  require_relative "deprecation_collector/version"
4
4
  require_relative "deprecation_collector/deprecation"
5
5
  require_relative "deprecation_collector/collectors"
6
+ require_relative "deprecation_collector/storage"
6
7
  require "time"
7
- require "redis"
8
8
  require "json"
9
9
  require "set"
10
10
 
@@ -52,37 +52,68 @@ class DeprecationCollector
52
52
  end
53
53
 
54
54
  # NB: count is expensive in production env (but possible if needed) - produces a lot of redis writes
55
- attr_accessor :count, :raise_on_deprecation, :save_full_backtrace,
55
+ attr_accessor :raise_on_deprecation, :save_full_backtrace,
56
56
  :exclude_realms,
57
- :write_interval, :write_interval_jitter,
58
- :app_revision, :app_root,
57
+ :app_name, :app_revision, :app_root,
59
58
  :print_to_stderr, :print_recurring
60
- attr_writer :redis, :context_saver, :fingerprinter
59
+ attr_reader :count, :write_interval, :write_interval_jitter, :key_prefix
60
+ attr_writer :context_saver, :fingerprinter
61
61
 
62
62
  def initialize(mutex: nil)
63
- # on cruby hash itself is threadsafe, but we need to prevent races
64
- @deprecations_mutex = mutex || Mutex.new
65
- @deprecations = {}
66
- @known_digests = Set.new
67
- @last_write_time = current_time
68
63
  @enabled = true
64
+ @instance_mutex = mutex
69
65
 
70
66
  load_default_config
71
67
  end
72
68
 
73
69
  def load_default_config
74
- @redis = defined?($redis) && $redis # rubocop:disable Style/GlobalVars
75
- @count = false
70
+ if (redis = defined?($redis) && $redis) # rubocop:disable Style/GlobalVars
71
+ self.redis = redis
72
+ end
76
73
  @raise_on_deprecation = false
77
74
  @exclude_realms = []
78
75
  @ignore_message_regexp = nil
79
76
  @app_root = (defined?(Rails) && Rails.root.present? && Rails.root) || Dir.pwd
80
77
  # NB: in production with hugreds of workers may easily overload redis with writes, so more delay needed:
81
- @write_interval = 900 # 15.minutes
82
- @write_interval_jitter = 60
78
+ self.count = false
79
+ self.write_interval = 900 # 15.minutes
80
+ self.write_interval_jitter = 60
83
81
  @context_saver = nil
84
82
  end
85
83
 
84
+ def redis=(val)
85
+ raise ArgumentError, "redis should not be nil" unless val
86
+
87
+ @storage ||= DeprecationCollector::Storage::Redis.new(val, mutex: @instance_mutex, count: count, # rubocop:disable Naming/MemoizedInstanceVariableName
88
+ write_interval: write_interval,
89
+ write_interval_jitter: write_interval_jitter,
90
+ key_prefix: key_prefix)
91
+ end
92
+
93
+ def count=(val)
94
+ storage.count = val if storage.respond_to?(:count)
95
+ @count = val
96
+ end
97
+
98
+ def write_interval=(val)
99
+ storage.write_interval = val if storage.respond_to?(:write_interval)
100
+ @write_interval = val
101
+ end
102
+
103
+ def write_interval_jitter=(val)
104
+ storage.write_interval_jitter = val if storage.respond_to?(:write_interval_jitter)
105
+ @write_interval_jitter = val
106
+ end
107
+
108
+ def key_prefix=(val)
109
+ storage.key_prefix = val
110
+ @key_prefix = val
111
+ end
112
+
113
+ def storage
114
+ @storage ||= DeprecationCollector::Storage::StdErr.new
115
+ end
116
+
86
117
  def ignored_messages=(val)
87
118
  @ignore_message_regexp = (val && Regexp.union(val)) || nil
88
119
  end
@@ -127,59 +158,37 @@ class DeprecationCollector
127
158
  @count
128
159
  end
129
160
 
130
- def redis
131
- raise "DeprecationCollector#redis is not set" unless @redis
132
-
133
- @redis
134
- end
135
-
136
161
  def write_to_redis(force: false)
137
- return unless force || (@enabled && (current_time > @last_write_time + @write_interval))
138
-
139
- deprecations_to_flush = nil
140
- @deprecations_mutex.synchronize do
141
- deprecations_to_flush = @deprecations
142
- @deprecations = {}
143
- @last_write_time = current_time
144
- # checking in this section to prevent multiple parallel check requests
145
- return (@enabled = false) unless enabled_in_redis?
146
- end
147
-
148
- write_count_to_redis(deprecations_to_flush) if count?
149
-
150
- # make as few writes as possible, other workers may already have reported our warning
151
- fetch_known_digests
152
- deprecations_to_flush.reject! { |digest, _val| @known_digests.include?(digest) }
153
- return unless deprecations_to_flush.any?
162
+ return unless force || @enabled
154
163
 
155
- @known_digests.merge(deprecations_to_flush.keys)
156
- @redis.mapped_hmset("deprecations:data", deprecations_to_flush.transform_values(&:to_json))
164
+ storage.flush(force: force)
157
165
  end
158
166
 
159
167
  # prevent fresh process from wiring frequent already known messages
160
168
  def fetch_known_digests
161
- @known_digests.merge(@redis.hkeys("deprecations:data"))
169
+ storage.fetch_known_digests
162
170
  end
163
171
 
164
172
  def flush_redis(enable: false)
165
- @redis.del("deprecations:data", "deprecations:counter", "deprecations:notes")
166
- @redis.del("deprecations:enabled") if enable
167
- @deprecations.clear
168
- @known_digests.clear
173
+ storage.clear(enable: enable)
169
174
  end
170
175
 
171
176
  def enabled_in_redis?
172
- @redis.get("deprecations:enabled") != "false"
177
+ storage.enabled?
178
+ end
179
+
180
+ def enabled?
181
+ @enabled
173
182
  end
174
183
 
175
184
  def enable
185
+ storage.enable
176
186
  @enabled = true
177
- @redis.set("deprecations:enabled", "true")
178
187
  end
179
188
 
180
189
  def disable
181
190
  @enabled = false
182
- @redis.set("deprecations:enabled", "false")
191
+ storage.disable
183
192
  end
184
193
 
185
194
  def dump
@@ -191,71 +200,39 @@ class DeprecationCollector
191
200
  # TODO: some checks
192
201
 
193
202
  digests = dump.map { |dep| dep["digest"] }
194
- raise 'need digests' unless digests.none?(&:nil?)
203
+ raise "need digests" unless digests.none?(&:nil?)
195
204
 
196
- dump_hash = dump.map { |dep| [dep.delete('digest'), dep] }.to_h
205
+ dump_hash = dump.map { |dep| [dep.delete("digest"), dep] }.to_h
197
206
 
198
- @redis.mapped_hmset("deprecations:data", dump_hash.transform_values(&:to_json))
207
+ storage.import(dump_hash)
199
208
  end
200
209
 
201
210
  def read_each
202
211
  return to_enum(:read_each) unless block_given?
203
212
 
204
- cursor = 0
205
- loop do
206
- cursor, data_pairs = @redis.hscan("deprecations:data", cursor)
207
-
208
- if data_pairs.any?
209
- data_pairs.zip(
210
- @redis.hmget("deprecations:counter", data_pairs.map(&:first)),
211
- @redis.hmget("deprecations:notes", data_pairs.map(&:first))
212
- ).each do |(digest, data), count, notes|
213
- yield decode_deprecation(digest, data, count, notes)
214
- end
215
- end
216
- break if cursor == "0"
213
+ storage.read_each do |digest, data, count, notes|
214
+ yield decode_deprecation(digest, data, count, notes)
217
215
  end
218
216
  end
219
217
 
220
218
  def read_one(digest)
221
- decode_deprecation(
222
- digest,
223
- *@redis.pipelined do |pipe|
224
- pipe.hget("deprecations:data", digest)
225
- pipe.hget("deprecations:counter", digest)
226
- pipe.hget("deprecations:notes", digest)
227
- end
228
- )
219
+ decode_deprecation(*storage.read_one(digest))
229
220
  end
230
221
 
231
222
  def delete_deprecations(remove_digests)
232
- return 0 unless remove_digests.any?
233
-
234
- @redis.pipelined do |pipe|
235
- pipe.hdel("deprecations:data", *remove_digests)
236
- pipe.hdel("deprecations:notes", *remove_digests)
237
- pipe.hdel("deprecations:counter", *remove_digests) if @count
238
- end.first
239
- end
240
-
241
- def cleanup
242
- cursor = 0
243
- removed = total = 0
244
- loop do
245
- cursor, data_pairs = @redis.hscan("deprecations:data", cursor) # NB: some pages may be empty
246
- total += data_pairs.size
247
- removed += delete_deprecations(
248
- data_pairs.to_h.select { |_digest, data| !block_given? || yield(JSON.parse(data, symbolize_names: true)) }.keys
249
- )
250
- break if cursor == "0"
251
- end
252
- "#{removed} removed, #{total - removed} left"
223
+ storage.delete(remove_digests)
224
+ end
225
+
226
+ def cleanup(&block)
227
+ raise ArgumentError, "provide a block to filter deprecations" unless block
228
+
229
+ storage.cleanup(&block)
253
230
  end
254
231
 
255
232
  protected
256
233
 
257
234
  def unsent_deprecations
258
- @deprecations
235
+ storage.unsent_deprecations
259
236
  end
260
237
 
261
238
  def store_deprecation(deprecation, allow_context: true)
@@ -263,51 +240,34 @@ class DeprecationCollector
263
240
 
264
241
  deprecation.context = context_saver.call if context_saver && allow_context
265
242
  deprecation.custom_fingerprint = fingerprinter.call(deprecation) if fingerprinter && allow_context
243
+ deprecation.app_name = app_name if app_name
266
244
 
267
- fresh = !@deprecations.key?(deprecation.digest)
268
- @deprecations_mutex.synchronize do
269
- (@deprecations[deprecation.digest] ||= deprecation).touch
270
- end
271
-
272
- write_to_redis if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
273
- fresh
245
+ storage.store(deprecation)
274
246
  end
275
247
 
276
248
  def log_deprecation_if_needed(deprecation, fresh)
277
249
  return unless print_to_stderr && !deprecation.ignored?
278
250
  return unless fresh || print_recurring
279
251
 
252
+ log_deprecation(deprecation)
253
+ end
254
+
255
+ def log_deprecation(deprecation)
280
256
  msg = deprecation.message
281
257
  msg = "DEPRECATION: #{msg}" unless msg.start_with?("DEPRECAT")
282
258
  $stderr.puts(msg) # rubocop:disable Style/StderrPuts
283
259
  end
284
260
 
285
- def current_time
286
- return Time.zone.now if Time.respond_to?(:zone) && Time.zone
287
-
288
- Time.now
289
- end
290
-
291
261
  def decode_deprecation(digest, data, count, notes)
292
262
  return nil unless data
293
263
 
294
264
  data = JSON.parse(data, symbolize_names: true)
295
- unless data.is_a?(Hash)
296
- # this should not happen (this means broken Deprecation#to_json or some data curruption)
297
- return nil
298
- end
265
+ # this should not happen (this means broken Deprecation#to_json or some data curruption)
266
+ return nil unless data.is_a?(Hash)
299
267
 
300
268
  data[:digest] = digest
301
269
  data[:notes] = JSON.parse(notes, symbolize_names: true) if notes
302
270
  data[:count] = count.to_i if count
303
271
  data
304
272
  end
305
-
306
- def write_count_to_redis(deprecations_to_flush)
307
- @redis.pipelined do |pipe|
308
- deprecations_to_flush.each_pair do |digest, deprecation|
309
- pipe.hincrby("deprecations:counter", digest, deprecation.occurences)
310
- end
311
- end
312
- end
313
273
  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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vasily Fedoseyev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-28 00:00:00.000000000 Z
11
+ date: 2023-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -80,6 +80,7 @@ files:
80
80
  - lib/deprecation_collector.rb
81
81
  - lib/deprecation_collector/collectors.rb
82
82
  - lib/deprecation_collector/deprecation.rb
83
+ - lib/deprecation_collector/storage.rb
83
84
  - lib/deprecation_collector/version.rb
84
85
  - lib/deprecation_collector/web.rb
85
86
  - lib/deprecation_collector/web/application.rb
@@ -98,7 +99,7 @@ metadata:
98
99
  homepage_uri: https://github.com/Vasfed/deprecation_collector
99
100
  source_code_uri: https://github.com/Vasfed/deprecation_collector
100
101
  changelog_uri: https://github.com/Vasfed/deprecation_collector/blob/main/CHANGELOG.md
101
- post_install_message:
102
+ post_install_message:
102
103
  rdoc_options: []
103
104
  require_paths:
104
105
  - lib
@@ -113,8 +114,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
114
  - !ruby/object:Gem::Version
114
115
  version: '0'
115
116
  requirements: []
116
- rubygems_version: 3.1.6
117
- signing_key:
117
+ rubygems_version: 3.4.10
118
+ signing_key:
118
119
  specification_version: 4
119
120
  summary: Collector for ruby/rails deprecations and warnings, suitable for production
120
121
  test_files: []