standard_procedure_operations 0.6.0 → 0.7.1
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 +146 -195
- data/app/jobs/operations/delete_old_task_job.rb +5 -0
- data/app/jobs/operations/wake_task_job.rb +5 -0
- data/app/models/concerns/operations/participant.rb +4 -9
- data/app/models/operations/task/index.rb +22 -0
- data/app/models/operations/task/plan/action_handler.rb +7 -6
- data/app/models/operations/task/plan/decision_handler.rb +9 -20
- data/app/models/operations/task/plan/interaction_handler.rb +20 -0
- data/app/models/operations/task/plan/result_handler.rb +2 -9
- data/app/models/operations/{agent → task/plan}/wait_handler.rb +6 -6
- data/app/models/operations/task/plan.rb +44 -11
- data/app/models/operations/{agent → task}/runner.rb +4 -7
- data/app/models/operations/task/testing.rb +3 -55
- data/app/models/operations/task.rb +50 -80
- data/app/models/operations/task_participant.rb +2 -7
- data/db/migrate/20250701190516_rename_existing_operations_tables.rb +19 -0
- data/db/migrate/20250701190716_create_new_operations_tasks.rb +20 -0
- data/db/migrate/20250702113801_create_task_participants.rb +10 -0
- data/lib/operations/version.rb +1 -1
- data/lib/operations.rb +1 -3
- metadata +12 -23
- data/app/jobs/operations/agent/find_timeouts_job.rb +0 -5
- data/app/jobs/operations/agent/runner_job.rb +0 -5
- data/app/jobs/operations/agent/timeout_job.rb +0 -5
- data/app/jobs/operations/agent/wake_agents_job.rb +0 -5
- data/app/models/operations/agent/interaction_handler.rb +0 -30
- data/app/models/operations/agent/plan.rb +0 -38
- data/app/models/operations/agent.rb +0 -31
- data/app/models/operations/task/data_carrier.rb +0 -16
- data/app/models/operations/task/deletion.rb +0 -17
- data/app/models/operations/task/exports.rb +0 -45
- data/app/models/operations/task/input_validation.rb +0 -17
- data/db/migrate/20250127160616_create_operations_tasks.rb +0 -17
- data/db/migrate/20250309160616_create_operations_task_participants.rb +0 -15
- data/db/migrate/20250404085321_add_becomes_zombie_at_field.operations.rb +0 -7
- data/db/migrate/20250407143513_agent_fields.rb +0 -9
- data/db/migrate/20250408124423_add_task_participant_indexes.rb +0 -5
- data/lib/operations/exporters/svg.rb +0 -399
- data/lib/operations/has_data_attributes.rb +0 -50
@@ -1,16 +1,9 @@
|
|
1
1
|
class Operations::Task::Plan::ResultHandler
|
2
|
-
def initialize name
|
2
|
+
def initialize name
|
3
3
|
@name = name.to_sym
|
4
|
-
@required_inputs = inputs
|
5
|
-
@optional_inputs = optional
|
6
|
-
@handler = handler
|
7
4
|
end
|
8
5
|
|
9
6
|
def immediate? = true
|
10
7
|
|
11
|
-
def call(task,
|
12
|
-
results = OpenStruct.new
|
13
|
-
data.instance_exec(results, &@handler) unless @handler.nil?
|
14
|
-
data.complete(results)
|
15
|
-
end
|
8
|
+
def call(task) = task.update task_status: "completed", completed_at: Time.current
|
16
9
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class Operations::
|
1
|
+
class Operations::Task::Plan::WaitHandler
|
2
2
|
def initialize name, &config
|
3
3
|
@name = name.to_sym
|
4
4
|
@conditions = []
|
@@ -19,10 +19,10 @@ class Operations::Agent::WaitHandler
|
|
19
19
|
|
20
20
|
def condition_labels = @condition_labels ||= {}
|
21
21
|
|
22
|
-
def call(task
|
23
|
-
Rails.logger.debug { "#{task}: waiting until #{@name}
|
24
|
-
condition = @conditions.find { |condition|
|
25
|
-
next_state = (condition.nil? || @conditions.index(condition).nil?) ? task.
|
26
|
-
|
22
|
+
def call(task)
|
23
|
+
Rails.logger.debug { "#{task}: waiting until #{@name}" }
|
24
|
+
condition = @conditions.find { |condition| task.instance_eval(&condition) }
|
25
|
+
next_state = (condition.nil? || @conditions.index(condition).nil?) ? task.current_state : @destinations[@conditions.index(condition)]
|
26
|
+
task.go_to next_state
|
27
27
|
end
|
28
28
|
end
|
@@ -2,20 +2,21 @@ module Operations::Task::Plan
|
|
2
2
|
extend ActiveSupport::Concern
|
3
3
|
|
4
4
|
included do
|
5
|
-
|
6
|
-
validate :state_is_valid
|
5
|
+
validate :current_state_is_legal
|
7
6
|
end
|
8
7
|
|
9
8
|
class_methods do
|
10
|
-
def starts_with(value) = @initial_state = value.
|
9
|
+
def starts_with(value) = @initial_state = value.to_s
|
11
10
|
|
12
|
-
def
|
11
|
+
def action(name, &handler) = state_handlers[name.to_s] = ActionHandler.new(name, &handler)
|
13
12
|
|
14
|
-
def decision(name, &config) = state_handlers[name.
|
13
|
+
def decision(name, &config) = state_handlers[name.to_s] = DecisionHandler.new(name, &config)
|
15
14
|
|
16
|
-
def
|
15
|
+
def wait_until(name, &config) = state_handlers[name.to_s] = WaitHandler.new(name, &config)
|
17
16
|
|
18
|
-
def
|
17
|
+
def interaction(name, &implementation) = interaction_handlers[name.to_s] = InteractionHandler.new(name, self, &implementation)
|
18
|
+
|
19
|
+
def result(name) = state_handlers[name.to_s] = ResultHandler.new(name)
|
19
20
|
|
20
21
|
def go_to(state)
|
21
22
|
# Get the most recently defined action handler
|
@@ -25,13 +26,45 @@ module Operations::Task::Plan
|
|
25
26
|
last_action.next_state = state.to_sym
|
26
27
|
end
|
27
28
|
|
29
|
+
def initial_state = @initial_state || "start"
|
30
|
+
|
31
|
+
def delay(value) = @background_delay = value
|
32
|
+
|
33
|
+
def timeout(value) = @execution_timeout = value
|
34
|
+
|
35
|
+
def delete_after(value) = @deletion_time = value
|
36
|
+
|
37
|
+
def on_timeout(&handler) = @on_timeout = handler
|
38
|
+
|
39
|
+
def background_delay = @background_delay ||= 1.minute
|
40
|
+
|
41
|
+
def execution_timeout = @execution_timeout ||= 24.hours
|
42
|
+
|
43
|
+
def timeout_handler = @on_timeout
|
44
|
+
|
45
|
+
def deletion_time = @deletion_time ||= 90.days
|
46
|
+
|
28
47
|
def state_handlers = @state_handlers ||= {}
|
29
48
|
|
30
|
-
def handler_for(state) = state_handlers[state.
|
49
|
+
def handler_for(state) = state_handlers[state.to_s]
|
50
|
+
|
51
|
+
def interaction_handlers = @interaction_handlers ||= {}
|
52
|
+
|
53
|
+
def interaction_handler_for(name) = interaction_handlers[name.to_s]
|
54
|
+
|
55
|
+
def default_times = {wakes_at: background_delay.from_now, expires_at: execution_timeout.from_now, delete_at: deletion_time.from_now}
|
31
56
|
end
|
32
57
|
|
33
|
-
|
34
|
-
|
35
|
-
|
58
|
+
def in?(state) = current_state == state.to_s
|
59
|
+
alias_method :waiting_until?, :in?
|
60
|
+
|
61
|
+
private def handler_for(state) = self.class.handler_for(state)
|
62
|
+
private def default_times = self.class.default_times
|
63
|
+
private def background_delay = self.class.background_delay
|
64
|
+
private def execution_timeout = self.class.execution_timeout
|
65
|
+
private def timeout_handler = self.class.timeout_handler
|
66
|
+
private def timeout_expired? = expires_at.present? && expires_at < Time.now.utc
|
67
|
+
private def current_state_is_legal
|
68
|
+
errors.add :current_state, :invalid if current_state.blank? || handler_for(current_state).nil?
|
36
69
|
end
|
37
70
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Operations
|
2
|
-
class
|
2
|
+
class Task::Runner
|
3
3
|
def initialize
|
4
4
|
@stopped = false
|
5
5
|
end
|
@@ -9,8 +9,9 @@ module Operations
|
|
9
9
|
register_signal_handlers
|
10
10
|
puts "...signal handlers registered"
|
11
11
|
until @stopped
|
12
|
-
|
13
|
-
|
12
|
+
Rails.application.eager_load! if Rails.env.development? # Ensure all sub-classes are loaded in dev mode
|
13
|
+
Task.wake_sleeping
|
14
|
+
Task.delete_old
|
14
15
|
sleep 30
|
15
16
|
end
|
16
17
|
puts "...stopping"
|
@@ -22,10 +23,6 @@ module Operations
|
|
22
23
|
|
23
24
|
def self.start = new.start
|
24
25
|
|
25
|
-
private def process_timed_out_agents = Agent::FindTimeoutsJob.perform_later
|
26
|
-
|
27
|
-
private def process_waiting_agents = Agent::WakeAgentsJob.perform_later
|
28
|
-
|
29
26
|
private def register_signal_handlers
|
30
27
|
%w[INT TERM].each do |signal|
|
31
28
|
trap(signal) { @stopped = true }
|
@@ -2,62 +2,10 @@ module Operations::Task::Testing
|
|
2
2
|
extend ActiveSupport::Concern
|
3
3
|
|
4
4
|
class_methods do
|
5
|
-
def
|
6
|
-
|
7
|
-
|
8
|
-
# Use our own test-specific data carrier so we can examine results
|
9
|
-
data = TestResultCarrier.new(data.merge(task: task))
|
10
|
-
|
11
|
-
# Testing doesn't use the database, so handle serialization by overriding task's go_to
|
12
|
-
# to avoid serialization errors
|
13
|
-
def task.go_to(state, data = {}, message: nil)
|
14
|
-
self.state = state
|
15
|
-
# Don't call super to avoid serialization
|
5
|
+
def test state, **attributes
|
6
|
+
create!(current_state: state, **attributes).tap do |task|
|
7
|
+
task.call_handler
|
16
8
|
end
|
17
|
-
|
18
|
-
task.data = data.to_h.except(:task)
|
19
|
-
handler_for(state).call(task, data)
|
20
|
-
data.completion_results.nil? ? block.call(data) : block.call(data.completion_results)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# Instead of extending DataCarrier (which no longer has go_to),
|
25
|
-
# create a new class with similar functionality but keeps the go_to method for testing
|
26
|
-
class TestResultCarrier < Operations::Task::DataCarrier
|
27
|
-
def go_to(state, message = nil)
|
28
|
-
self.next_state = state
|
29
|
-
self.status_message = message || next_state.to_s
|
30
|
-
end
|
31
|
-
|
32
|
-
def fail_with(message)
|
33
|
-
self.failure_message = message
|
34
|
-
end
|
35
|
-
|
36
|
-
def inputs(*names)
|
37
|
-
missing_inputs = (names.map(&:to_sym) - to_h.keys)
|
38
|
-
raise ArgumentError.new("Missing inputs: #{missing_inputs.join(", ")}") if missing_inputs.any?
|
39
|
-
end
|
40
|
-
|
41
|
-
def optional(*names) = nil
|
42
|
-
|
43
|
-
def call(sub_task_class, **data, &result_handler)
|
44
|
-
record_sub_task sub_task_class
|
45
|
-
super
|
46
|
-
end
|
47
|
-
|
48
|
-
def start(sub_task_class, **data, &result_handler)
|
49
|
-
record_sub_task sub_task_class
|
50
|
-
# Just record the sub_task for testing, don't actually start it
|
51
|
-
nil
|
52
|
-
end
|
53
|
-
|
54
|
-
def complete(results)
|
55
|
-
self.completion_results = results
|
56
|
-
end
|
57
|
-
|
58
|
-
private def record_sub_task sub_task_class
|
59
|
-
self.sub_tasks ||= []
|
60
|
-
self.sub_tasks << sub_task_class
|
61
9
|
end
|
62
10
|
end
|
63
11
|
end
|
@@ -1,104 +1,74 @@
|
|
1
1
|
module Operations
|
2
2
|
class Task < ApplicationRecord
|
3
|
+
include HasAttributes
|
3
4
|
include Plan
|
4
|
-
include
|
5
|
+
include Index
|
5
6
|
include Testing
|
6
|
-
include Exports
|
7
|
-
include HasDataAttributes
|
8
|
-
extend InputValidation
|
9
7
|
|
10
|
-
|
11
|
-
scope :
|
8
|
+
scope :ready_to_wake, -> { ready_to_wake_at(Time.current) }
|
9
|
+
scope :ready_to_wake_at, ->(time) { where(wakes_at: ..time) }
|
10
|
+
scope :expired, -> { expires_at(Time.current) }
|
11
|
+
scope :expired_at, ->(time) { where(expires_at: ..time) }
|
12
|
+
scope :ready_to_delete, -> { ready_to_delete_at(Time.current) }
|
13
|
+
scope :ready_to_delete_at, ->(time) { where(delete_at: ..time) }
|
14
|
+
|
15
|
+
# Task hierarchy relationships
|
16
|
+
belongs_to :parent, class_name: "Operations::Task", optional: true
|
17
|
+
has_many :sub_tasks, class_name: "Operations::Task", foreign_key: "parent_id", dependent: :nullify
|
18
|
+
has_many :active_sub_tasks, -> { where(status: ["active", "waiting"]) }, class_name: "Operations::Task", foreign_key: "parent_id"
|
19
|
+
has_many :failed_sub_tasks, -> { failed }, class_name: "Operations::Task", foreign_key: "parent_id"
|
20
|
+
has_many :completed_sub_tasks, -> { completed }, class_name: "Operations::Task", foreign_key: "parent_id"
|
21
|
+
|
22
|
+
enum :task_status, active: 0, waiting: 10, completed: 100, failed: -1
|
23
|
+
serialize :data, coder: JSON, type: Hash, default: {}
|
24
|
+
has_attribute :exception_class, :string
|
25
|
+
has_attribute :exception_message, :string
|
26
|
+
has_attribute :exception_backtrace, :string
|
27
|
+
|
28
|
+
def call(immediate: false)
|
29
|
+
while active?
|
30
|
+
Rails.logger.debug { "--- #{self}: #{current_state}" }
|
31
|
+
(handler_for(current_state).immediate? || immediate) ? call_handler : go_to_sleep!
|
32
|
+
end
|
33
|
+
rescue => ex
|
34
|
+
record_error! ex
|
35
|
+
raise ex
|
36
|
+
end
|
37
|
+
|
38
|
+
def go_to(next_state) = update! current_state: next_state
|
12
39
|
|
13
|
-
|
14
|
-
serialize :results, coder: GlobalIdSerialiser, type: Hash, default: {}
|
40
|
+
def wake_up! = timeout_expired? ? call_timeout_handler : activate_and_call
|
15
41
|
|
16
|
-
|
17
|
-
after_save :record_participants
|
42
|
+
def start(task_class, **attributes) = task_class.later(**attributes.merge(parent: self))
|
18
43
|
|
19
|
-
def
|
44
|
+
def record_error!(exception) = update!(task_status: "failed", exception_class: exception.class.to_s, exception_message: exception.message.to_s, exception_backtrace: exception.backtrace)
|
20
45
|
|
21
|
-
def
|
46
|
+
def call_handler = handler_for(current_state).call(self)
|
22
47
|
|
23
|
-
def
|
48
|
+
private def go_to_sleep! = update!(default_times.merge(task_status: "waiting"))
|
24
49
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
result_handler&.call(sub_task.results)
|
29
|
-
sub_task
|
50
|
+
private def activate_and_call
|
51
|
+
active!
|
52
|
+
call(immediate: true)
|
30
53
|
end
|
31
54
|
|
32
|
-
def
|
33
|
-
|
34
|
-
in_progress!
|
35
|
-
Rails.logger.debug { "#{self}: performing #{state} with #{data}" }
|
36
|
-
handler_for(state).call(self, carrier_for(data))
|
55
|
+
private def call_timeout_handler
|
56
|
+
timeout_handler.nil? ? raise(Operations::Timeout.new("Timeout expired", self)) : timeout_handler.call
|
37
57
|
rescue => ex
|
38
|
-
|
58
|
+
record_error! ex
|
39
59
|
raise ex
|
40
60
|
end
|
41
61
|
|
42
|
-
|
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
|
50
|
-
|
51
|
-
def inputs(*names)
|
52
|
-
super
|
53
|
-
data_attributes(*names)
|
54
|
-
end
|
55
|
-
|
56
|
-
def optional(*names)
|
57
|
-
super
|
58
|
-
data_attributes(*names)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def go_to(state, data = {}, message: nil)
|
63
|
-
record_state_transition! state: state, data: data.to_h.except(:task), status_message: (message || state).to_s.truncate(240)
|
64
|
-
perform
|
65
|
-
end
|
62
|
+
def self.call(task_status: "active", **attributes) = create!(attributes.merge(task_status: task_status, current_state: initial_state).merge(default_times)).tap { |t| t.call }
|
66
63
|
|
67
|
-
def
|
68
|
-
Rails.logger.error { "#{self}: failed #{message}" }
|
69
|
-
raise Operations::Failure.new(message, self)
|
70
|
-
end
|
64
|
+
def self.later(**attributes) = call(task_status: "waiting", **attributes)
|
71
65
|
|
72
|
-
def
|
73
|
-
Rails.logger.debug { "#{self}: completed #{results}" }
|
74
|
-
update!(status: "completed", status_message: "completed", results: results.to_h)
|
75
|
-
end
|
66
|
+
def self.perform_now(...) = call(...)
|
76
67
|
|
77
|
-
|
78
|
-
Rails.logger.debug { "#{self}: state transition to #{state}" }
|
79
|
-
params[:data] = params[:data].to_h.except(:task)
|
80
|
-
update! params
|
81
|
-
end
|
68
|
+
def self.perform_later(...) = later(...)
|
82
69
|
|
83
|
-
|
70
|
+
def self.wake_sleeping = Task.ready_to_wake.find_each { |t| Operations::WakeTaskJob.perform_later(t) }
|
84
71
|
|
85
|
-
|
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
|
-
|
90
|
-
private def record_participants
|
91
|
-
record_participants_in :data, data.select { |key, value| value.is_a? Participant }
|
92
|
-
record_participants_in :results, results.select { |key, value| value.is_a? Participant }
|
93
|
-
end
|
94
|
-
|
95
|
-
private def record_participants_in context, participants
|
96
|
-
task_participants.where(context: context).where.not(role: participants.keys).delete_all
|
97
|
-
participants.each do |role, participant|
|
98
|
-
task_participants.where(context: context, role: role).first_or_initialize.tap do |task_participant|
|
99
|
-
task_participant.update! participant: participant
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
72
|
+
def self.delete_old = Task.ready_to_delete.find_each { |t| Operations::DeleteOldTaskJob.perform_later(t) }
|
103
73
|
end
|
104
74
|
end
|
@@ -1,14 +1,9 @@
|
|
1
1
|
module Operations
|
2
2
|
class TaskParticipant < ApplicationRecord
|
3
|
-
scope :as, ->(role) { where(role: role) }
|
4
|
-
scope :context, ->(context) { where(context: context) }
|
5
3
|
belongs_to :task
|
6
4
|
belongs_to :participant, polymorphic: true
|
7
5
|
|
8
|
-
validates :
|
9
|
-
|
10
|
-
validates :task_id, uniqueness: {scope: [:participant_type, :participant_id, :role, :context]}
|
11
|
-
|
12
|
-
scope :in, ->(context) { where(context: context) }
|
6
|
+
validates :attribute_name, presence: true
|
7
|
+
normalizes :attribute_name, with: ->(n) { n.to_s.strip }
|
13
8
|
end
|
14
9
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class RenameExistingOperationsTables < ActiveRecord::Migration[8.0]
|
2
|
+
def up
|
3
|
+
remove_foreign_key :operations_task_participants, :operations_tasks, if_exists: true if table_exists?("operations_task_participants")
|
4
|
+
|
5
|
+
rename_table :operations_tasks, :operations_tasks_legacy if table_exists?("operations_tasks")
|
6
|
+
rename_table :operations_task_participants, :operations_task_participants_legacy if table_exists?("operations_task_participants")
|
7
|
+
|
8
|
+
add_foreign_key :operations_task_participants_legacy, :operations_tasks_legacy, column: :task_id if table_exists?("operations_task_participants_legacy")
|
9
|
+
end
|
10
|
+
|
11
|
+
def down
|
12
|
+
remove_foreign_key :operations_task_participants_legacy, :operations_tasks_legacy if table_exists?("operations_task_participants_legacy")
|
13
|
+
|
14
|
+
rename_table :operations_tasks_legacy, :operations_tasks if table_exists?("operations_task_legacy")
|
15
|
+
rename_table :operations_task_participants_legacy, :operations_task_participants if table_exists?("operations_task_participants_legacy")
|
16
|
+
|
17
|
+
add_foreign_key :operations_task_participants, :operations_tasks, column: :task_id if table_exists?("operations_task_participants")
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class CreateNewOperationsTasks < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :operations_tasks do |t|
|
4
|
+
t.belongs_to :parent, foreign_key: {to_table: "operations_tasks"}, null: true
|
5
|
+
t.string :type
|
6
|
+
t.integer :task_status, default: 0, null: false
|
7
|
+
t.string :current_state, default: "start", null: false
|
8
|
+
t.text :data
|
9
|
+
t.datetime :wakes_at
|
10
|
+
t.datetime :expires_at
|
11
|
+
t.datetime :completed_at
|
12
|
+
t.datetime :failed_at
|
13
|
+
t.datetime :delete_at
|
14
|
+
t.timestamps
|
15
|
+
|
16
|
+
t.index [:task_status, :wakes_at], name: "operations_task_wakes_at"
|
17
|
+
t.index [:task_status, :delete_at], name: "operations_task_delete_at"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class CreateTaskParticipants < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :operations_task_participants do |t|
|
4
|
+
t.belongs_to :task, foreign_key: {to_table: "operations_tasks"}
|
5
|
+
t.belongs_to :participant, polymorphic: true, index: true
|
6
|
+
t.string :attribute_name, default: "", null: false
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/lib/operations/version.rb
CHANGED
data/lib/operations.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require "ostruct"
|
2
|
-
require "
|
2
|
+
require "has_attributes"
|
3
3
|
|
4
4
|
module Operations
|
5
5
|
class Error < StandardError
|
@@ -9,12 +9,10 @@ module Operations
|
|
9
9
|
end
|
10
10
|
attr_reader :task
|
11
11
|
end
|
12
|
-
require "operations/has_data_attributes"
|
13
12
|
require "operations/version"
|
14
13
|
require "operations/engine"
|
15
14
|
require "operations/failure"
|
16
15
|
require "operations/timeout"
|
17
16
|
require "operations/no_decision"
|
18
17
|
require "operations/invalid_state"
|
19
|
-
require "operations/exporters/svg"
|
20
18
|
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.7.1
|
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-07-04 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -24,7 +24,7 @@ dependencies:
|
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: 7.1.3
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
27
|
+
name: standard_procedure_has_attributes
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
30
|
- - ">="
|
@@ -47,39 +47,28 @@ 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
|
54
50
|
- app/jobs/operations/application_job.rb
|
51
|
+
- app/jobs/operations/delete_old_task_job.rb
|
52
|
+
- app/jobs/operations/wake_task_job.rb
|
55
53
|
- 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
|
61
54
|
- app/models/operations/task.rb
|
62
|
-
- app/models/operations/task/
|
63
|
-
- app/models/operations/task/deletion.rb
|
64
|
-
- app/models/operations/task/exports.rb
|
65
|
-
- app/models/operations/task/input_validation.rb
|
55
|
+
- app/models/operations/task/index.rb
|
66
56
|
- app/models/operations/task/plan.rb
|
67
57
|
- app/models/operations/task/plan/action_handler.rb
|
68
58
|
- app/models/operations/task/plan/decision_handler.rb
|
59
|
+
- app/models/operations/task/plan/interaction_handler.rb
|
69
60
|
- app/models/operations/task/plan/result_handler.rb
|
61
|
+
- app/models/operations/task/plan/wait_handler.rb
|
62
|
+
- app/models/operations/task/runner.rb
|
70
63
|
- app/models/operations/task/testing.rb
|
71
64
|
- app/models/operations/task_participant.rb
|
72
65
|
- config/routes.rb
|
73
|
-
- db/migrate/
|
74
|
-
- db/migrate/
|
75
|
-
- db/migrate/
|
76
|
-
- db/migrate/20250407143513_agent_fields.rb
|
77
|
-
- db/migrate/20250408124423_add_task_participant_indexes.rb
|
66
|
+
- db/migrate/20250701190516_rename_existing_operations_tables.rb
|
67
|
+
- db/migrate/20250701190716_create_new_operations_tasks.rb
|
68
|
+
- db/migrate/20250702113801_create_task_participants.rb
|
78
69
|
- lib/operations.rb
|
79
70
|
- lib/operations/engine.rb
|
80
|
-
- lib/operations/exporters/svg.rb
|
81
71
|
- lib/operations/failure.rb
|
82
|
-
- lib/operations/has_data_attributes.rb
|
83
72
|
- lib/operations/invalid_state.rb
|
84
73
|
- lib/operations/matchers.rb
|
85
74
|
- lib/operations/no_decision.rb
|
@@ -1,30 +0,0 @@
|
|
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
|
@@ -1,38 +0,0 @@
|
|
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
|