asynchronic 2.0.1 → 4.0.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.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +7 -10
  3. data/README.md +1 -2
  4. data/asynchronic.gemspec +3 -2
  5. data/lib/asynchronic.rb +13 -3
  6. data/lib/asynchronic/data_store/in_memory.rb +17 -15
  7. data/lib/asynchronic/data_store/key.rb +3 -3
  8. data/lib/asynchronic/data_store/lazy_value.rb +5 -3
  9. data/lib/asynchronic/data_store/redis.rb +22 -14
  10. data/lib/asynchronic/data_store/scoped_store.rb +18 -19
  11. data/lib/asynchronic/environment.rb +6 -6
  12. data/lib/asynchronic/error.rb +2 -3
  13. data/lib/asynchronic/garbage_collector.rb +2 -0
  14. data/lib/asynchronic/job.rb +12 -12
  15. data/lib/asynchronic/notifier/broadcaster.rb +34 -0
  16. data/lib/asynchronic/notifier/in_memory.rb +33 -0
  17. data/lib/asynchronic/process.rb +52 -38
  18. data/lib/asynchronic/queue_engine/in_memory.rb +17 -11
  19. data/lib/asynchronic/queue_engine/ost.rb +15 -12
  20. data/lib/asynchronic/queue_engine/synchronic.rb +19 -7
  21. data/lib/asynchronic/version.rb +1 -1
  22. data/lib/asynchronic/worker.rb +7 -10
  23. data/spec/data_store/data_store_examples.rb +17 -9
  24. data/spec/data_store/in_memory_spec.rb +0 -2
  25. data/spec/data_store/key_spec.rb +1 -1
  26. data/spec/data_store/lazy_value_examples.rb +7 -6
  27. data/spec/data_store/redis_spec.rb +4 -10
  28. data/spec/expectations.rb +2 -2
  29. data/spec/facade_spec.rb +5 -5
  30. data/spec/jobs.rb +12 -12
  31. data/spec/minitest_helper.rb +6 -12
  32. data/spec/process/life_cycle_examples.rb +111 -64
  33. data/spec/process/life_cycle_in_memory_spec.rb +1 -1
  34. data/spec/process/life_cycle_redis_spec.rb +1 -1
  35. data/spec/queue_engine/in_memory_spec.rb +1 -3
  36. data/spec/queue_engine/ost_spec.rb +1 -7
  37. data/spec/queue_engine/queue_engine_examples.rb +17 -9
  38. data/spec/queue_engine/synchronic_spec.rb +1 -1
  39. data/spec/worker/in_memory_spec.rb +2 -2
  40. data/spec/worker/redis_spec.rb +1 -6
  41. data/spec/worker/worker_examples.rb +7 -5
  42. metadata +38 -17
@@ -1,18 +1,6 @@
1
1
  module Asynchronic
2
2
  class Job
3
3
 
4
- def initialize(process)
5
- @process = process
6
- end
7
-
8
- def params
9
- @process.params
10
- end
11
-
12
- def result(reference)
13
- @process[reference].result
14
- end
15
-
16
4
  def self.queue(name=nil)
17
5
  name ? @queue = name : @queue
18
6
  end
@@ -23,6 +11,18 @@ module Asynchronic
23
11
  process.id
24
12
  end
25
13
 
14
+ def initialize(process)
15
+ @process = process
16
+ end
17
+
18
+ def params
19
+ process.params
20
+ end
21
+
22
+ def result(reference)
23
+ process[reference].result
24
+ end
25
+
26
26
  private
27
27
 
28
28
  attr_reader :process
