standard_procedure_operations 0.5.3 → 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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +111 -350
  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/has_data_attributes.rb +50 -0
  26. data/lib/operations/invalid_state.rb +2 -0
  27. data/lib/operations/version.rb +1 -1
  28. data/lib/operations.rb +2 -1
  29. data/lib/tasks/operations_tasks.rake +3 -3
  30. metadata +20 -11
  31. data/app/jobs/operations/task_runner_job.rb +0 -11
  32. data/app/models/operations/task/background.rb +0 -39
  33. data/lib/operations/cannot_wait_in_foreground.rb +0 -2
@@ -3,7 +3,7 @@ module Operations
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- has_many :operations_task_participants, class_name: "Operations::TaskParticipant", as: :participant, dependent: :destroy
6
+ has_many :operations_task_participants, -> { includes(:task).order "created_at desc" }, class_name: "Operations::TaskParticipant", as: :participant, dependent: :destroy
7
7
  has_many :operations_tasks, class_name: "Operations::Task", through: :operations_task_participants, source: :task
8
8
 
9
9
  scope :involved_in_operation_as, ->(role:, context: "data") do
@@ -0,0 +1,30 @@
1
+ class Operations::Agent::InteractionHandler
2
+ def initialize name, klass, &implementation
3
+ @legal_states = []
4
+ build_method_on klass, name, self, implementation
5
+ end
6
+ attr_reader :legal_states
7
+
8
+ def when *legal_states
9
+ @legal_states = legal_states.map(&:to_sym).freeze
10
+ end
11
+
12
+ private def call(task, data, *args)
13
+ data.instance_exec(*args, &@implementation)
14
+ end
15
+
16
+ private def build_method_on klass, name, handler, implementation
17
+ klass.define_method name.to_sym do |*args|
18
+ raise Operations::InvalidState.new("#{klass}##{name} cannot be called in #{state}") if !handler.legal_states.empty? && !handler.legal_states.include?(state.to_sym)
19
+ Rails.logger.debug { "#{data[:task]}: interaction #{name} with #{data}" }
20
+ carrier_for(data).tap do |data|
21
+ data.instance_exec(*args, &implementation)
22
+ record_state_transition! data: data
23
+ perform
24
+ end
25
+ rescue => ex
26
+ record_exception(ex)
27
+ raise ex
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ module Operations::Agent::Plan
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def delay(value) = @background_delay = value
6
+
7
+ def timeout(value) = @execution_timeout = value
8
+
9
+ def on_timeout(&handler) = @on_timeout = handler
10
+
11
+ def wait_until(name, &config) = state_handlers[name.to_sym] = Operations::Agent::WaitHandler.new(name, &config)
12
+
13
+ def interaction(name, &implementation) = interaction_handlers[name.to_sym] = Operations::Agent::InteractionHandler.new(name, self, &implementation)
14
+
15
+ def background_delay = @background_delay ||= 5.minutes
16
+
17
+ def execution_timeout = @execution_timeout ||= 24.hours
18
+
19
+ def timeout_handler = @on_timeout
20
+
21
+ def interaction_handlers = @interaction_handlers ||= {}
22
+
23
+ def interaction_handler_for(name) = interaction_handlers[name.to_sym]
24
+ end
25
+
26
+ def timeout!
27
+ call_timeout_handler if timeout_expired?
28
+ end
29
+
30
+ private def background_delay = self.class.background_delay
31
+ private def execution_timeout = self.class.execution_timeout
32
+ private def timeout_handler = self.class.timeout_handler
33
+ private def timeout_expired? = times_out_at.present? && times_out_at < Time.now.utc
34
+ private def call_timeout_handler
35
+ record_state_transition!
36
+ timeout_handler.nil? ? raise(Operations::Timeout.new("Timeout expired", self)) : timeout_handler.call
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ module Operations
2
+ class Agent::Runner
3
+ def initialize
4
+ @stopped = false
5
+ end
6
+
7
+ def start
8
+ puts "Starting #{self.class.name}"
9
+ register_signal_handlers
10
+ puts "...signal handlers registered"
11
+ until @stopped
12
+ process_timed_out_agents
13
+ process_waiting_agents
14
+ sleep 30
15
+ end
16
+ puts "...stopping"
17
+ end
18
+
19
+ def stop
20
+ @stopped = true
21
+ end
22
+
23
+ def self.start = new.start
24
+
25
+ private def process_timed_out_agents = Agent::FindTimeoutsJob.perform_later
26
+
27
+ private def process_waiting_agents = Agent::WakeAgentsJob.perform_later
28
+
29
+ private def register_signal_handlers
30
+ %w[INT TERM].each do |signal|
31
+ trap(signal) { @stopped = true }
32
+ end
33
+
34
+ trap(:QUIT) { exit! }
35
+ end
36
+ end
37
+ end
@@ -1,4 +1,4 @@
1
- class Operations::Task::StateManagement::WaitHandler
1
+ class Operations::Agent::WaitHandler
2
2
  def initialize name, &config
