dynflow 0.7.9 → 0.8.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.
Files changed (118) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +16 -1
  3. data/Gemfile +13 -1
  4. data/doc/pages/source/_drafts/2015-03-01-new-documentation.markdown +10 -0
  5. data/doc/pages/source/_includes/menu.html +1 -0
  6. data/doc/pages/source/_includes/menu_right.html +1 -1
  7. data/doc/pages/source/_sass/_bootstrap-variables.sass +1 -0
  8. data/doc/pages/source/_sass/_style.scss +4 -0
  9. data/doc/pages/source/blog/index.html +12 -0
  10. data/doc/pages/source/documentation/index.md +330 -5
  11. data/dynflow.gemspec +3 -1
  12. data/examples/example_helper.rb +18 -11
  13. data/examples/orchestrate_evented.rb +2 -1
  14. data/examples/remote_executor.rb +53 -20
  15. data/lib/dynflow.rb +16 -6
  16. data/lib/dynflow/action/suspended.rb +1 -1
  17. data/lib/dynflow/action/with_sub_plans.rb +3 -6
  18. data/lib/dynflow/actor.rb +56 -0
  19. data/lib/dynflow/clock.rb +43 -38
  20. data/lib/dynflow/config.rb +107 -0
  21. data/lib/dynflow/connectors.rb +7 -0
  22. data/lib/dynflow/connectors/abstract.rb +41 -0
  23. data/lib/dynflow/connectors/database.rb +175 -0
  24. data/lib/dynflow/connectors/direct.rb +71 -0
  25. data/lib/dynflow/coordinator.rb +280 -0
  26. data/lib/dynflow/coordinator_adapters.rb +8 -0
  27. data/lib/dynflow/coordinator_adapters/abstract.rb +28 -0
  28. data/lib/dynflow/coordinator_adapters/sequel.rb +29 -0
  29. data/lib/dynflow/dispatcher.rb +58 -0
  30. data/lib/dynflow/dispatcher/abstract.rb +14 -0
  31. data/lib/dynflow/dispatcher/client_dispatcher.rb +139 -0
  32. data/lib/dynflow/dispatcher/executor_dispatcher.rb +86 -0
  33. data/lib/dynflow/errors.rb +7 -1
  34. data/lib/dynflow/execution_history.rb +46 -0
  35. data/lib/dynflow/execution_plan.rb +19 -15
  36. data/lib/dynflow/executors.rb +0 -1
  37. data/lib/dynflow/executors/abstract.rb +5 -10
  38. data/lib/dynflow/executors/parallel.rb +16 -13
  39. data/lib/dynflow/executors/parallel/core.rb +76 -78
  40. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +4 -5
  41. data/lib/dynflow/executors/parallel/pool.rb +22 -52
  42. data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -2
  43. data/lib/dynflow/executors/parallel/worker.rb +5 -10
  44. data/lib/dynflow/persistence.rb +14 -0
  45. data/lib/dynflow/persistence_adapters/abstract.rb +14 -3
  46. data/lib/dynflow/persistence_adapters/sequel.rb +142 -38
  47. data/lib/dynflow/persistence_adapters/sequel_migrations/004_coordinator_records.rb +14 -0
  48. data/lib/dynflow/persistence_adapters/sequel_migrations/005_envelopes.rb +14 -0
  49. data/lib/dynflow/round_robin.rb +37 -0
  50. data/lib/dynflow/serializable.rb +1 -2
  51. data/lib/dynflow/serializer.rb +46 -0
  52. data/lib/dynflow/testing/dummy_executor.rb +2 -2
  53. data/lib/dynflow/testing/dummy_world.rb +1 -1
  54. data/lib/dynflow/transaction_adapters/abstract.rb +0 -5
  55. data/lib/dynflow/transaction_adapters/active_record.rb +0 -10
  56. data/lib/dynflow/version.rb +1 -1
  57. data/lib/dynflow/web.rb +26 -0
  58. data/lib/dynflow/web/console.rb +108 -0
  59. data/lib/dynflow/web/console_helpers.rb +158 -0
  60. data/lib/dynflow/web/filtering_helpers.rb +85 -0
  61. data/lib/dynflow/web/world_helpers.rb +9 -0
  62. data/lib/dynflow/web_console.rb +3 -310
  63. data/lib/dynflow/world.rb +188 -119
  64. data/test/abnormal_states_recovery_test.rb +152 -0
  65. data/test/action_test.rb +2 -3
  66. data/test/clock_test.rb +1 -5
  67. data/test/coordinator_test.rb +152 -0
  68. data/test/dispatcher_test.rb +146 -0
  69. data/test/execution_plan_test.rb +2 -1
  70. data/test/executor_test.rb +534 -612
  71. data/test/middleware_test.rb +4 -4
  72. data/test/persistence_test.rb +17 -0
  73. data/test/prepare_travis_env.sh +35 -0
  74. data/test/rescue_test.rb +5 -3
  75. data/test/round_robin_test.rb +28 -0
  76. data/test/support/code_workflow_example.rb +0 -73
  77. data/test/support/dummy_example.rb +130 -0
  78. data/test/support/test_execution_log.rb +41 -0
  79. data/test/test_helper.rb +222 -116
  80. data/test/testing_test.rb +10 -10
  81. data/test/web_console_test.rb +3 -3
  82. data/test/world_test.rb +23 -0
  83. data/web/assets/images/logo-square.png +0 -0
  84. data/web/assets/stylesheets/application.css +9 -0
  85. data/web/assets/vendor/bootstrap/config.json +429 -0
  86. data/web/assets/vendor/bootstrap/css/bootstrap-theme.css +479 -0
  87. data/web/assets/vendor/bootstrap/css/bootstrap-theme.min.css +10 -0
  88. data/web/assets/vendor/bootstrap/css/bootstrap.css +5377 -4980
  89. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -8
  90. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  91. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  92. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  93. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  94. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  95. data/web/assets/vendor/bootstrap/js/bootstrap.js +1674 -1645
  96. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +11 -5
  97. data/web/views/execution_history.erb +17 -0
  98. data/web/views/index.erb +4 -6
  99. data/web/views/layout.erb +44 -8
  100. data/web/views/show.erb +4 -5
  101. data/web/views/worlds.erb +26 -0
  102. metadata +116 -23
  103. checksums.yaml +0 -15
  104. data/lib/dynflow/daemon.rb +0 -30
  105. data/lib/dynflow/executors/remote_via_socket.rb +0 -43
  106. data/lib/dynflow/executors/remote_via_socket/core.rb +0 -184
  107. data/lib/dynflow/future.rb +0 -173
  108. data/lib/dynflow/listeners.rb +0 -7
  109. data/lib/dynflow/listeners/abstract.rb +0 -17
  110. data/lib/dynflow/listeners/serialization.rb +0 -77
  111. data/lib/dynflow/listeners/socket.rb +0 -117
  112. data/lib/dynflow/micro_actor.rb +0 -102
  113. data/lib/dynflow/simple_world.rb +0 -19
  114. data/test/remote_via_socket_test.rb +0 -170
  115. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +0 -1109
  116. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +0 -9
  117. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  118. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
