resque-concurrent-restriction 0.5.0 → 0.5.1

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.
@@ -2,6 +2,43 @@ module Resque
2
2
  module Plugins
3
3
  module ConcurrentRestriction
4
4
 
5
+ # Redis Data Structures
6
+ #
7
+ # concurrent.lock.tracking_id => timestamp
8
+ # Maintains the distributed lock for the tracking_key to ensure
9
+ # atomic modification of other data structures
10
+ #
11
+ # concurrent.count.tracking_id => count
12
+ # The count of currently running jobs for the tracking_id
13
+ #
14
+ # concurrent.queue.queue_name.tracking_id => List[job1, job2, ...]
15
+ # The queue of items that is currently unable to run due to count being exceeded
16
+ #
17
+ # concurrent.queue_availability.tracking_key => Set[queue_name1, queue_name2, ...]
18
+ # Maintains the set of queues that currently have something
19
+ # runnable for each tracking_id
20
+ #
21
+ # concurrent.runnable[.queue_name] => Set[tracking_id1, tracking_id2, ...]
22
+ # Maintains the set of tracking_ids that have something
23
+ # runnable for each queue (globally without .queue_name postfix in key)
24
+ #
25
+ # The behavior has two points of entry:
26
+ #
27
+ # When the Resque::Worker is looking for a job to run from a restriction
28
+ # queue, we use the queue_name to look up the set of tracking IDs that
29
+ # are currently runnable for that queue. If we get a tracking id, we
30
+ # know that there is a restriction queue with something runnable in it,
31
+ # and we then use that tracking_id and queue to look up and pop something
32
+ # off of the restriction queue.
33
+ #
34
+ # When the Resque::Worker gets a job off of a normal resque queue, it uses
35
+ # the count to see if that job is currently restricted. If not, it runs it
36
+ # as normal, but if it is restricted, then it sticks it on a restriction queue.
37
+ #
38
+ # In both cases, before a job is handed off to resque to be run, we increment
39
+ # the count so we can keep tracking of how many are currently running. When
40
+ # the job finishes, we then decrement the count.
41
+
5
42
  # Used by the user in their job class to set the concurrency limit
6
43
  def concurrent(limit)
7
44
  @concurrent = limit
@@ -20,26 +57,26 @@ module Resque
20
57
  # The key used to acquire a lock so we can operate on multiple
21
58
  # redis structures (runnables set, running_count) atomically
22
59
  def lock_key(tracking_key)
23
- parts = tracking_key.split(":")
24
- "concurrent:lock:#{parts[2..-1].join(':')}"
60
+ parts = tracking_key.split(".")
61
+ "concurrent.lock.#{parts[2..-1].join('.')}"
25
62
  end
26
63
 
27
64
  # The redis key used to store the number of currently running
28
65
  # jobs for the restriction_identifier
29
66
  def running_count_key(tracking_key)
30
- parts = tracking_key.split(":")
31
- "concurrent:count:#{parts[2..-1].join(':')}"
67
+ parts = tracking_key.split(".")
68
+ "concurrent.count.#{parts[2..-1].join('.')}"
32
69
  end
33
70
 
34
71
  # The key for the redis list where restricted jobs for the given resque queue are stored
35
72
  def restriction_queue_key(tracking_key, queue)
36
- parts = tracking_key.split(":")
37
- "concurrent:queue:#{queue}:#{parts[2..-1].join(':')}"
73
+ parts = tracking_key.split(".")
74
+ "concurrent.queue.#{queue}.#{parts[2..-1].join('.')}"
38
75
  end
39
76
 
40
77
  def restriction_queue_availability_key(tracking_key)
41
- parts = tracking_key.split(":")
42
- "concurrent:queue_availability:#{parts[2..-1].join(':')}"
78
+ parts = tracking_key.split(".")
79
+ "concurrent.queue_availability.#{parts[2..-1].join('.')}"
43
80
  end
44
81
 
45
82
  # The key that groups all jobs of the same restriction_identifier together