3
3
  @name = name.to_sym
4
4
  @conditions = []
@@ -6,6 +6,8 @@ class Operations::Task::StateManagement::WaitHandler
6
6
  instance_eval(&config)
7
7
  end
8
8
 
9
+ def immediate? = false
10
+
9
11
  def condition(options = {}, &condition)
10
12
  @conditions << condition
11
13
  @condition_labels ||= {}
@@ -18,7 +20,7 @@ class Operations::Task::StateManagement::WaitHandler
18
20
  def condition_labels = @condition_labels ||= {}
19
21
 
20
22
  def call(task, data)
21
- raise Operations::CannotWaitInForeground.new("#{task.class} cannot wait in the foreground", task) unless task.background?
23
+ Rails.logger.debug { "#{task}: waiting until #{@name} with #{data}" }
22
24
  condition = @conditions.find { |condition| data.instance_eval(&condition) }
23
25
  next_state = (condition.nil? || @conditions.index(condition).nil?) ? task.state : @destinations[@conditions.index(condition)]
24
26
  data.go_to next_state
@@ -0,0 +1,31 @@
1
+ module Operations
2
+ class Agent < Task
3
+ include Plan
4
+ scope :ready_to_wake, -> { ready_to_wake_at(Time.now) }
5
+ scope :ready_to_wake_at, ->(time) { where(wakes_at: ..time) }
6
+ scope :timed_out, -> { timed_out_at(Time.now) }
7
+ scope :timed_out_at, ->(time) { where(times_out_at: ..time) }
8
+
9
+ def go_to(state, data = {}, message: nil)
10
+ record_state_transition! state: state, data: data.to_h, status_message: (message || state).to_s.truncate(240)
11
+ handler_for(state).immediate? ? perform : wait
12
+ end
13
+
14
+ def perform! = waiting? ? perform : nil
15
+
16
+ alias_method :waiting_until?, :is?
17
+
18
+ protected def record_state_transition! **params
19
+ params[:wakes_at] = Time.now.utc + background_delay
20
+ params[:times_out_at] ||= Time.now.utc + execution_timeout
21
+ super
22
+ end
23
+
24
+ private def wait
25
+ waiting!
26
+ rescue => ex
27
+ record_exception(ex)
28
+ raise ex
29
+ end
30
+ end
31
+ end
@@ -3,8 +3,6 @@ class Operations::Task::DataCarrier < OpenStruct
3
3
 
4
4
  def call(sub_task_class, **data, &result_handler) = task.call(sub_task_class, **data, &result_handler)
5
5
 
6
- def start(sub_task_class, **data, &result_handler) = task.start(sub_task_class, **data, &result_handler)
7
-
8
6
  def go_to(state, data = nil) = task.go_to state, data || self
9
7
 
10
8
  def complete(results) = task.complete(results)
@@ -9,13 +9,13 @@ module Operations::Task::Exports
9
9
 
10
10
  def handler_to_h(handler)
11
11
  case handler
12
- when Operations::Task::StateManagement::DecisionHandler
12
+ when Operations::Task::Plan::DecisionHandler
13
13
  {type: :decision, transitions: decision_transitions(handler), inputs: extract_inputs(handler), optional_inputs: extract_optional_inputs(handler)}
14
- when Operations::Task::StateManagement::ActionHandler
14
+ when Operations::Task::Plan::ActionHandler
15
15
  {type: :action, next_state: handler.next_state, inputs: extract_inputs(handler), optional_inputs: extract_optional_inputs(handler)}
16
- when Operations::Task::StateManagement::WaitHandler
16
+ when Operations::Agent::WaitHandler
17
17
  {type: :wait, transitions: wait_transitions(handler), inputs: extract_inputs(handler), optional_inputs: extract_optional_inputs(handler)}
18
- when Operations::Task::StateManagement::CompletionHandler
18
+ when Operations::Task::Plan::ResultHandler
19
19
  {type: :result, inputs: extract_inputs(handler), optional_inputs: extract_optional_inputs(handler)}
20
20
  else
21
21
  {type: :unknown}
@@ -1,4 +1,4 @@
1
- class Operations::Task::StateManagement::ActionHandler
1
+ class Operations::Task::Plan::ActionHandler
2
2
  attr_accessor :next_state
3
3
 
4
4
  def initialize name, &action
@@ -9,6 +9,8 @@ class Operations::Task::StateManagement::ActionHandler
9
9
  @next_state = nil
10
10
  end
11
11
 
12
+ def immediate? = true
13
+
12
14
  def call(task, data)
13
15
  data.instance_exec(&@action).tap do |result|