@@ -0,0 +1,8 @@
1
+ module Dynflow
2
+ module CoordinatorAdapters
3
+
4
+ require 'dynflow/coordinator_adapters/abstract'
5
+ require 'dynflow/coordinator_adapters/sequel'
6
+
7
+ end
8
+ end
@@ -0,0 +1,28 @@
1
+ module Dynflow
2
+ module CoordinatorAdapters
3
+ class Abstract
4
+ include Algebrick::TypeCheck
5
+
6
+ def initialize(world)
7
+ Type! world, World
8
+ @world = world
9
+ end
10
+
11
+ def create_record(record)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def update_record(record)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def delete_record(record)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def find_records(record)
24
+ raise NotImplementedError
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ module Dynflow
2
+ module CoordinatorAdapters
3
+ class Sequel < Abstract
4
+ def initialize(world)
5
+ super
6
+ @sequel_adapter = world.persistence.adapter
7
+ Type! @sequel_adapter, PersistenceAdapters::Sequel
8
+ end
9
+
10
+ def create_record(record)
11
+ @sequel_adapter.insert_coordinator_record(record.to_hash)
12
+ rescue ::Sequel::UniqueConstraintViolation
13
+ raise Coordinator::DuplicateRecordError.new(record)
14
+ end
15
+
16
+ def update_record(record)
17
+ @sequel_adapter.update_coordinator_record(record.class.name, record.id, record.to_hash)
18
+ end
19
+
20
+ def delete_record(record)
21
+ @sequel_adapter.delete_coordinator_record(record.class.name, record.id)
22
+ end
23
+
24
+ def find_records(filter_options)
25
+ @sequel_adapter.find_coordinator_records(filters: filter_options)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,58 @@
1
+ module Dynflow
2
+ module Dispatcher
3
+ Request = Algebrick.type do
4
+ Event = type do
5
+ fields! execution_plan_id: String,
6
+ step_id: Fixnum,
7
+ event: Object
8
+ end
9
+
10
+ Execution = type do
11
+ fields! execution_plan_id: String
12
+ end
13
+
14
+ Ping = type do
15
+ fields! receiver_id: String
16
+ end
17
+
18
+ variants Event, Execution, Ping
19
+ end
20
+
21
+ Response = Algebrick.type do
22
+ variants Accepted = atom,
23
+ Failed = type { fields! error: String },
24
+ Done = atom,
25
+ Pong = atom
26
+ end
27
+
28
+ Envelope = Algebrick.type do
29
+ fields! request_id: Integer,
30
+ sender_id: String,
31
+ receiver_id: type { variants String, AnyExecutor = atom, UnknownWorld = atom },
32
+ message: type { variants Request, Response }
33
+ end
34
+
35
+ module Envelope
36
+ def build_response_envelope(response_message, sender)
37
+ Envelope[self.request_id,
38
+ sender.id,
39
+ self.sender_id,
40
+ response_message]
41
+ end
42
+ end
43
+
44
+ module Event
45
+ def to_hash
46
+ super.update event: Base64.strict_encode64(Marshal.dump(event))
47
+ end
48
+
49
+ def self.product_from_hash(hash)
50
+ super(hash.merge 'event' => Marshal.load(Base64.strict_decode64(hash.fetch('event'))))
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ require 'dynflow/dispatcher/abstract'
57
+ require 'dynflow/dispatcher/client_dispatcher'
58
+ require 'dynflow/dispatcher/executor_dispatcher'
@@ -0,0 +1,14 @@
1
+ module Dynflow
2
+ module Dispatcher
3
+ class Abstract < Actor
4
+ def connector
5
+ @world.connector
6
+ end
7
+
8
+ def respond(request_envelope, response)
9
+ response_envelope = request_envelope.build_response_envelope(response, @world)
10
+ connector.send(response_envelope)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,139 @@
1
+ module Dynflow
2
+ module Dispatcher
3
+ class ClientDispatcher < Abstract
4
+
5
+ TrackedRequest = Algebrick.type do
6
+ fields! id: Integer, request: Request,
7
+ accepted: Concurrent::Edge::Future, finished: Concurrent::Edge::Future
8
+ end
9
+
10
+ module TrackedRequest
11
+ def accept!
12
+ accepted.success true unless accepted.completed?
13
+ self
14
+ end
15
+
16
+ def fail!(error)
17
+ accepted.fail error unless accepted.completed?
18
+ finished.fail error
19
+ self
20
+ end
21
+
22
+ def success!(resolve_to)
23
+ accepted.success true unless accepted.completed?
24
+ finished.success(resolve_to)
25
+ self
26
+ end
27
+ end
28
+
29
+ def initialize(world)
30
+ @world = Type! world, World
31
+ @last_id = 0
32
+ @tracked_requests = {}
33
+ @terminated = nil
34
+ end
35
+
36
+ def publish_request(future, request, timeout)
37
+ track_request(future, request, timeout) do |tracked_request|
38
+ dispatch_request(request, @world.id, tracked_request.id)
39
+ end
40
+ end
41
+
42
+ def timeout(request_id)
43
+ resolve_tracked_request(request_id, Dynflow::Error.new("Request timeout"))
44
+ end
45
+
46
+ def start_termination(*args)
47
+ super
48
+ @tracked_requests.values.each { |tracked_request| tracked_request.fail!(Dynflow::Error.new('Dispatcher terminated')) }
49
+ @tracked_requests.clear
50
+ finish_termination
51
+ end
52
+
53
+ def dispatch_request(request, client_world_id, request_id)
54
+ executor_id = match request,
55
+ (on ~Execution do |execution|
56
+ AnyExecutor
57
+ end),
58
+ (on ~Event do |event|
59
+ find_executor(event.execution_plan_id)
60
+ end),
61
+ (on Ping.(~any) do |receiver_id|
62
+ receiver_id
63
+ end)
64
+ request = Envelope[request_id, client_world_id, executor_id, request]
65
+ if Dispatcher::UnknownWorld === request.receiver_id
66
+ raise Dynflow::Error, "Could not find an executor for #{request}"
67
+ end
68
+ connector.send(request).value!
69
+ rescue => e
70
+ respond(request, Failed[e.message])
71
+ end
72
+
73
+ def dispatch_response(envelope)
74
+ return unless @tracked_requests.key?(envelope.request_id)
75
+ match envelope.message,
76
+ (on ~Accepted do
77
+ @tracked_requests[envelope.request_id].accept!
78
+ end),
79
+ (on ~Failed do |msg|
80
+ resolve_tracked_request(envelope.request_id, Dynflow::Error.new(msg.error))
81
+ end),
82
+ (on Done | Pong do
83
+ resolve_tracked_request(envelope.request_id)
84
+ end)
85
+ end
86
+
87
+ private
88
+
89
+ def find_executor(execution_plan_id)
90
+ execution_lock = @world.coordinator.find_locks(class: Coordinator::ExecutionLock.name,
91
+ id: "execution-plan:#{execution_plan_id}").first
92
+ if execution_lock
93
+ execution_lock.world_id
94
+ else
95
+ Dispatcher::UnknownWorld
96
+ end
97
+ end
98
+
99
+ def track_request(finished, request, timeout)
100
+ id = @last_id += 1
101
+ tracked_request = TrackedRequest[id, request, Concurrent.future, finished]
102
+ @tracked_requests[id] = tracked_request
103
+ @world.clock.ping(self, timeout, [:timeout, id]) if timeout
104
+ yield tracked_request
105
+ rescue Dynflow::Error => e
106
+ resolve_tracked_request(tracked_request.id, e)
107
+ log(Logger::ERROR, e)
108
+ end
109
+
110
+ def reset_tracked_request(tracked_request)
111
+ if tracked_request.finished.completed?
112
+ raise Dynflow::Error.new('Can not reset resolved tracked request')
113
+ end
114
+ unless tracked_request.accepted.completed?
115
+ tracked_request.accept! # otherwise nobody would set the accept future
116
+ end
117
+ @tracked_requests[tracked_request.id] = TrackedRequest[tracked_request.id, tracked_request.request, Concurrent.future, tracked_request.finished]
118
+ end
119
+
120
+ def resolve_tracked_request(id, error = nil)
121
+ return unless @tracked_requests.key?(id)
122
+ if error
123
+ @tracked_requests.delete(id).fail! error
124
+ else
125
+ tracked_request = @tracked_requests[id]
126
+ resolve_to = match tracked_request.request,
127
+ (on Execution.(execution_plan_id: ~any) do |uuid|
128
+ @world.persistence.load_execution_plan(uuid)
129
+ end),
130
+ (on Event | Ping do
131
+ true
132
+ end)
133
+ @tracked_requests.delete(id).success! resolve_to
134
+ end
135
+ end
136
+
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,86 @@
1
+ module Dynflow
2
+ module Dispatcher
3
+ class ExecutorDispatcher < Abstract
4
+ def initialize(world)
5
+ @world = Type! world, World
6
+ @current_futures = Set.new
7
+ end
8
+
9
+ def handle_request(envelope)
10
+ match(envelope.message,
11
+ on(Execution) { perform_execution(envelope, envelope.message) },
12
+ on(Event) { perform_event(envelope, envelope.message) })
13
+ end
14
+
15
+ protected
16
+
17
+ def perform_execution(envelope, execution)
18
+ allocate_executor(execution.execution_plan_id, envelope.sender_id, envelope.request_id)
19
+ execution_lock = Coordinator::ExecutionLock.new(@world, execution.execution_plan_id, envelope.sender_id, envelope.request_id)
20
+ future = on_finish do |f|
21
+ f.then do |plan|
22
+ if plan.state == :running
23
+ @world.invalidate_execution_lock(execution_lock)
24
+ else
25
+ @world.coordinator.release(execution_lock)
26
+ respond(envelope, Done)
27
+ end
28
+ end.rescue do |reason|
29
+ @world.coordinator.release(execution_lock)
30
+ respond(envelope, Failed[reason.to_s])
31
+ end
32
+ end
33
+ @world.executor.execute(execution.execution_plan_id, future)
34
+ respond(envelope, Accepted)
35
+ rescue Dynflow::Error => e
36
+ future.fail(e) if future && !future.completed?
37
+ respond(envelope, Failed[e.message])
38
+ end
39
+
40
+ def perform_event(envelope, event_request)
41
+ future = on_finish do |f|
42
+ f.then do
43
+ respond(envelope, Done)
44
+ end.rescue do |reason|
45
+ respond(envelope, Failed[reason.to_s])
46
+ end
47
+ end
48
+ @world.executor.event(event_request.execution_plan_id, event_request.step_id, event_request.event, future)
49
+ rescue Dynflow::Error => e
50
+ future.fail(e) if future && !future.completed?
51
+ end
52
+
53
+ def start_termination(*args)
54
+ super
55
+ if @current_futures.empty?
56
+ reference.tell(:finish_termination)
57
+ else
58
+ Concurrent.zip(*@current_futures).then { reference.tell(:finish_termination) }
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def allocate_executor(execution_plan_id, client_world_id, request_id)
65
+ execution_lock = Coordinator::ExecutionLock.new(@world, execution_plan_id, client_world_id, request_id)
66
+ @world.coordinator.acquire(execution_lock)
67
+ end
68
+
69
+ def on_finish
70
+ raise "Dispatcher terminating: no new work can be started" if terminating?
71
+ future = Concurrent.future
72
+ callbacks_future = (yield future).rescue { |reason| @world.logger.error("Unexpected fail on future #{reason}") }
73
+ # we track currently running futures to make sure to not
74
+ # terminate until the execution is finished (including
75
+ # cleaning of locks etc)
76
+ @current_futures << callbacks_future
77
+ callbacks_future.on_completion! { reference.tell([:finish_execution, callbacks_future]) }
78
+ return future
79
+ end
80
+
81
+ def finish_execution(future)
82
+ @current_futures.delete(future)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -25,7 +25,13 @@ module Dynflow
25
25
  end
