standard_procedure_operations 0.5.2 → 0.6.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -356
  3. data/app/jobs/operations/agent/find_timeouts_job.rb +5 -0
  4. data/app/jobs/operations/agent/runner_job.rb +5 -0
  5. data/app/jobs/operations/agent/timeout_job.rb +5 -0
  6. data/app/jobs/operations/agent/wake_agents_job.rb +5 -0
  7. data/app/models/concerns/operations/participant.rb +1 -1
  8. data/app/models/operations/agent/interaction_handler.rb +30 -0
  9. data/app/models/operations/agent/plan.rb +38 -0
  10. data/app/models/operations/agent/runner.rb +37 -0
  11. data/app/models/operations/{task/state_management → agent}/wait_handler.rb +4 -2
  12. data/app/models/operations/agent.rb +31 -0
  13. data/app/models/operations/task/data_carrier.rb +0 -2
  14. data/app/models/operations/task/exports.rb +4 -4
  15. data/app/models/operations/task/{state_management → plan}/action_handler.rb +3 -1
  16. data/app/models/operations/task/{state_management → plan}/decision_handler.rb +3 -1
  17. data/app/models/operations/task/{state_management/completion_handler.rb → plan/result_handler.rb} +3 -1
  18. data/app/models/operations/task/{state_management.rb → plan.rb} +2 -4
  19. data/app/models/operations/task/testing.rb +2 -1
  20. data/app/models/operations/task.rb +46 -30
  21. data/app/models/operations/task_participant.rb +2 -0
  22. data/db/migrate/{20250403075414_add_becomes_zombie_at_field.rb → 20250404085321_add_becomes_zombie_at_field.operations.rb} +1 -0
  23. data/db/migrate/20250407143513_agent_fields.rb +9 -0
  24. data/db/migrate/20250408124423_add_task_participant_indexes.rb +5 -0
  25. data/lib/operations/exporters/svg.rb +399 -0
  26. data/lib/operations/has_data_attributes.rb +50 -0
  27. data/lib/operations/invalid_state.rb +2 -0
  28. data/lib/operations/version.rb +1 -1
  29. data/lib/operations.rb +3 -2
  30. data/lib/tasks/operations_tasks.rake +3 -3
  31. metadata +21 -12
  32. data/app/jobs/operations/task_runner_job.rb +0 -11
  33. data/app/models/operations/task/background.rb +0 -39
  34. data/lib/operations/cannot_wait_in_foreground.rb +0 -2
  35. data/lib/operations/exporters/graphviz.rb +0 -164
