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
@@ -1,15 +1,10 @@
1
1
  module Dynflow
2
2
  module Executors
3
3
  class Parallel < Abstract
4
- class Worker < MicroActor
5
- def initialize(pool, transaction_adapter)
6
- super(pool.logger, pool, transaction_adapter)
7
- end
4
+ class Worker < Actor
8
5
 
9
- private
10
-
11
- def delayed_initialize(pool, transaction_adapter)
12
- @pool = pool
6
+ def initialize(pool, transaction_adapter)
7
+ @pool = Type! pool, Concurrent::Actor::Reference
13
8
  @transaction_adapter = Type! transaction_adapter, TransactionAdapters::Abstract
14
9
  end
15
10
 
@@ -23,9 +18,9 @@ module Dynflow
23
18
  sequential_manager.finalize
24
19
  end)
25
20
  rescue Errors::PersistenceError => e
26
- @pool << e
21
+ @pool.tell([:handle_persistence_error, e])
27
22
  ensure
28
- @pool << WorkerDone[work: message, worker: self]
23
+ @pool.tell([:worker_done, reference, message])
29
24
  @transaction_adapter.cleanup
30
25
  end
31
26
  end
@@ -4,6 +4,8 @@ module Dynflow
4
4
 
5
5
  class Persistence
6
6
 
7
+ include Algebrick::TypeCheck
8
+
7
9
  attr_reader :adapter
8
10
 
9
11
  def initialize(world, persistence_adapter)
@@ -56,5 +58,17 @@ module Dynflow
56
58
  adapter.save_step(step.execution_plan_id, step.id, step.to_hash)
57
59
  end
58
60
 
61
+ def push_envelope(envelope)
62
+ Type! envelope, Dispatcher::Envelope
63
+ adapter.push_envelope(Dynflow.serializer.dump(envelope))
64
+ end
65
+
66
+ def pull_envelopes(world_id)
67
+ adapter.pull_envelopes(world_id).map do |data|
68
+ envelope = Dynflow.serializer.load(data)
69
+ Type! envelope, Dispatcher::Envelope
70
+ envelope
71
+ end
72
+ end
59
73
  end
60
74
  end
@@ -6,18 +6,21 @@ module Dynflow
6
6
  attr_accessor :logger
7
7
 
8
8
  def register_world(world)
9
- @worlds ||= Set.new
10
- @worlds << world
9
+ @logger ||= world.logger
11
10
  end
12
11
 
13
12
  def log(level, message)
14
- (@worlds.first && @worlds.first.logger).send(level, message)
13
+ logger.send(level, message) if logger
15
14
  end
16
15
 
17
16
  def pagination?
18
17
  false
19
18
  end
20
19
 
20
+ def transaction
21
+ raise NotImplementedError
22
+ end
23
+
21
24
  def filtering_by
22
25
  []
23
26
  end
@@ -72,6 +75,14 @@ module Dynflow
72
75
  def to_hash
73
76
  raise NotImplementedError
74
77
  end
78
+
79
+ def pull_envelopes(receiver_id)
80
+ raise NotImplementedError
81
+ end
82
+
83
+ def push_envelope(envelope)
84
+ raise NotImplementedError
85
+ end
75
86
  end
76
87
  end
77
88
  end
@@ -8,6 +8,7 @@ module Dynflow
8
8
 
9
9
  class Sequel < Abstract
10
10
  include Algebrick::TypeCheck
11
+ include Algebrick::Matching
11
12
 
12
13
  MAX_RETRIES = 10
13
14
  RETRY_DELAY = 1
@@ -26,26 +27,40 @@ module Dynflow
26
27
  META_DATA.fetch :execution_plan
27
28
  end
28
29
 
29
- META_DATA = { execution_plan: %w(state result started_at ended_at real_time execution_time),
30
- action: %w(caller_execution_plan_id caller_action_id),
31
- step: %w(state started_at ended_at real_time execution_time action_id progress_done progress_weight) }
30
+ META_DATA = { execution_plan: %w(state result started_at ended_at real_time execution_time),
31
+ action: %w(caller_execution_plan_id caller_action_id),
32
+ step: %w(state started_at ended_at real_time execution_time action_id progress_done progress_weight),
33
+ envelope: %w(receiver_id),
34
+ coordinator_record: %w(id owner_id class) }
32
35
 