@@ -0,0 +1,34 @@
1
+ module Asynchronic
2
+ module Notifier
3
+ class Broadcaster
4
+
5
+ def initialize(options={})
6
+ options[:logger] ||= Asynchronic.logger
7
+ @broadcaster = ::Broadcaster.new options
8
+ end
9
+
10
+ def publish(pid, event, data=nil)
11
+ broadcaster.publish DataStore::Key[pid][event], data
12
+ end
13
+
14
+ def subscribe(pid, event, &block)
15
+ broadcaster.subscribe DataStore::Key[pid][event] do |data|
16
+ block.call data
17
+ end
18
+ end
19
+
20
+ def unsubscribe(subscription_id)
21
+ broadcaster.unsubscribe subscription_id
22
+ end
23
+
24
+ def unsubscribe_all
25
+ broadcaster.unsubscribe_all
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :broadcaster
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ module Asynchronic
2
+ module Notifier
3
+ class InMemory
4
+
5
+ def publish(pid, event, data=nil)
6
+ subscriptions[DataStore::Key[pid][event]].each_value do |block|
7
+ block.call data
8
+ end
9
+ end
10
+
11
+ def subscribe(pid, event, &block)
12
+ SecureRandom.uuid.tap do |subscription_id|
13
+ subscriptions[DataStore::Key[pid][event]][subscription_id] = block
14
+ end
15
+ end
16
+
17
+ def unsubscribe(subscription_id)
18
+ subscriptions.each_value { |s| s.delete subscription_id }
19
+ end
20
+
21
+ def unsubscribe_all
22
+ subscriptions.clear
23
+ end
24
+
25
+ private
26
+
27
+ def subscriptions
28
+ @subscriptions ||= Hash.new { |h,k| h[k] = {} }
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -13,10 +13,36 @@ module Asynchronic
13
13
 
14
14
  ATTRIBUTE_NAMES = [:type, :name, :queue, :status, :dependencies, :data, :result, :error, :connection_name] | TIME_TRACKING_MAP.values.uniq
15
15
 
16
+ AUTOMATIC_ABORTED_ERROR_MESSAGE = 'Automatic aborted before execution'
16
17
  CANCELED_ERROR_MESSAGE = 'Canceled'
18
+ DEAD_ERROR_MESSAGE = 'Process connection broken'
17
19
 
18
20
  attr_reader :id
19
21
 
22
+ def self.create(environment, type, params={})
23
+ id = params.delete(:id) || SecureRandom.uuid
24
+
25
+ Asynchronic.logger.debug('Asynchronic') { "Created process #{type} - #{id} - #{params}" }
26
+
27
+ new(environment, id) do
28
+ self.type = type
29
+ self.name = (params.delete(:alias) || type).to_s
30
+ self.queue = params.delete(:queue) || type.queue || parent_queue
31
+ self.dependencies = Array(params.delete(:dependencies)) | Array(params.delete(:dependency)) | infer_dependencies(params)
32
+ self.params = params
33
+ self.data = {}
34
+ pending!
35
+ end
36
+ end
37
+
38
+ def self.all(environment)
39
+ environment.data_store.keys
40
+ .select { |k| k.sections.count == 2 && k.match(/created_at$/) }
41
+ .sort_by { |k| environment.data_store[k] }
42
+ .reverse
43
+ .map { |k| Process.new environment, k.remove_last }
44
+ end
45
+
20
46
  def initialize(environment, id, &block)
21
47
  @environment = environment
22
48
  @id = DataStore::Key[id]
@@ -51,6 +77,10 @@ module Asynchronic
51
77
  (running? && !connected?) || processes.any?(&:dead?)
52
78
  end
53
79
 
80
+ def abort_if_dead
81
+ abort! DEAD_ERROR_MESSAGE if dead?
82
+ end
83
+
54
84
  def destroy
55
85
  data_store.delete_cascade
56
86
  end
@@ -78,9 +108,11 @@ module Asynchronic
78
108
  end
79
109
 
80
110
  def processes
81
- data_store.scoped(:processes).keys.
82
- select { |k| k.sections.count == 2 && k.match(/\|name$/) }.
83
- sort.map { |k| Process.new environment, id[:processes][k.remove_last] }
111
+ data_store.scoped(:processes)
112
+ .keys
113
+ .select { |k| k.sections.count == 2 && k.match(/\|name$/) }
114
+ .sort
115
+ .map { |k| Process.new environment, id[:processes][k.remove_last] }
84
116
  end