@@ -0,0 +1,399 @@
1
+ module Operations
2
+ module Exporters
3
+ # A pure Ruby SVG exporter for task visualization with no external dependencies
4
+ class SVG
5
+ attr_reader :task_class
6
+
7
+ COLORS = {
8
+ decision: "#4e79a7", # Blue
9
+ action: "#f28e2b", # Orange
10
+ wait: "#76b7b2", # Teal
11
+ result: "#59a14f", # Green
12
+ start: "#59a14f", # Green
13
+ block: "#bab0ab" # Grey
14
+ }
15
+
16
+ def self.export(task_class)
17
+ new(task_class).to_svg
18
+ end
19
+
20
+ def initialize(task_class)
21
+ @task_class = task_class
22
+ @node_positions = {}
23
+ @next_id = 0
24
+ end
25
+
26
+ # Generate SVG representation of the task flow
27
+ def to_svg
28
+ task_hash = task_class.to_h
29
+
30
+ # Calculate node positions using simple layout algorithm
31
+ calculate_node_positions(task_hash)
32
+
33
+ # Generate SVG
34
+ generate_svg(task_hash)
35
+ end
36
+
37
+ # Save the SVG to a file
38
+ def save(filename, format: :svg)
39
+ if format != :svg && format != :png
40
+ raise ArgumentError, "Only SVG format is supported without GraphViz"
41
+ end
42
+
43
+ File.write(filename, to_svg)
44
+ end
45
+
46
+ private
47
+
48
+ def generate_id
49
+ id = "node_#{@next_id}"
50
+ @next_id += 1
51
+ id
52
+ end
53
+
54
+ def calculate_node_positions(task_hash)
55
+ # Simple layout algorithm that positions nodes in columns by state type
56
+ # This is a minimal implementation; a real layout algorithm would be more complex
57
+
58
+ # Group nodes by type for column-based layout
59
+ nodes_by_type = {decision: [], action: [], wait: [], result: []}
60
+
61
+ task_hash[:states].each do |state_name, state_info|
62
+ nodes_by_type[state_info[:type]] << state_name if nodes_by_type.key?(state_info[:type])
63
+ end
64
+
65
+ # Process destination states from the go_to transitions to ensure they're included
66
+ task_hash[:states].each do |state_name, state_info|
67
+ state_info[:transitions]&.each do |_, target|
68
+ next if target.nil? || target.is_a?(Proc)
69
+ target_sym = target.to_sym
70
+
71
+ # Ensure target state exists in task hash
72
+ unless task_hash[:states].key?(target_sym)
73
+ # This is a placeholder for a missing state (likely a result)
74
+ task_hash[:states][target_sym] = {type: :result}
75
+ nodes_by_type[:result] << target_sym if nodes_by_type.key?(:result)
76
+ end
77
+ end
78
+
79
+ # Also process next_state for actions
80
+ if state_info[:next_state] && !task_hash[:states].key?(state_info[:next_state])
81
+ task_hash[:states][state_info[:next_state]] = {type: :result}
82
+ nodes_by_type[:result] << state_info[:next_state] if nodes_by_type.key?(:result)
83
+ end
84
+ end
85
+
86
+ # Calculate positions (this is simplified - a real algorithm would handle edge crossings better)
87
+ x_offset = 200 # Increased from 100 to give more space
88
+ column_width = 200
89
+ row_height = 150
90
+
91
+ # Position initial state (usually a decision)
92
+ task_hash[:initial_state]
93
+ @node_positions["START"] = [100, 100] # Moved START from 50 to 100
94
+
95
+ # Position nodes by type in columns
96
+ [:decision, :action, :wait, :result].each_with_index do |type, col_idx|
97
+ nodes_by_type[type].each_with_index do |node_name, row_idx|
98
+ # Position nodes in a grid layout
99
+ x = x_offset + (col_idx * column_width)
100
+ y = 100 + (row_idx * row_height)
101
+ @node_positions[node_name] = [x, y]
102
+ end
103
+ end
104
+ end
105
+
106
+ def generate_svg(task_hash)
107
+ # SVG dimensions based on node positions
108
+ max_x = @node_positions.values.map(&:first).max + 150
109
+ max_y = @node_positions.values.map(&:last).max + 150
110
+
111
+ # Start SVG document
112
+ svg = <<~SVG
113
+ <svg xmlns="http://www.w3.org/2000/svg" width="#{max_x}" height="#{max_y}" viewBox="0 0 #{max_x} #{max_y}">
114
+ <defs>
115
+ <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
116
+ <polygon points="0 0, 10 3.5, 0 7" fill="#333" />
117
+ </marker>
118
+ </defs>
119
+ SVG
120
+
121
+ # Draw edges (connections between nodes)
122
+ svg += draw_edges(task_hash)
123
+
124
+ # Draw all nodes
125
+ svg += draw_nodes(task_hash)
126
+
127
+ # Close SVG document
128
+ svg += "</svg>"
129
+
130
+ svg
131
+ end
132
+
133
+ def draw_nodes(task_hash)
134
+ svg = ""
135
+
136
+ # Add starting node
137
+ if task_hash[:initial_state] && @node_positions[task_hash[:initial_state]]
138
+ svg += draw_circle(
139
+ @node_positions["START"][0],
140
+ @node_positions["START"][1],
141
+ 20,
142
+ COLORS[:start],
143
+ "START"
144
+ )
145
+ end
146
+
147
+ # Add nodes for each state
148
+ task_hash[:states].each do |state_name, state_info|
149
+ next unless @node_positions[state_name]
150
+
151
+ x, y = @node_positions[state_name]
152
+ node_label = create_node_label(state_name, state_info)
153
+
154
+ svg += case state_info[:type]
155
+ when :decision
156
+ draw_diamond(x, y, 120, 80, COLORS[:decision], node_label)
157
+ when :action
158
+ draw_rectangle(x, y, 160, 80, COLORS[:action], node_label)
159
+ when :wait
160
+ draw_rectangle(x, y, 160, 80, COLORS[:wait], node_label, dashed: true)
161
+ when :result
162
+ draw_rectangle(x, y, 160, 80, COLORS[:result], node_label)
163
+ else
164
+ draw_rectangle(x, y, 160, 80, "#cccccc", node_label)
165
+ end
166
+ end
167
+
168
+ svg
169
+ end
170
+
171
+ def draw_edges(task_hash)
172
+ svg = ""
173
+
174
+ # Add edge from START to initial state
175
+ if task_hash[:initial_state] && @node_positions[task_hash[:initial_state]]
176
+ start_x, start_y = @node_positions["START"]
177
+ end_x, end_y = @node_positions[task_hash[:initial_state]]
178
+ svg += draw_arrow(start_x + 20, start_y, end_x - 60, end_y, "")
179
+ end
180
+
181
+ # Add edges for transitions
182
+ task_hash[:states].each do |state_name, state_info|
183
+ case state_info[:type]
184
+ when :decision
185
+ state_info[:transitions]&.each do |condition, target|
186
+ # Skip Proc targets as they're custom actions or nil targets
187
+ next if target.nil? || target.is_a?(Proc)
188
+
189
+ if @node_positions[state_name] && @node_positions[target.to_sym]
190
+ start_x, start_y = @node_positions[state_name]
191
+ end_x, end_y = @node_positions[target.to_sym]
192
+
193
+ # Use condition as label, or target state name if no condition
194
+ label = condition.to_s
195
+ if label.empty? || label == "nil"
196
+ label = "→ #{target}"
197
+ end
198
+
199
+ svg += draw_arrow(start_x + 60, start_y, end_x - 80, end_y, label)
200
+ end
201
+ end
202
+ when :action
203
+ if state_info[:next_state] && @node_positions[state_name] && @node_positions[state_info[:next_state]]
204
+ start_x, start_y = @node_positions[state_name]
205
+ end_x, end_y = @node_positions[state_info[:next_state]]
206
+ label = "→ #{state_info[:next_state]}"
207
+ svg += draw_arrow(start_x + 80, start_y, end_x - 80, end_y, label)
208
+ end
209
+ when :wait
210
+ # Add a self-loop for wait condition
211
+ if @node_positions[state_name]
212
+ x, y = @node_positions[state_name]
213
+ svg += draw_self_loop(x, y, 160, 80, "waiting")
214
+
215
+ # Add transitions
216
+ state_info[:transitions]&.each do |condition, target|
217
+ # Skip Proc targets or nil targets
218
+ next if target.nil? || target.is_a?(Proc)
219
+
220
+ if @node_positions[target.to_sym]
221
+ start_x, start_y = @node_positions[state_name]
222
+ end_x, end_y = @node_positions[target.to_sym]
223
+
224
+ # Use condition as label, or target state name if no condition
225
+ label = condition.to_s
226
+ if label.empty? || label == "nil"
227
+ label = "→ #{target}"
228
+ end
229
+
230
+ svg += draw_arrow(start_x + 80, start_y, end_x - 80, end_y, label)
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ svg
238
+ end
239
+
240
+ # Helper methods for drawing SVG elements
241
+
242
+ def draw_rectangle(x, y, width, height, color, text, dashed: false)
243
+ style = dashed ? "fill:#{color};stroke:#333;stroke-width:2;stroke-dasharray:5,5;" : "fill:#{color};stroke:#333;stroke-width:2;"
244
+
245
+ # Create a unique ID for a group to contain both the shape and text
246
+ id = "rect-#{generate_id}"
247
+
248
+ <<~SVG
249
+ <g id="#{id}">
250
+ <rect x="#{x - width / 2}" y="#{y - height / 2}" width="#{width}" height="#{height}" rx="10" ry="10" style="#{style}" />
251
+ <text x="#{x}" y="#{y}" text-anchor="middle" fill="white" font-family="Arial" font-size="12px">#{escape_text(text)}</text>
252
+ </g>
253
+ SVG
254
+ end
255
+
256
+ def draw_diamond(x, y, width, height, color, text)
257
+ points = [
258
+ [x, y - height / 2],
259
+ [x + width / 2, y],
260
+ [x, y + height / 2],
261
+ [x - width / 2, y]
262
+ ].map { |px, py| "#{px},#{py}" }.join(" ")
263
+
264
+ # Create a unique ID for a group to contain both the shape and text
265
+ id = "diamond-#{generate_id}"
266
+
267
+ <<~SVG
268
+ <g id="#{id}">
269
+ <polygon points="#{points}" style="fill:#{color};stroke:#333;stroke-width:2;" />
270
+ <text x="#{x}" y="#{y}" text-anchor="middle" fill="white" font-family="Arial" font-size="12px">#{escape_text(text)}</text>
271
+ </g>
272
+ SVG
273
+ end
274
+
275
+ def draw_circle(x, y, radius, color, text)
276
+ # Create a unique ID for a group to contain both the shape and text
277
+ id = "circle-#{generate_id}"
278
+
279
+ <<~SVG
280
+ <g id="#{id}">
281
+ <circle cx="#{x}" cy="#{y}" r="#{radius}" style="fill:#{color};stroke:#333;stroke-width:2;" />
282
+ <text x="#{x}" y="#{y}" text-anchor="middle" fill="white" font-family="Arial" font-size="12px">#{escape_text(text)}</text>
283
+ </g>
284
+ SVG
285
+ end
286
+
287
+ def draw_arrow(x1, y1, x2, y2, label, dashed: false)
288
+ # Calculate the line and add an offset to avoid overlap with nodes
289
+ dx = x2 - x1
290
+ dy = y2 - y1
291
+ length = Math.sqrt(dx * dx + dy * dy)
292
+
293
+ # Make sure we don't divide by zero
294
+ if length.zero?
295
+ return ""
296
+ end
297
+
298
+ # Calculate control points for a slight curve
299
+ # This helps when there are multiple edges between the same nodes
300
+ mx = (x1 + x2) / 2
301
+ my = (y1 + y2) / 2
302
+
303
+ # Add slight curve for better visualization
304
+ cx = mx + dy * 0.2
305
+ cy = my - dx * 0.2
306
+
307
+ style = dashed ? "stroke:#333;stroke-width:2;fill:none;stroke-dasharray:5,5;" : "stroke:#333;stroke-width:2;fill:none;"
308
+ marker = "marker-end=\"url(#arrowhead)\""
309
+
310
+ # Create a unique ID for a group to contain both the path and label
311
+ id = "arrow-#{generate_id}"
312
+
313
+ svg = <<~SVG
314
+ <g id="#{id}">
315
+ <path d="M#{x1},#{y1} Q#{cx},#{cy} #{x2},#{y2}" style="#{style}" #{marker} />
316
+ SVG
317
+
318
+ # Add label if provided
319
+ if label && !label.empty?
320
+ # Create a white background for the label to improve readability
321
+ svg += <<~SVG
322
+ <rect x="#{mx + dy * 0.1 - 40}" y="#{my - dx * 0.1 - 10}" width="80" height="20" rx="5" ry="5" fill="white" fill-opacity="0.8" />
323
+ <text x="#{mx + dy * 0.1}" y="#{my - dx * 0.1}" text-anchor="middle" fill="#333" font-family="Arial" font-size="10px">#{escape_text(label)}</text>
324
+ SVG
325
+ end
326
+
327
+ svg += "</g>"
328
+ svg
329
+ end
330
+
331
+ def draw_self_loop(x, y, width, height, label)
332
+ # Create a self-loop arrow (circle with an arrow)
333
+ style = "stroke:#333;stroke-width:2;fill:none;stroke-dasharray:5,5;"
334
+
335
+ # Create a unique ID for a group to contain both the loop and label
336
+ id = "loop-#{generate_id}"
337
+
338
+ svg = <<~SVG
339
+ <g id="#{id}">
340
+ <ellipse cx="#{x - width / 2}" cy="#{y}" rx="20" ry="30" style="#{style}" />
341
+ <path d="M#{x - width / 2 - 10},#{y - 10} L#{x - width / 2 - 20},#{y} L#{x - width / 2 - 10},#{y + 10}" style="stroke:#333;stroke-width:2;fill:none;" />
342
+ SVG
343
+
344
+ # Add label with background
345
+ label_x = x - width / 2 - 30
346
+ label_y = y - 25
347
+
348
+ svg += <<~SVG
349
+ <rect x="#{label_x - 30}" y="#{label_y - 10}" width="60" height="20" rx="5" ry="5" fill="white" fill-opacity="0.8" />
350
+ <text x="#{label_x}" y="#{label_y}" text-anchor="middle" fill="#333" font-family="Arial" font-size="10px">#{escape_text(label)}</text>
351
+ </g>
352
+ SVG
353
+
354
+ svg
355
+ end
356
+
357
+ def create_node_label(state_name, state_info)
358
+ label = state_name.to_s
359
+
360
+ # Add inputs if present
361
+ if state_info[:inputs].present?
362
+ inputs_list = state_info[:inputs].map(&:to_s).join(", ")
363
+ label += "\nInputs: #{inputs_list}"
364
+ end
365
+
366
+ # Add optional inputs if present
367
+ if state_info[:optional_inputs].present?
368
+ optional_list = state_info[:optional_inputs].map(&:to_s).join(", ")
369
+ label += "\nOptional: #{optional_list}"
370
+ end
371
+
372
+ label
373
+ end
374
+
375
+ def escape_text(text)
376
+ # Split the text into lines for multi-line support
377
+ lines = text.to_s.split("\n")
378
+
379
+ if lines.length <= 1
380
+ # Simple case: just escape the text for a single line
381
+ return text.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub("\"", "&quot;")
382
+ end
383
+
384
+ # For multi-line text, create tspan elements with proper alignment
385
+ line_height = 1.2 # em units
386
+ total_height = (lines.length - 1) * line_height
387
+ y_offset = -total_height / 2 # Start at negative half height to center vertically
388
+
389
+ # Escape XML special characters and add tspan elements for multi-line text
390
+ lines.map do |line|
391
+ line_text = line.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub("\"", "&quot;")
392
+ tspan = "<tspan x=\"0\" y=\"#{y_offset}em\" text-anchor=\"middle\">#{line_text}</tspan>"
393
+ y_offset += line_height # Move down for next line
394
+ tspan
395
+ end.join("")
396
+ end
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,50 @@
1
+ # TODO: Move this into its own gem as I'm already using elsewhere
2
+ module Operations::HasDataAttributes
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def data_attribute_in field_name, name, cast_type = :string, **options
7
+ name = name.to_sym
8
+ typecaster = cast_type.nil? ? nil : ActiveRecord::Type.lookup(cast_type)
9
+ typecast_value = ->(value) { typecaster.nil? ? value : typecaster.cast(value) }
10
+ define_attribute_method name
11
+ if cast_type != :boolean
12
+ define_method(name) { typecast_value.call(send(field_name.to_sym)[name]) || options[:default] }
13
+ else
14
+ define_method(name) do
15
+ value = typecast_value.call(send(field_name.to_sym)[name])
16
+ [true, false].include?(value) ? value : options[:default]
17
+ end
18
+ alias_method :"#{name}?", name
19
+ end
20
+ define_method(:"#{name}=") do |value|
21
+ attribute_will_change! name
22
+ send(field_name.to_sym)[name] = typecast_value.call(value)
23
+ end
24
+ end
25
+
26
+ def model_attribute_in field_name, name, class_name = nil, **options
27
+ id_attribute_name = :"#{name}_global_id"
28
+ data_attribute_in field_name, id_attribute_name, :string, **options
29
+
30
+ define_method(name.to_sym) do
31
+ id = send id_attribute_name.to_sym
32
+ id.nil? ? nil : GlobalID::Locator.locate(id)
33
+ rescue ActiveRecord::RecordNotFound
34
+ nil
35
+ end
36
+
37
+ define_method(:"#{name}=") do |model|
38
+ raise ArgumentError.new("#{model} is not #{class_name} - #{name}") if class_name.present? && model.present? && !model.is_a?(class_name.constantize)
39
+ id = model.nil? ? nil : model.to_global_id.to_s
40
+ send :"#{id_attribute_name}=", id
41
+ end
42
+ end
43
+
44
+ def data_attributes(*attributes) = attributes.each { |attribute| data_attribute(attribute) }
45
+
46
+ def data_attribute(name, cast_type = nil, **options) = data_attribute_in :data, name, cast_type, **options
47
+
48
+ def data_model(name, class_name = nil, **options) = model_attribute_in :data, name, class_name, **options
49
+ end
50
+ end
@@ -0,0 +1,2 @@
1
+ class Operations::InvalidState < Operations::Error
2
+ end
@@ -1,3 +1,3 @@
1
1
  module Operations