33
36
  def initialize(config)
37
+ config = config.dup
38
+ @additional_responsibilities = { coordinator: true, connector: true }
39
+ if config.is_a?(Hash) && config.key?(:additional_responsibilities)
40
+ @additional_responsibilities.merge!(config.delete(:additional_responsibilities))
41
+ end
34
42
  @db = initialize_db config
35
43
  migrate_db
36
44
  end
37
45
 
38
- def find_execution_plans(options = {})
39
- data_set = filter(order(paginate(table(:execution_plan), options), options), options[:filters])
46
+ def transaction(&block)
47
+ db.transaction(&block)
48
+ end
40
49
 
41
- data_set.map do |record|
42
- HashWithIndifferentAccess.new(MultiJson.load(record[:data]))
43
- end
50
+ def find_execution_plans(options = {})
51
+ options[:order_by] ||= :started_at
52
+ data_set = filter(:execution_plan,
53
+ order(:execution_plan,
54
+ paginate(table(:execution_plan), options),
55
+ options),
56
+ options[:filters])
57
+
58
+ data_set.map { |record| load_data(record) }
44
59
  end
45
60
 
46
61
  def delete_execution_plans(filters, batch_size = 1000)
47
62
  count = 0
48
- filter(table(:execution_plan), filters).each_slice(batch_size) do |plans|
63
+ filter(:execution_plan, table(:execution_plan), filters).each_slice(batch_size) do |plans|
49
64
  uuids = plans.map { |p| p.fetch(:uuid) }
50
65
  @db.transaction do
51
66
  table(:step).where(execution_plan_uuid: uuids).delete
@@ -80,17 +95,76 @@ module Dynflow
80
95
  save :action, { execution_plan_uuid: execution_plan_id, id: action_id }, value
81
96
  end
82
97
 
98
+ def connector_feature!
99
+ unless @additional_responsibilities[:connector]
100
+ raise "The sequel persistence adapter connector feature used but not enabled in additional_features"
101
+ end
102
+ end
103
+
104
+ def save_envelope(data)
105
+ connector_feature!
106
+ save :envelope, {}, data
107
+ end
108
+
109
+ def pull_envelopes(receiver_id)
110
+ connector_feature!
111
+ db.transaction do
112
+ data_set = table(:envelope).where(receiver_id: receiver_id).to_a
113
+
114
+ envelopes = data_set.map { |record| load_data(record) }
115
+
116
+ table(:envelope).where(id: data_set.map { |d| d[:id] }).delete
117
+ return envelopes
118
+ end
119
+ end
120
+
121
+ def push_envelope(envelope)
122
+ connector_feature!
123
+ table(:envelope).insert(prepare_record(:envelope, envelope))
124
+ end
125
+
126
+ def coordinator_feature!
127
+ unless @additional_responsibilities[:coordinator]
128
+ raise "The sequel persistence adapter coordinator feature used but not enabled in additional_features"
129
+ end
130
+ end
131
+
132
+ def insert_coordinator_record(value)
133
+ coordinator_feature!
134
+ save :coordinator_record, {}, value
135
+ end
136
+
137
+ def update_coordinator_record(class_name, record_id, value)
138
+ coordinator_feature!
139
+ save :coordinator_record, {class: class_name, :id => record_id}, value
140
+ end
141
+
142
+ def delete_coordinator_record(class_name, record_id)
143
+ coordinator_feature!
144
+ table(:coordinator_record).where(class: class_name, id: record_id).delete
145
+ end
146
+
147
+ def find_coordinator_records(options)
148
+ coordinator_feature!
149
+ options = options.dup
150
+ data_set = filter(:coordinator_record, table(:coordinator_record), options[:filters])
151
+ data_set.map { |record| load_data(record) }
152
+ end
153
+
83
154
  def to_hash
84
- { execution_plans: table(:execution_plan).all.to_a,
85
- steps: table(:step).all.to_a,
86
- actions: table(:action).all.to_a }
155
+ { execution_plans: table(:execution_plan).all.to_a,
156
+ steps: table(:step).all.to_a,
157
+ actions: table(:action).all.to_a,
158
+ envelopes: table(:envelope).all.to_a }
87
159
  end
