dynflow 1.4.8 → 1.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/{test/prepare_travis_env.sh → .github/install_dependencies.sh} +2 -2
  3. data/.github/workflows/ruby.yml +116 -0
  4. data/dynflow.gemspec +1 -0
  5. data/examples/chunked_output_benchmark.rb +77 -0
  6. data/extras/expand/main.go +180 -0
  7. data/lib/dynflow/action/suspended.rb +4 -4
  8. data/lib/dynflow/action/timeouts.rb +2 -2
  9. data/lib/dynflow/action.rb +15 -4
  10. data/lib/dynflow/clock.rb +2 -2
  11. data/lib/dynflow/delayed_executors/abstract_core.rb +11 -9
  12. data/lib/dynflow/director.rb +42 -5
  13. data/lib/dynflow/dispatcher/client_dispatcher.rb +8 -2
  14. data/lib/dynflow/dispatcher/executor_dispatcher.rb +12 -2
  15. data/lib/dynflow/dispatcher.rb +7 -2
  16. data/lib/dynflow/execution_history.rb +1 -1
  17. data/lib/dynflow/execution_plan/hooks.rb +1 -1
  18. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +1 -0
  19. data/lib/dynflow/execution_plan.rb +16 -5
  20. data/lib/dynflow/executors/abstract/core.rb +10 -1
  21. data/lib/dynflow/executors/parallel.rb +6 -2
  22. data/lib/dynflow/extensions/msgpack.rb +41 -0
  23. data/lib/dynflow/extensions.rb +6 -0
  24. data/lib/dynflow/flows/abstract.rb +14 -0
  25. data/lib/dynflow/flows/abstract_composed.rb +2 -7
  26. data/lib/dynflow/flows/atom.rb +2 -2
  27. data/lib/dynflow/flows/concurrence.rb +2 -0
  28. data/lib/dynflow/flows/registry.rb +32 -0
  29. data/lib/dynflow/flows/sequence.rb +2 -0
  30. data/lib/dynflow/flows.rb +1 -0
  31. data/lib/dynflow/persistence.rb +10 -0
  32. data/lib/dynflow/persistence_adapters/sequel.rb +51 -16
  33. data/lib/dynflow/persistence_adapters/sequel_migrations/021_create_output_chunks.rb +30 -0
  34. data/lib/dynflow/persistence_adapters/sequel_migrations/022_store_flows_as_msgpack.rb +90 -0
  35. data/lib/dynflow/persistence_adapters/sequel_migrations/023_sqlite_workarounds.rb +19 -0
  36. data/lib/dynflow/serializable.rb +2 -2
  37. data/lib/dynflow/testing/dummy_coordinator.rb +10 -0
  38. data/lib/dynflow/testing/dummy_planned_action.rb +4 -0
  39. data/lib/dynflow/testing/dummy_world.rb +2 -1
  40. data/lib/dynflow/testing/in_thread_executor.rb +2 -2
  41. data/lib/dynflow/testing/in_thread_world.rb +5 -5
  42. data/lib/dynflow/testing.rb +1 -0
  43. data/lib/dynflow/version.rb +1 -1
  44. data/lib/dynflow/world.rb +16 -4
  45. data/lib/dynflow.rb +2 -1
  46. data/test/dispatcher_test.rb +6 -0
  47. data/test/execution_plan_hooks_test.rb +36 -0
  48. data/test/extensions_test.rb +42 -0
  49. data/test/flows_test.rb +44 -0
  50. data/test/future_execution_test.rb +6 -3
  51. data/test/persistence_test.rb +2 -2
  52. data/web/views/flow_step.erb +1 -0
  53. metadata +37 -5
  54. data/.travis.yml +0 -33
@@ -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
@@ -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
@@ -12,7 +12,7 @@ module Dynflow
12
12
 
13
13
  module Event
14
14
  def inspect
15
- "#{Time.at(time).utc}: #{name}".tap { |s| s << " @ #{world_id}" if world_id }
15
+ ["#{Time.at(time).utc}: #{name}", world_id].compact.join(' @ ')
16
16
  end
17
17
  end
18
18
 
@@ -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: HOOK_KINDS)
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]
@@ -31,6 +31,7 @@ module Dynflow
31
31
  action = persistence.load_action(self)
32
32
  yield action
33
33
  persistence.save_action(execution_plan_id, action)
34
+ persistence.save_output_chunks(execution_plan_id, action.id, action.pending_output_chunks)
34
35
  save
35
36
 
36
37
  return self
@@ -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
- Flows::Abstract.from_hash(hash[:run_flow]),
452
- Flows::Abstract.from_hash(hash[:finalize_flow]),
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Dynflow
3
+ module Extensions
4
+ require 'dynflow/extensions/msgpack'
5
+ end
6
+ 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 to_hash
15
- super.merge recursive_to_hash(:flows => flows)
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)
@@ -5,8 +5,8 @@ module Dynflow
5
5
 
6
6
  attr_reader :step_id
7
7
 
8
- def to_hash
9
- super.merge(:step_id => step_id)
8
+ def encode
9
+ step_id
10
10
  end
11
11
 
12
12
  def initialize(step_id)
@@ -25,5 +25,7 @@ module Dynflow
25
25
  return Concurrence.new(extracted_sub_flows)
26
26
  end
27
27
  end
28
+
29
+ Registry.register!(Concurrence, 'C')
28
30
  end
29
31
  end