26
26
  end
27
27
 
28
- class PersistenceError < StandardError
28
+ class InactiveWorldError < Dynflow::Error
29
+ def initialize(world)
30
+ super("The world #{world.id} is not active (terminating or terminated)")
31
+ end
32
+ end
33
+
34
+ class PersistenceError < Dynflow::Error
29
35
  def self.delegate(original_exception)
30
36
  self.new("caused by #{original_exception.class}: #{original_exception.message}").tap do |e|
31
37
  e.set_backtrace original_exception.backtrace
@@ -0,0 +1,46 @@
1
+ module Dynflow
2
+ class ExecutionHistory
3
+ include Algebrick::TypeCheck
4
+ include Enumerable
5
+
6
+ Event = Algebrick.type do
7
+ fields! time: Integer,
8
+ name: String,
9
+ world_id: type { variants String, NilClass }
10
+ end
11
+
12
+ module Event
13
+ def inspect
14
+ "#{Time.at(time).utc}: #{name}".tap { |s| s << " @ #{world_id}" if world_id }
15
+ end
16
+ end
17
+
18
+ attr_reader :events
19
+
20
+ def initialize(events = [])
21
+ @events = (events || []).each { |e| Type! e, Event }
22
+ end
23
+
24
+ def each(&block)
25
+ @events.each(&block)
26
+ end
27
+
28
+ def add(name, world_id = nil)
29
+ @events << Event[Time.now.to_i, name, world_id]
30
+ end
31
+
32
+ def to_hash
33
+ @events.map(&:to_hash)
34
+ end
35
+
36
+ def inspect
37
+ "ExecutionHistory: #{ @events.inspect }"
38
+ end
39
+
40
+ def self.new_from_hash(value)
41
+ value ||= [] # for compatibility with tasks before the
42
+ # introduction of execution history
43
+ self.new(value.map { |hash| Event[hash] })
44
+ end
45
+ end
46
+ end
@@ -13,7 +13,7 @@ module Dynflow
13
13
  require 'dynflow/execution_plan/dependency_graph'