88
160
 
89
161
  private
90
162
 
91
- TABLES = { execution_plan: :dynflow_execution_plans,
92
- action: :dynflow_actions,
93
- step: :dynflow_steps }
163
+ TABLES = { execution_plan: :dynflow_execution_plans,
164
+ action: :dynflow_actions,
165
+ step: :dynflow_steps,
166
+ envelope: :dynflow_envelopes,
167
+ coordinator_record: :dynflow_coordinator_records }
94
168
 
95
169
  def table(which)
96
170
  db[TABLES.fetch(which)]
@@ -108,18 +182,22 @@ module Dynflow
108
182
  ::Sequel::Migrator.run(db, self.class.migrations_path, table: 'dynflow_schema_info')
109
183
  end
110
184
 
185
+ def prepare_record(table_name, value, base = {})
186
+ record = base.dup
187
+ if table(table_name).columns.include?(:data)
188
+ record[:data] = dump_data(value)
189
+ end
190
+ record.merge! extract_metadata(table_name, value)
191
+ record.each { |k, v| record[k] = v.to_s if v.is_a? Symbol }
192
+ record
193
+ end
194
+
111
195
  def save(what, condition, value)
112
196
  table = table(what)
113
- existing_record = with_retry { table.first condition }
197
+ existing_record = with_retry { table.first condition } unless condition.empty?
114
198
 
115
199
  if value
116
- value = value.with_indifferent_access
117
- record = existing_record || condition
118
- record[:data] = MultiJson.dump Type!(value, Hash)
119
- meta_data = META_DATA.fetch(what).inject({}) { |h, k| h.update k.to_sym => value.fetch(k) }
120
- record.merge! meta_data
121
- record.each { |k, v| record[k] = v.to_s if v.is_a? Symbol }
122
-
200
+ record = prepare_record(what, value, (existing_record || condition))
123
201
  if existing_record
124
202
  with_retry { table.where(condition).update(record) }
125
203
  else
@@ -135,47 +213,71 @@ module Dynflow
135
213
  def load(what, condition)
136
214
  table = table(what)
137
215
  if (record = with_retry { table.first(condition.symbolize_keys) } )
138
- HashWithIndifferentAccess.new MultiJson.load(record[:data])
216
+ load_data(record)
139
217
  else
140
218
  raise KeyError, "searching: #{what} by: #{condition.inspect}"
141
219
  end
142
220
  end
143
221
 
222
+ def load_data(record)
223
+ HashWithIndifferentAccess.new(MultiJson.load(record[:data]))
224
+ end
225
+
226
+ def delete(what, condition)
227
+ table(what).where(condition.symbolize_keys).delete
228
+ end
229
+
230
+ def extract_metadata(what, value)
231
+ meta_keys = META_DATA.fetch(what)
232
+ value = value.with_indifferent_access
233
+ meta_keys.inject({}) { |h, k| h.update k.to_sym => value[k] }
234
+ end
235
+
236
+ def dump_data(value)
237
+ MultiJson.dump Type!(value, Hash)
238
+ end
239
+
144
240
  def paginate(data_set, options)
145
- page = Integer(options[:page] || 0)
146
- per_page = Integer(options[:per_page] || 20)
241
+ page = Integer(options[:page]) if options[:page]
242
+ per_page = Integer(options[:per_page]) if options[:per_page]
147
243
 
148
244
  if page
245
+ raise ArgumentError, "page specified without per_page attribute" unless per_page
149
246
  data_set.limit per_page, per_page * page
150
247
  else
151
248
  data_set
152
249
  end
153
250
  end
154
251
 
155
- def order(data_set, options)
156
- order_by = (options[:order_by] || :started_at).to_s
157
- unless META_DATA.fetch(:execution_plan).include? order_by
252
+ def order(what, data_set, options)
253
+ order_by = (options[:order_by]).to_s
254
+ return data_set if order_by.empty?
255
+ unless META_DATA.fetch(what).include? order_by
158
256
  raise ArgumentError, "unknown column #{order_by.inspect}"
159
257
  end
160
258
  order_by = order_by.to_sym
161
259
  data_set.order_by options[:desc] ? ::Sequel.desc(order_by) : order_by
162
260
  end
