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
|
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
|
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
|
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
|
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 = "
|
52
|
-
"concurrent
|
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("
|
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 = "
|
62
|
-
"concurrent
|
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
|
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
|
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
|
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)
|
@@ -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
|
22
|
-
IdentifiedRestrictionJob.tracking_key.should == "concurrent
|
23
|
-
IdentifiedRestrictionJob.tracking_key(1).should == "concurrent
|
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
|
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
|
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
|