14
16
  data.go_to @next_state unless @next_state.nil?
@@ -1,4 +1,4 @@
1
- class Operations::Task::StateManagement::DecisionHandler
1
+ class Operations::Task::Plan::DecisionHandler
2
2
  include Operations::Task::InputValidation
3
3
 
4
4
  def initialize name, &config
@@ -10,6 +10,8 @@ class Operations::Task::StateManagement::DecisionHandler
10
10
  instance_eval(&config)
11
11
  end
12
12
 
13
+ def immediate? = true
14
+
13
15
  def condition(destination = nil, options = {}, &condition)
14
16
  @conditions << condition
15
17
  @destinations << destination if destination
@@ -1,4 +1,4 @@
1
- class Operations::Task::StateManagement::CompletionHandler
1
+ class Operations::Task::Plan::ResultHandler
2
2
  def initialize name, inputs = [], optional = [], &handler
3
3
  @name = name.to_sym
4
4
  @required_inputs = inputs
@@ -6,6 +6,8 @@ class Operations::Task::StateManagement::CompletionHandler
6
6
  @handler = handler
7
7
  end
8
8
 
9
+ def immediate? = true
10
+
9
11
  def call(task, data)
10
12
  results = OpenStruct.new
11
13
  data.instance_exec(results, &@handler) unless @handler.nil?
@@ -1,4 +1,4 @@
1
- module Operations::Task::StateManagement
1
+ module Operations::Task::Plan
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
@@ -15,9 +15,7 @@ module Operations::Task::StateManagement
15
15
 
16
16
  def action(name, &handler) = state_handlers[name.to_sym] = ActionHandler.new(name, &handler)
17
17
 
18
- def wait_until(name, &config) = state_handlers[name.to_sym] = WaitHandler.new(name, &config)
19
-
20
- def result(name, inputs: [], optional: [], &results) = state_handlers[name.to_sym] = CompletionHandler.new(name, inputs, optional, &results)
18
+ def result(name, inputs: [], optional: [], &results) = state_handlers[name.to_sym] = ResultHandler.new(name, inputs, optional, &results)
21
19
 
22
20
  def go_to(state)
23
21
  # Get the most recently defined action handler
@@ -4,7 +4,7 @@ module Operations::Task::Testing
4
4
  class_methods do
5
5
  def handling state, background: false, **data, &block
6
6
  # Create a task specifically for testing - avoid serialization issues
7
- task = new(state: state, background: background)
7
+ task = create!(state: state)
8
8
  # Use our own test-specific data carrier so we can examine results
9
9
  data = TestResultCarrier.new(data.merge(task: task))
10
10
 
@@ -15,6 +15,7 @@ module Operations::Task::Testing
15
15
  # Don't call super to avoid serialization
16
16
  end
17
17
 
18
+ task.data = data.to_h.except(:task)
18
19
  handler_for(state).call(task, data)
19
20
  data.completion_results.nil? ? block.call(data) : block.call(data.completion_results)
20
21
  end
@@ -1,13 +1,14 @@
1
1
  module Operations
2
2
  class Task < ApplicationRecord
3
- include StateManagement
3
+ include Plan
4
4
  include Deletion
5
5
  include Testing
6
- include Background
7
6
  include Exports
7
+ include HasDataAttributes
8
8
  extend InputValidation
9
9
 
10
10
  enum :status, in_progress: 0, waiting: 10, completed: 100, failed: -1
11
+ scope :active, -> { where(status: %w[in_progress waiting]) }
11
12
 
12
13
  serialize :data, coder: GlobalIdSerialiser, type: Hash, default: {}
13
14
  serialize :results, coder: GlobalIdSerialiser, type: Hash, default: {}
@@ -15,57 +16,77 @@ module Operations
15
16
  has_many :task_participants, class_name: "Operations::TaskParticipant", dependent: :destroy
16
17
  after_save :record_participants
17
18
 
19
+ def to_s = "#{model_name.human}:#{id}"
20
+
21
+ def is?(state) = self.state.to_s == state.to_s
22
+
23
+ def active? = in_progress? || waiting?
24
+
18
25
  def call sub_task_class, **data, &result_handler
26
+ Rails.logger.debug { "#{self}: call #{sub_task_class}" }
19
27
  sub_task = sub_task_class.call(**data)
20
28
  result_handler&.call(sub_task.results)
21
- sub_task.results
22
- end
23
-
24
- def start sub_task_class, **data, &result_handler
25
- sub_task_class.start(**data)
29
+ sub_task
26
30
  end
27
31
 
28
32
  def perform
29
- timeout!
33
+ return if failed?
30
34
  in_progress!
35
+ Rails.logger.debug { "#{self}: performing #{state} with #{data}" }
31
36
  handler_for(state).call(self, carrier_for(data))
