pseudo_cleaner 0.0.35 → 0.0.36

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.
@@ -0,0 +1,665 @@
1
+ require "redis"
2
+ require "redis-namespace"
3
+ require "pseudo_cleaner/master_cleaner"
4
+ require "pseudo_cleaner/configuration"
5
+ require "pseudo_cleaner/logger"
6
+ require "colorize"
7
+
8
+ module PseudoCleaner
9
+ ##
10
+ # This class "cleans" a single redis connection.
11
+ #
12
+ # The cleaning is done by opening a monitor connection on redis and monitoring it for any actions that change values
13
+ # in redis.
14
+ #
15
+ # Long term, I was thinking about keeping some stats on the redis calls, but for now, I'm not.
16
+ #
17
+ # The cleaner roughly works as follows:
18
+ # * Get a list of all existing keys in the database as a level starting point.
19
+ # * Start a monitor
20
+ # * The monitor records any key that is updated or changed
21
+ # * When a test ends
22
+ # * Ask the monitor for a list of all changed keys.
23
+ # * The monitor resets the list of all changed keys.
24
+ # * When the suite ends
25
+ # * Get a list of all of the keys
26
+ # * Compare that list to the starting level
27
+ #
28
+ # Again, like the TableCleaner, if items are updated, this won't be able to "fix" that, but it will report on it.
29
+ #
30
+ # At this time, my code only pays attention to one database. It ignores other databases. We could extend things
31
+ # and have the monitor watch multiple databases. I'll have to do that later if I find a need.
32
+
33
+ # NOTE: Like the database cleaner, if the test is interrupted and test_end isn't called, the redis database may be
34
+ # left in an uncertain state.
35
+
36
+ # I'm not a huge fan of sleeps. In the non-rails world, I used to be able to do a sleep(0) to signal the system to
37
+ # check if somebody else needed to do some work. Testing with Rails, I find I have to actually sleep, so I do a
38
+ # very short time like 0.01.
39
+ class RedisMonitorCleaner
40
+ # SUITE_KEY = "PseudoDelete::RedisMonitorCleaner:initial_redis_state"
41
+
42
+ FLUSH_COMMANDS =
43
+ [
44
+ "flushall",
45
+ "flushdb"
46
+ ]
47
+ WRITE_COMMANDS =
48
+ [
49
+ "append",
50
+ "bitop",
51
+ "blpop",
52
+ "brpop",
53
+ "brpoplpush",
54
+ "decr",
55
+ "decrby",
56
+ "del",
57
+ "expire",
58
+ "expireat",
59
+ "getset",
60
+ "hset",
61
+ "hsetnx",
62
+ "hincrby",
63
+ "hincrbyfloat",
64
+ "hmset",
65
+ "hdel",
66
+ "incr",
67
+ "incrby",
68
+ "incrbyfloat",
69
+ "linsert",
70
+ "lpop",
71
+ "lpush",
72
+ "lpushx",
73
+ "lrem",
74
+ "lset",
75
+ "ltrim",
76
+ "mapped_hmset",
77
+ "mapped_mset",
78
+ "mapped_msetnx",
79
+ "move",
80
+ "mset",
81
+ "msetnx",
82
+ "persist",
83
+ "pexpire",
84
+ "pexpireat",
85
+ "psetex",
86
+ "rename",
87
+ "renamenx",
88
+ "restore",
89
+ "rpop",
90
+ "rpoplpush",
91
+ "rpush",
92
+ "rpushx",
93
+ "sadd",
94
+ "sdiffstore",
95
+ "set",
96
+ "setbit",
97
+ "setex",
98
+ "setnx",
99
+ "setrange",
100
+ "sinterstore",
101
+ "smove",
102
+ "sort",
103
+ "spop",
104
+ "srem",
105
+ "sunionstore",
106
+ "zadd",
107
+ "zincrby",
108
+ "zinterstore",
109
+ "zrem",
110
+ "zremrangebyrank",
111
+ "zremrangebyscore",
112
+ ]
113
+ READ_COMMANDS =
114
+ [
115
+ "bitcount",
116
+ "bitop",
117
+ "dump",
118
+ "exists",
119
+ "get",
120
+ "getbit",
121
+ "getrange",
122
+ "hget",
123
+ "hmget",
124
+ "hexists",
125
+ "hlen",
126
+ "hkeys",
127
+ "hscan",
128
+ "hscan_each",
129
+ "hvals",
130
+ "hgetall",
131
+ "lindex",
132
+ "llen",
133
+ "lrange",
134
+ "mapped_hmget",
135
+ "mapped_mget",
136
+ "mget",
137
+ "persist",
138
+ "scard",
139
+ "scan",
140
+ "scan_each",
141
+ "sdiff",
142
+ "sismember",
143
+ "smembers",
144
+ "srandmember",
145
+ "sscan",
146
+ "sscan_each",
147
+ "strlen",
148
+ "sunion",
149
+ "type",
150
+ "zcard",
151
+ "zcount",
152
+ "zrange",
153
+ "zrangebyscore",
154
+ "zrank",
155
+ "zrevrange",
156
+ "zrevrangebyscore",
157
+ "zrevrank",
158
+ "zscan",
159
+ "zscan_each",
160
+ "zscore",
161
+ ]
162
+
163
+ attr_reader :monitor_thread
164
+ attr_reader :initial_keys
165
+ attr_accessor :options
166
+
167
+ class RedisMessage
168
+ attr_reader :message
169
+ attr_reader :time_stamp
170
+ attr_reader :db
171
+ attr_reader :host
172
+ attr_reader :port
173
+ attr_reader :command
174
+ attr_reader :cur_pos
175
+
176
+ def initialize(message_string)
177
+ @message = message_string
178
+
179
+ parse_message
180
+ end
181
+
182
+ def parse_message
183
+ if @message =~ /[0-9]+\.[0-9]+ \[[0-9]+ [^:]+:[^\]]+\] \"[^\"]+\"/
184
+ end_pos = @message.index(" ")
185
+ @time_stamp = @message[0..end_pos - 1]
186
+
187
+ @cur_pos = end_pos + 2 # " ["
188
+ end_pos = @message.index(" ", @cur_pos)
189
+ @db = @message[@cur_pos..end_pos - 1].to_i
190
+
191
+ @cur_pos = end_pos + 1
192
+ end_pos = @message.index(":", @cur_pos)
193
+ @host = @message[@cur_pos..end_pos - 1]
194
+
195
+ @cur_pos = end_pos + 1
196
+ end_pos = @message.index("]", @cur_pos)
197
+ @port = @message[@cur_pos..end_pos - 1].to_i
198
+
199
+ @cur_pos = end_pos + 2 # "] "
200
+ @command = next_value.downcase
201
+ else
202
+ @command = @message
203
+ end
204
+ end
205
+
206
+ def next_value
207
+ in_quote = (@message[@cur_pos] == '"')
208
+ if in_quote
209
+ @cur_pos += 1
210
+ end_pos = @cur_pos
211
+ while (end_pos && end_pos < @message.length)
212
+ end_pos = @message.index("\"", end_pos)
213
+
214
+ num_backslashes = 0
215
+ back_pos = end_pos
216
+
217
+ while @message[back_pos - 1] == "\\"
218
+ num_backslashes += 1
219
+ back_pos -= 1
220
+ end
221
+
222
+ break if (num_backslashes % 2) == 0
223
+ end_pos += 1
224
+ end
225
+ else
226
+ end_pos = @message.index(" ", @cur_pos)
227
+ end
228
+ the_value = @message[@cur_pos..end_pos - 1]
229
+ end_pos += 1 if in_quote
230
+
231
+ @cur_pos = end_pos + 1
232
+
233
+ the_value.gsub("\\\\", "\\").gsub("\\\"", "\"")
234
+ end
235
+
236
+ def keys
237
+ unless defined?(@message_keys)
238
+ @message_keys = []
239
+
240
+ if Redis::Namespace::COMMANDS.include? command
241
+ handling = Redis::Namespace::COMMANDS[command.to_s.downcase]
242
+
243
+ (before, after) = handling
244
+
245
+ case before
246
+ when :first
247
+ @message_keys << next_value
248
+
249
+ when :all
250
+ while @cur_pos < @message.length
251
+ @message_keys << next_value
252
+ end
253
+
254
+ when :exclude_first
255
+ next_value
256
+ while @cur_pos < @message.length
257
+ @message_keys << next_value
258
+ end
259
+
260
+ when :exclude_last
261
+ while @cur_pos < @message.length
262
+ @message_keys << next_value
263
+ end
264
+ @message_keys.delete_at(@message_keys.length - 1)
265
+
266
+ when :exclude_options
267
+ options = ["weights", "aggregate", "sum", "min", "max"]
268
+ while @cur_pos < @message.length
269
+ @message_keys << next_value
270
+ if options.include?(@message_keys[-1].downcase)
271
+ @message_keys.delete_at(@message_keys.length - 1)
272
+ break
273
+ end
274
+ end
275
+
276
+ when :alternate
277
+ while @cur_pos < @message.length
278
+ @message_keys << next_value
279
+ next_value
280
+ end
281
+
282
+ when :sort
283
+ next_value
284
+
285
+ while @cur_pos < @message.length
286
+ a_value = next_value
287
+ if a_value.downcase == "store"
288
+ @message_keys[0] = next_value
289
+ end
290
+ end
291
+
292
+ # when :eval_style
293
+ #
294
+ # when :scan_style
295
+ end
296
+ end
297
+ end
298
+
299
+ @message_keys
300
+ end
301
+
302
+ def to_s
303
+ {
304
+ time_stamp: time_stamp,
305
+ db: db,
306
+ host: host,
307
+ port: port,
308
+ command: command,
309
+ message: message,
310
+ cur_pos: cur_pos
311
+ }.to_s
312
+ end
313
+ end
314
+
315
+ def initialize(start_method, end_method, table, options)
316
+ @initial_keys = SortedSet.new
317
+ @monitor_thread = nil
318
+ @redis_name = nil
319
+ @suite_altered_keys = SortedSet.new
320
+
321
+ unless PseudoCleaner::MasterCleaner::VALID_START_METHODS.include?(start_method)
322
+ raise "You must specify a valid start function from: #{PseudoCleaner::MasterCleaner::VALID_START_METHODS}."
323
+ end
324
+ unless PseudoCleaner::MasterCleaner::VALID_END_METHODS.include?(end_method)
325
+ raise "You must specify a valid end function from: #{PseudoCleaner::MasterCleaner::VALID_END_METHODS}."
326
+ end
327
+
328
+ @options = options
329
+
330
+ @options[:table_start_method] ||= start_method
331
+ @options[:table_end_method] ||= end_method
332
+ @options[:output_diagnostics] ||= PseudoCleaner::Configuration.current_instance.output_diagnostics ||
333
+ PseudoCleaner::Configuration.current_instance.post_transaction_analysis
334
+
335
+ @redis = table
336
+ end
337
+
338
+ def <=>(right_object)
339
+ if (right_object.is_a?(PseudoCleaner::RedisMonitorCleaner))
340
+ return 0
341
+ elsif (right_object.is_a?(PseudoCleaner::TableCleaner))
342
+ return 1
343
+ else
344
+ if right_object.respond_to?(:<=>)
345
+ comparison = (right_object <=> self)
346
+ if comparison
347
+ return -1 * comparison
348
+ end
349
+ end
350
+ end
351
+
352
+ return 1
353
+ end
354
+
355
+ def redis
356
+ @redis ||= Redis.current
357
+ end
358
+
359
+ def suite_start test_strategy
360
+ @test_strategy ||= test_strategy
361
+
362
+ # if redis.type(PseudoCleaner::RedisMonitorCleaner::SUITE_KEY) == "set"
363
+ # @initial_keys = SortedSet.new(redis.smembers(PseudoCleaner::RedisMonitorCleaner::SUITE_KEY))
364
+ # report_end_of_suite_state "before suite start"
365
+ # end
366
+ # redis.del PseudoCleaner::RedisMonitorCleaner::SUITE_KEY
367
+
368
+ start_monitor
369
+ end
370
+
371
+ def test_start test_strategy
372
+ @test_strategy ||= test_strategy
373
+
374
+ synchronize_test_values do |test_values|
375
+ if test_values && !test_values.empty?
376
+ report_dirty_values "values altered before the test started", test_values
377
+
378
+ test_values.each do |value|
379
+ redis.del value unless initial_keys.include?(value)
380
+ end
381
+ end
382
+ end
383
+ end
384
+
385
+ def test_end test_strategy
386
+ synchronize_test_values do |updated_values|
387
+ if updated_values && !updated_values.empty?
388
+ report_keys = []
389
+
390
+ if @options[:output_diagnostics]
391
+ report_dirty_values "updated values", updated_values
392
+ end
393
+
394
+ updated_values.each do |value|
395
+ if initial_keys.include?(value)
396
+ report_keys << value
397
+ @suite_altered_keys << value
398
+ else
399
+ redis.del(value)
400
+ end
401
+ end
402
+
403
+ report_dirty_values "initial values altered by test", report_keys
404
+ end
405
+ end
406
+ end
407
+
408
+ def suite_end test_strategy
409
+ report_end_of_suite_state "suite end"
410
+
411
+ if monitor_thread
412
+ monitor_thread.kill
413
+ @monitor_thread = nil
414
+ end
415
+ end
416
+
417
+ def reset_suite
418
+ report_end_of_suite_state "reset suite"
419
+
420
+ if monitor_thread
421
+ monitor_thread.kill
422
+ @monitor_thread = nil
423
+ start_monitor
424
+ end
425
+ end
426
+
427
+ def ignore_regexes
428
+ []
429
+ end
430
+
431
+ def ignore_key(key)
432
+ ignore_regexes.detect { |ignore_regex| key =~ ignore_regex }
433
+ end
434
+
435
+ def redis_name
436
+ unless @redis_name
437
+ redis_options = redis.client.options.with_indifferent_access
438
+ @redis_name = "#{redis_options[:host]}:#{redis_options[:port]}/#{redis_options[:db]}"
439
+ end
440
+
441
+ @redis_name
442
+ end
443
+
444
+ def review_rows(&block)
445
+ synchronize_test_values do |updated_values|
446
+ if updated_values && !updated_values.empty?
447
+ updated_values.each do |updated_value|
448
+ unless ignore_key(updated_value)
449
+ block.yield redis_name, report_record(updated_value)
450
+ end
451
+ end
452
+ end
453
+ end
454
+ end
455
+
456
+ def peek_values
457
+ synchronize_test_values do |updated_values|
458
+ if updated_values && !updated_values.empty?
459
+ output_values = false
460
+
461
+ if PseudoCleaner::MasterCleaner.report_table
462
+ Cornucopia::Util::ReportTable.new(nested_table: PseudoCleaner::MasterCleaner.report_table,
463
+ nested_table_label: redis_name,
464
+ suppress_blank_table: true) do |report_table|
465
+ updated_values.each do |updated_value|
466
+ unless ignore_key(updated_value)
467
+ output_values = true
468
+ report_table.write_stats updated_value, report_record(updated_value)
469
+ end
470
+ end
471
+ end
472
+ else
473
+ PseudoCleaner::Logger.write(" #{redis_name}")
474
+
475
+ updated_values.each do |updated_value|
476
+ unless ignore_key(updated_value)
477
+ output_values = true
478
+ PseudoCleaner::Logger.write(" #{updated_value}: #{report_record(updated_value)}")
479
+ end
480
+ end
481
+ end
482
+
483
+ PseudoCleaner::MasterCleaner.report_error if output_values
484
+ end
485
+ end
486
+ end
487
+
488
+ def synchronize_key
489
+ @synchronize_key ||= "redis_cleaner::synchronization_key_#{rand(1..1_000_000_000_000_000_000)}_#{rand(1..1_000_000_000_000_000_000)}"
490
+ end
491
+
492
+ def synchronize_end_key
493
+ @synchronize_end_key ||= "redis_cleaner::synchronization_end_key_#{rand(1..1_000_000_000_000_000_000)}_#{rand(1..1_000_000_000_000_000_000)}"
494
+ end
495
+
496
+ def report_end_of_suite_state report_reason
497
+ current_keys = SortedSet.new(redis.keys)
498
+
499
+ deleted_keys = initial_keys - current_keys
500
+ new_keys = current_keys - initial_keys
501
+
502
+ # filter out values we inserted that will go away on their own.
503
+ new_keys = new_keys.select { |key| (key =~ /redis_cleaner::synchronization_(?:end_)?key_[0-9]+_[0-9]+/).nil? }
504
+
505
+ report_dirty_values "new values as of #{report_reason}", new_keys
506
+ report_dirty_values "values deleted before #{report_reason}", deleted_keys
507
+ report_dirty_values "initial values changed during suite run", @suite_altered_keys
508
+
509
+ @suite_altered_keys = SortedSet.new
510
+
511
+ new_keys.each do |key_value|
512
+ redis.del key_value
513
+ end
514
+ end
515
+
516
+ def synchronize_test_values(&block)
517
+ updated_values = nil
518
+
519
+ if monitor_thread
520
+ redis.setex(synchronize_key, 1, true)
521
+ updated_values = queue.pop
522
+ end
523
+
524
+ block.yield updated_values
525
+
526
+ redis.setex(synchronize_end_key, 1, true)
527
+ end
528
+
529
+ def queue
530
+ @queue ||= Queue.new
531
+ end
532
+
533
+ def start_monitor
534
+ cleaner_class = self
535
+
536
+ @initial_keys = SortedSet.new(redis.keys)
537
+ # @initial_keys.add(PseudoCleaner::RedisMonitorCleaner::SUITE_KEY)
538
+ # @initial_keys.each do |key_value|
539
+ # redis.sadd(PseudoCleaner::RedisMonitorCleaner::SUITE_KEY, key_value)
540
+ # end
541
+ if @options[:output_diagnostics]
542
+ if PseudoCleaner::MasterCleaner.report_table
543
+ Cornucopia::Util::ReportTable.new(nested_table: PseudoCleaner::MasterCleaner.report_table,
544
+ nested_table_label: redis_name,
545
+ suppress_blank_table: true) do |report_table|
546
+ report_table.write_stats "initial keys count", @initial_keys.count
547
+ end
548
+ else
549
+ PseudoCleaner::Logger.write("#{redis_name}")
550
+ PseudoCleaner::Logger.write(" Initial keys count - #{@initial_keys.count}")
551
+ end
552
+ end
553
+
554
+ unless @monitor_thread
555
+ @monitor_thread = Thread.new do
556
+ in_redis_cleanup = false
557
+ updated_keys = SortedSet.new
558
+
559
+ monitor_redis = Redis.new(cleaner_class.redis.client.options)
560
+ redis_options = monitor_redis.client.options.with_indifferent_access
561
+ cleaner_class_db = redis_options[:db]
562
+
563
+ monitor_redis.monitor do |message|
564
+ redis_message = RedisMessage.new message
565
+
566
+ if redis_message.db == cleaner_class_db
567
+ process_command = true
568
+
569
+ if redis_message.command == "setex"
570
+ if redis_message.keys[0] == cleaner_class.synchronize_key
571
+ process_command = false
572
+
573
+ in_redis_cleanup = true
574
+ return_values = updated_keys
575
+ updated_keys = SortedSet.new
576
+ cleaner_class.queue << return_values
577
+ elsif redis_message.keys[0] == cleaner_class.synchronize_end_key
578
+ in_redis_cleanup = false
579
+ cleaner_class.monitor_thread[:updated] = nil
580
+ process_command = false
581
+ end
582
+ elsif redis_message.command == "del"
583
+ if in_redis_cleanup
584
+ process_command = false
585
+ end
586
+ end
587
+
588
+ if process_command
589
+ # flush...
590
+ if PseudoCleaner::RedisMonitorCleaner::WRITE_COMMANDS.include? redis_message.command
591
+ updated_keys.merge(redis_message.keys)
592
+ elsif PseudoCleaner::RedisMonitorCleaner::FLUSH_COMMANDS.include? redis_message.command
593
+ # Not sure I can get the keys at this point...
594
+ # updated_keys.merge(cleaner_class.redis.keys)
595
+ end
596
+ end
597
+ elsif "flushall" == redis_message.command
598
+ # Not sure I can get the keys at this point...
599
+ # updated_keys.merge(cleaner_class.redis.keys)
600
+ end
601
+ end
602
+ end
603
+
604
+ sleep(0.01)
605
+ redis.get(synchronize_key)
606
+ end
607
+ end
608
+
609
+ def report_record(key_name)
610
+ key_hash = { key: key_name, type: redis.type(key_name), ttl: redis.ttl(key_name) }
611
+ case key_hash[:type]
612
+ when "string"
613
+ key_hash[:value] = redis.get(key_name)
614
+ when "list"
615
+ key_hash[:list] = { len: redis.llen(key_name), values: redis.lrange(key_name, 0, -1) }
616
+ when "set"
617
+ key_hash[:set] = redis.smembers(key_name)
618
+ when "zset"
619
+ key_hash[:sorted_set] = redis.smembers(key_name)
620
+ when "hash"
621
+ key_hash[:list] = { len: redis.hlen(key_name), values: redis.hgetall(key_name) }
622
+ end
623
+
624
+ if key_hash[:value].nil? &&
625
+ key_hash[:list].nil? &&
626
+ key_hash[:set].nil? &&
627
+ key_hash[:sorted_set].nil? &&
628
+ key_hash[:hash].nil?
629
+ key_hash[:value] = "[[DELETED]]"
630
+ end
631
+
632
+ key_hash
633
+ end
634
+
635
+ def report_dirty_values message, test_values
636
+ if test_values && !test_values.empty?
637
+ output_values = false
638
+
639
+ if PseudoCleaner::MasterCleaner.report_table
640
+ Cornucopia::Util::ReportTable.new(nested_table: PseudoCleaner::MasterCleaner.report_table,
641
+ nested_table_label: redis_name,
642
+ suppress_blank_table: true) do |report_table|
643
+ report_table.write_stats "action", message
644
+ test_values.each_with_index do |key_name, index|
645
+ unless ignore_key(key_name)
646
+ output_values = true
647
+ report_table.write_stats index, report_record(key_name)
648
+ end
649
+ end
650
+ end
651
+ else
652
+ PseudoCleaner::Logger.write("********* RedisMonitorCleaner - #{message}".red.on_light_white)
653
+ test_values.each do |key_name|
654
+ unless ignore_key(key_name)
655
+ output_values = true
656
+ PseudoCleaner::Logger.write(" #{key_name}: #{report_record(key_name)}".red.on_light_white)
657
+ end
658
+ end
659
+ end
660
+
661
+ PseudoCleaner::MasterCleaner.report_error if output_values
662
+ end
663
+ end
664
+ end
665
+ end