dynflow 0.1.0 → 0.2.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 (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +0 -10
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +99 -37
  6. data/Rakefile +2 -6
  7. data/doc/images/logo.png +0 -0
  8. data/dynflow.gemspec +10 -1
  9. data/examples/generate_work_for_daemon.rb +24 -0
  10. data/examples/orchestrate.rb +121 -0
  11. data/examples/run_daemon.rb +17 -0
  12. data/examples/web_console.rb +29 -0
  13. data/lib/dynflow.rb +27 -6
  14. data/lib/dynflow/action.rb +185 -77
  15. data/lib/dynflow/action/cancellable_polling.rb +18 -0
  16. data/lib/dynflow/action/finalize_phase.rb +18 -0
  17. data/lib/dynflow/action/flow_phase.rb +44 -0
  18. data/lib/dynflow/action/format.rb +46 -0
  19. data/lib/dynflow/action/missing.rb +26 -0
  20. data/lib/dynflow/action/plan_phase.rb +85 -0
  21. data/lib/dynflow/action/polling.rb +49 -0
  22. data/lib/dynflow/action/presenter.rb +51 -0
  23. data/lib/dynflow/action/progress.rb +62 -0
  24. data/lib/dynflow/action/run_phase.rb +43 -0
  25. data/lib/dynflow/action/suspended.rb +21 -0
  26. data/lib/dynflow/clock.rb +133 -0
  27. data/lib/dynflow/daemon.rb +29 -0
  28. data/lib/dynflow/execution_plan.rb +285 -33
  29. data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
  30. data/lib/dynflow/execution_plan/output_reference.rb +52 -0
  31. data/lib/dynflow/execution_plan/steps.rb +12 -0
  32. data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
  33. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
  34. data/lib/dynflow/execution_plan/steps/error.rb +33 -0
  35. data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
  36. data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
  37. data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
  38. data/lib/dynflow/executors.rb +9 -0
  39. data/lib/dynflow/executors/abstract.rb +32 -0
  40. data/lib/dynflow/executors/parallel.rb +88 -0
  41. data/lib/dynflow/executors/parallel/core.rb +119 -0
  42. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
  43. data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
  44. data/lib/dynflow/executors/parallel/pool.rb +102 -0
  45. data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
  46. data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
  47. data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
  48. data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
  49. data/lib/dynflow/executors/parallel/worker.rb +30 -0
  50. data/lib/dynflow/executors/remote_via_socket.rb +38 -0
  51. data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
  52. data/lib/dynflow/flows.rb +13 -0
  53. data/lib/dynflow/flows/abstract.rb +36 -0
  54. data/lib/dynflow/flows/abstract_composed.rb +104 -0
  55. data/lib/dynflow/flows/atom.rb +36 -0
  56. data/lib/dynflow/flows/concurrence.rb +28 -0
  57. data/lib/dynflow/flows/sequence.rb +13 -0
  58. data/lib/dynflow/future.rb +173 -0
  59. data/lib/dynflow/listeners.rb +7 -0
  60. data/lib/dynflow/listeners/abstract.rb +13 -0
  61. data/lib/dynflow/listeners/serialization.rb +41 -0
  62. data/lib/dynflow/listeners/socket.rb +88 -0
  63. data/lib/dynflow/logger_adapters.rb +8 -0
  64. data/lib/dynflow/logger_adapters/abstract.rb +30 -0
  65. data/lib/dynflow/logger_adapters/delegator.rb +13 -0
  66. data/lib/dynflow/logger_adapters/formatters.rb +8 -0
  67. data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
  68. data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
  69. data/lib/dynflow/logger_adapters/simple.rb +59 -0
  70. data/lib/dynflow/micro_actor.rb +102 -0
  71. data/lib/dynflow/persistence.rb +53 -0
  72. data/lib/dynflow/persistence_adapters.rb +6 -0
  73. data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
  74. data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
  75. data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
  76. data/lib/dynflow/serializable.rb +66 -0
  77. data/lib/dynflow/simple_world.rb +18 -0
  78. data/lib/dynflow/stateful.rb +40 -0
  79. data/lib/dynflow/testing.rb +32 -0
  80. data/lib/dynflow/testing/assertions.rb +64 -0
  81. data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
  82. data/lib/dynflow/testing/dummy_executor.rb +29 -0
  83. data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
  84. data/lib/dynflow/testing/dummy_step.rb +19 -0
  85. data/lib/dynflow/testing/dummy_world.rb +33 -0
  86. data/lib/dynflow/testing/factories.rb +83 -0
  87. data/lib/dynflow/testing/managed_clock.rb +23 -0
  88. data/lib/dynflow/testing/mimic.rb +38 -0
  89. data/lib/dynflow/transaction_adapters.rb +9 -0
  90. data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
  91. data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
  92. data/lib/dynflow/transaction_adapters/none.rb +12 -0
  93. data/lib/dynflow/version.rb +1 -1
  94. data/lib/dynflow/web_console.rb +277 -0
  95. data/lib/dynflow/world.rb +168 -0
  96. data/test/action_test.rb +89 -11
  97. data/test/clock_test.rb +59 -0
  98. data/test/code_workflow_example.rb +382 -0
  99. data/test/execution_plan_test.rb +195 -64
  100. data/test/executor_test.rb +692 -0
  101. data/test/persistance_adapters_test.rb +173 -0
  102. data/test/test_helper.rb +316 -1
  103. data/test/testing_test.rb +148 -0
  104. data/test/web_console_test.rb +38 -0
  105. data/web/assets/javascripts/application.js +25 -0
  106. data/web/assets/stylesheets/application.css +101 -0
  107. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
  108. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
  109. data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
  110. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
  111. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  112. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
  113. data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
  114. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
  115. data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
  116. data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
  117. data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
  118. data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
  119. data/web/assets/vendor/jquery/jquery.js +9807 -0
  120. data/web/views/flow.erb +19 -0
  121. data/web/views/flow_step.erb +31 -0
  122. data/web/views/index.erb +39 -0
  123. data/web/views/layout.erb +20 -0
  124. data/web/views/plan_step.erb +11 -0
  125. data/web/views/show.erb +54 -0
  126. metadata +250 -11
  127. data/examples/events.rb +0 -71
  128. data/examples/workflow.rb +0 -140
  129. data/lib/dynflow/bus.rb +0 -168
  130. data/lib/dynflow/dispatcher.rb +0 -36
  131. data/lib/dynflow/logger.rb +0 -34
  132. data/lib/dynflow/step.rb +0 -234
  133. data/test/bus_test.rb +0 -150
@@ -0,0 +1,29 @@
1
+ module Dynflow
2
+ class Daemon
3
+ include Algebrick::TypeCheck
4
+
5
+ def initialize(listener, world, lock_file = nil)
6
+ @listener = Type! listener, Listeners::Abstract
7
+ @world = Type! world, World
8
+ @lock_file = Type! lock_file, String, NilClass
9
+ end
10
+
11
+ def run
12
+ with_lock_file do
13
+ terminated = Future.new
14
+ trap('SIGINT') { @world.terminate terminated }
15
+ terminated.wait
16
+ end
17
+ end
18
+
19
+ def with_lock_file(&block)
20
+ if @lock_file
21
+ raise "Lockfile #{@lock_file} is already present." if File.exist?(@lock_file)
22
+ File.write(@lock_file, "Locked at #{Time.now} by process #{$$}\n")
23
+ end
24
+ block.call
25
+ ensure
26
+ File.delete(@lock_file) if @lock_file
27
+ end
28
+ end
29
+ end
@@ -1,56 +1,308 @@
1
- require 'forwardable'
1
+ require 'uuidtools'
2
2
 
3
3
  module Dynflow
4
- class ExecutionPlan
5
4
 
6
- attr_reader :plan_steps, :run_steps, :finalize_steps
5
+ # TODO extract planning logic to an extra class ExecutionPlanner
6
+ class ExecutionPlan < Serializable
7
+ include Algebrick::TypeCheck
8
+ include Stateful
7
9
 
8
- # allows storing and reloading the execution plan to something
9
- # more persistent than memory
10
- attr_accessor :persistence
11
- # one of [new, running, paused, aborted, finished]
12
- attr_accessor :status
10
+ require 'dynflow/execution_plan/steps'
11
+ require 'dynflow/execution_plan/output_reference'
12
+ require 'dynflow/execution_plan/dependency_graph'
13
13
 
14
- extend Forwardable
14
+ attr_reader :id, :world, :root_plan_step, :steps, :run_flow, :finalize_flow,
15
+ :started_at, :ended_at, :execution_time, :real_time
15
16
 
16
- def initialize(plan_steps = [], run_steps = [], finalize_steps = [])
17
- @plan_steps = plan_steps
18
- @run_steps = run_steps
19
- @finalize_steps = finalize_steps
20
- @status = 'new'
17
+ def self.states
18
+ @states ||= [:pending, :planning, :planned, :running, :paused, :stopped]
21
19
  end
22
20
 
23
- def steps
24
- self.plan_steps + self.run_steps + self.finalize_steps
21
+ def self.state_transitions
22
+ @state_transitions ||= { pending: [:planning],
23
+ planning: [:planned, :stopped],
24
+ planned: [:running],
25
+ running: [:paused, :stopped],
26
+ paused: [:running],
27
+ stopped: [] }
25
28
  end
26
29
 
27
- def failed_steps
28
- self.steps.find_all { |step| step.status == 'error' }
30
+ # all params with default values are part of *private* api
31
+ def initialize(world,
32
+ id = UUIDTools::UUID.random_create.to_s,
33
+ state = :pending,
34
+ root_plan_step = nil,
35
+ run_flow = Flows::Concurrence.new([]),
36
+ finalize_flow = Flows::Sequence.new([]),
37
+ steps = {},
38
+ started_at = nil,
39
+ ended_at = nil,
40
+ execution_time = 0.0,
41
+ real_time = 0.0)
42
+
43
+ @id = Type! id, String
44
+ @world = Type! world, World
45
+ self.state = state
46
+ @run_flow = Type! run_flow, Flows::Abstract
47
+ @finalize_flow = Type! finalize_flow, Flows::Abstract
48
+ @root_plan_step = root_plan_step
49
+ @started_at = Type! started_at, Time, NilClass
50
+ @ended_at = Type! ended_at, Time, NilClass
51
+ @execution_time = Type! execution_time, Float
52
+ @real_time = Type! real_time, Float
53
+
54
+ steps.all? do |k, v|
55
+ Type! k, Integer
56
+ Type! v, Steps::Abstract
57
+ end
58
+ @steps = steps
59
+ end
60
+
61
+ def logger
62
+ @world.logger
63
+ end
64
+
65
+ def update_state(state)
66
+ original = self.state
67
+ case self.state = state
68
+ when :planning
69
+ @started_at = Time.now
70
+ when :stopped
71
+ @ended_at = Time.now
72
+ @real_time = @ended_at - @started_at
73
+ else
74
+ # ignore
75
+ end
76
+ logger.debug "execution plan #{id} #{original} >> #{state}"
77
+ self.save
78
+ end
79
+
80
+ def update_execution_time(execution_time)
81
+ @execution_time += execution_time
82
+ end
83
+
84
+ def result
85
+ all_steps = steps.values
86
+ if all_steps.any? { |step| step.state == :error }
87
+ return :error
88
+ elsif all_steps.all? { |step| [:success, :skipped].include?(step.state) }
89
+ return :success
90
+ else
91
+ return :pending
92
+ end
93
+ end
94
+
95
+ def error?
96
+ result == :error
97
+ end
98
+
99
+ def generate_action_id
100
+ @last_action_id ||= 0
101
+ @last_action_id += 1
29
102
  end
30
103
 
31
- def <<(action)
32
- run_step = Step::Run.new(action)
33
- @run_steps << run_step if action.respond_to? :run
34
- @finalize_steps << Step::Finalize.new(run_step) if action.respond_to? :finalize
104
+ def generate_step_id
105
+ @last_step_id ||= 0
106
+ @last_step_id += 1
35
107
  end
36
108
 
37
- def concat(other)
38
- self.plan_steps.concat(other.plan_steps)
39
- self.run_steps.concat(other.run_steps)
40
- self.finalize_steps.concat(other.finalize_steps)
41
- self.status = other.status
109
+ def prepare(action_class)
110
+ save
111
+ @root_plan_step = add_step(Steps::PlanStep, action_class, generate_action_id)
42
112
  end
43
113
 
44
- # update the persistence based on the current status
45
- def persist(include_steps = false)
46
- if @persistence
47
- @persistence.persist(self)
114
+ def plan(*args)
115
+ update_state(:planning)
116
+ world.transaction_adapter.transaction do
117
+ with_planning_scope do
118
+ root_plan_step.execute(self, nil, *args)
48
119
 
49
- if include_steps
50
- steps.each { |step| step.persist }
120
+ if @dependency_graph.unresolved?
121
+ raise "Some dependencies were not resolved: #{@dependency_graph.inspect}"
122
+ end
51
123
  end
124
+
125
+ if @run_flow.size == 1
126
+ @run_flow = @run_flow.sub_flows.first
127
+ end
128
+
129
+ world.transaction_adapter.rollback if error?
130
+ end
131
+ steps.values.each(&:save)
132
+ update_state(error? ? :stopped : :planned)
133
+ end
134
+
135
+ def skip(step)
136
+ raise "plan step can't be skipped" if step.is_a? Steps::PlanStep
137
+ steps_to_skip = steps_to_skip(step).each do |s|
138
+ s.state = :skipped
139
+ s.save
140
+ end
141
+ self.save
142
+ return steps_to_skip
143
+ end
144
+
145
+ # All the steps that need to get skipped when wanting to skip the step
146
+ # includes the step itself, all steps dependent on it (even transitively)
147
+ # FIND maybe move to persistence to let adapter to do it effectively?
148
+ # @return [Array<Steps::Abstract>]
149
+ def steps_to_skip(step)
150
+ dependent_steps = @steps.values.find_all do |s|
151
+ next if s.is_a? Steps::PlanStep
152
+ action = persistence.load_action(s)
153
+ action.required_step_ids.include?(step.id)
154
+ end
155
+
156
+ steps_to_skip = dependent_steps.map do |dependent_step|
157
+ steps_to_skip(dependent_step)
158
+ end.flatten
159
+
160
+ steps_to_skip << step
161
+
162
+ if step.is_a? Steps::RunStep
163
+ finalize_step_id = persistence.load_action(step).finalize_step_id
164
+ steps_to_skip << steps[finalize_step_id] if finalize_step_id
165
+ end
166
+
167
+ return steps_to_skip.uniq
168
+ end
169
+
170
+ # @api private
171
+ def current_run_flow
172
+ @run_flow_stack.last
173
+ end
174
+
175
+ # @api private
176
+ def with_planning_scope(&block)
177
+ @run_flow_stack = []
178
+ @dependency_graph = DependencyGraph.new
179
+ switch_flow(run_flow, &block)
180
+ ensure
181
+ @run_flow_stack = nil
182
+ @dependency_graph = nil
183
+ end
184
+
185
+ # @api private
186
+ # Switches the flow type (Sequence, Concurrence) to be used within the block.
187
+ def switch_flow(new_flow, &block)
188
+ @run_flow_stack << new_flow
189
+ return block.call
190
+ ensure
191
+ @run_flow_stack.pop
192
+ current_run_flow.add_and_resolve(@dependency_graph, new_flow) if current_run_flow
193
+ end
194
+
195
+ def add_plan_step(action_class, planned_by)
196
+ add_step(Steps::PlanStep, action_class, generate_action_id, planned_by.plan_step_id)
197
+ end
198
+
199
+ def add_run_step(action)
200
+ add_step(Steps::RunStep, action.action_class, action.id).tap do |step|
201
+ @dependency_graph.add_dependencies(step, action)
202
+ current_run_flow.add_and_resolve(@dependency_graph, Flows::Atom.new(step.id))
203
+ end
204
+ end
205
+
206
+ def add_finalize_step(action)
207
+ add_step(Steps::FinalizeStep, action.action_class, action.id).tap do |step|
208
+ finalize_flow << Flows::Atom.new(step.id)
209
+ end
210
+ end
211
+
212
+ def to_hash
213
+ recursive_to_hash id: self.id,
214
+ class: self.class.to_s,
215
+ state: self.state,
216
+ result: result,
217
+ root_plan_step_id: root_plan_step && root_plan_step.id,
218
+ run_flow: run_flow,
219
+ finalize_flow: finalize_flow,
220
+ step_ids: steps.map { |id, _| id },
221
+ started_at: time_to_str(started_at),
222
+ ended_at: time_to_str(ended_at),
223
+ execution_time: execution_time,
224
+ real_time: real_time
225
+ end
226
+
227
+ def save
228
+ persistence.save_execution_plan(self)
229
+ end
230
+
231
+ def self.new_from_hash(hash, world)
232
+ check_class_matching hash
233
+ execution_plan_id = hash[:id]
234
+ steps = steps_from_hash(hash[:step_ids], execution_plan_id, world)
235
+ self.new(world,
236
+ execution_plan_id,
237
+ hash[:state],
238
+ steps[hash[:root_plan_step_id]],
239
+ Flows::Abstract.from_hash(hash[:run_flow]),
240
+ Flows::Abstract.from_hash(hash[:finalize_flow]),
241
+ steps,
242
+ string_to_time(hash[:started_at]),
243
+ string_to_time(hash[:ended_at]),
244
+ hash[:execution_time],
245
+ hash[:real_time])
246
+ end
247
+
248
+ # @return [0..1] the percentage of the progress. See Action::Progress for more
249
+ # info
250
+ def progress
251
+ flow_step_ids = run_flow.all_step_ids + finalize_flow.all_step_ids
252
+ plan_done, plan_total = flow_step_ids.reduce([0.0, 0]) do |(done, total), step_id|
253
+ step_progress_done, step_progress_weight = self.steps[step_id].progress
254
+ [done + (step_progress_done * step_progress_weight),
255
+ total + step_progress_weight]
256
+ end
257
+ plan_total > 0 ? (plan_done / plan_total) : 1
258
+ end
259
+
260
+ # This method can be used to access result of the whole execution plan and detailed
261
+ # progress.
262
+ # @return [Array<Action::Presenter>] presenter of the actions
263
+ # involved in the plan
264
+ def actions
265
+ action_steps = Hash.new { |h, k| h[k] = [] }
266
+ all_actions = []
267
+ steps.values.each do |step|
268
+ action_steps[step.action_id] << step
269
+ end
270
+ action_steps.each do |action_id, involved_steps|
271
+ action = Action::Presenter.load(self,
272
+ action_id,
273
+ involved_steps,
274
+ all_actions)
275
+ all_actions << action
276
+ end
277
+ return all_actions
278
+ end
279
+
280
+ private
281
+
282
+ def persistence
283
+ world.persistence
284
+ end
285
+
286
+ def add_step(step_class, action_class, action_id, planned_by_step_id = nil)
287
+ step_class.new(self.id,
288
+ self.generate_step_id,
289
+ :pending,
290
+ action_class,
291
+ action_id,
292
+ nil,
293
+ world).tap do |new_step|
294
+ @steps[new_step.id] = new_step
295
+ @steps[planned_by_step_id].children << new_step.id if planned_by_step_id
296
+ end
297
+ end
298
+
299
+ def self.steps_from_hash(step_ids, execution_plan_id, world)
300
+ step_ids.inject({}) do |hash, step_id|
301
+ step = world.persistence.load_step(execution_plan_id, step_id, world)
302
+ hash.update(step_id.to_i => step)
52
303
  end
53
304
  end
54
305
 
306
+ private_class_method :steps_from_hash
55
307
  end
56
308
  end
@@ -0,0 +1,29 @@
1
+ module Dynflow
2
+ class ExecutionPlan::DependencyGraph
3
+
4
+ def initialize
5
+ @graph = Hash.new { |h, k| h[k] = Set.new }
6
+ end
7
+
8
+ # adds dependencies to graph that +step+ has based
9
+ # on the steps referenced in its +input+
10
+ def add_dependencies(step, action)
11
+ action.required_step_ids.each do |required_step_id|
12
+ @graph[step.id] << required_step_id
13
+ end
14
+ end
15
+
16
+ def required_step_ids(step_id)
17
+ @graph[step_id]
18
+ end
19
+
20
+ def mark_satisfied(step_id, required_step_id)
21
+ @graph[step_id].delete(required_step_id)
22
+ end
23
+
24
+ def unresolved?
25
+ @graph.any? { |step_id, required_step_ids| required_step_ids.any? }
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ module Dynflow
2
+ class ExecutionPlan::OutputReference < Serializable
3
+
4
+ attr_reader :step_id, :action_id, :subkeys
5
+
6
+ def initialize(step_id, action_id, subkeys = [])
7
+ @step_id = step_id
8
+ @action_id = action_id
9
+ @subkeys = subkeys
10
+ end
11
+
12
+ def [](subkey)
13
+ return self.class.new(step_id, action_id, subkeys.dup << subkey)
14
+ end
15
+
16
+ def to_hash
17
+ recursive_to_hash class: self.class.to_s,
18
+ step_id: step_id,
19
+ action_id: action_id,
20
+ subkeys: subkeys
21
+ end
22
+
23
+ def to_s
24
+ "Step(#{@step_id}).output".tap do |ret|
25
+ ret << @subkeys.map { |k| "[:#{k}]" }.join('') if @subkeys.any?
26
+ end
27
+ end
28
+
29
+ alias_method :inspect, :to_s
30
+
31
+ def dereference(persistence, execution_plan_id)
32
+ action_data = persistence.adapter.load_action(execution_plan_id, action_id)
33
+ deref = action_data[:output]
34
+ @subkeys.each do |subkey|
35
+ if deref.respond_to?(:[])
36
+ deref = deref[subkey]
37
+ else
38
+ raise "We were not able to dereference subkey #{@subkeys} from #{self.inspect}"
39
+ end
40
+ end
41
+ return deref
42
+ end
43
+
44
+ protected
45
+
46
+ def self.new_from_hash(hash)
47
+ check_class_matching hash
48
+ new(hash[:step_id], hash[:action_id], hash[:subkeys])
49
+ end
50
+
51
+ end
52
+ end