@@ -48,18 +85,18 @@ module Resque
48
85
  # for those queues are stored
49
86
  def tracking_key(*args)
50
87
  id = concurrent_identifier(*args)
51
- id = ":#{id}" if id && id.strip.size > 0
52
- "concurrent:tracking:#{self.to_s}#{id}"
88
+ id = ".#{id}" if id && id.strip.size > 0
89
+ "concurrent.tracking.#{self.to_s}#{id}"
53
90
  end
54
91
 
55
92
  def tracking_class(tracking_key)
56
- Resque.constantize(tracking_key.split(":")[2])
93
+ Resque.constantize(tracking_key.split(".")[2])
57
94
  end
58
95
 
59
96
  # The key to the redis set where we keep a list of runnable tracking_keys
60
97
  def runnables_key(queue=nil)
61
- key = ":#{queue}" if queue
62
- "concurrent:runnable#{key}"
98
+ key = ".#{queue}" if queue
99
+ "concurrent.runnable#{key}"
63
100
  end
64
101
 
65
102
  # Encodes the job intot he restriction queue
@@ -328,16 +365,44 @@ module Resque
328
365
  end
329
366
  end
330
367
 
368
+ # Resets everything to be runnable
369
+ def reset_restrictions
370
+
371
+ counts_reset = 0
372
+ count_keys = Resque.redis.keys("concurrent.count.*")
373
+ counts_reset = Resque.redis.del(*count_keys) if count_keys.size > 0
374
+
375
+ runnable_keys = Resque.redis.keys("concurrent.runnable*")
376
+ Resque.redis.del(*runnable_keys) if runnable_keys.size > 0
377
+
378
+ queues_enabled = 0
379
+ queue_keys = Resque.redis.keys("concurrent.queue.*")
380
+ queue_keys.each do |k|
381
+ if Resque.redis.llen(k) > 0
382
+ parts = k.split(".")
383
+ queue = parts[2]
384
+ ident = parts[3..-1].join('.')
385
+ tracking_key = "concurrent.tracking.#{ident}"
386
+ update_queues_available(tracking_key, queue, :add)
387
+ mark_runnable(tracking_key, true)
388
+ queues_enabled += 1
389
+ end
390
+ end
391
+
392
+ return counts_reset, queues_enabled
393
+
394
+ end
395
+
331
396
  def stats
332
397
  results = {}
333
398
 
334
- queue_keys = Resque.redis.keys("concurrent:queue:*")
399
+ queue_keys = Resque.redis.keys("concurrent.queue.*")
335
400
 
336
401
  queue_sizes = {}
337
402
  ident_sizes = {}
338
403
  queue_keys.each do |k|
339
- parts = k.split(":")
340
- ident = parts[3..-1].join(":")
404
+ parts = k.split(".")
405
+ ident = parts[3..-1].join(".")
341
406
  queue_name = parts[2]
342
407
  size = Resque.redis.llen(k)
343
408
  queue_sizes[queue_name] ||= 0
@@ -346,15 +411,15 @@ module Resque
346
411
  ident_sizes[ident] += size
347
412
  end
348
413
 
349
- count_keys = Resque.redis.keys("concurrent:count:*")
414
+ count_keys = Resque.redis.keys("concurrent.count.*")
350
415
  running_counts = {}
351
416
  count_keys.each do |k|
352
- parts = k.split(":")
353
- ident = parts[2..-1].join(":")
417
+ parts = k.split(".")
418
+ ident = parts[2..-1].join(".")
354
419
  running_counts[ident] = Resque.redis.get(k).to_i
355
420
  end
356
421
 
357
- lock_keys = Resque.redis.keys("concurrent:lock:*")
422
+ lock_keys = Resque.redis.keys("concurrent.lock.*")
358
423
  lock_count = lock_keys.size
359
424
 
360
425
  runnable_count = Resque.redis.scard(runnables_key)
@@ -1,7 +1,7 @@
1
1
  module Resque
