dynflow 1.4.7 → 1.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/{test/prepare_travis_env.sh → .github/install_dependencies.sh} +2 -2
- data/.github/workflows/ruby.yml +116 -0
- data/dynflow.gemspec +1 -0
- data/examples/chunked_output_benchmark.rb +77 -0
- data/extras/expand/main.go +180 -0
- data/lib/dynflow/action/suspended.rb +4 -4
- data/lib/dynflow/action/timeouts.rb +2 -2
- data/lib/dynflow/action.rb +15 -4
- data/lib/dynflow/actor.rb +20 -4
- data/lib/dynflow/clock.rb +2 -2
- data/lib/dynflow/delayed_executors/abstract_core.rb +11 -9
- data/lib/dynflow/director/running_steps_manager.rb +2 -2
- data/lib/dynflow/director.rb +42 -5
- data/lib/dynflow/dispatcher/client_dispatcher.rb +8 -2
- data/lib/dynflow/dispatcher/executor_dispatcher.rb +12 -2
- data/lib/dynflow/dispatcher.rb +7 -2
- data/lib/dynflow/execution_history.rb +1 -1
- data/lib/dynflow/execution_plan/hooks.rb +1 -1
- data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +1 -0
- data/lib/dynflow/execution_plan.rb +16 -5
- data/lib/dynflow/executors/abstract/core.rb +10 -1
- data/lib/dynflow/executors/parallel.rb +6 -2
- data/lib/dynflow/extensions/msgpack.rb +41 -0
- data/lib/dynflow/extensions.rb +6 -0
- data/lib/dynflow/flows/abstract.rb +14 -0
- data/lib/dynflow/flows/abstract_composed.rb +2 -7
- data/lib/dynflow/flows/atom.rb +2 -2
- data/lib/dynflow/flows/concurrence.rb +2 -0
- data/lib/dynflow/flows/registry.rb +32 -0
- data/lib/dynflow/flows/sequence.rb +2 -0
- data/lib/dynflow/flows.rb +1 -0
- data/lib/dynflow/persistence.rb +10 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +51 -16
- data/lib/dynflow/persistence_adapters/sequel_migrations/021_create_output_chunks.rb +30 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/022_store_flows_as_msgpack.rb +90 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/023_sqlite_workarounds.rb +19 -0
- data/lib/dynflow/serializable.rb +2 -2
- data/lib/dynflow/testing/dummy_coordinator.rb +10 -0
- data/lib/dynflow/testing/dummy_planned_action.rb +4 -0
- data/lib/dynflow/testing/dummy_world.rb +2 -1
- data/lib/dynflow/testing/in_thread_executor.rb +2 -2
- data/lib/dynflow/testing/in_thread_world.rb +5 -5
- data/lib/dynflow/testing.rb +1 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world.rb +16 -4
- data/lib/dynflow.rb +2 -1
- data/test/dispatcher_test.rb +6 -0
- data/test/execution_plan_hooks_test.rb +36 -0
- data/test/extensions_test.rb +42 -0
- data/test/flows_test.rb +44 -0
- data/test/future_execution_test.rb +6 -3
- data/test/persistence_test.rb +2 -2
- data/web/views/flow_step.erb +1 -0
- metadata +34 -8
- data/.travis.yml +0 -33
data/lib/dynflow/director.rb
CHANGED
@@ -15,7 +15,8 @@ module Dynflow
|
|
15
15
|
execution_plan_id: String,
|
16
16
|
step_id: Integer,
|
17
17
|
event: Object,
|
18
|
-
result: Concurrent::Promises::ResolvableFuture
|
18
|
+
result: Concurrent::Promises::ResolvableFuture,
|
19
|
+
optional: Algebrick::Types::Boolean
|
19
20
|
end
|
20
21
|
|
21
22
|
UnprocessableEvent = Class.new(Dynflow::Error)
|
@@ -52,7 +53,7 @@ module Dynflow
|
|
52
53
|
end
|
53
54
|
|
54
55
|
def self.new_from_hash(hash, *_args)
|
55
|
-
self.new(hash[:execution_plan_id], hash[:queue])
|
56
|
+
self.new(hash[:execution_plan_id], hash[:queue], hash[:sender_orchestrator_id])
|
56
57
|
end
|
57
58
|
end
|
58
59
|
|
@@ -107,6 +108,26 @@ module Dynflow
|
|
107
108
|
end
|
108
109
|
end
|
109
110
|
|
111
|
+
class PlanningWorkItem < WorkItem
|
112
|
+
def execute
|
113
|
+
plan = world.persistence.load_delayed_plan(execution_plan_id)
|
114
|
+
return if plan.nil? || plan.execution_plan.state != :scheduled
|
115
|
+
|
116
|
+
if !plan.start_before.nil? && plan.start_before < Time.now.utc()
|
117
|
+
plan.timeout
|
118
|
+
return
|
119
|
+
end
|
120
|
+
|
121
|
+
world.coordinator.acquire(Coordinator::PlanningLock.new(world, plan.execution_plan_uuid)) do
|
122
|
+
plan.plan
|
123
|
+
end
|
124
|
+
plan.execute
|
125
|
+
rescue => e
|
126
|
+
world.logger.warn e.message
|
127
|
+
world.logger.debug e.backtrace.join("\n")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
110
131
|
class FinalizeWorkItem < WorkItem
|
111
132
|
attr_reader :finalize_steps_data
|
112
133
|
|
@@ -146,12 +167,18 @@ module Dynflow
|
|
146
167
|
@logger = world.logger
|
147
168
|
@execution_plan_managers = {}
|
148
169
|
@rescued_steps = {}
|
170
|
+
@planning_plans = []
|
149
171
|
end
|
150
172
|
|
151
173
|
def current_execution_plan_ids
|
152
174
|
@execution_plan_managers.keys
|
153
175
|
end
|
154
176
|
|
177
|
+
def handle_planning(execution_plan_uuid)
|
178
|
+
@planning_plans << execution_plan_uuid
|
179
|
+
[PlanningWorkItem.new(execution_plan_uuid, :default, @world.id)]
|
180
|
+
end
|
181
|
+
|
155
182
|
def start_execution(execution_plan_id, finished)
|
156
183
|
manager = track_execution_plan(execution_plan_id, finished)
|
157
184
|
return [] unless manager
|
@@ -163,6 +190,9 @@ module Dynflow
|
|
163
190
|
execution_plan_manager = @execution_plan_managers[event.execution_plan_id]
|
164
191
|
if execution_plan_manager
|
165
192
|
execution_plan_manager.event(event)
|
193
|
+
elsif event.optional
|
194
|
+
event.result.reject "no manager for #{event.inspect}"
|
195
|
+
[]
|
166
196
|
else
|
167
197
|
raise Dynflow::Error, "no manager for #{event.inspect}"
|
168
198
|
end
|
@@ -172,9 +202,16 @@ module Dynflow
|
|
172
202
|
end
|
173
203
|
|
174
204
|
def work_finished(work)
|
175
|
-
|
176
|
-
|
177
|
-
|
205
|
+
case work
|
206
|
+
when PlanningWorkItem
|
207
|
+
@planning_plans.delete(work.execution_plan_id)
|
208
|
+
@world.persistence.delete_delayed_plans(:execution_plan_uuid => work.execution_plan_id)
|
209
|
+
[]
|
210
|
+
else
|
211
|
+
manager = @execution_plan_managers[work.execution_plan_id]
|
212
|
+
return [] unless manager # skip case when getting event from execution plan that is not running anymore
|
213
|
+
unless_done(manager, manager.what_is_next(work))
|
214
|
+
end
|
178
215
|
end
|
179
216
|
|
180
217
|
# called when there was an unhandled exception during the execution
|
@@ -132,11 +132,13 @@ module Dynflow
|
|
132
132
|
end
|
133
133
|
|
134
134
|
def dispatch_request(request, client_world_id, request_id)
|
135
|
+
ignore_unknown = false
|
135
136
|
executor_id = match request,
|
136
|
-
(on ~Execution do |execution|
|
137
|
+
(on ~Execution | ~Planning do |execution|
|
137
138
|
AnyExecutor
|
138
139
|
end),
|
139
140
|
(on ~Event do |event|
|
141
|
+
ignore_unknown = event.optional
|
140
142
|
find_executor(event.execution_plan_id)
|
141
143
|
end),
|
142
144
|
(on Ping.(~any, ~any) | Status.(~any, ~any) do |receiver_id, _|
|
@@ -144,7 +146,11 @@ module Dynflow
|
|
144
146
|
end)
|
145
147
|
envelope = Envelope[request_id, client_world_id, executor_id, request]
|
146
148
|
if Dispatcher::UnknownWorld === envelope.receiver_id
|
147
|
-
raise Dynflow::Error, "Could not find an executor for #{envelope}"
|
149
|
+
raise Dynflow::Error, "Could not find an executor for #{envelope}" unless ignore_unknown
|
150
|
+
|
151
|
+
message = "Could not find an executor for optional #{envelope}, discarding."
|
152
|
+
log(Logger::DEBUG, message)
|
153
|
+
return respond(envelope, Failed[message])
|
148
154
|
end
|
149
155
|
connector.send(envelope).value!
|
150
156
|
rescue => e
|
@@ -9,6 +9,7 @@ module Dynflow
|
|
9
9
|
|
10
10
|
def handle_request(envelope)
|
11
11
|
match(envelope.message,
|
12
|
+
on(Planning) { perform_planning(envelope, envelope.message)},
|
12
13
|
on(Execution) { perform_execution(envelope, envelope.message) },
|
13
14
|
on(Event) { perform_event(envelope, envelope.message) },
|
14
15
|
on(Status) { get_execution_status(envelope, envelope.message) })
|
@@ -16,6 +17,13 @@ module Dynflow
|
|
16
17
|
|
17
18
|
protected
|
18
19
|
|
20
|
+
def perform_planning(envelope, planning)
|
21
|
+
@world.executor.plan(planning.execution_plan_id)
|
22
|
+
respond(envelope, Accepted)
|
23
|
+
rescue Dynflow::Error => e
|
24
|
+
respond(envelope, Failed[e.message])
|
25
|
+
end
|
26
|
+
|
19
27
|
def perform_execution(envelope, execution)
|
20
28
|
allocate_executor(execution.execution_plan_id, envelope.sender_id, envelope.request_id)
|
21
29
|
execution_lock = Coordinator::ExecutionLock.new(@world, execution.execution_plan_id, envelope.sender_id, envelope.request_id)
|
@@ -52,12 +60,14 @@ module Dynflow
|
|
52
60
|
end
|
53
61
|
end
|
54
62
|
if event_request.time.nil? || event_request.time < Time.now
|
55
|
-
@world.executor.event(envelope.request_id, event_request.execution_plan_id, event_request.step_id, event_request.event, future
|
63
|
+
@world.executor.event(envelope.request_id, event_request.execution_plan_id, event_request.step_id, event_request.event, future,
|
64
|
+
optional: event_request.optional)
|
56
65
|
else
|
57
66
|
@world.clock.ping(
|
58
67
|
@world.executor,
|
59
68
|
event_request.time,
|
60
|
-
Director::Event[envelope.request_id, event_request.execution_plan_id, event_request.step_id, event_request.event, Concurrent::Promises.resolvable_future
|
69
|
+
Director::Event[envelope.request_id, event_request.execution_plan_id, event_request.step_id, event_request.event, Concurrent::Promises.resolvable_future,
|
70
|
+
event_request.optional],
|
61
71
|
:delayed_event
|
62
72
|
)
|
63
73
|
# resolves the future right away - currently we do not wait for the clock ping
|
data/lib/dynflow/dispatcher.rb
CHANGED
@@ -6,13 +6,18 @@ module Dynflow
|
|
6
6
|
fields! execution_plan_id: String,
|
7
7
|
step_id: Integer,
|
8
8
|
event: Object,
|
9
|
-
time: type { variants Time, NilClass }
|
9
|
+
time: type { variants Time, NilClass },
|
10
|
+
optional: Algebrick::Types::Boolean
|
10
11
|
end
|
11
12
|
|
12
13
|
Execution = type do
|
13
14
|
fields! execution_plan_id: String
|
14
15
|
end
|
15
16
|
|
17
|
+
Planning = type do
|
18
|
+
fields! execution_plan_id: String
|
19
|
+
end
|
20
|
+
|
16
21
|
Ping = type do
|
17
22
|
fields! receiver_id: String,
|
18
23
|
use_cache: type { variants TrueClass, FalseClass }
|
@@ -23,7 +28,7 @@ module Dynflow
|
|
23
28
|
execution_plan_id: type { variants String, NilClass }
|
24
29
|
end
|
25
30
|
|
26
|
-
variants Event, Execution, Ping, Status
|
31
|
+
variants Event, Execution, Ping, Status, Planning
|
27
32
|
end
|
28
33
|
|
29
34
|
Response = Algebrick.type do
|
@@ -21,7 +21,7 @@ module Dynflow
|
|
21
21
|
# @param class_name [Class] class of the hook to be run
|
22
22
|
# @param on [Symbol, Array<Symbol>] when should the hook be run, one of {HOOK_KINDS}
|
23
23
|
# @return [void]
|
24
|
-
def use(class_name, on:
|
24
|
+
def use(class_name, on: ExecutionPlan.states)
|
25
25
|
on = Array[on] unless on.kind_of?(Array)
|
26
26
|
validate_kinds!(on)
|
27
27
|
if hooks[class_name]
|
@@ -254,6 +254,7 @@ module Dynflow
|
|
254
254
|
def delay(caller_action, action_class, delay_options, *args)
|
255
255
|
save
|
256
256
|
@root_plan_step = add_scheduling_step(action_class, caller_action)
|
257
|
+
run_hooks(:pending)
|
257
258
|
serializer = root_plan_step.delay(delay_options, args)
|
258
259
|
delayed_plan = DelayedPlan.new(@world,
|
259
260
|
id,
|
@@ -276,7 +277,9 @@ module Dynflow
|
|
276
277
|
raise "Unexpected options #{options.keys.inspect}" unless options.empty?
|
277
278
|
save
|
278
279
|
@root_plan_step = add_plan_step(action_class, caller_action)
|
279
|
-
@root_plan_step.save
|
280
|
+
step = @root_plan_step.save
|
281
|
+
run_hooks(:pending)
|
282
|
+
step
|
280
283
|
end
|
281
284
|
|
282
285
|
def plan(*args)
|
@@ -418,6 +421,14 @@ module Dynflow
|
|
418
421
|
end
|
419
422
|
end
|
420
423
|
|
424
|
+
def self.load_flow(flow_hash)
|
425
|
+
if flow_hash.is_a? Hash
|
426
|
+
Flows::Abstract.from_hash(flow_hash)
|
427
|
+
else
|
428
|
+
Flows::Abstract.decode(flow_hash)
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
421
432
|
def to_hash
|
422
433
|
recursive_to_hash id: id,
|
423
434
|
class: self.class.to_s,
|
@@ -425,8 +436,8 @@ module Dynflow
|
|
425
436
|
state: state,
|
426
437
|
result: result,
|
427
438
|
root_plan_step_id: root_plan_step && root_plan_step.id,
|
428
|
-
run_flow: run_flow,
|
429
|
-
finalize_flow: finalize_flow,
|
439
|
+
run_flow: run_flow.encode,
|
440
|
+
finalize_flow: finalize_flow.encode,
|
430
441
|
step_ids: steps.map { |id, _| id },
|
431
442
|
started_at: time_to_str(started_at),
|
432
443
|
ended_at: time_to_str(ended_at),
|
@@ -448,8 +459,8 @@ module Dynflow
|
|
448
459
|
hash[:label],
|
449
460
|
hash[:state],
|
450
461
|
steps[hash[:root_plan_step_id]],
|
451
|
-
|
452
|
-
|
462
|
+
load_flow(hash[:run_flow]),
|
463
|
+
load_flow(hash[:finalize_flow]),
|
453
464
|
steps,
|
454
465
|
string_to_time(hash[:started_at]),
|
455
466
|
string_to_time(hash[:ended_at]),
|
@@ -35,9 +35,18 @@ module Dynflow
|
|
35
35
|
handle_work(@director.handle_event(event))
|
36
36
|
end
|
37
37
|
|
38
|
+
def handle_planning(execution_plan_id)
|
39
|
+
if terminating?
|
40
|
+
raise Dynflow::Error,
|
41
|
+
"cannot accept event: #{event} core is terminating"
|
42
|
+
end
|
43
|
+
|
44
|
+
handle_work(@director.handle_planning(execution_plan_id))
|
45
|
+
end
|
46
|
+
|
38
47
|
def plan_events(delayed_events)
|
39
48
|
delayed_events.each do |event|
|
40
|
-
@world.plan_event(event.execution_plan_id, event.step_id, event.event, event.time)
|
49
|
+
@world.plan_event(event.execution_plan_id, event.step_id, event.event, event.time, optional: event.optional)
|
41
50
|
end
|
42
51
|
end
|
43
52
|
|
@@ -33,11 +33,15 @@ module Dynflow
|
|
33
33
|
raise e
|
34
34
|
end
|
35
35
|
|
36
|
-
def event(request_id, execution_plan_id, step_id, event, future = nil)
|
37
|
-
@core.ask([:handle_event, Director::Event[request_id, execution_plan_id, step_id, event, future]])
|
36
|
+
def event(request_id, execution_plan_id, step_id, event, future = nil, optional: false)
|
37
|
+
@core.ask([:handle_event, Director::Event[request_id, execution_plan_id, step_id, event, future, optional]])
|
38
38
|
future
|
39
39
|
end
|
40
40
|
|
41
|
+
def plan(execution_plan_id)
|
42
|
+
@core.ask([:handle_planning, execution_plan_id])
|
43
|
+
end
|
44
|
+
|
41
45
|
def delayed_event(director_event)
|
42
46
|
@core.ask([:handle_event, director_event])
|
43
47
|
director_event.result
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'msgpack'
|
3
|
+
|
4
|
+
module Dynflow
|
5
|
+
module Extensions
|
6
|
+
module MsgPack
|
7
|
+
module Time
|
8
|
+
def to_msgpack(out = ''.dup)
|
9
|
+
::MessagePack.pack(self, out)
|
10
|
+
out
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
::Time.include ::Dynflow::Extensions::MsgPack::Time
|
15
|
+
::MessagePack::DefaultFactory.register_type(0x00, Time, packer: MessagePack::Time::Packer, unpacker: MessagePack::Time::Unpacker)
|
16
|
+
|
17
|
+
begin
|
18
|
+
require 'active_support/time_with_zone'
|
19
|
+
unpacker = ->(payload) do
|
20
|
+
tv = MessagePack::Timestamp.from_msgpack_ext(payload)
|
21
|
+
::Time.zone.at(tv.sec, tv.nsec, :nanosecond)
|
22
|
+
end
|
23
|
+
::ActiveSupport::TimeWithZone.include ::Dynflow::Extensions::MsgPack::Time
|
24
|
+
::MessagePack::DefaultFactory.register_type(0x01, ActiveSupport::TimeWithZone, packer: MessagePack::Time::Packer, unpacker: unpacker)
|
25
|
+
|
26
|
+
::DateTime.include ::Dynflow::Extensions::MsgPack::Time
|
27
|
+
::MessagePack::DefaultFactory.register_type(0x02, DateTime,
|
28
|
+
packer: ->(datetime) { MessagePack::Time::Packer.(datetime.to_time) },
|
29
|
+
unpacker: ->(payload) { unpacker.(payload).to_datetime })
|
30
|
+
|
31
|
+
::Date.include ::Dynflow::Extensions::MsgPack::Time
|
32
|
+
::MessagePack::DefaultFactory.register_type(0x03, Date,
|
33
|
+
packer: ->(date) { MessagePack::Time::Packer.(date.to_time) },
|
34
|
+
unpacker: ->(payload) { unpacker.(payload).to_date })
|
35
|
+
rescue LoadError
|
36
|
+
# This is fine
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -32,6 +32,20 @@ module Dynflow
|
|
32
32
|
def flatten!
|
33
33
|
raise NotImplementedError
|
34
34
|
end
|
35
|
+
|
36
|
+
def self.new_from_hash(hash)
|
37
|
+
check_class_matching hash
|
38
|
+
new(hash[:flows].map { |flow_hash| from_hash(flow_hash) })
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.decode(data)
|
42
|
+
if data.is_a? Integer
|
43
|
+
Flows::Atom.new(data)
|
44
|
+
else
|
45
|
+
kind, *subflows = data
|
46
|
+
Registry.decode(kind).new(subflows.map { |subflow| self.decode(subflow) })
|
47
|
+
end
|
48
|
+
end
|
35
49
|
end
|
36
50
|
end
|
37
51
|
end
|
@@ -11,8 +11,8 @@ module Dynflow
|
|
11
11
|
@flows = flows
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
|
14
|
+
def encode
|
15
|
+
[Registry.encode(self)] + flows.map(&:encode)
|
16
16
|
end
|
17
17
|
|
18
18
|
def <<(v)
|
@@ -61,11 +61,6 @@ module Dynflow
|
|
61
61
|
|
62
62
|
protected
|
63
63
|
|
64
|
-
def self.new_from_hash(hash)
|
65
|
-
check_class_matching hash
|
66
|
-
new(hash[:flows].map { |flow_hash| from_hash(flow_hash) })
|
67
|
-
end
|
68
|
-
|
69
64
|
# adds the +new_flow+ in a way that it's in sequence with
|
70
65
|
# the +satisfying_flows+
|
71
66
|
def add_to_sequence(satisfying_flows, new_flow)
|
data/lib/dynflow/flows/atom.rb
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Dynflow
|
3
|
+
module Flows
|
4
|
+
class Registry
|
5
|
+
class IdentifierTaken < ArgumentError; end
|
6
|
+
class UnknownIdentifier < ArgumentError; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def register!(klass, identifier)
|
10
|
+
if (found = serialization_map[identifier])
|
11
|
+
raise IdentifierTaken, "Error setting up mapping #{identifier} to #{klass}, it already maps to #{found}"
|
12
|
+
else
|
13
|
+
serialization_map.update(identifier => klass)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def encode(klass)
|
18
|
+
klass = klass.class unless klass.is_a?(Class)
|
19
|
+
serialization_map.invert[klass] || raise(UnknownIdentifier, "Could not find mapping for #{klass}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def decode(identifier)
|
23
|
+
serialization_map[identifier] || raise(UnknownIdentifier, "Could not find mapping for #{identifier}")
|
24
|
+
end
|
25
|
+
|
26
|
+
def serialization_map
|
27
|
+
@serialization_map ||= {}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/dynflow/flows.rb
CHANGED
data/lib/dynflow/persistence.rb
CHANGED
@@ -46,6 +46,16 @@ module Dynflow
|
|
46
46
|
adapter.save_action(execution_plan_id, action.id, action.to_hash)
|
47
47
|
end
|
48
48
|
|
49
|
+
def save_output_chunks(execution_plan_id, action_id, chunks)
|
50
|
+
return if chunks.empty?
|
51
|
+
|
52
|
+
adapter.save_output_chunks(execution_plan_id, action_id, chunks)
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_output_chunks(execution_plan_id, action_id)
|
56
|
+
adapter.load_output_chunks(execution_plan_id, action_id)
|
57
|
+
end
|
58
|
+
|
49
59
|
def find_execution_plans(options)
|
50
60
|
adapter.find_execution_plans(options).map do |execution_plan_hash|
|
51
61
|
ExecutionPlan.new_from_hash(execution_plan_hash, @world)
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'sequel'
|
3
|
-
require '
|
3
|
+
require 'msgpack'
|
4
4
|
require 'fileutils'
|
5
5
|
require 'csv'
|
6
6
|
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
7
8
|
module Dynflow
|
8
9
|
module PersistenceAdapters
|
9
10
|
|
@@ -37,12 +38,14 @@ module Dynflow
|
|
37
38
|
class action_class execution_plan_uuid queue),
|
38
39
|
envelope: %w(receiver_id),
|
39
40
|
coordinator_record: %w(id owner_id class),
|
40
|
-
delayed: %w(execution_plan_uuid start_at start_before args_serializer frozen)
|
41
|
+
delayed: %w(execution_plan_uuid start_at start_before args_serializer frozen),
|
42
|
+
output_chunk: %w(execution_plan_uuid action_id kind timestamp) }
|
41
43
|
|
42
44
|
SERIALIZABLE_COLUMNS = { action: %w(input output),
|
43
45
|
delayed: %w(serialized_args),
|
44
46
|
execution_plan: %w(run_flow finalize_flow execution_history step_ids),
|
45
|
-
step: %w(error children)
|
47
|
+
step: %w(error children),
|
48
|
+
output_chunk: %w(chunk) }
|
46
49
|
|
47
50
|
def initialize(config)
|
48
51
|
migrate = true
|
@@ -83,15 +86,17 @@ module Dynflow
|
|
83
86
|
table(:delayed).where(execution_plan_uuid: uuids).delete
|
84
87
|
|
85
88
|
steps = table(:step).where(execution_plan_uuid: uuids)
|
86
|
-
backup_to_csv(steps, backup_dir, 'steps.csv') if backup_dir
|
89
|
+
backup_to_csv(:step, steps, backup_dir, 'steps.csv') if backup_dir
|
87
90
|
steps.delete
|
88
91
|
|
92
|
+
output_chunks = table(:output_chunk).where(execution_plan_uuid: uuids).delete
|
93
|
+
|
89
94
|
actions = table(:action).where(execution_plan_uuid: uuids)
|
90
|
-
backup_to_csv(actions, backup_dir, 'actions.csv') if backup_dir
|
95
|
+
backup_to_csv(:action, actions, backup_dir, 'actions.csv') if backup_dir
|
91
96
|
actions.delete
|
92
97
|
|
93
98
|
execution_plans = table(:execution_plan).where(uuid: uuids)
|
94
|
-
backup_to_csv(execution_plans, backup_dir, 'execution_plans.csv') if backup_dir
|
99
|
+
backup_to_csv(:execution_plan, execution_plans, backup_dir, 'execution_plans.csv') if backup_dir
|
95
100
|
count += execution_plans.delete
|
96
101
|
end
|
97
102
|
end
|
@@ -173,6 +178,18 @@ module Dynflow
|
|
173
178
|
save :action, { execution_plan_uuid: execution_plan_id, id: action_id }, value, with_data: false
|
174
179
|
end
|
175
180
|
|
181
|
+
def save_output_chunks(execution_plan_id, action_id, chunks)
|
182
|
+
chunks.each do |chunk|
|
183
|
+
chunk[:execution_plan_uuid] = execution_plan_id
|
184
|
+
chunk[:action_id] = action_id
|
185
|
+
save :output_chunk, {}, chunk, with_data: false
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def load_output_chunks(execution_plan_id, action_id)
|
190
|
+
load_records :output_chunk, { execution_plan_uuid: execution_plan_id, action_id: action_id }, [:timestamp, :kind, :chunk]
|
191
|
+
end
|
192
|
+
|
176
193
|
def connector_feature!
|
177
194
|
unless @additional_responsibilities[:connector]
|
178
195
|
raise "The sequel persistence adapter connector feature used but not enabled in additional_features"
|
@@ -265,14 +282,16 @@ module Dynflow
|
|
265
282
|
step: :dynflow_steps,
|
266
283
|
envelope: :dynflow_envelopes,
|
267
284
|
coordinator_record: :dynflow_coordinator_records,
|
268
|
-
delayed: :dynflow_delayed_plans
|
285
|
+
delayed: :dynflow_delayed_plans,
|
286
|
+
output_chunk: :dynflow_output_chunks }
|
269
287
|
|
270
288
|
def table(which)
|
271
289
|
db[TABLES.fetch(which)]
|
272
290
|
end
|
273
291
|
|
274
292
|
def initialize_db(db_path)
|
275
|
-
|
293
|
+
logger = Logger.new($stderr) if ENV['DYNFLOW_SQL_LOG']
|
294
|
+
::Sequel.connect db_path, logger: logger
|
276
295
|
end
|
277
296
|
|
278
297
|
def self.migrations_path
|
@@ -281,10 +300,15 @@ module Dynflow
|
|
281
300
|
|
282
301
|
def prepare_record(table_name, value, base = {}, with_data = true)
|
283
302
|
record = base.dup
|
284
|
-
|
303
|
+
has_data_column = table(table_name).columns.include?(:data)
|
304
|
+
if with_data && has_data_column
|
285
305
|
record[:data] = dump_data(value)
|
286
306
|
else
|
287
|
-
|
307
|
+
if has_data_column
|
308
|
+
record[:data] = nil
|
309
|
+
else
|
310
|
+
record.delete(:data)
|
311
|
+
end
|
288
312
|
record.merge! serialize_columns(table_name, value)
|
289
313
|
end
|
290
314
|
|
@@ -339,7 +363,11 @@ module Dynflow
|
|
339
363
|
records = with_retry do
|
340
364
|
filtered = table.filter(Utils.symbolize_keys(condition))
|
341
365
|
# Filter out requested columns which the table doesn't have, load data just in case
|
342
|
-
|
366
|
+
unless keys.nil?
|
367
|
+
columns = table.columns & keys
|
368
|
+
columns |= [:data] if table.columns.include?(:data)
|
369
|
+
filtered = filtered.select(*columns)
|
370
|
+
end
|
343
371
|
filtered.all
|
344
372
|
end
|
345
373
|
records = records.map { |record| load_data(record, what) }
|
@@ -355,11 +383,11 @@ module Dynflow
|
|
355
383
|
hash = if record[:data].nil?
|
356
384
|
SERIALIZABLE_COLUMNS.fetch(what, []).each do |key|
|
357
385
|
key = key.to_sym
|
358
|
-
record[key] =
|
386
|
+
record[key] = MessagePack.unpack((record[key])) unless record[key].nil?
|
359
387
|
end
|
360
388
|
record
|
361
389
|
else
|
362
|
-
|
390
|
+
MessagePack.unpack(record[:data])
|
363
391
|
end
|
364
392
|
Utils.indifferent_hash(hash)
|
365
393
|
end
|
@@ -368,7 +396,7 @@ module Dynflow
|
|
368
396
|
FileUtils.mkdir_p(backup_dir) unless File.directory?(backup_dir)
|
369
397
|
end
|
370
398
|
|
371
|
-
def backup_to_csv(dataset, backup_dir, file_name)
|
399
|
+
def backup_to_csv(table_name, dataset, backup_dir, file_name)
|
372
400
|
ensure_backup_dir(backup_dir)
|
373
401
|
csv_file = File.join(backup_dir, file_name)
|
374
402
|
appending = File.exist?(csv_file)
|
@@ -376,7 +404,12 @@ module Dynflow
|
|
376
404
|
File.open(csv_file, 'a') do |csv|
|
377
405
|
csv << columns.to_csv unless appending
|
378
406
|
dataset.each do |row|
|
379
|
-
|
407
|
+
values = columns.map do |col|
|
408
|
+
value = row[col]
|
409
|
+
value = value.unpack('H*').first if value && SERIALIZABLE_COLUMNS.fetch(table_name, []).include?(col.to_s)
|
410
|
+
value
|
411
|
+
end
|
412
|
+
csv << values.to_csv
|
380
413
|
end
|
381
414
|
end
|
382
415
|
dataset
|
@@ -394,7 +427,8 @@ module Dynflow
|
|
394
427
|
|
395
428
|
def dump_data(value)
|
396
429
|
return if value.nil?
|
397
|
-
|
430
|
+
packed = MessagePack.pack(Type!(value, Hash, Array, Integer, String))
|
431
|
+
::Sequel.blob(packed)
|
398
432
|
end
|
399
433
|
|
400
434
|
def paginate(data_set, options)
|
@@ -477,3 +511,4 @@ module Dynflow
|
|
477
511
|
end
|
478
512
|
end
|
479
513
|
end
|
514
|
+
# rubocop:enable Metrics/ClassLength
|