asynchronic 0.1.0 → 0.2.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 (37) hide show
  1. data/lib/asynchronic.rb +0 -2
  2. data/lib/asynchronic/data_store/helper.rb +42 -0
  3. data/lib/asynchronic/data_store/in_memory.rb +18 -22
  4. data/lib/asynchronic/data_store/key.rb +19 -1
  5. data/lib/asynchronic/data_store/lazy_store.rb +17 -0
  6. data/lib/asynchronic/data_store/lazy_value.rb +34 -0
  7. data/lib/asynchronic/data_store/readonly_store.rb +17 -0
  8. data/lib/asynchronic/data_store/redis.rb +16 -27
  9. data/lib/asynchronic/data_store/scoped_store.rb +52 -0
  10. data/lib/asynchronic/environment.rb +7 -27
  11. data/lib/asynchronic/job.rb +15 -27
  12. data/lib/asynchronic/process.rb +105 -76
  13. data/lib/asynchronic/queue_engine/in_memory.rb +5 -1
  14. data/lib/asynchronic/queue_engine/ost.rb +5 -1
  15. data/lib/asynchronic/queue_engine/synchronic.rb +68 -0
  16. data/lib/asynchronic/transparent_proxy.rb +52 -0
  17. data/lib/asynchronic/version.rb +1 -1
  18. data/spec/data_store/data_store_examples.rb +48 -32
  19. data/spec/data_store/in_memory_spec.rb +5 -0
  20. data/spec/data_store/key_spec.rb +36 -12
  21. data/spec/data_store/lazy_value_examples.rb +38 -0
  22. data/spec/data_store/redis_spec.rb +17 -0
  23. data/spec/data_store/scoped_store_spec.rb +60 -0
  24. data/spec/expectations.rb +7 -7
  25. data/spec/facade_spec.rb +15 -13
  26. data/spec/jobs.rb +70 -49
  27. data/spec/minitest_helper.rb +11 -1
  28. data/spec/process/life_cycle_examples.rb +149 -135
  29. data/spec/queue_engine/synchronic_spec.rb +27 -0
  30. data/spec/transparent_proxy_spec.rb +36 -0
  31. data/spec/worker/worker_examples.rb +1 -1
  32. metadata +117 -79
  33. checksums.yaml +0 -7
  34. data/lib/asynchronic/data_store/lookup.rb +0 -27
  35. data/lib/asynchronic/hash.rb +0 -31
  36. data/lib/asynchronic/runtime.rb +0 -40
  37. data/spec/data_store/lookup_spec.rb +0 -92
@@ -4,43 +4,77 @@ module Asynchronic
4
4
  STATUSES = [:pending, :queued, :running, :waiting, :completed, :aborted]
5
5
 
6
6
  TIME_TRACKING_MAP = {
7
+ pending: :created_at,
7
8
  queued: :queued_at,
8
9
  running: :started_at,
9
10
  completed: :finalized_at,
10
11
  aborted: :finalized_at
11
12
  }
12
13
 
13
- extend Forwardable
14
+ ATTRIBUTE_NAMES = [:type, :name, :queue, :status, :dependencies, :result, :error] | TIME_TRACKING_MAP.values.uniq
14
15
 
15
- def_delegators :job, :id, :name, :queue
16
- def_delegators :data, :[]
16
+ attr_reader :id
17
17
 
18
- attr_reader :job
19
- attr_reader :env
18
+ def initialize(environment, id, &block)
19
+ @environment = environment
20
+ @id = DataStore::Key.new id
21
+ instance_eval &block if block_given?
22
+ end
23
+
24
+ ATTRIBUTE_NAMES.each do |attribute|
25
+ define_method attribute do
26
+ data_store[attribute]
27
+ end
28
+ end
29
+
30
+ STATUSES.each do |status|
31
+ define_method "#{status}?" do
32
+ self.status == status
33
+ end
34
+ end
35
+
36
+ def ready?
37
+ pending? && dependencies.all?(&:completed?)
38
+ end
39
+
40
+ def finalized?
41
+ completed? || aborted?
42
+ end
20
43
 
