resque_jobs_tree 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
- * stokcs in Redis the childhood relationsips between them.
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
- The rule: the name of a tree of jobs should be uniq, and the name of a node should be uniq in a scope of a tree.
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
 
@@ -8,6 +8,7 @@ module ResqueJobsTree::Factory
8
8
  ResqueJobsTree::Tree.new(name).tap do |tree|
9
9
  @trees << tree
10
10
  tree.instance_eval &block
11
+ tree.validate!
11
12
  end
12
13
  end
13
14
 
@@ -3,28 +3,42 @@ class ResqueJobsTree::Job
3
3
  class << self
4
4
 
5
5
  def perform *args
6
- tree, node, resources = tree_node_and_resources(*args)
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
- tree, node, resources = tree_node_and_resources(*args)
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
- Resque.enqueue_to tree.name, ResqueJobsTree::Job, *parent_job_args
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 tree_node_and_resources *args
23
- tree_name , job_name = args.shift(2)
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
- [tree, node, resources]
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 name, *resources
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
- JSON.load parent_key(node, resources).gsub(/JobsTree:Node:/, '')
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
@@ -1,3 +1,3 @@
1
1
  module ResqueJobsTree
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -9,4 +9,7 @@ require 'resque_jobs_tree/resources_serializer'
9
9
  require 'resque_jobs_tree/storage'
10
10
 
11
11
  module ResqueJobsTree
12
+ class TreeInvalid < Exception ; end
13
+ class NodeInvalid < Exception ; end
14
+ class JobNotUniq < Exception ; end
12
15
  end
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
- @tree = ResqueJobsTree::Factory.create :tree1 do
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
- @tree = ResqueJobsTree::Factory.create :tree1 do
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, @tree.find_node_by_name('job1'), [1, 2, 3]]
30
- assert_equal result, ResqueJobsTree::Job.send(:tree_node_and_resources, *@args)
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
- @tree = ResqueJobsTree::Factory.create :tree1 do
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
- perform_without_hook *args
28
- after_perform_enqueue_parent *args
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
- @tree = ResqueJobsTree::Factory.create :tree1 do
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.2
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-09 00:00:00.000000000 Z
12
+ date: 2013-04-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler