asynchronic 0.1.0 → 0.2.0

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