dirty_pipeline 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/dirty_pipeline.rb +9 -1
- data/lib/dirty_pipeline/base.rb +272 -0
- data/lib/dirty_pipeline/locker.rb +111 -0
- data/lib/dirty_pipeline/storage.rb +193 -0
- data/lib/dirty_pipeline/transition.rb +25 -0
- data/lib/dirty_pipeline/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c67b4adb4c6e578a8add63619a5836d1d9d820c1b4c07fd53f2dd1877f1851d0
|
4
|
+
data.tar.gz: 94d3c03c15d6e74079f3e64b985392f88b1c07247c394de1bf58e2feb1b94693
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52c62a8e38f6ab759bf7368b0f32e86008894b48e4cc4a4fd93643d84848c436fb8b2aa93d200faa29e98431bc8136805b45fa76ba4d76e72cd262450a5f16da
|
7
|
+
data.tar.gz: 0e83e12bdfb27820726ebd789b0d6226df9cdce1256d3a9cd5e4137656ef83ec8ccbaaf8cb6ccc12d4727c588bc5b8676760b3712df8bb601505301d92ba77e1
|
data/lib/dirty_pipeline.rb
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
require "dirty_pipeline/version"
|
2
2
|
|
3
3
|
module DirtyPipeline
|
4
|
-
|
4
|
+
require_relative "dirty_pipeline/locker.rb"
|
5
|
+
require_relative "dirty_pipeline/storage.rb"
|
6
|
+
require_relative "dirty_pipeline/transition.rb"
|
7
|
+
require_relative "dirty_pipeline/base.rb"
|
8
|
+
|
9
|
+
# This method should yield raw Redis connection
|
10
|
+
def self.with_redis
|
11
|
+
fail NotImplementedError
|
12
|
+
end
|
5
13
|
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
class Base
|
3
|
+
DEFAULT_RETRY_DELAY = 5 * 60 # 5 minutes
|
4
|
+
DEFAULT_CLEANUP_DELAY = 60 * 60 * 24 # 1 day
|
5
|
+
RESERVED_STATUSES = [
|
6
|
+
Storage::FAILED_STATUS,
|
7
|
+
Storage::PROCESSING_STATUS,
|
8
|
+
Storage::RETRY_STATUS,
|
9
|
+
Locker::CLEAN,
|
10
|
+
]
|
11
|
+
|
12
|
+
class ReservedStatusError < StandardError; end
|
13
|
+
class InvalidTransition < StandardError; end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def find_subject(*args)
|
17
|
+
fail NotImplemented
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :transitions_map
|
21
|
+
def inherited(child)
|
22
|
+
child.instance_variable_set(:@transitions_map, Hash.new)
|
23
|
+
end
|
24
|
+
# PG JSONB column
|
25
|
+
# {
|
26
|
+
# status: :errored,
|
27
|
+
# state: {
|
28
|
+
# field: "value",
|
29
|
+
# },
|
30
|
+
# errors: [
|
31
|
+
# {
|
32
|
+
# error: "RuPost::API::Error",
|
33
|
+
# error_message: "Timeout error",
|
34
|
+
# created_at: 2018-01-01T13:22Z
|
35
|
+
# },
|
36
|
+
# ],
|
37
|
+
# events: [
|
38
|
+
# {
|
39
|
+
# action: Init,
|
40
|
+
# input: ...,
|
41
|
+
# created_at: ...,
|
42
|
+
# updated_at: ...,
|
43
|
+
# attempts_count: 2,
|
44
|
+
# },
|
45
|
+
# {...},
|
46
|
+
# ]
|
47
|
+
# }
|
48
|
+
attr_accessor :pipeline_storage, :retry_delay, :cleanup_delay
|
49
|
+
|
50
|
+
def transition(action, from:, to:, name: action.to_s, attempts: 1)
|
51
|
+
raise ReservedStatusError unless valid_statuses?(from, to)
|
52
|
+
@transitions_map[name] = {
|
53
|
+
action: action,
|
54
|
+
from: Array(from).map(&:to_s),
|
55
|
+
to: to.to_s,
|
56
|
+
attempts: attempts,
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def valid_statuses?(from, to)
|
63
|
+
((Array(to) + Array(from)) & RESERVED_STATUSES).empty?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def enqueue(transition_name, *args)
|
68
|
+
Shipping::PipelineWorker.perform_async(
|
69
|
+
"enqueued_pipeline" => self.class.to_s,
|
70
|
+
"find_subject_args" => find_subject_args,
|
71
|
+
"transition_args" => args.unshift(transition_name),
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def reset!
|
76
|
+
storage.reset_pipeline_status!
|
77
|
+
end
|
78
|
+
|
79
|
+
attr_reader :subject, :error, :storage
|
80
|
+
def initialize(subject)
|
81
|
+
@subject = subject
|
82
|
+
@storage = Storage.new(subject, self.class.pipeline_storage)
|
83
|
+
@locker = Locker.new(@subject, @storage)
|
84
|
+
end
|
85
|
+
|
86
|
+
def call(*args)
|
87
|
+
return self if succeeded == false
|
88
|
+
self.succeeded = nil
|
89
|
+
after_commit = nil
|
90
|
+
|
91
|
+
# transaction with support of external calls
|
92
|
+
transaction(*args) do |destination, action, *transition_args|
|
93
|
+
output = {}
|
94
|
+
fail_cause = nil
|
95
|
+
|
96
|
+
output, *after_commit = catch(:success) do
|
97
|
+
fail_cause = catch(:fail_with_error) do
|
98
|
+
return Abort() if catch(:abort) do
|
99
|
+
throw :success, action.(subject, *transition_args)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
if fail_cause
|
106
|
+
ExpectedError(fail_cause)
|
107
|
+
else
|
108
|
+
Success(destination, output)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
Array(after_commit).each { |cb| cb.call(subject) } if after_commit
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
def clear!
|
117
|
+
storage.clear!
|
118
|
+
end
|
119
|
+
|
120
|
+
def success?
|
121
|
+
succeeded
|
122
|
+
end
|
123
|
+
|
124
|
+
def when_success(callback = nil)
|
125
|
+
return self unless success?
|
126
|
+
if block_given?
|
127
|
+
yield(self)
|
128
|
+
else
|
129
|
+
callback.call(self)
|
130
|
+
end
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
def when_failed(callback = nil)
|
135
|
+
return self unless storage.failed?
|
136
|
+
if block_given?
|
137
|
+
yield(self)
|
138
|
+
else
|
139
|
+
callback.call(self)
|
140
|
+
end
|
141
|
+
self
|
142
|
+
end
|
143
|
+
|
144
|
+
def errored?
|
145
|
+
return if succeeded.nil?
|
146
|
+
ready? && !succeeded
|
147
|
+
end
|
148
|
+
|
149
|
+
def when_error(callback = nil)
|
150
|
+
return self unless errored?
|
151
|
+
if block_given?
|
152
|
+
yield(self)
|
153
|
+
else
|
154
|
+
callback.call(self)
|
155
|
+
end
|
156
|
+
self
|
157
|
+
end
|
158
|
+
|
159
|
+
def ready?
|
160
|
+
storage.pipeline_status.nil?
|
161
|
+
end
|
162
|
+
|
163
|
+
def when_processing(callback = nil)
|
164
|
+
return self unless storage.processing?
|
165
|
+
if block_given?
|
166
|
+
yield(self)
|
167
|
+
else
|
168
|
+
callback.call(self)
|
169
|
+
end
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
attr_writer :error
|
176
|
+
attr_reader :locker
|
177
|
+
attr_accessor :succeeded, :previous_status
|
178
|
+
|
179
|
+
def find_subject_args
|
180
|
+
subject.id
|
181
|
+
end
|
182
|
+
|
183
|
+
def retry_delay
|
184
|
+
self.class.retry_delay || DEFAULT_RETRY_DELAY
|
185
|
+
end
|
186
|
+
|
187
|
+
def cleanup_delay
|
188
|
+
self.class.cleanup_delay || DEFAULT_CLEANUP_DELAY
|
189
|
+
end
|
190
|
+
|
191
|
+
def Retry(error, *args)
|
192
|
+
storage.save_retry!(error)
|
193
|
+
Shipping::PipelineWorker.perform_in(
|
194
|
+
retry_delay,
|
195
|
+
"enqueued_pipeline" => self.class.to_s,
|
196
|
+
"find_subject_args" => find_subject_args,
|
197
|
+
"retry" => true,
|
198
|
+
)
|
199
|
+
end
|
200
|
+
|
201
|
+
def ExpectedError(cause)
|
202
|
+
self.error = cause
|
203
|
+
storage.fail_event!
|
204
|
+
self.succeeded = false
|
205
|
+
end
|
206
|
+
|
207
|
+
def Error(error)
|
208
|
+
storage.save_exception!(error)
|
209
|
+
self.error = error
|
210
|
+
self.succeeded = false
|
211
|
+
end
|
212
|
+
|
213
|
+
def Abort()
|
214
|
+
self.succeeded = false
|
215
|
+
throw :abort_transaction, true
|
216
|
+
end
|
217
|
+
|
218
|
+
def Success(destination, output)
|
219
|
+
storage.complete!(output, destination)
|
220
|
+
self.succeeded = true
|
221
|
+
end
|
222
|
+
|
223
|
+
def try_again?(max_attempts_count)
|
224
|
+
return unless max_attempts_count
|
225
|
+
storage.last_event["attempts_count"].to_i < max_attempts_count
|
226
|
+
end
|
227
|
+
|
228
|
+
def find_transition(name)
|
229
|
+
if (const_name = self.class.const_get(name) rescue nil)
|
230
|
+
name = const_name.to_s
|
231
|
+
end
|
232
|
+
self.class.transitions_map.fetch(name.to_s).tap do |from:, **kwargs|
|
233
|
+
next if from == Array(storage.status)
|
234
|
+
next if from.include?(storage.status.to_s)
|
235
|
+
raise InvalidTransition, "from `#{storage.status}` by `#{name}`"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def schedule_cleanup
|
240
|
+
Shipping::PipelineWorker.perform_in(
|
241
|
+
cleanup_delay,
|
242
|
+
"enqueued_pipeline" => self.class.to_s,
|
243
|
+
"find_subject_args" => find_subject_args,
|
244
|
+
"transition_args" => [Locker::CLEAN],
|
245
|
+
)
|
246
|
+
end
|
247
|
+
|
248
|
+
def transaction(*args)
|
249
|
+
locker.with_lock(*args) do |transition, *transition_args|
|
250
|
+
begin
|
251
|
+
schedule_cleanup
|
252
|
+
destination, action, max_attempts_count =
|
253
|
+
find_transition(transition).values_at(:to, :action, :attempts)
|
254
|
+
|
255
|
+
subject.transaction(requires_new: true) do
|
256
|
+
raise ActiveRecord::Rollback if catch(:abort_transaction) do
|
257
|
+
yield(destination, action, *transition_args); nil
|
258
|
+
end
|
259
|
+
end
|
260
|
+
rescue => error
|
261
|
+
if try_again?(max_attempts_count)
|
262
|
+
Retry(error)
|
263
|
+
else
|
264
|
+
# FIXME: Somehow :error is a Hash, all the time
|
265
|
+
Error(error)
|
266
|
+
end
|
267
|
+
raise
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
class Locker
|
3
|
+
CLEAN = "clean".freeze
|
4
|
+
|
5
|
+
attr_reader :storage, :subject
|
6
|
+
def initialize(subject, storage)
|
7
|
+
@subject = subject
|
8
|
+
@storage = storage
|
9
|
+
end
|
10
|
+
|
11
|
+
class Normal
|
12
|
+
attr_reader :locker, :transition, :transition_args
|
13
|
+
def initialize(locker, args)
|
14
|
+
@locker = locker
|
15
|
+
@transition = args.shift
|
16
|
+
@transition_args = args
|
17
|
+
end
|
18
|
+
|
19
|
+
# NORMAL MODE
|
20
|
+
# if state is PROCESSING_STATE - finish
|
21
|
+
# if state is FAILED_STATE - finish
|
22
|
+
# otherwise - start
|
23
|
+
def skip_any_action?
|
24
|
+
[
|
25
|
+
Storage::PROCESSING_STATUS,
|
26
|
+
Storage::FAILED_STATUS,
|
27
|
+
].include?(locker.storage.pipeline_status)
|
28
|
+
end
|
29
|
+
|
30
|
+
def start!
|
31
|
+
locker.storage.start!(transition, transition_args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def lock!
|
35
|
+
return if skip_any_action?
|
36
|
+
start!
|
37
|
+
begin
|
38
|
+
yield(transition, *transition_args)
|
39
|
+
ensure
|
40
|
+
if locker.storage.pipeline_status == Storage::PROCESSING_STATUS
|
41
|
+
locker.storage.reset_pipeline_status!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rescue
|
45
|
+
if locker.storage.pipeline_status == Storage::PROCESSING_STATUS
|
46
|
+
locker.storage.commit_pipeline_status!(Storage::FAILED_STATUS)
|
47
|
+
end
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Retry < Normal
|
53
|
+
def initialize(locker, _args)
|
54
|
+
@locker = locker
|
55
|
+
@transition = locker.storage.last_event["transition"]
|
56
|
+
@transition_args = locker.storage.last_event["input"]
|
57
|
+
end
|
58
|
+
|
59
|
+
# RETRY MODE
|
60
|
+
# if state is not RETRY_STATE - finish
|
61
|
+
# if state is RETRY_STATE - start
|
62
|
+
def skip_any_action?
|
63
|
+
storage.status != Storage::RETRY_STATUS
|
64
|
+
end
|
65
|
+
|
66
|
+
def start!
|
67
|
+
locker.storage.start_retry!
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# run in time much more then time to process an item
|
72
|
+
class Clean < Retry
|
73
|
+
ONE_DAY = 60 * 60 * 24
|
74
|
+
|
75
|
+
def skip_any_action?
|
76
|
+
return true if storage.status != Storage::PROCESSING_STATUS
|
77
|
+
started_less_then_a_day_ago?
|
78
|
+
end
|
79
|
+
|
80
|
+
def started_less_then_a_day_ago?
|
81
|
+
return unless (updated_at = locker.storage.last_event["updated_at"])
|
82
|
+
updated_at > one_day_ago
|
83
|
+
end
|
84
|
+
|
85
|
+
def one_day_ago
|
86
|
+
Time.now - ONE_DAY
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def with_lock(*args)
|
91
|
+
lock!(*args) do |transition, *transition_args|
|
92
|
+
yield(transition, *transition_args)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def lock!(*args)
|
97
|
+
locker_klass(*args).new(self, args).lock! { |*largs| yield(*largs) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def locker_klass(transition, *)
|
101
|
+
case transition
|
102
|
+
when Storage::RETRY_STATUS
|
103
|
+
Retry
|
104
|
+
when CLEAN
|
105
|
+
Clean
|
106
|
+
else
|
107
|
+
Normal
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
class Storage
|
3
|
+
FAILED_STATUS = "failed".freeze
|
4
|
+
RETRY_STATUS = "retry".freeze
|
5
|
+
PROCESSING_STATUS = "processing".freeze
|
6
|
+
class InvalidPipelineStorage < StandardError; end
|
7
|
+
|
8
|
+
attr_reader :subject, :field
|
9
|
+
attr_accessor :store
|
10
|
+
alias :to_h :store
|
11
|
+
def initialize(subject, field)
|
12
|
+
@subject = subject
|
13
|
+
@field = field
|
14
|
+
init_store(field)
|
15
|
+
end
|
16
|
+
|
17
|
+
def init_store(store_field)
|
18
|
+
self.store = subject.send(store_field).to_h
|
19
|
+
clear! if store.empty?
|
20
|
+
return if (store.keys & %w(status events errors state)).size == 4
|
21
|
+
raise InvalidPipelineStorage, store
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear
|
25
|
+
self.store = subject.send(
|
26
|
+
"#{field}=",
|
27
|
+
"status" => nil,
|
28
|
+
"pipeline_status" => nil,
|
29
|
+
"state" => {},
|
30
|
+
"events" => [],
|
31
|
+
"errors" => [],
|
32
|
+
)
|
33
|
+
DirtyPipeline.with_redis { |r| r.del(pipeline_status_key) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear!
|
37
|
+
clear
|
38
|
+
commit!
|
39
|
+
end
|
40
|
+
|
41
|
+
def start!(transition, args)
|
42
|
+
events << {
|
43
|
+
"transition" => transition,
|
44
|
+
"args" => args,
|
45
|
+
"created_at" => Time.now
|
46
|
+
}
|
47
|
+
increment_attempts_count
|
48
|
+
self.pipeline_status = PROCESSING_STATUS
|
49
|
+
# self.status = "processing", should be set by Locker
|
50
|
+
commit!
|
51
|
+
end
|
52
|
+
|
53
|
+
def start_retry!
|
54
|
+
last_event.merge!(updated_at: Time.now)
|
55
|
+
increment_attempts_count
|
56
|
+
self.pipeline_status = PROCESSING_STATUS
|
57
|
+
# self.status = "processing", should be set by Locker
|
58
|
+
commit!
|
59
|
+
end
|
60
|
+
|
61
|
+
def complete!(output, destination)
|
62
|
+
store["status"] = destination
|
63
|
+
state.merge!(output)
|
64
|
+
last_event.merge!(
|
65
|
+
"output" => output,
|
66
|
+
"updated_at" => Time.now,
|
67
|
+
"success" => true,
|
68
|
+
)
|
69
|
+
commit!
|
70
|
+
end
|
71
|
+
|
72
|
+
def fail_event!
|
73
|
+
fail_event
|
74
|
+
commit!
|
75
|
+
end
|
76
|
+
|
77
|
+
def fail_event
|
78
|
+
last_event["failed"] = true
|
79
|
+
end
|
80
|
+
|
81
|
+
def status
|
82
|
+
store["status"]
|
83
|
+
end
|
84
|
+
|
85
|
+
def pipeline_status_key
|
86
|
+
"pipeline-status:#{subject.class}:#{subject.id}:#{field}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def pipeline_status=(value)
|
90
|
+
DirtyPipeline.with_redis do |r|
|
91
|
+
if value
|
92
|
+
r.set(pipeline_status_key, value)
|
93
|
+
else
|
94
|
+
r.del(pipeline_status_key)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
store["pipeline_status"] = value
|
98
|
+
end
|
99
|
+
|
100
|
+
def commit_pipeline_status!(value = nil)
|
101
|
+
self.pipeline_status = value
|
102
|
+
commit!
|
103
|
+
end
|
104
|
+
alias :reset_pipeline_status! :commit_pipeline_status!
|
105
|
+
|
106
|
+
def pipeline_status
|
107
|
+
DirtyPipeline.with_redis do |r|
|
108
|
+
store["pipeline_status"] = r.get(pipeline_status_key)
|
109
|
+
end
|
110
|
+
store.fetch("pipeline_status")
|
111
|
+
end
|
112
|
+
|
113
|
+
def state
|
114
|
+
store.fetch("state")
|
115
|
+
end
|
116
|
+
|
117
|
+
def events
|
118
|
+
store.fetch("events")
|
119
|
+
end
|
120
|
+
|
121
|
+
def last_event
|
122
|
+
events.last.to_h
|
123
|
+
end
|
124
|
+
|
125
|
+
def last_event_error(event_idx = nil)
|
126
|
+
event = events[event_idx] if event_idx
|
127
|
+
event ||= last_event
|
128
|
+
errors[event["error_idx"]].to_h
|
129
|
+
end
|
130
|
+
|
131
|
+
def errors
|
132
|
+
store.fetch("errors")
|
133
|
+
end
|
134
|
+
|
135
|
+
def last_error
|
136
|
+
errors.last.to_h
|
137
|
+
end
|
138
|
+
|
139
|
+
def increment_attempts_count
|
140
|
+
last_event.merge!(
|
141
|
+
"attempts_count" => last_event["attempts_count"].to_i + 1
|
142
|
+
)
|
143
|
+
end
|
144
|
+
|
145
|
+
def increment_attempts_count!
|
146
|
+
increment_attempts_count
|
147
|
+
commit!
|
148
|
+
end
|
149
|
+
|
150
|
+
def save_retry(error)
|
151
|
+
save_error(error)
|
152
|
+
self.pipeline_status = RETRY_STATUS
|
153
|
+
end
|
154
|
+
|
155
|
+
def save_retry!(error)
|
156
|
+
save_retry(error)
|
157
|
+
commit!
|
158
|
+
end
|
159
|
+
|
160
|
+
def save_exception(exception)
|
161
|
+
errors << {
|
162
|
+
"error" => exception.class.to_s,
|
163
|
+
"error_message" => exception.message,
|
164
|
+
"created_at" => Time.current,
|
165
|
+
}
|
166
|
+
last_event["error_idx"] = errors.size - 1
|
167
|
+
fail_event
|
168
|
+
self.pipeline_status = FAILED_STATUS
|
169
|
+
end
|
170
|
+
|
171
|
+
def save_exception!(error)
|
172
|
+
save_exception(error)
|
173
|
+
commit!
|
174
|
+
end
|
175
|
+
|
176
|
+
def commit!
|
177
|
+
subject.assign_attributes(field => store)
|
178
|
+
subject.save!
|
179
|
+
end
|
180
|
+
|
181
|
+
def ready?
|
182
|
+
storage.pipeline_status.nil?
|
183
|
+
end
|
184
|
+
|
185
|
+
def failed?
|
186
|
+
pipeline_status == FAILED_STATUS
|
187
|
+
end
|
188
|
+
|
189
|
+
def processing?
|
190
|
+
[PROCESSING_STATUS, RETRY_STATUS].include?(pipeline_status)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module DirtyPipeline
|
2
|
+
class Transition
|
3
|
+
def Abort()
|
4
|
+
throw :abort, true
|
5
|
+
end
|
6
|
+
|
7
|
+
def Error(error)
|
8
|
+
throw :fail_with_error, error
|
9
|
+
end
|
10
|
+
|
11
|
+
def Success(output = nil, after_commit: nil, &block)
|
12
|
+
result = [output.to_h]
|
13
|
+
after_commit = Array(after_commit) << block if block_given?
|
14
|
+
result += Array(after_commit) if after_commit
|
15
|
+
throw :success, result
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.call(*args, **kwargs)
|
19
|
+
subject = args.shift
|
20
|
+
instance = new(*args, **kwargs)
|
21
|
+
instance.compensate(subject) if instance.respond_to?(:compensate)
|
22
|
+
instance.call(subject)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dirty_pipeline
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergey Dolganov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-08-
|
11
|
+
date: 2018-08-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -71,6 +71,10 @@ files:
|
|
71
71
|
- bin/setup
|
72
72
|
- dirty_pipeline.gemspec
|
73
73
|
- lib/dirty_pipeline.rb
|
74
|
+
- lib/dirty_pipeline/base.rb
|
75
|
+
- lib/dirty_pipeline/locker.rb
|
76
|
+
- lib/dirty_pipeline/storage.rb
|
77
|
+
- lib/dirty_pipeline/transition.rb
|
74
78
|
- lib/dirty_pipeline/version.rb
|
75
79
|
homepage: https://github.com/sclinede/dirty_pipeline
|
76
80
|
licenses:
|