sidekiq-superworker 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -53,8 +53,6 @@ Superworker.create(:MySuperworker, :user_id, :comment_id) do
53
53
  end
54
54
  ```
55
55
 
56
- If you're also using [sidekiq_monitor](https://github.com/socialpandas/sidekiq_monitor), you can easily monitor when a superworker is running and when it has finished.
57
-
58
56
  Installation
59
57
  ------------
60
58
 
@@ -100,6 +98,45 @@ Superworker.create(:MySuperworker, :user_id, :comment_id) do
100
98
  end
101
99
  ```
102
100
 
101
+ ### Monitoring
102
+
103
+ Using [sidekiq_monitor](https://github.com/socialpandas/sidekiq_monitor) with Sidekiq Superworker is strongly encouraged, as it lets you easily monitor when a superjob is running, when it has finished, whether it has encountered errors, and the status of all of its subjobs.
104
+
105
+ ### Batch Jobs
106
+
107
+ By using a `batch` block, you can create batches of subjobs that are all associated with the superjob. The following will run Worker1 and Worker2 in serial for every user ID in the array passed to perform_async.
108
+
109
+ ```ruby
110
+ Superworker.create(:MyBatchSuperworker, :user_ids) do
111
+ batch user_ids: :user_id do
112
+ Worker1 :user_id
113
+ Worker2 :user_id
114
+ end
115
+ end
116
+
117
+ MyBatchSuperworker.perform_async([30, 31, 32, 33, 34, 35])
118
+ ```
119
+
120
+ Grouping jobs into batches greatly improves your ability to audit them and determine when batches have finished.
121
+
122
+ ### Superjob Names
123
+
124
+ If you're using sidekiq_monitor and want to set a name for a superjob, you can set it in an additional argument, like so:
125
+
126
+ ```ruby
127
+ # Unnamed
128
+ MySuperworker.perform_async(23)
129
+
130
+ # Named
131
+ MySuperworker.perform_async(23, name: 'My job name')
132
+ ```
133
+
134
+ ### Errors
135
+
136
+ If a subjob encounters an exception, the subjobs that depend on it won't run, but the rest of the subjobs will continue as usual.
137
+
138
+ If sidekiq_monitor is being used, the exception will be bubbled up to the superjob, which lets you easily see when your superjobs have failed.
139
+
103
140
  License
104
141
  -------
105
142
 
@@ -0,0 +1,16 @@
1
+ - subjobs = Sidekiq::Superworker::Subjob.where(superjob_id: job.jid).where('subworker_class != ?', 'batch').order('subjob_id')
2
+
3
+ table.table.table-condensed.table-striped.table-hover
4
+ tr
5
+ th ID
6
+ th Class
7
+ th Args
8
+ th Status
9
+ th JID
10
+ - subjobs.each do |subjob|
11
+ tr
12
+ td = subjob.id
13
+ td = subjob.subworker_class
14
+ td = subjob.arg_values
15
+ td = subjob.status
16
+ td = subjob.jid
@@ -28,4 +28,14 @@ Sidekiq.configure_server do |config|
28
28
  end
29
29
 
30
30
  Superworker = Sidekiq::Superworker::Worker unless Object.const_defined?('Superworker')
31
- Sidekiq::Monitor::Cleaner.add_ignored_queue(Sidekiq::Superworker::SuperjobProcessor.queue_name) if defined?(Sidekiq::Monitor)
31
+
32
+ # If sidekiq_monitor is being used, customize how superjobs are monitored
33
+ if defined?(Sidekiq::Monitor)
34
+ # Make Cleaner ignore superjobs, as they don't exist in Redis and thus won't be synced with Sidekiq::Monitor::Job
35
+ Sidekiq::Monitor::Cleaner.add_ignored_queue(Sidekiq::Superworker::SuperjobProcessor.queue_name) if defined?(Sidekiq::Monitor)
36
+
37
+ # Add a custom view that shows the subjobs for a superjob
38
+ Sidekiq::Monitor::CustomViews.add 'Subjobs', "#{directory}/../../app/views/sidekiq/superworker/subjobs" do |job|
39
+ job.queue == Sidekiq::Superworker::SuperjobProcessor.queue_name.to_s
40
+ end
41
+ end
@@ -0,0 +1,152 @@
1
+ module Sidekiq
2
+ module Superworker
3
+ class DSLHash
4
+ attr_accessor :record_id, :records
5
+
6
+ def initialize
7
+ reset
8
+ end
9
+
10
+ def nested_hash_to_records(nested_hash, args)
11
+ reset
12
+ @args = args
13
+ nested_hash_to_records_recursive(nested_hash)
14
+ end
15
+
16
+ def rewrite_ids_of_nested_hash(nested_hash, record_id)
17
+ reset
18
+ @record_id = record_id
19
+ rewrite_ids_of_nested_hash_recursive(nested_hash)
20
+ end
21
+
22
+ private
23
+
24
+ def reset
25
+ @args = {}
26
+ @records = {}
27
+ @record_id = 1
28
+ end
29
+
30
+ def nested_hash_to_records_recursive(nested_hash, options={})
31
+ return @records if nested_hash.blank?
32
+
33
+ defaults = {
34
+ parent_id: nil,
35
+ scoped_args: nil # Args that are scoped to this subset of the nested hash (necessary for batch hashes)
36
+ }
37
+ options.reverse_merge!(defaults)
38
+ parent_id = options[:parent_id]
39
+ last_id = nil
40
+
41
+ nested_hash.values.each do |value|
42
+ id = @record_id
43
+ @record_id += 1
44
+ arg_values = value[:arg_keys].collect do |arg_key|
45
+ # Allow for subjob arg_values to be set within the superworker definition; if a symbol is
46
+ # used in the DSL, use @args[arg_key], and otherwise use arg_key as the value
47
+ if arg_key.is_a?(Symbol)
48
+ options[:scoped_args] ? options[:scoped_args][arg_key] : @args[arg_key]
49
+ else
50
+ arg_key
51
+ end
52
+ end
53
+
54
+ @records[id] = {
55
+ subjob_id: id,
56
+ subworker_class: value[:subworker_class].to_s,
57
+ arg_keys: value[:arg_keys],
58
+ arg_values: arg_values,
59
+ parent_id: parent_id
60
+ }
61
+ if value[:subworker_class] == :batch
62
+ @records[id][:children_ids] = children_ids_for_batch(value[:children], arg_values[0])
63
+ end
64
+
65
+ @records[last_id][:next_id] = id if @records[last_id]
66
+ last_id = id
67
+
68
+ if parent_id && @records[parent_id]
69
+ @records[parent_id][:children_ids] ||= []
70
+ @records[parent_id][:children_ids] << id
71
+ end
72
+
73
+ nested_hash_to_records_recursive(value[:children], parent_id: id, scoped_args: options[:scoped_args]) if value[:children] && value[:subworker_class] != :batch
74
+ end
75
+ @records
76
+ end
77
+
78
+ def rewrite_ids_of_nested_hash_recursive(nested_hash)
79
+ new_hash = {}
80
+ nested_hash.each do |old_record_id, record|
81
+ @record_id += 1
82
+ parent_record_id = @record_id
83
+ new_hash[parent_record_id] = record
84
+ if record[:children]
85
+ new_hash[parent_record_id][:children] = rewrite_ids_of_nested_hash_recursive(record[:children])
86
+ end
87
+ end
88
+ new_hash
89
+ end
90
+
91
+ def children_ids_for_batch(subjobs, batch_keys_to_iteration_keys)
92
+ iteration_keys = batch_keys_to_iteration_keys.values
93
+ batch_iteration_arg_value_arrays = get_batch_iteration_arg_value_arrays(batch_keys_to_iteration_keys)
94
+
95
+ batch_id = @record_id - 1
96
+
97
+ children_ids = []
98
+ batch_iteration_arg_value_arrays.each do |batch_iteration_arg_value_array|
99
+ iteration_args = {}
100
+ batch_iteration_arg_value_array.each_with_index do |arg_value, arg_index|
101
+ arg_key = iteration_keys[arg_index]
102
+ iteration_args[arg_key] = arg_value
103
+ end
104
+
105
+ batch_child_id = @record_id
106
+ batch_child = {
107
+ subjob_id: batch_child_id,
108
+ subworker_class: 'batch_child',
109
+ arg_keys: iteration_keys,
110
+ arg_values: iteration_args.values,
111
+ parent_id: batch_id
112
+ }
113
+ @records[batch_child_id] = batch_child
114
+
115
+ @record_id += 1
116
+ subjobs.values.each_with_index do |subjob, index|
117
+ subjob_id = @record_id
118
+ @record_id += 1
119
+ subjob = subjob.dup
120
+ children = subjob.delete(:children)
121
+ subjob[:subjob_id] = subjob_id
122
+ subjob[:parent_id] = batch_child_id
123
+ subjob[:arg_values] = iteration_args.values
124
+ @records[subjob_id] = subjob
125
+ nested_hash_to_records_recursive(children, parent_id: subjob_id, scoped_args: iteration_args)
126
+ subjob[:next_id] = subjob_id + 1 if index < (subjobs.values.length - 1)
127
+ end
128
+
129
+ children_ids << batch_child_id
130
+ end
131
+
132
+ children_ids
133
+ end
134
+
135
+ # Returns an array of argument value arrays, each of which should be passed to each of the
136
+ # batch iterations
137
+ def get_batch_iteration_arg_value_arrays(batch_keys_to_iteration_keys)
138
+ batch_keys = batch_keys_to_iteration_keys.keys
139
+ batch_keys_to_batch_values = @args.slice(*(batch_keys))
140
+
141
+ batch_values = batch_keys_to_batch_values.values
142
+ first_batch_value = batch_values.pop
143
+ if batch_values.length > 0
144
+ batch_values = first_batch_value.zip(batch_values)
145
+ else
146
+ batch_values = first_batch_value.zip
147
+ end
148
+ batch_values
149
+ end
150
+ end
151
+ end
152
+ end
@@ -3,75 +3,42 @@ module Sidekiq
3
3
  class DSLParser
4
4
  def self.parse(block)
5
5
  @dsl_evaluator = DSLEvaluator.new
6
- set_records_from_block(block)
7
- {
8
- records: @records,
9
- nested_records: @nested_records
10
- }
11
- end
12
-
13
- def self.set_records_from_block(block)
6
+ @dsl_hash = DSLHash.new
14
7
  @record_id = 0
15
- @records = {}
16
- @nested_records = block_to_nested_records(block)
17
- set_records_from_nested_records(@nested_records)
18
- end
19
-
20
- def self.set_records_from_nested_records(nested_records, parent_id=nil)
21
- last_id = nil
22
- nested_records.each do |id, value|
23
- @records[id] = {
24
- subjob_id: id,
25
- subworker_class: value[:subworker_class].to_s,
26
- arg_keys: value[:arg_keys],
27
- parent_id: parent_id,
28
- children_ids: value[:children] ? value[:children].keys : nil
29
- }
30
- @records[last_id][:next_id] = id if @records[last_id]
31
- last_id = id
32
- set_records_from_nested_records(value[:children], id) if value[:children]
33
- end
8
+ block_to_nested_hash(block)
34
9
  end
35
10
 
36
- def self.block_to_nested_records(block)
11
+ def self.block_to_nested_hash(block)
37
12
  fiber = Fiber.new do
38
13
  @dsl_evaluator.instance_eval(&block)
39
14
  end
40
15
 
41
- nested_records = {}
16
+ nested_hash = {}
42
17
  while (method_result = fiber.resume)
43
- method, arg_keys, block = method_result
18
+ method, args, block = method_result
44
19
  @record_id += 1
45
20
  if block
46
- nested_records[@record_id] = { subworker_class: method, arg_keys: arg_keys, children: block_to_nested_records(block) }
21
+ if method == :batch
22
+ nested_hash[@record_id] = { subworker_class: method, arg_keys: args, children: block_to_nested_hash(block) }
23
+ else
24
+ nested_hash[@record_id] = { subworker_class: method, arg_keys: args, children: block_to_nested_hash(block) }
25
+ end
47
26
  else
48
- nested_records[@record_id] = { subworker_class: method, arg_keys: arg_keys }
27
+ nested_hash[@record_id] = { subworker_class: method, arg_keys: args }
49
28
  end
50
29
 
51
- # For superworkers nested within other superworkers, we'll take the subworkers' nested_records,
30
+ # For superworkers nested within other superworkers, we'll take the subworkers' nested_hash,
52
31
  # adjust their ids to fit in with our current @record_id value, and add them into the tree.
53
- if method != :parallel
32
+ unless [:parallel, :batch].include?(method)
54
33
  subworker_class = method.to_s.constantize
55
34
  if subworker_class.respond_to?(:is_a_superworker?) && subworker_class.is_a_superworker?
56
35
  parent_record_id = @record_id
57
- nested_records[parent_record_id][:children] = rewrite_ids_of_subworker_records(subworker_class.nested_records)
36
+ nested_hash[parent_record_id][:children] = @dsl_hash.rewrite_ids_of_nested_hash(subworker_class.nested_hash, @record_id)
37
+ @record_id = @dsl_hash.record_id
58
38
  end
59
39
  end
60
40
  end
61
- nested_records
62
- end
63
-
64
- def self.rewrite_ids_of_subworker_records(nested_records)
65
- new_hash = {}
66
- nested_records.each do |old_record_id, record|
67
- @record_id += 1
68
- parent_record_id = @record_id
69
- new_hash[parent_record_id] = record
70
- if record[:children]
71
- new_hash[parent_record_id][:children] = rewrite_ids_of_subworker_records(record[:children])
72
- end
73
- end
74
- new_hash
41
+ nested_hash
75
42
  end
76
43
  end
77
44
  end
@@ -16,6 +16,15 @@ module Sidekiq
16
16
  enqueue(child)
17
17
  end
18
18
  jid = jids.first
19
+ elsif subjob.subworker_class == 'batch'
20
+ subjob.update_attribute(:status, 'running')
21
+
22
+ Superworker.debug "#{subjob.to_info}: Enqueueing batch children"
23
+ jids = subjob.children.collect do |child|
24
+ child.update_attribute(:status, 'running')
25
+ enqueue(child.children.first)
26
+ end
27
+ jid = jids.first
19
28
  else
20
29
  klass = "::#{subjob.subworker_class}".constantize
21
30
 
@@ -77,11 +86,14 @@ module Sidekiq
77
86
  Superworker.debug "#{subjob.to_info}: Descendants are complete"
78
87
  subjob.update_attribute(:descendants_are_complete, true)
79
88
 
89
+ if subjob.subworker_class == 'batch_child' || subjob.subworker_class == 'batch'
90
+ complete(subjob)
91
+ end
92
+
80
93
  parent = subjob.parent
81
94
  is_child_of_parallel = parent && parent.subworker_class == 'parallel'
82
95
 
83
- # If this is a child of a parallel subjob, check to see if the parent's descendants are all complete
84
- # and call descendants_are_complete(parent) if so
96
+ # If a parent exists, check whether this subjob's siblings are all complete
85
97
  if parent
86
98
  siblings_descendants_are_complete = parent.children.all? { |child| child.descendants_are_complete }
87
99
  if siblings_descendants_are_complete
@@ -99,11 +111,8 @@ module Sidekiq
99
111
  return
100
112
  end
101
113
 
102
- # Otherwise, if a parent exists, the parent's descendants are complete
103
- if parent
104
- descendants_are_complete(parent)
105
- # Otherwise, this is the final subjob of the superjob
106
- else
114
+ # If there isn't a parent, then, this is the final subjob of the superjob
115
+ unless parent
107
116
  Superworker.debug "#{subjob.to_info}: Superjob is complete"
108
117
  SuperjobProcessor.complete(subjob.superjob_id)
109
118
  end
@@ -5,7 +5,9 @@ module Sidekiq
5
5
  :superworker
6
6
  end
7
7
 
8
- def self.create(superjob_id, superworker_class_name, args, subjobs)
8
+ def self.create(superjob_id, superworker_class_name, args, subjobs, options={})
9
+ Superworker.debug "Superworker ##{superjob_id}: create"
10
+
9
11
  # If sidekiq_monitor is being used, create a Sidekiq::Monitor::Job for the superjob
10
12
  if defined?(Sidekiq::Monitor)
11
13
  now = Time.now
@@ -16,7 +18,8 @@ module Sidekiq
16
18
  args: args,
17
19
  enqueued_at: now,
18
20
  started_at: now,
19
- status: 'running'
21
+ status: 'running',
22
+ name: options[:name]
20
23
  )