2
2
  module Plugins
3
3
  module ConcurrentRestriction
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
6
6
  end
7
7
  end
@@ -18,15 +18,17 @@ describe Resque::Plugins::ConcurrentRestriction do
18
18
 
19
19
  context "keys" do
20
20
  it "should always contain the classname in tracking_key" do
21
- ConcurrentRestrictionJob.tracking_key.should == "concurrent:tracking:ConcurrentRestrictionJob"
22
- IdentifiedRestrictionJob.tracking_key.should == "concurrent:tracking:IdentifiedRestrictionJob"
23
- IdentifiedRestrictionJob.tracking_key(1).should == "concurrent:tracking:IdentifiedRestrictionJob:1"
21
+ ConcurrentRestrictionJob.tracking_key.should == "concurrent.tracking.ConcurrentRestrictionJob"
22
+ IdentifiedRestrictionJob.tracking_key.should == "concurrent.tracking.IdentifiedRestrictionJob"
23
+ IdentifiedRestrictionJob.tracking_key(1).should == "concurrent.tracking.IdentifiedRestrictionJob.1"
24
+ Jobs::NestedRestrictionJob.tracking_key.should == "concurrent.tracking.Jobs::NestedRestrictionJob"
24
25
  end
25
26
 
26
27
  it "should be able to get the class from tracking_key" do
27
28
  ConcurrentRestrictionJob.tracking_class(ConcurrentRestrictionJob.tracking_key).should == ConcurrentRestrictionJob
28
29
  IdentifiedRestrictionJob.tracking_class(IdentifiedRestrictionJob.tracking_key).should == IdentifiedRestrictionJob
29
30
  IdentifiedRestrictionJob.tracking_class(IdentifiedRestrictionJob.tracking_key(1)).should == IdentifiedRestrictionJob
31
+ Jobs::NestedRestrictionJob.tracking_class(Jobs::NestedRestrictionJob.tracking_key).should == Jobs::NestedRestrictionJob
30
32
  end
31
33
 
32
34
  end
@@ -418,6 +420,46 @@ describe Resque::Plugins::ConcurrentRestriction do
418
420
 
419
421
  end
420
422
 
423
+ context "#reset_restrictions" do
424
+ it "should do nothing when nothing is in redis" do
425
+ ConcurrentRestrictionJob.reset_restrictions.should == [0, 0]
426
+ end
427
+
428
+ it "should reset counts" do
429
+ ConcurrentRestrictionJob.set_running_count(ConcurrentRestrictionJob.tracking_key, 5)
430
+ IdentifiedRestrictionJob.set_running_count(IdentifiedRestrictionJob.tracking_key(1), 5)
431
+ IdentifiedRestrictionJob.set_running_count(IdentifiedRestrictionJob.tracking_key(2), 5)
432
+
433
+ ConcurrentRestrictionJob.reset_restrictions.should == [3, 0]
434
+ ConcurrentRestrictionJob.running_count(ConcurrentRestrictionJob.tracking_key).should == 0
435
+ IdentifiedRestrictionJob.running_count(IdentifiedRestrictionJob.tracking_key(1)).should == 0
436
+ IdentifiedRestrictionJob.running_count(IdentifiedRestrictionJob.tracking_key(2)).should == 0
437
+ end
438
+
439
+ it "should reset restriction queue runnable state" do
440
+ job1 = Resque::Job.new("queue1", {"class" => "IdentifiedRestrictionJob", "args" => [1]})
441
+ job2 = Resque::Job.new("queue2", {"class" => "IdentifiedRestrictionJob", "args" => [1]})
442
+ job3 = Resque::Job.new("queue1", {"class" => "IdentifiedRestrictionJob", "args" => [2]})
443
+ job4 = Resque::Job.new("queue2", {"class" => "IdentifiedRestrictionJob", "args" => [2]})
444
+ job5 = Resque::Job.new("queue3", {"class" => "ConcurrentRestrictionJob", "args" => []})
445
+
446
+ IdentifiedRestrictionJob.push_to_restriction_queue(job1)
447
+ IdentifiedRestrictionJob.push_to_restriction_queue(job2)
448
+ IdentifiedRestrictionJob.push_to_restriction_queue(job3)
449
+ IdentifiedRestrictionJob.push_to_restriction_queue(job4)
450
+ ConcurrentRestrictionJob.push_to_restriction_queue(job5)
451
+
452
+ ConcurrentRestrictionJob.reset_restrictions.should == [0, 5]
453
+ IdentifiedRestrictionJob.runnables(:queue1).sort.should == [IdentifiedRestrictionJob.tracking_key(1), IdentifiedRestrictionJob.tracking_key(2)]
454
+ IdentifiedRestrictionJob.runnables(:queue2).sort.should == [IdentifiedRestrictionJob.tracking_key(1), IdentifiedRestrictionJob.tracking_key(2)]
455
+ ConcurrentRestrictionJob.runnables(:queue3).sort.should == [ConcurrentRestrictionJob.tracking_key]
456
+
457
+ IdentifiedRestrictionJob.runnables.sort.should == [ConcurrentRestrictionJob.tracking_key, IdentifiedRestrictionJob.tracking_key(1), IdentifiedRestrictionJob.tracking_key(2)]
458
+
459
+ end
460
+
461
+ end
462
+
421
463
  context "#stats" do
