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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +163 -474
  3. data/app/jobs/operations/delete_old_task_job.rb +5 -0
  4. data/app/jobs/operations/wake_task_job.rb +5 -0
  5. data/app/models/concerns/operations/participant.rb +4 -9
  6. data/app/models/operations/task/index.rb +22 -0
  7. data/app/models/operations/task/plan/action_handler.rb +20 -0
  8. data/app/models/operations/task/plan/decision_handler.rb +40 -0
  9. data/app/models/operations/task/plan/interaction_handler.rb +20 -0
  10. data/app/models/operations/task/plan/result_handler.rb +9 -0
  11. data/app/models/operations/task/{state_management → plan}/wait_handler.rb +8 -6
  12. data/app/models/operations/task/plan.rb +70 -0
  13. data/app/models/operations/task/runner.rb +34 -0
  14. data/app/models/operations/task.rb +50 -68
  15. data/app/models/operations/task_participant.rb +2 -5
  16. data/db/migrate/20250701190516_rename_existing_operations_tables.rb +19 -0
  17. data/db/migrate/20250701190716_create_new_operations_tasks.rb +20 -0
  18. data/db/migrate/20250702113801_create_task_participants.rb +10 -0
  19. data/lib/operations/invalid_state.rb +2 -0
  20. data/lib/operations/version.rb +1 -1
  21. data/lib/operations.rb +2 -3
  22. data/lib/tasks/operations_tasks.rake +3 -3
  23. metadata +17 -20
  24. data/app/jobs/operations/task_runner_job.rb +0 -11
  25. data/app/models/operations/task/background.rb +0 -39
  26. data/app/models/operations/task/data_carrier.rb +0 -18
  27. data/app/models/operations/task/deletion.rb +0 -17
  28. data/app/models/operations/task/exports.rb +0 -45
  29. data/app/models/operations/task/input_validation.rb +0 -17
  30. data/app/models/operations/task/state_management/action_handler.rb +0 -17
  31. data/app/models/operations/task/state_management/completion_handler.rb +0 -14
  32. data/app/models/operations/task/state_management/decision_handler.rb +0 -49
  33. data/app/models/operations/task/state_management.rb +0 -39
  34. data/app/models/operations/task/testing.rb +0 -62
  35. data/db/migrate/20250127160616_create_operations_tasks.rb +0 -17
  36. data/db/migrate/20250309160616_create_operations_task_participants.rb +0 -15
  37. data/db/migrate/20250403075414_add_becomes_zombie_at_field.rb +0 -6
  38. data/lib/operations/cannot_wait_in_foreground.rb +0 -2
  39. data/lib/operations/exporters/svg.rb +0 -399
@@ -0,0 +1,5 @@
1
+ class Operations::DeleteOldTaskJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(task) = task.destroy
5
+ end
@@ -0,0 +1,5 @@
1
+ class Operations::WakeTaskJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(task) = task.wake_up!
5
+ end
@@ -3,15 +3,10 @@ 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
7
- has_many :operations_tasks, class_name: "Operations::Task", through: :operations_task_participants, source: :task
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
@@ -0,0 +1,9 @@
1
+ class Operations::Task::Plan::ResultHandler
2
+ def initialize name
3
+ @name = name.to_sym
4
+ end
5
+
6
+ def immediate? = true
7
+
8
+ def call(task) = task.update task_status: "completed", completed_at: Time.current
9
+ end
@@ -1,4 +1,4 @@
1
- class Operations::Task::StateManagement::WaitHandler
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, data)
21
- raise Operations::CannotWaitInForeground.new("#{task.class} cannot wait in the foreground", task) unless task.background?
22
- condition = @conditions.find { |condition| data.instance_eval(&condition) }
23
- next_state = (condition.nil? || @conditions.index(condition).nil?) ? task.state : @destinations[@conditions.index(condition)]
24
- data.go_to next_state
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 StateManagement
4
- include Deletion
5
- include Testing
6
- include Background
7
- include Exports
8
- extend InputValidation
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
- enum :status, in_progress: 0, waiting: 10, completed: 100, failed: -1
36
+ def go_to(next_state) = update! current_state: next_state
11
37
 
12
- serialize :data, coder: GlobalIdSerialiser, type: Hash, default: {}
13
- serialize :results, coder: GlobalIdSerialiser, type: Hash, default: {}
38
+ def wake_up! = timeout_expired? ? call_timeout_handler : activate_and_call
14
39
 
15
- has_many :task_participants, class_name: "Operations::TaskParticipant", dependent: :destroy
16
- after_save :record_participants
40
+ def start(task_class, **attributes) = task_class.later(**attributes.merge(parent: self))
17
41
 
18
- def call sub_task_class, **data, &result_handler
19
- sub_task = sub_task_class.call(**data)
20
- result_handler&.call(sub_task.results)
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 start sub_task_class, **data, &result_handler
25
- sub_task_class.start(**data)
46
+ private def activate_and_call
47
+ active!
48
+ call(immediate: true)
26
49
  end
27
50
 
28
- def perform
29
- timeout!
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
- 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}
54
+ record_error! ex
34
55
  raise ex
35
56
  end
36
57
 
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
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.start(**data)
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 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
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
- private def record_participants
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
- private def record_participants_in context, participants
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 :role, presence: true
7
- validates :context, presence: true
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
@@ -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.7.0"
3
3
  end
data/lib/operations.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "ostruct"
2
- require "global_id_serialiser"
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/exporters/svg"
17
+ require "operations/invalid_state"
19
18
  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.7.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-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: standard_procedure_global_id_serialiser
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/task_runner_job.rb
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/background.rb
55
- - app/models/operations/task/data_carrier.rb
56
- - app/models/operations/task/deletion.rb
57
- - app/models/operations/task/exports.rb
58
- - 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
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/20250127160616_create_operations_tasks.rb
68
- - db/migrate/20250309160616_create_operations_task_participants.rb
69
- - db/migrate/20250403075414_add_becomes_zombie_at_field.rb
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,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