21
24
  end
22
25
 
@@ -26,6 +29,8 @@ module Sidekiq
26
29
  end
27
30
 
28
31
  def self.complete(superjob_id)
32
+ Superworker.debug "Superworker ##{superjob_id}: complete"
33
+
29
34
  # Set the superjob Sidekiq::Monitor::Job as being complete
30
35
  if defined?(Sidekiq::Monitor)
31
36
  job = Sidekiq::Monitor::Job.where(queue: queue_name, jid: superjob_id).first
@@ -39,6 +44,8 @@ module Sidekiq
39
44
  end
40
45
 
41
46
  def self.error(superjob_id, worker, item, exception)
47
+ Superworker.debug "Superworker ##{superjob_id}: error"
48
+
42
49
  if defined?(Sidekiq::Monitor)
43
50
  job = Sidekiq::Monitor::Job.where(queue: queue_name, jid: superjob_id).first
44
51
  if job
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Superworker
3
- VERSION = '0.0.5'
3
+ VERSION = '0.0.6'
4
4
  end
5
5
  end
@@ -3,44 +3,56 @@ module Sidekiq
3
3
  class Worker
4
4
  def self.create(*args, &block)
5
5
  class_name = args.shift.to_sym
6
- dsl = DSLParser.parse(block)
7
- create_class(class_name, args, dsl)
6
+ nested_hash = DSLParser.parse(block)
7
+ create_class(class_name, args, nested_hash)
8
8
  end