14
14
 
15
15
  attr_reader :id, :world, :root_plan_step, :steps, :run_flow, :finalize_flow,
16
- :started_at, :ended_at, :execution_time, :real_time
16
+ :started_at, :ended_at, :execution_time, :real_time, :execution_history
17
17
 
18
18
  def self.states
19
19
  @states ||= [:pending, :planning, :planned, :running, :paused, :stopped]
@@ -39,18 +39,20 @@ module Dynflow
39
39
  started_at = nil,
40
40
  ended_at = nil,
41
41
  execution_time = nil,
42
- real_time = 0.0)
43
-
44
- @id = Type! id, String
45
- @world = Type! world, World
46
- self.state = state
47
- @run_flow = Type! run_flow, Flows::Abstract
48
- @finalize_flow = Type! finalize_flow, Flows::Abstract
49
- @root_plan_step = root_plan_step
50
- @started_at = Type! started_at, Time, NilClass
51
- @ended_at = Type! ended_at, Time, NilClass
52
- @execution_time = Type! execution_time, Numeric, NilClass
53
- @real_time = Type! real_time, Numeric
42
+ real_time = 0.0,
43
+ execution_history = ExecutionHistory.new)
44
+
45
+ @id = Type! id, String
46
+ @world = Type! world, World
47
+ self.state = state
48
+ @run_flow = Type! run_flow, Flows::Abstract
49
+ @finalize_flow = Type! finalize_flow, Flows::Abstract
50
+ @root_plan_step = root_plan_step
51
+ @started_at = Type! started_at, Time, NilClass
52
+ @ended_at = Type! ended_at, Time, NilClass
53
+ @execution_time = Type! execution_time, Numeric, NilClass
54
+ @real_time = Type! real_time, Numeric
55
+ @execution_history = Type! execution_history, ExecutionHistory
54
56
 
55
57
  steps.all? do |k, v|
56
58
  Type! k, Integer
@@ -274,7 +276,8 @@ module Dynflow
274
276
  started_at: time_to_str(started_at),
275
277
  ended_at: time_to_str(ended_at),
276
278
  execution_time: execution_time,
277
- real_time: real_time
279
+ real_time: real_time,
280
+ execution_history: execution_history.to_hash
278
281
  end
279
282
 
280
283
  def save
@@ -295,7 +298,8 @@ module Dynflow
295
298
  string_to_time(hash[:started_at]),
296
299
  string_to_time(hash[:ended_at]),
297
300
  hash[:execution_time].to_f,
298
- hash[:real_time].to_f)
301
+ hash[:real_time].to_f,
302
+ ExecutionHistory.new_from_hash(hash[:execution_history]))
299
303
  end
300
304
 
301
305
  def compute_execution_time