85
117
 
86
118
  def parent
@@ -92,22 +124,20 @@ module Asynchronic
92
124
  end
93
125
 
94
126
  def real_error
95
- return nil unless error
127
+ return nil if !aborted?
96
128
 
97
- processes.each do |child|
98
- return child.real_error if child.error
99
- end
129
+ first_aborted_child = processes.select(&:aborted?).sort_by(&:finalized_at).first
100
130
 
101
- error.message
131
+ first_aborted_child ? first_aborted_child.real_error : error.message
102
132
  end
103
133
 
104
134
  def dependencies
105
135
  return [] if parent.nil? || data_store[:dependencies].empty?
106
-
136
+
107
137
  parent_processes = parent.processes.each_with_object({}) do |process, hash|
108
138
  hash[process.name] = process
109
139
  end
110
-
140
+
111
141
  data_store[:dependencies].map { |d| parent_processes[d.to_s] }
112
142
  end
113
143
 
@@ -131,7 +161,7 @@ module Asynchronic
131
161
  wakeup_children
132
162
  end
133
163
  Asynchronic.logger.info('Asynchronic') { "Wakeup finalized #{type} (#{id})" }
134
-
164
+
135
165
  parent.wakeup if parent && finalized?
136
166
  end
137
167
 
@@ -147,29 +177,6 @@ module Asynchronic
147
177
  self.data = self.data.merge key => value
148
178
  end
149
179
 
150
- def self.create(environment, type, params={})
151
- id = params.delete(:id) || SecureRandom.uuid
152
-
153
- Asynchronic.logger.debug('Asynchronic') { "Created process #{type} - #{id} - #{params}" }
154
-
155
- new(environment, id) do
156
- self.type = type
157
- self.name = (params.delete(:alias) || type).to_s
158
- self.queue = params.delete(:queue) || type.queue || parent_queue
159
- self.dependencies = Array(params.delete(:dependencies)) | Array(params.delete(:dependency)) | infer_dependencies(params)
160
- self.params = params
161
- self.data = {}
162
- pending!
163
- end
164
- end
165
-
166
- def self.all(environment)
167
- environment.data_store.keys.
168
- select { |k| k.sections.count == 2 && k.match(/created_at$/) }.
169
- sort_by { |k| environment.data_store[k] }.reverse.
170
- map { |k| Process.new environment, k.remove_last }
171
- end
172
-
173
180
  private
174
181
 
175
182
  attr_reader :environment
@@ -190,8 +197,12 @@ module Asynchronic
190
197
 
191
198
  def status=(status)
192
199
  Asynchronic.logger.info('Asynchronic') { "#{status.to_s.capitalize} #{type} (#{id})" }
200
+
193
201
  data_store[:status] = status
194
202
  data_store[TIME_TRACKING_MAP[status]] = Time.now if TIME_TRACKING_MAP.key? status
203
+
204
+ environment.notifier.publish id, :status_changed, status
205
+ environment.notifier.publish id, :finalized if finalized?
195
206
  end
196
207
 
197
208
  STATUSES.each do |status|
@@ -206,8 +217,8 @@ module Asynchronic
206
217
  end
207
218
  end
208
219
 
209
- def abort!(exception=nil)
210
- self.error = Error.new exception if exception
220
+ def abort!(exception)
221
+ self.error = Error.new exception
211
222
  aborted!
212
223
  end
213
224
 
@@ -215,13 +226,13 @@ module Asynchronic
215
226
  self.connection_name = Asynchronic.connection_name
216
227
 
217
228
  if root.aborted?
218
- abort!
229
+ abort! AUTOMATIC_ABORTED_ERROR_MESSAGE
219
230
  else
220
231
  running!
221
232
  self.result = job.call
222
233
  waiting!
223
234
  end
224
-
235
+
225
236
  rescue Exception => ex