422
464
 
423
465
  it "should have blank info when nothing going on" do
@@ -444,7 +486,7 @@ describe Resque::Plugins::ConcurrentRestriction do
444
486
 
445
487
  stats = IdentifiedRestrictionJob.stats
446
488
  stats[:queue_totals][:by_queue_name].should == {"queue1" => 2, "queue2" => 2, "queue3" => 1}
447
- stats[:queue_totals][:by_identifier].should == {"IdentifiedRestrictionJob:1" => 2, "IdentifiedRestrictionJob:2" => 2, "ConcurrentRestrictionJob" => 1}
489
+ stats[:queue_totals][:by_identifier].should == {"IdentifiedRestrictionJob.1" => 2, "IdentifiedRestrictionJob.2" => 2, "ConcurrentRestrictionJob" => 1}
448
490
  end
449
491
 
450
492
  it "should track running_counts" do
@@ -452,7 +494,7 @@ describe Resque::Plugins::ConcurrentRestriction do
452
494
  IdentifiedRestrictionJob.set_running_count(IdentifiedRestrictionJob.tracking_key(2), 3)
453
495
  ConcurrentRestrictionJob.set_running_count(ConcurrentRestrictionJob.tracking_key, 4)
454
496
  stats = IdentifiedRestrictionJob.stats
455
- stats[:running_counts].should == {"IdentifiedRestrictionJob:1" => 2, "IdentifiedRestrictionJob:2" => 3, "ConcurrentRestrictionJob" => 4}
497
+ stats[:running_counts].should == {"IdentifiedRestrictionJob.1" => 2, "IdentifiedRestrictionJob.2" => 3, "ConcurrentRestrictionJob" => 4}
456
498
  end
457
499
 
458
500
  it "should track lock_count" do
data/spec/spec_helper.rb CHANGED
@@ -115,6 +115,15 @@ class RestrictionJob
115
115
  @queue = 'normal'
116
116
  end
117
117
 
118
+ module Jobs
119
+ class NestedRestrictionJob
120
+ extend RunCountHelper
121
+ extend Resque::Plugins::ConcurrentRestriction
122
+ concurrent 1
123
+ @queue = 'normal'
124
+ end
125
+ end
126
+
118
127
  class IdentifiedRestrictionJob
119
128
  extend RunCountHelper
120
129
  extend Resque::Plugins::ConcurrentRestriction
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: resque-concurrent-restriction
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.5.0
5
+ version: 0.5.1
6
6
  platform: ruby
7
7
  authors:
8
8
  - Matt Conway