pseudo_cleaner 0.0.35 → 0.0.36

Sign up to get free protection for your applications and to get access to all the features.
@@ -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