2
- VERSION = "0.5.2"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/operations.rb CHANGED
@@ -9,11 +9,12 @@ module Operations
9
9
  end
10
10
  attr_reader :task
11
11
  end
12
+ require "operations/has_data_attributes"
12
13
  require "operations/version"
13
14
  require "operations/engine"
14
15
  require "operations/failure"
15
- require "operations/cannot_wait_in_foreground"
16
16
  require "operations/timeout"
17
17
  require "operations/no_decision"
18
- require "operations/exporters/graphviz"
18
+ require "operations/invalid_state"
19
+ require "operations/exporters/svg"
19
20
  end
@@ -1,4 +1,4 @@
1
- desc "Restart any zombie tasks"
2
- task :restart_zombie_tasks do
3
- Operations::Task.restart_zombie_tasks
1
+ desc "Start the Agent Runner process"
2
+ task :agent_runner do
3
+ Operations::Agent::Runner.start
4
4
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_procedure_operations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-03 00:00:00.000000000 Z
10
+ date: 2025-05-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -47,31 +47,40 @@ files:
47
47
  - LICENSE
48
48
  - README.md
49
49
  - Rakefile
50
+ - app/jobs/operations/agent/find_timeouts_job.rb
51
+ - app/jobs/operations/agent/runner_job.rb
52
+ - app/jobs/operations/agent/timeout_job.rb
53
+ - app/jobs/operations/agent/wake_agents_job.rb
50
54
  - app/jobs/operations/application_job.rb