163
261
 
164
- def filter(data_set, filters)
262
+ def filter(what, data_set, filters)
165
263
  Type! filters, NilClass, Hash
166
264
  return data_set if filters.nil?
167
- unknown = filters.keys - META_DATA.fetch(:execution_plan) - %w[uuid caller_execution_plan_id caller_action_id]
168
265
 
169
- if filters.key?('caller_action_id') && !filters.key?('caller_execution_plan_id')
170
- raise ArgumentError, "caller_action_id given but caller_execution_plan_id missing"
171
- end
266
+ unknown = filters.keys.map(&:to_s) - META_DATA.fetch(what)
267
+ if what == :execution_plan
268
+ unknown -= %w[uuid caller_execution_plan_id caller_action_id]
172
269
 
173
- if filters.key?('caller_execution_plan_id')
174
- data_set = data_set.join_table(:inner, TABLES[:action], :execution_plan_uuid => :uuid).
175
- select_all(TABLES[:execution_plan]).distinct
270
+ if filters.key?('caller_action_id') && !filters.key?('caller_execution_plan_id')
271
+ raise ArgumentError, "caller_action_id given but caller_execution_plan_id missing"
272
+ end
273
+
274
+ if filters.key?('caller_execution_plan_id')
275
+ data_set = data_set.join_table(:inner, TABLES[:action], :execution_plan_uuid => :uuid).
276
+ select_all(TABLES[:execution_plan]).distinct
277
+ end
176
278
  end
177
279
 
178
- unless (unknown).empty?
280
+ unless unknown.empty?
179
281
  raise ArgumentError, "unkown columns: #{unknown.inspect}"
180
282
  end
181
283
 
@@ -186,6 +288,8 @@ module Dynflow
186
288
  attempts = 0
187
289
  begin
188
290
  yield
291
+ rescue ::Sequel::UniqueConstraintViolation => e
292
+ raise e
189
293
  rescue Exception => e
190
294
  attempts += 1
191
295
  log(:error, e)
@@ -0,0 +1,14 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:dynflow_coordinator_records) do
4
+ column :id, String
5
+ column :class, String
6
+ primary_key [:id, :class]
7
+ index :class
8
+ column :owner_id, String
9
+ index :owner_id
10
+ column :data, String, text: true
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,14 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:dynflow_envelopes) do
4
+ primary_key :id
5
+ # we don't add a foreign key to worlds here as there might be an envelope created for the world
6
+ # while the world gets terminated, and it would mess the whole thing up:
7
+ # error on the world deletion because some envelopes arrived in the meantime
8
+ # we still do our best to remove the envelopes if we can
9
+ column :receiver_id, String, size: 36, fixed: true
10
+ index :receiver_id
11
+ column :data, String, text: true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ module Dynflow
2
+ # A simple round-robin scheduling implementation used at various
3
+ # places in Dynflow
4
+ class RoundRobin
5
+ def initialize
6
+ @data = []
7
+ @cursor = 0
8
+ end
9
+
10
+ def add(item)
11
+ @data.push item
12
+ self
13
+ end
14
+
15
+ def delete(item)
16
+ @data.delete item
17
+ self
18
+ end
19
+
20
+ def next
21
+ @cursor = 0 if @cursor > @data.size-1
22
+ @data[@cursor]
23
+ ensure
24
+ @cursor += 1
25
+ end
26
+
27
+ def empty?
28
+ @data.empty?
29
+ end
30
+
31
+ # the `add` and `delete` methods should be preferred, but
32
+ # sometimes the list of things to iterate though can not be owned
33
+ # by the round robin object itself
34
+ attr_writer :data
35
+ end
36
+ end
37
+
@@ -23,7 +23,7 @@ module Dynflow
23
23
  end
24
24
 
25
25
  def self.check_class_key_present(hash)
26
- raise ArgumentError, 'missing :class' unless hash[:class]
26
+ raise ArgumentError, "missing :class in #{hash.inspect}" unless hash[:class]
27
27
  end
28
28
 
29
29
  def self.constantize(action_name)
@@ -74,6 +74,5 @@ module Dynflow
74
74
  end
75
75
 
76
76
  private_class_method :string_to_time, :hash_to_error
77
-
78
77
  end
79
78
  end