9
9
 
10
10
  protected
11
11
 
12
- def self.create_class(class_name, arg_keys, dsl)
12
+ def self.create_class(class_name, arg_keys, nested_hash)
13
13
  klass = Class.new do
14
14
  @class_name = class_name
15
15
  @arg_keys = arg_keys
16
- @records = dsl[:records]
17
- @nested_records = dsl[:nested_records]
16
+ @nested_hash = nested_hash
17
+ @dsl_hash = DSLHash.new
18
18
 
19
19
  class << self
20
- attr_reader :nested_records
20
+ attr_reader :nested_hash
21
21
 
22
22
  def is_a_superworker?
23
23
  true
24
24
  end
25
25
 
26
26
  def perform_async(*arg_values)
27
+ superjob_options = {}
28
+
29
+ # If an additional argument value is given, it's the superjob's options
30
+ if (arg_values.length == @arg_keys.length + 1) && arg_values.last.is_a?(Hash)
31
+ superjob_options = arg_values.last
32
+ elsif @arg_keys.length != arg_values.length
33
+ raise "Wrong number of arguments for #{name}.perform_async (#{arg_values.length} for #{@arg_keys.length})"
34
+ end
35
+
27
36
  @args = Hash[@arg_keys.zip(arg_values)]
28
- subjobs = create_subjobs
29
- SuperjobProcessor.create(@superjob_id, @class_name, arg_values, subjobs)
37
+ @superjob_id = SecureRandom.hex(12)
38
+ subjobs = create_subjobs(arg_values)
39
+ SuperjobProcessor.create(@superjob_id, @class_name, arg_values, subjobs, superjob_options)
30
40
  end
