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 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