51
- - app/jobs/operations/task_runner_job.rb
52
55
  - app/models/concerns/operations/participant.rb
56
+ - app/models/operations/agent.rb
57
+ - app/models/operations/agent/interaction_handler.rb
58
+ - app/models/operations/agent/plan.rb
59
+ - app/models/operations/agent/runner.rb
60
+ - app/models/operations/agent/wait_handler.rb
53
61
  - app/models/operations/task.rb
54
- - app/models/operations/task/background.rb
55
62
  - app/models/operations/task/data_carrier.rb
56
63
  - app/models/operations/task/deletion.rb
57
64
  - app/models/operations/task/exports.rb
58
65
  - app/models/operations/task/input_validation.rb
59
- - app/models/operations/task/state_management.rb
60
- - app/models/operations/task/state_management/action_handler.rb
61
- - app/models/operations/task/state_management/completion_handler.rb
62
- - app/models/operations/task/state_management/decision_handler.rb
63
- - app/models/operations/task/state_management/wait_handler.rb
66
+ - app/models/operations/task/plan.rb
67
+ - app/models/operations/task/plan/action_handler.rb
68
+ - app/models/operations/task/plan/decision_handler.rb
69
+ - app/models/operations/task/plan/result_handler.rb
64
70
  - app/models/operations/task/testing.rb