226
237
  message = "Failed process #{type} (#{id})\n#{ex.class} #{ex.message}\n#{ex.backtrace.join("\n")}"
227
238
  Asynchronic.logger.error('Asynchronic') { message }
@@ -258,6 +269,9 @@ module Asynchronic
258
269
 
259
270
  def connected?
260
271
  connection_name && environment.queue_engine.active_connections.include?(connection_name)
272
+ rescue => ex
273
+ Asynchronic.logger.error('Asynchronic') { "#{ex.message}\n#{ex.backtrace.join("\n")}" }
274
+ true
261
275
  end
262
276
 
263
277
  end
@@ -2,27 +2,25 @@ module Asynchronic
2
2
  module QueueEngine
3
3
  class InMemory
4
4
 
5
- attr_reader :default_queue
6
-
7
5
  def initialize(options={})
8
- @default_queue = options[:default_queue]
6
+ @options = options
9
7
  @queues ||= Hash.new { |h,k| h[k] = Queue.new }
10
8
  end
11
9
 
12
10
  def default_queue
13
- @default_queue ||= Asynchronic.default_queue
11
+ @default_queue ||= options.fetch(:default_queue, Asynchronic.default_queue)
14
12
  end
15
13
 
16
14
  def [](name)
17
- @queues[name]
15
+ queues[name]
18
16
  end
19
17
 
20
- def queues
21
- @queues.keys.map(&:to_sym)
18
+ def queue_names
19
+ queues.keys.map(&:to_sym)
22
20
  end
23
21
 
24
22
  def clear
25
- @queues.clear
23
+ queues.clear
26
24
  end
27
25
 
28
26
  def listener
@@ -37,12 +35,16 @@ module Asynchronic
37
35
  [Asynchronic.connection_name]
38
36
  end
39
37
 
38
+ private
39
+
40
+ attr_reader :queues, :options
41
+
40
42
 
41
43
  class Queue
42
44
 
43
45
  extend Forwardable
44
46
 
45
- def_delegators :@queue, :size, :empty?, :to_a
47
+ def_delegators :queue, :size, :empty?, :to_a
46
48
 
47
49
  def initialize
48
50
  @queue = []
@@ -50,13 +52,17 @@ module Asynchronic
50
52
  end
51
53
 
52
54
  def pop
53
- @mutex.synchronize { @queue.shift }
55
+ mutex.synchronize { queue.shift }
54
56
  end
55
57
 
56
58
  def push(message)
57
- @mutex.synchronize { @queue.push message }
59
+ mutex.synchronize { queue.push message }
58
60
  end
59
61
 
62
+ private
63
+
64
+ attr_reader :queue, :mutex
65
+
60
66
  end
61
67
 
62
68
 
@@ -5,23 +5,23 @@ module Asynchronic
5
5
  attr_reader :redis, :default_queue
6
6
 
7
7
  def initialize(options={})
8
- @redis = Redic.new(*Array(options[:redis]))
8
+ @redis = Asynchronic.establish_redis_connection options
9
9
  @default_queue = options.fetch(:default_queue, Asynchronic.default_queue)
10
10
  @queues ||= Hash.new { |h,k| h[k] = Queue.new k, redis }
11
11
  @keep_alive_thread = notify_keep_alive
12
12
  end
13
13
 
14
14
  def [](name)
15
- @queues[name]
15
+ queues[name]
16
16
  end
17
17
 
18
- def queues
19
- (@queues.values.map(&:key) | redis.call('KEYS', 'ost:*')).map { |q| q.to_s[4..-1].to_sym }
18
+ def queue_names
19
+ (queues.values.map(&:key) | redis.call!('KEYS', 'ost:*')).map { |q| q.to_s[4..-1].to_sym }
20
20
  end
21
21
 
22
22
  def clear
23
- @queues.clear
24
- redis.call('KEYS', 'ost:*').each { |k| redis.call('DEL', k) }
23
+ queues.clear
24
+ redis.call!('KEYS', 'ost:*').each { |k| redis.call!('DEL', k) }
25
25
  end
