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.
- checksums.yaml +4 -4
- data/README.md +111 -350
- data/app/jobs/operations/agent/find_timeouts_job.rb +5 -0
- data/app/jobs/operations/agent/runner_job.rb +5 -0
- data/app/jobs/operations/agent/timeout_job.rb +5 -0
- data/app/jobs/operations/agent/wake_agents_job.rb +5 -0
- data/app/models/concerns/operations/participant.rb +1 -1
- data/app/models/operations/agent/interaction_handler.rb +30 -0
- data/app/models/operations/agent/plan.rb +38 -0
- data/app/models/operations/agent/runner.rb +37 -0
- data/app/models/operations/{task/state_management → agent}/wait_handler.rb +4 -2
- data/app/models/operations/agent.rb +31 -0
- data/app/models/operations/task/data_carrier.rb +0 -2
- data/app/models/operations/task/exports.rb +4 -4
- data/app/models/operations/task/{state_management → plan}/action_handler.rb +3 -1
- data/app/models/operations/task/{state_management → plan}/decision_handler.rb +3 -1
- data/app/models/operations/task/{state_management/completion_handler.rb → plan/result_handler.rb} +3 -1
- data/app/models/operations/task/{state_management.rb → plan.rb} +2 -4
- data/app/models/operations/task/testing.rb +2 -1
- data/app/models/operations/task.rb +46 -30
- data/app/models/operations/task_participant.rb +2 -0
- data/db/migrate/{20250403075414_add_becomes_zombie_at_field.rb → 20250404085321_add_becomes_zombie_at_field.operations.rb} +1 -0
- data/db/migrate/20250407143513_agent_fields.rb +9 -0
- data/db/migrate/20250408124423_add_task_participant_indexes.rb +5 -0
- data/lib/operations/has_data_attributes.rb +50 -0
- data/lib/operations/invalid_state.rb +2 -0
- data/lib/operations/version.rb +1 -1
- data/lib/operations.rb +2 -1
- data/lib/tasks/operations_tasks.rake +3 -3
- metadata +20 -11
- data/app/jobs/operations/task_runner_job.rb +0 -11
- data/app/models/operations/task/background.rb +0 -39
- 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::
|
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
|
-
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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
|
data/app/models/operations/task/{state_management/completion_handler.rb → plan/result_handler.rb}
RENAMED
@@ -1,4 +1,4 @@
|
|
1
|
-
class Operations::Task::
|
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::
|
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
|
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 =
|
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
|
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
|
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
|
-
|
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
|
-
|
38
|
+
record_exception(ex)
|
34
39
|
raise ex
|
35
40
|
end
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
51
|
+
def inputs(*names)
|
52
|
+
super
|
53
|
+
data_attributes(*names)
|
46
54
|
end
|
47
|
-
end
|
48
55
|
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
68
|
+
Rails.logger.error { "#{self}: failed #{message}" }
|
62
69
|
raise Operations::Failure.new(message, self)
|
63
70
|
end
|
64
71
|
|
65
|
-
def complete(results)
|
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
|
@@ -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,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
|
data/lib/operations/version.rb
CHANGED
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 "
|
2
|
-
task :
|
3
|
-
Operations::
|
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.
|
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-
|
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/
|
60
|
-
- app/models/operations/task/
|
61
|
-
- app/models/operations/task/
|
62
|
-
- app/models/operations/task/
|
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/
|
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,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
|