31
41
 
32
42
  protected
33
43
 
34
- def create_subjobs
35
- @superjob_id = SecureRandom.hex(12)
36
- @records.collect do |id, record|
44
+ def create_subjobs(arg_values)
45
+ records = @dsl_hash.nested_hash_to_records(@nested_hash, @args)
46
+ records.collect do |id, record|
37
47
  record[:status] = 'initialized'
38
48
  record[:superjob_id] = @superjob_id
39
49
  record[:superworker_class] = @class_name
40
- record[:arg_values] = record[:arg_keys].collect do |arg_key|
41
- # Allow for subjob arg_values to be set within the superworker definition; if a symbol is
42
- # used in the DSL, use @args[arg_key], and otherwise use arg_key as the value
43
- arg_key.is_a?(Symbol) ? @args[arg_key] : arg_key
50
+ unless record.key?(:arg_values)
51
+ record[:arg_values] = record[:arg_keys].collect do |arg_key|
52
+ # Allow for subjob arg_values to be set within the superworker definition; if a symbol is
53
+ # used in the DSL, use @args[arg_key], and otherwise use arg_key as the value
54
+ arg_key.is_a?(Symbol) ? @args[arg_key] : arg_key
55
+ end
44
56
  end
45
57
  Sidekiq::Superworker::Subjob.create(record)
46
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-superworker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
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-07-02 00:00:00.000000000 Z
12
+ date: 2013-07-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
@@ -99,9 +99,11 @@ extensions: []
99
99
  extra_rdoc_files: []
100
100
  files:
101
101
  - app/models/sidekiq/superworker/subjob.rb
102
+ - app/views/sidekiq/superworker/subjobs.slim
102
103
  - lib/generators/sidekiq/superworker/install/install_generator.rb
103
104
  - lib/generators/sidekiq/superworker/install/templates/create_sidekiq_superworker_subjobs.rb
104
105
  - lib/sidekiq/superworker/dsl_evaluator.rb
106
+ - lib/sidekiq/superworker/dsl_hash.rb
105
107
  - lib/sidekiq/superworker/dsl_parser.rb
106
108
  - lib/sidekiq/superworker/logging.rb
107
109
  - lib/sidekiq/superworker/processor.rb