26
26
 
27
27
  def listener
@@ -33,17 +33,20 @@ module Asynchronic
33
33
  end
34
34
 
35
35
  def active_connections
36
- redis.call('CLIENT', 'LIST').split("\n").map do |connection_info|
37
- connection_info.split(' ').detect { |a| a.match(/name=/) }[5..-1]
38
- end.uniq.reject(&:empty?)
36
+ redis.call!('CLIENT', 'LIST').split("\n").map do |connection_info|
37
+ name_attr = connection_info.split(' ').detect { |a| a.match(/name=/) }
38
+ name_attr ? name_attr[5..-1] : nil
39
+ end.uniq.compact.reject(&:empty?)
39
40
  end
40
41
 
41
42
  private
42
43
 
44
+ attr_reader :queues
45
+
43
46
  def notify_keep_alive
44
47
  Thread.new do
45
48
  loop do
46
- redis.call 'CLIENT', 'SETNAME', Asynchronic.connection_name
49
+ redis.call! 'CLIENT', 'SETNAME', Asynchronic.connection_name
47
50
  sleep Asynchronic.keep_alive_timeout
48
51
  end
49
52
  end
@@ -58,11 +61,11 @@ module Asynchronic
58
61
  end
59
62
 
60
63
  def pop
61
- redis.call 'RPOP', key
64
+ redis.call! 'RPOP', key
62
65
  end
63
66
 
64
67
  def empty?
65
- redis.call('EXISTS', key) == 0
68
+ redis.call!('EXISTS', key) == 0
66
69
  end
67
70
 
68
71
  def size
@@ -5,7 +5,7 @@ module Asynchronic
5
5
  attr_reader :stubs
6
6
 
7
7
  def initialize(options={})
8
- @environment = options[:environment]
8
+ @options = options
9
9
  @stubs = {}
10
10
  end
11
11
 
@@ -14,7 +14,7 @@ module Asynchronic
14
14
  end
15
15
 
16
16
  def environment
17
- @environment ||= Asynchronic.environment
17
+ @environment ||= options.fetch(:environment, Asynchronic.environment)
18
18
  end
19
19
 
20
20
  def [](name)
@@ -22,7 +22,7 @@ module Asynchronic
22
22
  end
23
23
 
24
24
  def stub(job, &block)
25
- @stubs[job] = block
25
+ stubs[job] = block
26
26
  end
27
27
 
28
28
  def asynchronic?
@@ -33,6 +33,10 @@ module Asynchronic
33
33
  [Asynchronic.connection_name]
34
34
  end
35
35
 
36
+ private
37
+
38
+ attr_reader :options
39
+
36
40
 
37
41
  class Queue
38
42
 
@@ -41,11 +45,11 @@ module Asynchronic
41
45
  end
42
46
 
43
47
  def push(message)
44
- process = @engine.environment.load_process(message)
48
+ process = engine.environment.load_process(message)
45
49
 
46
- if @engine.stubs[process.type]
50
+ if engine.stubs[process.type]
47
51
  job = process.job
48
- block = @engine.stubs[process.type]
52
+ block = engine.stubs[process.type]
49
53
  process.define_singleton_method :job do
50
54
  MockJob.new job, process, &block
51
55
  end
@@ -54,6 +58,10 @@ module Asynchronic
54
58
  process.execute
55
59
  end
56
60
 
61
+ private
62
+
63
+ attr_reader :engine
64
+
57
65
  end
58
66
 
59
67
 
@@ -66,12 +74,16 @@ module Asynchronic
66
74
  end
67
75
 
68
76
  def call
69
- @block.call @process
77
+ block.call process
70
78
  end
71
79
 
72
80
  def before_finalize
73
81
  end
74
82
 
83
+ private
84
+
85
+ attr_reader :process, :block
86
+
75
87
  end
76
88
 
77
89
  end