@@ -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
@@ -10,5 +10,7 @@ module Dynflow
10
10
  self << dependent_flow
11
11
  end
12
12
  end
13
+
14
+ Registry.register!(Sequence, 'S')
13
15
  end
14
16
  end
data/lib/dynflow/flows.rb CHANGED
@@ -4,6 +4,7 @@ require 'forwardable'
4
4
  module Dynflow
5
5
  module Flows
6
6
 
7
+ require 'dynflow/flows/registry'
7
8
  require 'dynflow/flows/abstract'
8
9
  require 'dynflow/flows/atom'
9
10
  require 'dynflow/flows/abstract_composed'
@@ -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 'multi_json'
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
- ::Sequel.connect db_path
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
- if with_data && table(table_name).columns.include?(:data)
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
- record[:data] = nil
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
- filtered = filtered.select(:data, *(table.columns & keys)) unless keys.nil?
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] = MultiJson.load(record[key]) unless record[key].nil?
386
+ record[key] = MessagePack.unpack((record[key])) unless record[key].nil?
359
387
  end
360
388
  record
361
389
  else
362
- MultiJson.load(record[:data])
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
- csv << columns.collect { |col| row[col] }.to_csv
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
- MultiJson.dump Type!(value, Hash, Array)
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
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ Sequel.migration do
3
+ up do
4
+ type = database_type
5
+ create_table(:dynflow_output_chunks) do
6
+ primary_key :id
7
+
8
+ column_properties = if type.to_s.include?('postgres')
9
+ {type: :uuid}
10
+ else
11
+ {type: String, size: 36, fixed: true, null: false}
12
+ end
13
+ foreign_key :execution_plan_uuid, :dynflow_execution_plans, **column_properties
14
+ index :execution_plan_uuid
15
+
16
+ column :action_id, Integer, null: false
17
+ foreign_key [:execution_plan_uuid, :action_id], :dynflow_actions,
18
+ name: :dynflow_output_chunks_execution_plan_uuid_fkey1
19
+ index [:execution_plan_uuid, :action_id]
20
+
21
+ column :chunk, String, text: true
22
+ column :kind, String
23
+ column :timestamp, Time, null: false
24
+ end
25
+ end
26
+
27
+ down do
28
+ drop_table(:dynflow_output_chunks)
29
+ end
30
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+ require 'msgpack'
5
+
6
+ def table_pkeys(table)
7
+ case table
8
+ when :dynflow_execution_plans
9
+ [:uuid]
10
+ when :dynflow_actions, :dynflow_steps
11
+ [:execution_plan_uuid, :id]
12
+ when :dynflow_coordinator_records
13
+ [:id, :class]
14
+ when :dynflow_delayed_plans
15
+ [:execution_plan_uuid]
16
+ when :dynflow_envelopes
17
+ [:id]
18
+ when :dynflow_output_chunks
19
+ [:chunk]
20
+ else
21
+ raise "Unknown table '#{table}'"
22
+ end
23
+ end
24
+
25
+ def conditions_for_row(table, row)
26
+ row.slice(*table_pkeys(table))
27
+ end
28
+
29
+ def migrate_table(table, from_names, to_names, new_type)
30
+ alter_table(table) do
31
+ to_names.each do |new|
32
+ add_column new, new_type
33
+ end
34
+ end
35
+
36
+ relevant_columns = table_pkeys(table) | from_names
37
+
38
+ from(table).select(*relevant_columns).each do |row|
39
+ update = from_names.zip(to_names).reduce({}) do |acc, (from, to)|
40
+ row[from].nil? ? acc : acc.merge(to => yield(row[from]))
41
+ end
42
+ next if update.empty?
43
+ from(table).where(conditions_for_row(table, row)).update(update)
44
+ end
45
+
46
+ from_names.zip(to_names).each do |old, new|
47
+ alter_table(table) do
48
+ drop_column old
49
+ end
50
+
51
+ if database_type == :mysql
52
+ type = new_type == File ? 'blob' : 'mediumtext'
53
+ run "ALTER TABLE #{table} CHANGE COLUMN `#{new}` `#{old}` #{type};"
54
+ else
55
+ rename_column table, new, old
56
+ end
57
+ end
58
+ end
59
+
60
+ Sequel.migration do
61
+
62
+ TABLES = {
63
+ :dynflow_actions => [:data, :input, :output],
64
+ :dynflow_coordinator_records => [:data],
65
+ :dynflow_delayed_plans => [:serialized_args, :data],
66
+ :dynflow_envelopes => [:data],
67
+ :dynflow_execution_plans => [:run_flow, :finalize_flow, :execution_history, :step_ids],
68
+ :dynflow_steps => [:error, :children],
69
+ :dynflow_output_chunks => [:chunk]
70
+ }
71
+
72
+ up do
73
+ TABLES.each do |table, columns|
74
+ new_columns = columns.map { |c| "#{c}_blob" }
75
+
76
+ migrate_table table, columns, new_columns, File do |data|
77
+ ::Sequel.blob(MessagePack.pack(MultiJson.load(data)))
78
+ end
79
+ end
80
+ end
81
+
82
+ down do
83
+ TABLES.each do |table, columns|
84
+ new_columns = columns.map { |c| c + '_text' }
85
+ migrate_table table, columns, new_columns, String do |data|
86
+ MultiJson.dump(MessagePack.unpack(data))
87
+ end
88
+ end
89
+ end
90
+ end