65
71
  - app/models/operations/task_participant.rb
66
72
  - config/routes.rb
67
73
  - db/migrate/20250127160616_create_operations_tasks.rb
68
74
  - db/migrate/20250309160616_create_operations_task_participants.rb
69
- - db/migrate/20250403075414_add_becomes_zombie_at_field.rb
75
+ - db/migrate/20250404085321_add_becomes_zombie_at_field.operations.rb
76
+ - db/migrate/20250407143513_agent_fields.rb
77
+ - db/migrate/20250408124423_add_task_participant_indexes.rb
70
78
  - lib/operations.rb
71
- - lib/operations/cannot_wait_in_foreground.rb
72
79
  - lib/operations/engine.rb
73
- - lib/operations/exporters/graphviz.rb
80
+ - lib/operations/exporters/svg.rb
74
81
  - lib/operations/failure.rb
82
+ - lib/operations/has_data_attributes.rb
83
+ - lib/operations/invalid_state.rb
75
84
  - lib/operations/matchers.rb
76
85
  - lib/operations/no_decision.rb
77
86
  - lib/operations/timeout.rb
@@ -1,11 +0,0 @@
1
- module Operations
2
- class TaskRunnerJob < ApplicationJob
3
- queue_as :default
4
-
5
- def perform task
6
- task.perform if task.waiting?
7
- rescue => ex
8
- Rails.logger.error "TaskRunnerJob failed: #{ex.message} for #{task.inspect}"
9
- end
10
- end
11
- end
@@ -1,39 +0,0 @@
1
- module Operations::Task::Background
2
- extend ActiveSupport::Concern
3
-
4
- included do
5
- scope :zombies, -> { zombies_at(Time.now) }
6
- scope :zombies_at, ->(time) { where(becomes_zombie_at: ..time) }
7
- end
8
-
9
- class_methods do
10
- def delay(value) = @background_delay = value
11
-
12
- def timeout(value) = @execution_timeout = value
13
-
14
- def on_timeout(&handler) = @on_timeout = handler
15
-
16
- def background_delay = @background_delay ||= 1.second
17
-
18
- def execution_timeout = @execution_timeout ||= 5.minutes
19
-
20
- def timeout_handler = @on_timeout
21
-
22
- def with_timeout(data) = data.merge(_execution_timeout: execution_timeout.from_now.utc)
23
-
24
- def restart_zombie_tasks = zombies.find_each { |t| t.restart! }
25
- end
26
-
27
- def zombie? = Time.now > (updated_at + zombie_delay)
28
-
29
- private def background_delay = self.class.background_delay
30
- private def zombie_delay = background_delay * 3
31
- private def zombie_time = becomes_zombie_at || Time.now
32
- private def execution_timeout = self.class.execution_timeout
33
- private def timeout_handler = self.class.timeout_handler
34
- private def timeout!
35
- return unless timeout_expired?
36
- timeout_handler.nil? ? raise(Operations::Timeout.new("Timeout expired", self)) : timeout_handler.call
37
- end
38
- private def timeout_expired? = data[:_execution_timeout].present? && data[:_execution_timeout] < Time.now.utc
39
- end
@@ -1,2 +0,0 @@
1
- class Operations::CannotWaitInForeground < Operations::Error
2
- end