21
- def initialize(job, env)
22
- @job = job
23
- @env = env
44
+ def params
45
+ data_store.scoped(:params).readonly
24
46
  end
25
47
 
26
- def pid
27
- lookup.id
48
+ def result
49
+ data_store.lazy[:result]
28
50
  end
29
51
 
30
- def data
31
- parent ? parent.data : env.data_store.to_hash(lookup.data).with_indiferent_access
52
+ def job
53
+ type.new self
32
54
  end
33
55
 
34
- def merge(data)
35
- parent ? parent.merge(data) : env.data_store.merge(lookup.data, data)
56
+ def [](process_name)
57
+ processes.detect { |p| p.name == process_name }
36
58
  end
37
59
 
38
- def enqueue(data={})
39
- merge data
40
- env.enqueue lookup.id, queue
41
- update_status :queued
60
+ def processes
61
+ data_store.scoped(:processes).keys.
62
+ select { |k| k.sections.count == 2 && k.match(/name$/) }.
63
+ sort.map { |k| Process.new environment, id[:processes][k.remove_last] }
64
+ end
65
+
66
+ def parent
67
+ Process.new environment, id.remove_last(2) if id.nested?
68
+ end
69
+
70
+ def dependencies
71
+ return [] unless parent
72
+ data_store[:dependencies].map { |d| parent[d] }
73
+ end
42
74
 
43
- lookup.id
75
+ def enqueue
76
+ queued!
77
+ environment.enqueue id, queue || type.queue
44
78
  end
45
79
 
46
80
  def execute
@@ -51,98 +85,93 @@ module Asynchronic
51
85
  def wakeup
52
86
  if waiting?
53
87
  if processes.any?(&:aborted?)
54
- abort Error.new "Error caused by #{processes.select(&:aborted?).map{|p| p.job.name}.join(', ')}"
88
+ abort! Error.new "Error caused by #{processes.select(&:aborted?).map{|p| p.name}.join(', ')}"
89
+ elsif processes.all?(&:completed?)
90
+ completed!
55
91
  else
56
- if processes.all?(&:completed?)
57
- update_status :completed
58
- else
59
- processes.select(&:ready?).each { |p| p.enqueue }
60
- end
92
+ processes.select(&:ready?).each(&:enqueue)
61
93
  end
62
94
  end
63
95
 
64
96
  parent.wakeup if parent && finalized?
65
97
  end
66
98
 
67
- def error
68
- env[lookup.error]
99
+ def nest(type, params={})
100
+ self.class.create @environment, type, params.merge(id: id[:processes][processes.count])
69
101
  end
70
102
 
71
- def status
72
- env[lookup.status] || :pending
73
- end
103
+ def self.create(environment, type, params={})
104
+ id = params.delete(:id) || SecureRandom.uuid
74
105
 
75
- STATUSES.each do |status|
76
- define_method "#{status}?" do
77
- self.status == status
78
- end
79
- end
106
+ Asynchronic.logger.debug('Asynchronic') { "Created process #{type} - #{id} - #{params}" }
80
107
 
81
- def ready?
82
- pending? && dependencies.all?(&:completed?)
108
+ new(environment, id) do
109
+ self.type = type
110
+ self.name = params.delete(:alias) || type
111
+ self.queue = params.delete :queue
112
+ self.dependencies = Array(params.delete(:dependencies)) | Array(params.delete(:dependency)) | infer_dependencies(params)
113
+ self.params = params
114
+ pending!
115
+ end
83
116
  end
84
117
 
85
- def finalized?
86
- completed? || aborted?
118
+ def self.all(environment)
119
+ environment.data_store.keys.
120
+ select { |k| k.sections.count == 2 && k.match(/created_at$/) }.
121
+ sort_by { |k| environment.data_store[k] }.reverse.
122
+ map { |k| Process.new environment, k.remove_last }
87
123
  end
