asynchronic 2.0.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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