resque_jobs_tree 0.0.2 → 0.1.0
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.
- data/README.md +13 -2
- data/lib/resque_jobs_tree/factory.rb +1 -0
- data/lib/resque_jobs_tree/job.rb +22 -8
- data/lib/resque_jobs_tree/node.rb +25 -3
- data/lib/resque_jobs_tree/storage.rb +45 -2
- data/lib/resque_jobs_tree/tree.rb +9 -0
- data/lib/resque_jobs_tree/version.rb +1 -1
- data/lib/resque_jobs_tree.rb +3 -0
- data/test/factory_test.rb +1 -19
- data/test/job_test.rb +3 -21
- data/test/node_test.rb +86 -0
- data/test/storage_test.rb +18 -19
- data/test/test_helper.rb +47 -2
- data/test/tree_test.rb +14 -19
- metadata +2 -2
data/README.md
CHANGED
@@ -51,10 +51,21 @@ Organise each sequences of jobs into a single file
|
|
51
51
|
|
52
52
|
This code is defining the tree, then when it launches the sequence of jobs, it:
|
53
53
|
* stocks in Redis all the Resque jobs which needs to be done including the needed parameters to run them.
|
54
|
-
*
|
54
|
+
* stocks in Redis the childhood relationsips between them.
|
55
55
|
* enqueues in Resque the jobs which are the leaves of the tree
|
56
56
|
|
57
|
-
|
57
|
+
Limitations:
|
58
|
+
|
59
|
+
* the name of a tree of jobs should be uniq
|
60
|
+
* the name of a node should be uniq in a scope of a tree.
|
61
|
+
* the running jobs are identified by a their tree, their name and their resources.
|
62
|
+
So they should not overlap. In other words, for the same node,
|
63
|
+
you can't enqueue 2 times `[:mail, User.first]`
|
64
|
+
|
65
|
+
Node options:
|
66
|
+
|
67
|
+
* `{ async: true }` if you need your process to wait for an outsider to continue.
|
68
|
+
* `{ continue_on_fail: true}` if your process can continue even after a fail during a job.
|
58
69
|
|
59
70
|
## Contributing
|
60
71
|
|
data/lib/resque_jobs_tree/job.rb
CHANGED
@@ -3,28 +3,42 @@ class ResqueJobsTree::Job
|
|
3
3
|
class << self
|
4
4
|
|
5
5
|
def perform *args
|
6
|
-
|
6
|
+
node, resources = tree_node_and_resources(args)
|
7
7
|
node.perform.call resources
|
8
8
|
end
|
9
9
|
|
10
10
|
private
|
11
11
|
|
12
12
|
def after_perform_enqueue_parent *args
|
13
|
-
|
13
|
+
node, resources = tree_node_and_resources(args)
|
14
14
|
unless node.root?
|
15
|
-
parent_job_args = ResqueJobsTree::Storage.parent_job_args node, resources
|
16
15
|
ResqueJobsTree::Storage.remove(node, resources) do
|
17
|
-
|
16
|
+
parent_job_args = ResqueJobsTree::Storage.parent_job_args node, resources
|
17
|
+
Resque.enqueue_to node.tree.name, ResqueJobsTree::Job, *parent_job_args
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
|
22
|
+
def on_failure_cleanup exception, *args
|
23
|
+
node, resources = tree_node_and_resources args
|
24
|
+
if node.options[:continue_on_failure]
|
25
|
+
begin
|
26
|
+
after_perform_enqueue_parent *args
|
27
|
+
ensure
|
28
|
+
ResqueJobsTree::Storage.cleanup node, resources
|
29
|
+
end
|
30
|
+
else
|
31
|
+
ResqueJobsTree::Storage.cleanup node, resources, global: true
|
32
|
+
raise exception
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def tree_node_and_resources args
|
37
|
+
tree_name , job_name = args[0..1]
|
24
38
|
tree = ResqueJobsTree::Factory.find_tree_by_name tree_name
|
25
39
|
node = tree.find_node_by_name job_name
|
26
|
-
resources = ResqueJobsTree::ResourcesSerializer.to_resources args
|
27
|
-
[
|
40
|
+
resources = ResqueJobsTree::ResourcesSerializer.to_resources args[2..-1]
|
41
|
+
[node, resources]
|
28
42
|
end
|
29
43
|
|
30
44
|
end
|
@@ -1,12 +1,13 @@
|
|
1
1
|
class ResqueJobsTree::Node
|
2
2
|
|
3
|
-
attr_accessor :tree, :parent, :name, :node_childs
|
3
|
+
attr_accessor :tree, :parent, :name, :node_childs, :options
|
4
4
|
|
5
5
|
def initialize name, tree, parent=nil
|
6
6
|
@tree = tree
|
7
7
|
@name = name.to_s
|
8
8
|
@parent = parent
|
9
9
|
@node_childs = []
|
10
|
+
@options = {}
|
10
11
|
end
|
11
12
|
|
12
13
|
def resources &block
|
@@ -22,8 +23,9 @@ class ResqueJobsTree::Node
|
|
22
23
|
end
|
23
24
|
|
24
25
|
# Defines a child node.
|
25
|
-
def node name, &block
|
26
|
+
def node name, options={}, &block
|
26
27
|
ResqueJobsTree::Node.new(name, tree, self).tap do |node|
|
28
|
+
node.options = options
|
27
29
|
@node_childs << node
|
28
30
|
node.instance_eval(&block) if block_given?
|
29
31
|
end
|
@@ -47,7 +49,7 @@ class ResqueJobsTree::Node
|
|
47
49
|
ResqueJobsTree::Storage.store self, resources, parent, parent_resources
|
48
50
|
end
|
49
51
|
if node_childs.empty?
|
50
|
-
@tree.enqueue
|
52
|
+
@tree.enqueue(name, *resources) unless options[:async]
|
51
53
|
else
|
52
54
|
childs.call(resources).each do |name, *child_resources|
|
53
55
|
find_node_by_name(name).launch child_resources, resources
|
@@ -60,4 +62,24 @@ class ResqueJobsTree::Node
|
|
60
62
|
node_childs.detect{ |node| node.find_node_by_name _name }
|
61
63
|
end
|
62
64
|
|
65
|
+
def validate!
|
66
|
+
if childs.kind_of?(Proc) && node_childs.empty?
|
67
|
+
raise ResqueJobsTree::NodeInvalid,
|
68
|
+
"node `#{name}` from tree `#{tree.name}` defines childs without child nodes"
|
69
|
+
end
|
70
|
+
unless perform.kind_of? Proc
|
71
|
+
raise ResqueJobsTree::NodeInvalid,
|
72
|
+
"node `#{name}` from tree `#{tree.name}` has no perform block"
|
73
|
+
end
|
74
|
+
if (tree.nodes - [self]).map(&:name).include? name
|
75
|
+
raise ResqueJobsTree::NodeInvalid,
|
76
|
+
"node name `#{name}` is already taken in tree `#{tree.name}`"
|
77
|
+
end
|
78
|
+
node_childs.each &:validate!
|
79
|
+
end
|
80
|
+
|
81
|
+
def nodes
|
82
|
+
node_childs+node_childs.map(&:nodes)
|
83
|
+
end
|
84
|
+
|
63
85
|
end
|
@@ -9,7 +9,10 @@ module ResqueJobsTree::Storage
|
|
9
9
|
parent_key = key parent, parent_resources
|
10
10
|
Resque.redis.hset PARENTS_KEY, node_key, parent_key
|
11
11
|
childs_key = childs_key parent, parent_resources
|
12
|
-
Resque.redis.sadd childs_key, node_key
|
12
|
+
unless Resque.redis.sadd childs_key, node_key
|
13
|
+
raise ResqueJobsTree::JobNotUniq,
|
14
|
+
"Job #{parent.name} already has the child #{node.name} with resources: #{resources}"
|
15
|
+
end
|
13
16
|
end
|
14
17
|
|
15
18
|
def remove node, resources
|
@@ -20,8 +23,16 @@ module ResqueJobsTree::Storage
|
|
20
23
|
end
|
21
24
|
end
|
22
25
|
|
26
|
+
def cleanup node, resources, option={}
|
27
|
+
cleanup_childs node, resources
|
28
|
+
unless node.root?
|
29
|
+
remove_from_siblings node, resources
|
30
|
+
option[:global] ? cleanup_parent(node, resources) : remove_parent_key(node, resources)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
23
34
|
def parent_job_args node, resources
|
24
|
-
|
35
|
+
args_from_key parent_key(node, resources)
|
25
36
|
end
|
26
37
|
|
27
38
|
private
|
@@ -59,4 +70,36 @@ module ResqueJobsTree::Storage
|
|
59
70
|
Resque.redis.del key
|
60
71
|
end
|
61
72
|
|
73
|
+
def cleanup_childs *node_info
|
74
|
+
key = childs_key *node_info
|
75
|
+
Resque.redis.smembers(key).each do |child_key|
|
76
|
+
cleanup *node_info_from_key(child_key)
|
77
|
+
end
|
78
|
+
Resque.redis.del key
|
79
|
+
end
|
80
|
+
|
81
|
+
def cleanup_parent *node_info
|
82
|
+
parent, parent_resources = node_info_from_key(parent_key(*node_info))
|
83
|
+
remove_parent_key *node_info
|
84
|
+
cleanup parent, parent_resources
|
85
|
+
end
|
86
|
+
|
87
|
+
def remove_parent_key *node_info
|
88
|
+
Resque.redis.hdel PARENTS_KEY, key(*node_info)
|
89
|
+
end
|
90
|
+
|
91
|
+
def remove_from_siblings *node_info
|
92
|
+
Resque.redis.srem siblings_key(*node_info), key(*node_info)
|
93
|
+
end
|
94
|
+
|
95
|
+
def args_from_key key
|
96
|
+
JSON.load key.gsub(/JobsTree:Node:/, '')
|
97
|
+
end
|
98
|
+
|
99
|
+
def node_info_from_key key
|
100
|
+
tree_name, node_name, resources = args_from_key(key)
|
101
|
+
node = ResqueJobsTree::Factory.find_tree_by_name(tree_name).find_node_by_name(node_name)
|
102
|
+
[node, resources]
|
103
|
+
end
|
104
|
+
|
62
105
|
end
|
@@ -28,6 +28,15 @@ class ResqueJobsTree::Tree
|
|
28
28
|
root.find_node_by_name name.to_s
|
29
29
|
end
|
30
30
|
|
31
|
+
def validate!
|
32
|
+
raise(ResqueJobsTree::TreeInvalid, "`#{name}` has no root node") unless @root
|
33
|
+
root.validate!
|
34
|
+
end
|
35
|
+
|
36
|
+
def nodes
|
37
|
+
[root, root.nodes].flatten
|
38
|
+
end
|
39
|
+
|
31
40
|
private
|
32
41
|
|
33
42
|
def enqueue_leaves_jobs
|
data/lib/resque_jobs_tree.rb
CHANGED
data/test/factory_test.rb
CHANGED
@@ -3,25 +3,7 @@ require 'test_helper'
|
|
3
3
|
class FactoryTest < MiniTest::Unit::TestCase
|
4
4
|
|
5
5
|
def setup
|
6
|
-
|
7
|
-
root :job1 do
|
8
|
-
perform do |*args|
|
9
|
-
puts 'FactoryTest job1'
|
10
|
-
end
|
11
|
-
childs do |resources|
|
12
|
-
[].tap do |childs|
|
13
|
-
3.times do
|
14
|
-
childs << [:job2, resources.last]
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
node :job2 do
|
19
|
-
perform do |*args|
|
20
|
-
puts 'FactoryTest job2'
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
6
|
+
create_tree
|
25
7
|
end
|
26
8
|
|
27
9
|
def test_tree_creation
|
data/test/job_test.rb
CHANGED
@@ -3,31 +3,13 @@ require 'test_helper'
|
|
3
3
|
class JobTest < MiniTest::Unit::TestCase
|
4
4
|
|
5
5
|
def setup
|
6
|
-
|
7
|
-
root :job1 do
|
8
|
-
perform do |*args|
|
9
|
-
puts 'FactoryTest job1'
|
10
|
-
end
|
11
|
-
childs do |resources|
|
12
|
-
[].tap do |childs|
|
13
|
-
3.times do
|
14
|
-
childs << [:job2, resources.last]
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
node :job2 do
|
19
|
-
perform do |*args|
|
20
|
-
puts 'FactoryTest job2'
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
6
|
+
create_tree
|
25
7
|
@args = [@tree.name, @tree.find_node_by_name('job1').name, 1, 2, 3]
|
26
8
|
end
|
27
9
|
|
28
10
|
def test_tree_node_and_resources
|
29
|
-
result = [@tree
|
30
|
-
assert_equal result, ResqueJobsTree::Job.send(:tree_node_and_resources,
|
11
|
+
result = [@tree.find_node_by_name('job1'), [1, 2, 3]]
|
12
|
+
assert_equal result, ResqueJobsTree::Job.send(:tree_node_and_resources, @args)
|
31
13
|
end
|
32
14
|
|
33
15
|
end
|
data/test/node_test.rb
CHANGED
@@ -65,4 +65,90 @@ class NodeTest < MiniTest::Unit::TestCase
|
|
65
65
|
assert_equal @tree.jobs.first, ['tree1', 'node2', 1, 2, 3]
|
66
66
|
end
|
67
67
|
|
68
|
+
def test_childs_validation
|
69
|
+
assert_raises ResqueJobsTree::NodeInvalid do
|
70
|
+
ResqueJobsTree::Factory.create :tree1 do
|
71
|
+
root :job1 do
|
72
|
+
perform {}
|
73
|
+
childs {}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
ResqueJobsTree::Factory.create :tree1 do
|
78
|
+
root :job1 do
|
79
|
+
perform {}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_perform_validation
|
85
|
+
assert_raises ResqueJobsTree::NodeInvalid do
|
86
|
+
ResqueJobsTree::Factory.create :tree1 do
|
87
|
+
root :job1 do
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_perform_validation
|
94
|
+
assert_raises ResqueJobsTree::NodeInvalid do
|
95
|
+
ResqueJobsTree::Factory.create :tree1 do
|
96
|
+
root :job1 do
|
97
|
+
perform {}
|
98
|
+
childs {}
|
99
|
+
node :job2 do
|
100
|
+
perform {}
|
101
|
+
end
|
102
|
+
node :job2 do
|
103
|
+
perform {}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_options
|
111
|
+
create_async_tree
|
112
|
+
options = { async: true }
|
113
|
+
assert_equal options, @tree.find_node_by_name(:job2).options
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_launch_async
|
117
|
+
create_async_tree
|
118
|
+
resources = [1, 2, 3]
|
119
|
+
@tree.launch resources
|
120
|
+
assert @tree.jobs.empty?
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_launch_continue_on_failure
|
124
|
+
tree = ResqueJobsTree::Factory.create :tree1 do
|
125
|
+
root :job1 do
|
126
|
+
perform { raise 'an unexpected failure' }
|
127
|
+
childs { [:job2] }
|
128
|
+
node :job2, continue_on_failure: true do
|
129
|
+
perform { raise 'an expected failure' }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
resources = [1, 2, 3]
|
134
|
+
assert_raises RuntimeError, 'an unexpected failure' do
|
135
|
+
tree.launch resources
|
136
|
+
end
|
137
|
+
assert_equal [], Resque.keys
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def create_async_tree
|
143
|
+
@tree = ResqueJobsTree::Factory.create :tree1 do
|
144
|
+
root :job1 do
|
145
|
+
perform { raise 'should not arrive here' }
|
146
|
+
childs { [:job2] }
|
147
|
+
node :job2, async: true do
|
148
|
+
perform {}
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
68
154
|
end
|
data/test/storage_test.rb
CHANGED
@@ -3,25 +3,7 @@ require 'test_helper'
|
|
3
3
|
class StorageTest < MiniTest::Unit::TestCase
|
4
4
|
|
5
5
|
def setup
|
6
|
-
|
7
|
-
root :job1 do
|
8
|
-
perform do |*args|
|
9
|
-
puts 'FactoryTest job1'
|
10
|
-
end
|
11
|
-
childs do |resources|
|
12
|
-
[].tap do |childs|
|
13
|
-
3.times do
|
14
|
-
childs << [:job2, resources.last]
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
node :job2 do
|
19
|
-
perform do |*args|
|
20
|
-
puts 'FactoryTest job2'
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
6
|
+
create_tree
|
25
7
|
@resources = [1, 2, 3]
|
26
8
|
@root = @tree.find_node_by_name(:job1)
|
27
9
|
@leaf = @tree.find_node_by_name(:job2)
|
@@ -51,6 +33,23 @@ class StorageTest < MiniTest::Unit::TestCase
|
|
51
33
|
assert_equal 2, variable
|
52
34
|
end
|
53
35
|
|
36
|
+
def test_store_already_stored
|
37
|
+
wrong_tree = ResqueJobsTree::Factory.create :tree1 do
|
38
|
+
root :job1 do
|
39
|
+
perform {}
|
40
|
+
childs do |resources|
|
41
|
+
[ [:job2], [:job2] ]
|
42
|
+
end
|
43
|
+
node :job2 do
|
44
|
+
perform {}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
assert_raises ResqueJobsTree::JobNotUniq do
|
49
|
+
wrong_tree.launch
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
54
53
|
private
|
55
54
|
|
56
55
|
def store
|
data/test/test_helper.rb
CHANGED
@@ -9,8 +9,14 @@ $LOAD_PATH.unshift $dir + '/../lib'
|
|
9
9
|
require 'resque_jobs_tree'
|
10
10
|
$TESTING = true
|
11
11
|
|
12
|
+
require 'mock_redis'
|
13
|
+
Resque.redis = MockRedis.new
|
14
|
+
|
12
15
|
Resque.inline = true
|
13
16
|
|
17
|
+
#
|
18
|
+
# Fixtures
|
19
|
+
#
|
14
20
|
class Model
|
15
21
|
def id
|
16
22
|
@id ||= rand 1000
|
@@ -20,14 +26,53 @@ class Model
|
|
20
26
|
end
|
21
27
|
end
|
22
28
|
|
29
|
+
|
23
30
|
# Run resque callbacks in inline mode
|
24
31
|
class ResqueJobsTree::Job
|
25
32
|
class << self
|
26
33
|
def perform_with_hook *args
|
27
|
-
|
28
|
-
|
34
|
+
begin
|
35
|
+
perform_without_hook *args
|
36
|
+
after_perform_enqueue_parent *args
|
37
|
+
rescue => exception
|
38
|
+
on_failure_cleanup exception, *args
|
39
|
+
end
|
29
40
|
end
|
30
41
|
alias_method :perform_without_hook, :perform
|
31
42
|
alias_method :perform, :perform_with_hook
|
32
43
|
end
|
33
44
|
end
|
45
|
+
|
46
|
+
class MiniTest::Unit::TestCase
|
47
|
+
|
48
|
+
def teardown
|
49
|
+
redis.keys.each{ |key| redis.del key }
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_tree
|
53
|
+
@tree = ResqueJobsTree::Factory.create :tree1 do
|
54
|
+
root :job1 do
|
55
|
+
perform do |*args|
|
56
|
+
Resque.redis.rpush 'history', 'tree1 job1'
|
57
|
+
end
|
58
|
+
childs do |resources|
|
59
|
+
[].tap do |childs|
|
60
|
+
3.times do |n|
|
61
|
+
childs << [:job2, n]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
node :job2 do
|
66
|
+
perform do |*args|
|
67
|
+
Resque.redis.rpush 'history', 'tree1 job2'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def redis
|
75
|
+
Resque.redis
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
data/test/tree_test.rb
CHANGED
@@ -3,25 +3,7 @@ require 'test_helper'
|
|
3
3
|
class TreeTest < MiniTest::Unit::TestCase
|
4
4
|
|
5
5
|
def setup
|
6
|
-
|
7
|
-
root :job1 do
|
8
|
-
perform do |*args|
|
9
|
-
# puts 'TreeTest job1'
|
10
|
-
end
|
11
|
-
childs do |resources|
|
12
|
-
[].tap do |childs|
|
13
|
-
3.times do
|
14
|
-
childs << [:job2, resources.last]
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
node :job2 do
|
19
|
-
perform do |*args|
|
20
|
-
# puts 'TreeTest job2'
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
6
|
+
create_tree
|
25
7
|
end
|
26
8
|
|
27
9
|
def test_name
|
@@ -49,6 +31,19 @@ class TreeTest < MiniTest::Unit::TestCase
|
|
49
31
|
def test_launch
|
50
32
|
resources = [1, 2, 3]
|
51
33
|
@tree.launch *resources
|
34
|
+
history = ['tree1 job2']*3+['tree1 job1']
|
35
|
+
assert_equal history, redis.lrange('history', 0, -1)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_launch_with_no_resources
|
39
|
+
@tree.launch
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_should_have_root
|
43
|
+
assert_raises ResqueJobsTree::TreeInvalid do
|
44
|
+
ResqueJobsTree::Factory.create :tree1 do
|
45
|
+
end
|
46
|
+
end
|
52
47
|
end
|
53
48
|
|
54
49
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resque_jobs_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-04-
|
12
|
+
date: 2013-04-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|