88
124
 
89
- def processes(name=nil)
90
- processes = env.data_store.keys(lookup.jobs).
91
- select { |k| k.match Regexp.new("^#{lookup.jobs[Asynchronic::UUID_REGEXP]}$") }.
92
- map { |k| env.load_process k }
125
+ private
93
126
 
94
- name ? processes.detect { |p| p.name == name.to_s } : processes
127
+ def environment
128
+ @environment
95
129
  end
96
130
 
97
- def parent
98
- @parent ||= Process.new env[job.parent], env if job.parent
131
+ def data_store
132
+ @data_store ||= environment.data_store.scoped id
99
133
  end
100
134
 
101
- def dependencies
102
- @dependencies ||= parent.processes.select { |p| job.dependencies.include? p.name }
135
+ ATTRIBUTE_NAMES.each do |attribute|
136
+ define_method "#{attribute}=" do |value|
137
+ data_store[attribute] = value
138
+ end
103
139
  end
104
140
 
105
- def created_at
106
- env[lookup.created_at]
141
+ def params=(params)
142
+ data_store.scoped(:params).merge params
107
143
  end
108
144
 
109
- def queued_at
110
- env[lookup.queued_at]
145
+ def status=(status)
146
+ Asynchronic.logger.info('Asynchronic') { "#{status.to_s.capitalize} #{type} (#{id})" }
147
+ data_store[:status] = status
148
+ data_store[TIME_TRACKING_MAP[status]] = Time.now if TIME_TRACKING_MAP.key? status
111
149
  end
112
150
 
113
- def started_at
114
- env[lookup.started_at]
151
+ STATUSES.each do |status|
152
+ define_method "#{status}!" do
153
+ self.status = status
154
+ end
115
155
  end
116
156
 
117
- def finalized_at
118
- env[lookup.finalized_at]
157
+ def abort!(exception)
158
+ self.error = Error.new exception
159
+ aborted!
119
160
  end
120
161
 
121
- private
122
-
123
162
  def run
124
- update_status :running
125
- Runtime.evaluate self
126
- update_status :waiting
163
+ running!
164
+ self.result = job.call
165
+ waiting!
127
166
  rescue Exception => ex
128
- message = "Failed job #{job.name} (#{lookup.id})\n#{ex.class} #{ex.message}\n#{ex.backtrace.join("\n")}"
167
+ message = "Failed process #{type} (#{id})\n#{ex.class} #{ex.message}\n#{ex.backtrace.join("\n")}"
129
168
  Asynchronic.logger.error('Asynchronic') { message }
130
- abort ex
131
- end
132
-
133
- def update_status(status)
134
- Asynchronic.logger.info('Asynchronic') { "#{status.to_s.capitalize} #{job.name} (#{lookup.id})" }
135
- env[lookup.status] = status
136
- env[lookup.send(TIME_TRACKING_MAP[status])] = Time.now if TIME_TRACKING_MAP.key? status
137
- end
138
-
139
- def abort(exception)
140
- env[lookup.error] = Error.new(exception)
141
- update_status :aborted
169
+ abort! ex
142
170
  end
143
171
 
144
- def lookup
145
- @lookup ||= job.lookup
172
+ def infer_dependencies(params)
173
+ params.values.select { |v| v.respond_to?(:proxy?) && v.proxy_class == DataStore::LazyValue }
174
+ .map { |v| Process.new(environment, v.data_store.scope).name }
146
175
  end
147
176
 
148
177
  end
@@ -5,10 +5,14 @@ module Asynchronic
5
5
  attr_reader :default_queue
6
6
 
7
7
  def initialize(options={})
8
- @default_queue = options.fetch(:default_queue, Asynchronic.default_queue)
8
+ @default_queue = options[:default_queue]
9
9
  @queues ||= Hash.new { |h,k| h[k] = Queue.new }
10
10
  end
11
11
 
12
+ def default_queue
13
+ @default_queue ||= Asynchronic.default_queue
14
+ end
15
+
12
16
  def [](name)
