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.
- data/lib/asynchronic.rb +0 -2
- data/lib/asynchronic/data_store/helper.rb +42 -0
- data/lib/asynchronic/data_store/in_memory.rb +18 -22
- data/lib/asynchronic/data_store/key.rb +19 -1
- data/lib/asynchronic/data_store/lazy_store.rb +17 -0
- data/lib/asynchronic/data_store/lazy_value.rb +34 -0
- data/lib/asynchronic/data_store/readonly_store.rb +17 -0
- data/lib/asynchronic/data_store/redis.rb +16 -27
- data/lib/asynchronic/data_store/scoped_store.rb +52 -0
- data/lib/asynchronic/environment.rb +7 -27
- data/lib/asynchronic/job.rb +15 -27
- data/lib/asynchronic/process.rb +105 -76
- data/lib/asynchronic/queue_engine/in_memory.rb +5 -1
- data/lib/asynchronic/queue_engine/ost.rb +5 -1
- data/lib/asynchronic/queue_engine/synchronic.rb +68 -0
- data/lib/asynchronic/transparent_proxy.rb +52 -0
- data/lib/asynchronic/version.rb +1 -1
- data/spec/data_store/data_store_examples.rb +48 -32
- data/spec/data_store/in_memory_spec.rb +5 -0
- data/spec/data_store/key_spec.rb +36 -12
- data/spec/data_store/lazy_value_examples.rb +38 -0
- data/spec/data_store/redis_spec.rb +17 -0
- data/spec/data_store/scoped_store_spec.rb +60 -0
- data/spec/expectations.rb +7 -7
- data/spec/facade_spec.rb +15 -13
- data/spec/jobs.rb +70 -49
- data/spec/minitest_helper.rb +11 -1
- data/spec/process/life_cycle_examples.rb +149 -135
- data/spec/queue_engine/synchronic_spec.rb +27 -0
- data/spec/transparent_proxy_spec.rb +36 -0
- data/spec/worker/worker_examples.rb +1 -1
- metadata +117 -79
- checksums.yaml +0 -7
- data/lib/asynchronic/data_store/lookup.rb +0 -27
- data/lib/asynchronic/hash.rb +0 -31
- data/lib/asynchronic/runtime.rb +0 -40
- data/spec/data_store/lookup_spec.rb +0 -92
data/lib/asynchronic/process.rb
CHANGED
@@ -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
|
-
|
14
|
+
ATTRIBUTE_NAMES = [:type, :name, :queue, :status, :dependencies, :result, :error] | TIME_TRACKING_MAP.values.uniq
|
14
15
|
|
15
|
-
|
16
|
-
def_delegators :data, :[]
|
16
|
+
attr_reader :id
|
17
17
|
|
18
|
-
|
19
|
-
|
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
|
22
|
-
|
23
|
-
@env = env
|
44
|
+
def params
|
45
|
+
data_store.scoped(:params).readonly
|
24
46
|
end
|
25
47
|
|
26
|
-
def
|
27
|
-
|
48
|
+
def result
|
49
|
+
data_store.lazy[:result]
|
28
50
|
end
|
29
51
|
|
30
|
-
def
|
31
|
-
|
52
|
+
def job
|
53
|
+
type.new self
|
32
54
|
end
|
33
55
|
|
34
|
-
def
|
35
|
-
|
56
|
+
def [](process_name)
|
57
|
+
processes.detect { |p| p.name == process_name }
|
36
58
|
end
|
37
59
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
68
|
-
|
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
|
72
|
-
|
73
|
-
end
|
103
|
+
def self.create(environment, type, params={})
|
104
|
+
id = params.delete(:id) || SecureRandom.uuid
|
74
105
|
|
75
|
-
|
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
|
-
|
82
|
-
|
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
|
86
|
-
|
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
|
-
|
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
|
-
|
127
|
+
def environment
|
128
|
+
@environment
|
95
129
|
end
|
96
130
|
|
97
|
-
def
|
98
|
-
@
|
131
|
+
def data_store
|
132
|
+
@data_store ||= environment.data_store.scoped id
|
99
133
|
end
|
100
134
|
|
101
|
-
|
102
|
-
|
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
|
106
|
-
|
141
|
+
def params=(params)
|
142
|
+
data_store.scoped(:params).merge params
|
107
143
|
end
|
108
144
|
|
109
|
-
def
|
110
|
-
|
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
|
-
|
114
|
-
|
151
|
+
STATUSES.each do |status|
|
152
|
+
define_method "#{status}!" do
|
153
|
+
self.status = status
|
154
|
+
end
|
115
155
|
end
|
116
156
|
|
117
|
-
def
|
118
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
163
|
+
running!
|
164
|
+
self.result = job.call
|
165
|
+
waiting!
|
127
166
|
rescue Exception => ex
|
128
|
-
message = "Failed
|
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
|
145
|
-
|
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
|
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
|
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
|
data/lib/asynchronic/version.rb
CHANGED
@@ -1,62 +1,78 @@
|
|
1
1
|
module DataStoreExamples
|
2
2
|
|
3
3
|
it 'Get/Set value' do
|
4
|
-
data_store
|
5
|
-
data_store.
|
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.
|
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
|
15
|
-
data_store.keys.must_equal ['
|
14
|
+
data_store[:key] = 123
|
15
|
+
data_store.keys.must_equal ['key']
|
16
16
|
end
|
17
17
|
|
18
|
-
it '
|
19
|
-
data_store
|
20
|
-
data_store.
|
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 '
|
27
|
-
data_store
|
28
|
-
data_store
|
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
|
-
|
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 '
|
36
|
-
data_store
|
37
|
-
data_store.
|
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
|
42
|
-
data_store
|
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
|
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 '
|
52
|
-
data_store
|
53
|
-
data_store.
|
54
|
-
data_store.
|
55
|
-
|
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
|
72
|
+
data_store[:key] = 2
|
58
73
|
|
59
|
-
|
74
|
+
lazy_value.must_equal 1
|
75
|
+
lazy_value.reload.must_equal 2
|
60
76
|
end
|
61
77
|
|
62
78
|
end
|