32
37
  rescue => ex
33
- update! status: "failed", status_message: ex.message.to_s.truncate(240), results: {failure_message: ex.message, exception_class: ex.class.name, exception_backtrace: ex.backtrace}
38
+ record_exception(ex)
34
39
  raise ex
35
40
  end
36
41
 
37
- def perform_later
38
- update! status: "waiting", becomes_zombie_at: Time.now + zombie_delay
39
- TaskRunnerJob.set(wait_until: background_delay.from_now).perform_later self
40
- end
41
- alias_method :restart!, :perform_later
42
+ class << self
43
+ def call(**data)
44
+ validate_inputs! data
45
+ create!(state: initial_state, status: "in_progress", data: data, status_message: "").tap do |task|
46
+ task.perform
47
+ end
48
+ end
49
+ alias_method :start, :call
42
50
 
43
- def self.call(**)
44
- build(background: false, **).tap do |task|
45
- task.perform
51
+ def inputs(*names)
52
+ super
53
+ data_attributes(*names)
46
54
  end
47
- end
48
55
 
49
- def self.start(**data)
50
- build(background: true, **with_timeout(data)).tap do |task|
51
- task.perform_later
56
+ def optional(*names)
57
+ super
58
+ data_attributes(*names)
52
59
  end
53
60
  end
54
61
 
55
62
  def go_to(state, data = {}, message: nil)
56
- update!(state: state, data: data.to_h, status_message: (message || state).to_s.truncate(240))
57
- background? ? perform_later : perform
63
+ record_state_transition! state: state, data: data.to_h.except(:task), status_message: (message || state).to_s.truncate(240)
64
+ perform
58
65
  end
59
66
 
60
67
  def fail_with(message)
61
- update! status: "failed", status_message: message.to_s.truncate(240), results: {failure_message: message.to_s}
68
+ Rails.logger.error { "#{self}: failed #{message}" }
62
69
  raise Operations::Failure.new(message, self)
63
70
  end
64
71
 
65
- def complete(results) = update!(status: "completed", status_message: "completed", results: results.to_h)
72
+ def complete(results)
73
+ Rails.logger.debug { "#{self}: completed #{results}" }
74
+ update!(status: "completed", status_message: "completed", results: results.to_h)
75
+ end
76
+
77
+ protected def record_state_transition! **params
78
+ Rails.logger.debug { "#{self}: state transition to #{state}" }
79
+ params[:data] = params[:data].to_h.except(:task)
80
+ update! params
81
+ end
66
82
 
67
83
  private def carrier_for(data) = data.is_a?(DataCarrier) ? data : DataCarrier.new(data.merge(task: self))
68
84
 
85
+ private def record_exception(ex)
86
+ Rails.logger.error { "Exception in #{self} - #{ex.inspect}" }
87
+ update!(status: "failed", status_message: ex.message.to_s.truncate(240), results: {failure_message: ex.message, exception_class: ex.class.name, exception_backtrace: ex.backtrace})
88
+ end
89
+
69
90
  private def record_participants
70
91
  record_participants_in :data, data.select { |key, value| value.is_a? Participant }
71
92
  record_participants_in :results, results.select { |key, value| value.is_a? Participant }
@@ -79,10 +100,5 @@ module Operations
79
100
  end
80
101
  end
81
102
  end
82
-
83
- def self.build(background:, **data)
84
- validate_inputs! data
85
- create!(state: initial_state, status: background ? "waiting" : "in_progress", data: data, status_message: "", background: background)
86
- end
87
103
  end
88
104
  end
@@ -1,5 +1,7 @@
1
1
  module Operations
2
2
  class TaskParticipant < ApplicationRecord
3
+ scope :as, ->(role) { where(role: role) }
4
+ scope :context, ->(context) { where(context: context) }
3
5
  belongs_to :task
4
6
  belongs_to :participant, polymorphic: true
5
7
 
@@ -1,3 +1,4 @@
1
+ # This migration comes from operations (originally 20250403075414)
1
2
  class AddBecomesZombieAtField < ActiveRecord::Migration[8.0]
2
3
  def change
3
4
  add_column :operations_tasks, :becomes_zombie_at, :datetime, null: true
@@ -0,0 +1,9 @@
1
+ class AgentFields < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :operations_tasks, :wakes_at, :datetime, null: true
4
+ add_column :operations_tasks, :times_out_at, :datetime, null: true
5
+ remove_column :operations_tasks, :background, :boolean, default: false, null: false
6
+ add_index :operations_tasks, :wakes_at
7
+ add_index :operations_tasks, :times_out_at
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddTaskParticipantIndexes < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_index :operations_task_participants, [:participant_type, :participant_id, :created_at, :role, :context]
4
+ end
5
+ 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.3"
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/invalid_state"
18
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.3
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-04 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
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