13
17
  @queues[name]
14
18
  end
@@ -6,10 +6,14 @@ module Asynchronic
6
6
 
7
7
  def initialize(options={})
8
8
  ::Ost.connect options[:redis] if options.key?(:redis)
9
- @default_queue = options.fetch(:default_queue, Asynchronic.default_queue)
9
+ @default_queue = options[:default_queue]
10
10
  @queues ||= Hash.new { |h,k| h[k] = Queue.new k }
11
11
  end
12
12
 
13
+ def default_queue
14
+ @default_queue ||= Asynchronic.default_queue
15
+ end
16
+
13
17
  def [](name)
14
18
  @queues[name]
15
19
  end
@@ -0,0 +1,68 @@
1
+ module Asynchronic
2
+ module QueueEngine
3
+ class Synchronic
4
+
5
+ attr_reader :stubs
6
+
7
+ def initialize(options={})
8
+ @environment = options[:environment]
9
+ @stubs = {}
10
+ end
11
+
12
+ def default_queue
13
+ Asynchronic.default_queue
14
+ end
15
+
16
+ def environment
17
+ @environment ||= Asynchronic.environment
18
+ end
19
+
20
+ def [](name)
21
+ Queue.new self
22
+ end
23
+
24
+ def stub(job, &block)
25
+ @stubs[job] = block
26
+ end
27
+
28
+
29
+ class Queue
30
+
31
+ def initialize(engine)
32
+ @engine = engine
33
+ end
34
+
35
+ def push(message)
36
+ process = @engine.environment.load_process(message)
37
+
38
+ if @engine.stubs[process.type]
39
+ job = process.job
40
+ block = @engine.stubs[process.type]
41
+ process.define_singleton_method :job do
42
+ MockJob.new job, process, &block
43
+ end
44
+ end
45
+
46
+ process.execute
47
+ end
48
+
49
+ end
50
+
51
+
52
+ class MockJob < TransparentProxy
53
+
54
+ def initialize(job, process, &block)
55
+ super job
56
+ @process = process
57
+ @block = block
58
+ end
59
+
60
+ def call
61
+ @block.call @process
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,52 @@
1
+ module Asynchronic
2
+ class TransparentProxy
3
+
4
+ PROXY_METHODS = [:class, :methods, :respond_to?]
5
+
6
+ SAFE_METHODS = [:__send__, :__id__, :object_id, :tap] + PROXY_METHODS.map { |m| "proxy_#{m}".to_sym }
7
+
8
+ PROXY_METHODS.each { |m| alias_method "proxy_#{m}".to_sym, m }
9
+
10
+ instance_methods.reject { |m| SAFE_METHODS.include? m }.
11
+ each { |m| undef_method m }
12
+
13
+ def inspect
14
+ __getobj__.inspect
15
+ end
16
+
17
+ def proxy_inspect
18
+ "#<#{proxy_class} @object=#{inspect}>"
19
+ end
20
+
21
+ def methods(*args)
22
+ proxy_methods(*args) | __getobj__.methods(*args)
23
+ end
24
+
25
+ def respond_to?(*args)
26
+ proxy_respond_to?(*args) || __getobj__.respond_to?(*args)
27
+ end
28
+
29
+ def proxy?
30
+ true
31
+ end
32
+
33
+ private
34
+
35
+ def initialize(object)
36
+ __setobj__ object
37
+ end
38
+
39
+ def __getobj__
40
+ @object
41
+ end
42
+
43
+ def __setobj__(object)
44
+ @object = object
45
+ end
46
+
47
+ def method_missing(method, *args, &block)
48
+ __getobj__.send(method, *args, &block)
49
+ end
50
+
51
+ end
52
+ end
@@ -1,3 +1,3 @@
1
1
  module Asynchronic
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -1,62 +1,78 @@
1
1
  module DataStoreExamples
2
2
 
3
3
  it 'Get/Set value' do
