standard_procedure_operations 0.5.3 → 0.7.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 +163 -474
- 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 +20 -0
- data/app/models/operations/task/plan/decision_handler.rb +40 -0
- data/app/models/operations/task/plan/interaction_handler.rb +20 -0
- data/app/models/operations/task/plan/result_handler.rb +9 -0
- data/app/models/operations/task/{state_management → plan}/wait_handler.rb +8 -6
- data/app/models/operations/task/plan.rb +70 -0
- data/app/models/operations/task/runner.rb +34 -0
- data/app/models/operations/task.rb +50 -68
- data/app/models/operations/task_participant.rb +2 -5
- 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/invalid_state.rb +2 -0
- data/lib/operations/version.rb +1 -1
- data/lib/operations.rb +2 -3
- data/lib/tasks/operations_tasks.rake +3 -3
- metadata +17 -20
- data/app/jobs/operations/task_runner_job.rb +0 -11
- data/app/models/operations/task/background.rb +0 -39
- data/app/models/operations/task/data_carrier.rb +0 -18
- 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/app/models/operations/task/state_management/action_handler.rb +0 -17
- data/app/models/operations/task/state_management/completion_handler.rb +0 -14
- data/app/models/operations/task/state_management/decision_handler.rb +0 -49
- data/app/models/operations/task/state_management.rb +0 -39
- data/app/models/operations/task/testing.rb +0 -62
- data/db/migrate/20250127160616_create_operations_tasks.rb +0 -17
- data/db/migrate/20250309160616_create_operations_task_participants.rb +0 -15
- data/db/migrate/20250403075414_add_becomes_zombie_at_field.rb +0 -6
- data/lib/operations/cannot_wait_in_foreground.rb +0 -2
- data/lib/operations/exporters/svg.rb +0 -399
@@ -3,15 +3,10 @@ module Operations
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
has_many :
|
7
|
-
has_many :
|
8
|
-
|
9
|
-
scope :involved_in_operation_as, ->(role:, context: "data") do
|
10
|
-
joins(:operations_task_participants).tap do |scope|
|
11
|
-
scope.where(operations_task_participants: {role: role}) if role
|
12
|
-
scope.where(operations_task_participants: {context: context}) if context
|
13
|
-
end
|
14
|
-
end
|
6
|
+
has_many :operations_participants, class_name: "Operations::TaskParticipant", as: :participant, dependent: :destroy
|
7
|
+
has_many :operations, class_name: "Operations::Task", through: :operations_participants, source: :task
|
15
8
|
end
|
9
|
+
|
10
|
+
def operations_as(attribute_name) = operations.joins(:participants).where(participants: {attribute_name: attribute_name, participant: self})
|
16
11
|
end
|
17
12
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Operations::Task::Index
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
class_methods do
|
5
|
+
def index(*names) = @indexed_attributes = (@indexed_attributes || []) + names.map(&:to_sym)
|
6
|
+
|
7
|
+
def indexed_attributes = @indexed_attributes ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
included do
|
11
|
+
has_many :participants, class_name: "Operations::TaskParticipant", dependent: :destroy
|
12
|
+
after_save :update_index, if: -> { indexed_attributes.any? }
|
13
|
+
end
|
14
|
+
|
15
|
+
private def indexed_attributes = self.class.indexed_attributes
|
16
|
+
private def update_index = indexed_attributes.collect { |attribute| update_index_for(attribute) }
|
17
|
+
private def update_index_for(attribute)
|
18
|
+
models = Array.wrap(send(attribute))
|
19
|
+
participants.where(attribute_name: attribute).where.not(participant: models).delete_all
|
20
|
+
models.collect { |model| participants.where(participant: model, attribute_name: attribute).first_or_create! }
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Operations::Task::Plan::ActionHandler
|
2
|
+
attr_accessor :next_state
|
3
|
+
|
4
|
+
def initialize name, &action
|
5
|
+
@name = name.to_sym
|
6
|
+
@action = action
|
7
|
+
@next_state = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def then next_state
|
11
|
+
@next_state = next_state
|
12
|
+
end
|
13
|
+
|
14
|
+
def immediate? = true
|
15
|
+
|
16
|
+
def call(task)
|
17
|
+
task.instance_exec(&@action)
|
18
|
+
task.go_to @next_state
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Operations::Task::Plan::DecisionHandler
|
2
|
+
def initialize name, &config
|
3
|
+
@name = name.to_sym
|
4
|
+
@conditions = []
|
5
|
+
@destinations = []
|
6
|
+
@true_state = nil
|
7
|
+
@false_state = nil
|
8
|
+
instance_eval(&config)
|
9
|
+
end
|
10
|
+
|
11
|
+
def immediate? = true
|
12
|
+
|
13
|
+
def condition(&condition)
|
14
|
+
@conditions << condition
|
15
|
+
end
|
16
|
+
|
17
|
+
def go_to(destination) = @destinations << destination
|
18
|
+
|
19
|
+
def if_true(state = nil, &handler) = @true_state = state || handler
|
20
|
+
|
21
|
+
def if_false(state = nil, &handler) = @false_state = state || handler
|
22
|
+
|
23
|
+
def call(task)
|
24
|
+
has_true_false_handlers? ? handle_single_condition(task) : handle_multiple_conditions(task)
|
25
|
+
end
|
26
|
+
|
27
|
+
private def has_true_false_handlers? = !@true_state.nil? || !@false_state.nil?
|
28
|
+
|
29
|
+
private def handle_single_condition(task)
|
30
|
+
next_state = task.instance_eval(&@conditions.first) ? @true_state : @false_state
|
31
|
+
task.go_to(next_state)
|
32
|
+
end
|
33
|
+
|
34
|
+
private def handle_multiple_conditions(task)
|
35
|
+
condition = @conditions.find { |condition| task.instance_eval(&condition) }
|
36
|
+
raise Operations::NoDecision.new("No conditions matched #{@name}") if condition.nil?
|
37
|
+
index = @conditions.index condition
|
38
|
+
task.go_to(@destinations[index])
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Operations::Task::Plan::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_s).freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
private def build_method_on klass, name, handler, implementation
|
13
|
+
klass.define_method name.to_sym do |*args|
|
14
|
+
raise Operations::InvalidState.new("#{klass}##{name} cannot be called in #{current_state}") if handler.legal_states.any? && !handler.legal_states.include?(current_state.to_s)
|
15
|
+
Rails.logger.debug { "interaction #{name} with #{self}" }
|
16
|
+
instance_exec(*args, &implementation)
|
17
|
+
wake_up!
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class Operations::Task::
|
1
|
+
class Operations::Task::Plan::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 ||= {}
|
@@ -17,10 +19,10 @@ class Operations::Task::StateManagement::WaitHandler
|
|
17
19
|
|
18
20
|
def condition_labels = @condition_labels ||= {}
|
19
21
|
|
20
|
-
def call(task
|
21
|
-
|
22
|
-
condition = @conditions.find { |condition|
|
23
|
-
next_state = (condition.nil? || @conditions.index(condition).nil?) ? task.
|
24
|
-
|
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
|
25
27
|
end
|
26
28
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Operations::Task::Plan
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
validate :current_state_is_legal
|
6
|
+
end
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def starts_with(value) = @initial_state = value.to_s
|
10
|
+
|
11
|
+
def action(name, &handler) = state_handlers[name.to_s] = ActionHandler.new(name, &handler)
|
12
|
+
|
13
|
+
def decision(name, &config) = state_handlers[name.to_s] = DecisionHandler.new(name, &config)
|
14
|
+
|
15
|
+
def wait_until(name, &config) = state_handlers[name.to_s] = WaitHandler.new(name, &config)
|
16
|
+
|
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)
|
20
|
+
|
21
|
+
def go_to(state)
|
22
|
+
# Get the most recently defined action handler
|
23
|
+
last_action = state_handlers.values.reverse.find { |h| h.is_a?(ActionHandler) }
|
24
|
+
raise ArgumentError, "No action handler defined yet" unless last_action
|
25
|
+
|
26
|
+
last_action.next_state = state.to_sym
|
27
|
+
end
|
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
|
+
|
47
|
+
def state_handlers = @state_handlers ||= {}
|
48
|
+
|
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}
|
56
|
+
end
|
57
|
+
|
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?
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Operations
|
2
|
+
class Task::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
|
+
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
|
15
|
+
sleep 30
|
16
|
+
end
|
17
|
+
puts "...stopping"
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop
|
21
|
+
@stopped = true
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.start = new.start
|
25
|
+
|
26
|
+
private def register_signal_handlers
|
27
|
+
%w[INT TERM].each do |signal|
|
28
|
+
trap(signal) { @stopped = true }
|
29
|
+
end
|
30
|
+
|
31
|
+
trap(:QUIT) { exit! }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,88 +1,70 @@
|
|
1
1
|
module Operations
|
2
2
|
class Task < ApplicationRecord
|
3
|
-
include
|
4
|
-
include
|
5
|
-
include
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
include HasAttributes
|
4
|
+
include Plan
|
5
|
+
include Index
|
6
|
+
scope :ready_to_wake, -> { ready_to_wake_at(Time.current) }
|
7
|
+
scope :ready_to_wake_at, ->(time) { where(wakes_at: ..time) }
|
8
|
+
scope :expired, -> { expires_at(Time.current) }
|
9
|
+
scope :expired_at, ->(time) { where(expires_at: ..time) }
|
10
|
+
scope :ready_to_delete, -> { ready_to_delete_at(Time.current) }
|
11
|
+
scope :ready_to_delete_at, ->(time) { where(delete_at: ..time) }
|
12
|
+
|
13
|
+
# Task hierarchy relationships
|
14
|
+
belongs_to :parent, class_name: "Operations::Task", optional: true
|
15
|
+
has_many :sub_tasks, class_name: "Operations::Task", foreign_key: "parent_id", dependent: :nullify
|
16
|
+
has_many :active_sub_tasks, -> { where(status: ["active", "waiting"]) }, class_name: "Operations::Task", foreign_key: "parent_id"
|
17
|
+
has_many :failed_sub_tasks, -> { failed }, class_name: "Operations::Task", foreign_key: "parent_id"
|
18
|
+
has_many :completed_sub_tasks, -> { completed }, class_name: "Operations::Task", foreign_key: "parent_id"
|
19
|
+
|
20
|
+
enum :task_status, active: 0, waiting: 10, completed: 100, failed: -1
|
21
|
+
serialize :data, coder: JSON, type: Hash, default: {}
|
22
|
+
has_attribute :exception_class, :string
|
23
|
+
has_attribute :exception_message, :string
|
24
|
+
has_attribute :exception_backtrace, :string
|
25
|
+
|
26
|
+
def call(immediate: false)
|
27
|
+
while active?
|
28
|
+
Rails.logger.debug { "--- #{self}: #{current_state}" }
|
29
|
+
(handler_for(current_state).immediate? || immediate) ? handler_for(current_state).call(self) : go_to_sleep!
|
30
|
+
end
|
31
|
+
rescue => ex
|
32
|
+
record_error! ex
|
33
|
+
raise ex
|
34
|
+
end
|
9
35
|
|
10
|
-
|
36
|
+
def go_to(next_state) = update! current_state: next_state
|
11
37
|
|
12
|
-
|
13
|
-
serialize :results, coder: GlobalIdSerialiser, type: Hash, default: {}
|
38
|
+
def wake_up! = timeout_expired? ? call_timeout_handler : activate_and_call
|
14
39
|
|
15
|
-
|
16
|
-
after_save :record_participants
|
40
|
+
def start(task_class, **attributes) = task_class.later(**attributes.merge(parent: self))
|
17
41
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
sub_task.results
|
22
|
-
end
|
42
|
+
def record_error!(exception) = update!(task_status: "failed", exception_class: exception.class.to_s, exception_message: exception.message.to_s, exception_backtrace: exception.backtrace)
|
43
|
+
|
44
|
+
private def go_to_sleep! = update!(default_times.merge(task_status: "waiting"))
|
23
45
|
|
24
|
-
def
|
25
|
-
|
46
|
+
private def activate_and_call
|
47
|
+
active!
|
48
|
+
call(immediate: true)
|
26
49
|
end
|
27
50
|
|
28
|
-
def
|
29
|
-
|
30
|
-
in_progress!
|
31
|
-
handler_for(state).call(self, carrier_for(data))
|
51
|
+
private def call_timeout_handler
|
52
|
+
timeout_handler.nil? ? raise(Operations::Timeout.new("Timeout expired", self)) : timeout_handler.call
|
32
53
|
rescue => ex
|
33
|
-
|
54
|
+
record_error! ex
|
34
55
|
raise ex
|
35
56
|
end
|
36
57
|
|
37
|
-
def
|
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
|
58
|
+
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 }
|
42
59
|
|
43
|
-
def self.call(**)
|
44
|
-
build(background: false, **).tap do |task|
|
45
|
-
task.perform
|
46
|
-
end
|
47
|
-
end
|
60
|
+
def self.later(**attributes) = call(task_status: "waiting", **attributes)
|
48
61
|
|
49
|
-
def self.
|
50
|
-
build(background: true, **with_timeout(data)).tap do |task|
|
51
|
-
task.perform_later
|
52
|
-
end
|
53
|
-
end
|
62
|
+
def self.perform_now(...) = call(...)
|
54
63
|
|
55
|
-
def
|
56
|
-
update!(state: state, data: data.to_h, status_message: (message || state).to_s.truncate(240))
|
57
|
-
background? ? perform_later : perform
|
58
|
-
end
|
59
|
-
|
60
|
-
def fail_with(message)
|
61
|
-
update! status: "failed", status_message: message.to_s.truncate(240), results: {failure_message: message.to_s}
|
62
|
-
raise Operations::Failure.new(message, self)
|
63
|
-
end
|
64
|
-
|
65
|
-
def complete(results) = update!(status: "completed", status_message: "completed", results: results.to_h)
|
66
|
-
|
67
|
-
private def carrier_for(data) = data.is_a?(DataCarrier) ? data : DataCarrier.new(data.merge(task: self))
|
64
|
+
def self.perform_later(...) = later(...)
|
68
65
|
|
69
|
-
|
70
|
-
record_participants_in :data, data.select { |key, value| value.is_a? Participant }
|
71
|
-
record_participants_in :results, results.select { |key, value| value.is_a? Participant }
|
72
|
-
end
|
66
|
+
def self.wake_sleeping = Task.ready_to_wake.find_each { |t| Operations::WakeTaskJob.perform_later(t) }
|
73
67
|
|
74
|
-
|
75
|
-
task_participants.where(context: context).where.not(role: participants.keys).delete_all
|
76
|
-
participants.each do |role, participant|
|
77
|
-
task_participants.where(context: context, role: role).first_or_initialize.tap do |task_participant|
|
78
|
-
task_participant.update! participant: participant
|
79
|
-
end
|
80
|
-
end
|
81
|
-
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
|
68
|
+
def self.delete_old = Task.ready_to_delete.find_each { |t| Operations::DeleteOldTaskJob.perform_later(t) }
|
87
69
|
end
|
88
70
|
end
|
@@ -3,10 +3,7 @@ module Operations
|
|
3
3
|
belongs_to :task
|
4
4
|
belongs_to :participant, polymorphic: true
|
5
5
|
|
6
|
-
validates :
|
7
|
-
|
8
|
-
validates :task_id, uniqueness: {scope: [:participant_type, :participant_id, :role, :context]}
|
9
|
-
|
10
|
-
scope :in, ->(context) { where(context: context) }
|
6
|
+
validates :attribute_name, presence: true
|
7
|
+
normalizes :attribute_name, with: ->(n) { n.to_s.strip }
|
11
8
|
end
|
12
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
|
@@ -12,8 +12,7 @@ module Operations
|
|
12
12
|
require "operations/version"
|
13
13
|
require "operations/engine"
|
14
14
|
require "operations/failure"
|
15
|
-
require "operations/cannot_wait_in_foreground"
|
16
15
|
require "operations/timeout"
|
17
16
|
require "operations/no_decision"
|
18
|
-
require "operations/
|
17
|
+
require "operations/invalid_state"
|
19
18
|
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.7.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-07-02 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
|
- - ">="
|
@@ -48,30 +48,27 @@ files:
|
|
48
48
|
- README.md
|
49
49
|
- Rakefile
|
50
50
|
- app/jobs/operations/application_job.rb
|
51
|
-
- app/jobs/operations/
|
51
|
+
- app/jobs/operations/delete_old_task_job.rb
|
52
|
+
- app/jobs/operations/wake_task_job.rb
|
52
53
|
- app/models/concerns/operations/participant.rb
|
53
54
|
- app/models/operations/task.rb
|
54
|
-
- app/models/operations/task/
|
55
|
-
- app/models/operations/task/
|
56
|
-
- app/models/operations/task/
|
57
|
-
- app/models/operations/task/
|
58
|
-
- app/models/operations/task/
|
59
|
-
- app/models/operations/task/
|
60
|
-
- app/models/operations/task/
|
61
|
-
- app/models/operations/task/
|
62
|
-
- app/models/operations/task/state_management/decision_handler.rb
|
63
|
-
- app/models/operations/task/state_management/wait_handler.rb
|
64
|
-
- app/models/operations/task/testing.rb
|
55
|
+
- app/models/operations/task/index.rb
|
56
|
+
- app/models/operations/task/plan.rb
|
57
|
+
- app/models/operations/task/plan/action_handler.rb
|
58
|
+
- app/models/operations/task/plan/decision_handler.rb
|
59
|
+
- app/models/operations/task/plan/interaction_handler.rb
|
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
|
65
63
|
- app/models/operations/task_participant.rb
|
66
64
|
- config/routes.rb
|
67
|
-
- db/migrate/
|
68
|
-
- db/migrate/
|
69
|
-
- db/migrate/
|
65
|
+
- db/migrate/20250701190516_rename_existing_operations_tables.rb
|
66
|
+
- db/migrate/20250701190716_create_new_operations_tasks.rb
|
67
|
+
- db/migrate/20250702113801_create_task_participants.rb
|
70
68
|
- lib/operations.rb
|
71
|
-
- lib/operations/cannot_wait_in_foreground.rb
|
72
69
|
- lib/operations/engine.rb
|
73
|
-
- lib/operations/exporters/svg.rb
|
74
70
|
- lib/operations/failure.rb
|
71
|
+
- lib/operations/invalid_state.rb
|
75
72
|
- lib/operations/matchers.rb
|
76
73
|
- lib/operations/no_decision.rb
|
77
74
|
- 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
|