4
- data_store.set 'test_key', 123
5
- data_store.get('test_key').must_equal 123
4
+ data_store[:key] = 123
5
+ data_store[:key].must_equal 123
6
6
  end
7
7
 
8
8
  it 'Key not found' do
9
- data_store.get('test_key').must_be_nil
9
+ data_store[:key].must_be_nil
10
10
  end
11
11
 
12
12
  it 'Keys' do
13
13
  data_store.keys.must_be_empty
14
- data_store.set 'test_key', 123
15
- data_store.keys.must_equal ['test_key']
14
+ data_store[:key] = 123
15
+ data_store.keys.must_equal ['key']
16
16
  end
17
17
 
18
- it 'Merge' do
19
- data_store.set 'a:1', 0
20
- data_store.merge 'a', '1' => 1, '2' => 2
21
-
22
- data_store.get('a:1').must_equal 1
23
- data_store.get('a:2').must_equal 2
18
+ it 'Delete' do
19
+ data_store[:key] = 123
20
+ data_store.delete :key
21
+ data_store[:key].must_be_nil
24
22
  end
25
23
 
26
- it 'To hash' do
27
- data_store.set 'a', 0
28
- data_store.set 'a:1', 1
29
- data_store.set 'a:2', 2
30
- data_store.set 'b:3', 3
24
+ it 'Each' do
25
+ data_store[:a] = 1
26
+ data_store[:b] = 2
31
27
 
32
- data_store.to_hash('a').must_equal '1' => 1, '2' => 2
28
+ array = []
29
+ data_store.each { |k,v| array << "#{k} => #{v}" }
30
+ array.must_equal_contents ['a => 1', 'b => 2']
33
31
  end
34
32
 
35
- it 'Nested keys' do
36
- data_store.set 'a', 0
37
- data_store.set 'a:1', 1
38
- data_store.set 'a:2', 2
39
- data_store.set 'b:3', 3
33
+ it 'Merge' do
34
+ data_store[:a] = 0
35
+ data_store.merge a: 1, b: 2
40
36
 
41
- data_store.keys('a').must_equal_contents %w(a a:1 a:2)
42
- data_store.keys('a:').must_equal_contents %w(a:1 a:2)
37
+ data_store[:a].must_equal 1
38
+ data_store[:b].must_equal 2
43
39
  end
44
40
 
45
41
  it 'Clear' do
46
- data_store.set 'test_key', 123
42
+ data_store[:key] = 123
47
43
  data_store.clear
48
44
  data_store.keys.must_be_empty
49
45
  end
50
46
 
51
- it 'Nested clear' do
52
- data_store.set 'a', 0
53
- data_store.set 'a:1', 1
54
- data_store.set 'a:2', 2
55
- data_store.set 'b:3', 3
47
+ it 'Scoped' do
48
+ data_store['x|y|z'] = 1
49
+ data_store.scoped(:x)['y|z'].must_equal 1
50
+ data_store.scoped(:x).scoped(:y)[:z].must_equal 1
51
+ end
52
+
53
+ it 'Read only' do
54
+ data_store[:key] = 1
55
+ data_store.wont_be :readonly?
56
+ data_store.readonly.tap do |ds|
57
+ ds[:key].must_equal 1
58
+ ds.must_be :readonly?
59
+ proc { ds[:key] = 2 }.must_raise RuntimeError
60
+ end
61
+ end
62
+
63
+ it 'Lazy' do
64
+ data_store[:key] = 1
65
+ lazy_store = data_store.lazy
66
+ lazy_value = lazy_store[:key]
67
+
68
+ data_store.wont_be :lazy?
69
+ lazy_store.must_be :lazy?
70
+ lazy_value.must_equal 1
56
71
 
57
- data_store.clear 'a:'
72
+ data_store[:key] = 2
58
73
 
59
- data_store.keys.must_equal_contents %w(a b:3)
74
+ lazy_value.must_equal 1
75
+ lazy_value.reload.must_equal 2
60
76
  end
61
77
 
62
78
  end