standard_procedure_operations 0.6.0 → 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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -200
  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 +7 -6
  8. data/app/models/operations/task/plan/decision_handler.rb +9 -20
  9. data/app/models/operations/task/plan/interaction_handler.rb +20 -0
  10. data/app/models/operations/task/plan/result_handler.rb +2 -9
  11. data/app/models/operations/{agent → task/plan}/wait_handler.rb +6 -6
  12. data/app/models/operations/task/plan.rb +44 -11
  13. data/app/models/operations/{agent → task}/runner.rb +4 -7
  14. data/app/models/operations/task.rb +48 -82
  15. data/app/models/operations/task_participant.rb +2 -7
  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/version.rb +1 -1
  20. data/lib/operations.rb +1 -3
  21. metadata +12 -24
  22. data/app/jobs/operations/agent/find_timeouts_job.rb +0 -5
  23. data/app/jobs/operations/agent/runner_job.rb +0 -5
  24. data/app/jobs/operations/agent/timeout_job.rb +0 -5
  25. data/app/jobs/operations/agent/wake_agents_job.rb +0 -5
  26. data/app/models/operations/agent/interaction_handler.rb +0 -30
  27. data/app/models/operations/agent/plan.rb +0 -38
  28. data/app/models/operations/agent.rb +0 -31
  29. data/app/models/operations/task/data_carrier.rb +0 -16
  30. data/app/models/operations/task/deletion.rb +0 -17
  31. data/app/models/operations/task/exports.rb +0 -45
  32. data/app/models/operations/task/input_validation.rb +0 -17
  33. data/app/models/operations/task/testing.rb +0 -63
  34. data/db/migrate/20250127160616_create_operations_tasks.rb +0 -17
  35. data/db/migrate/20250309160616_create_operations_task_participants.rb +0 -15
  36. data/db/migrate/20250404085321_add_becomes_zombie_at_field.operations.rb +0 -7
  37. data/db/migrate/20250407143513_agent_fields.rb +0 -9
  38. data/db/migrate/20250408124423_add_task_participant_indexes.rb +0 -5
  39. data/lib/operations/exporters/svg.rb +0 -399
  40. data/lib/operations/has_data_attributes.rb +0 -50
@@ -1,4 +1,4 @@
1
- class Operations::Agent::WaitHandler
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, data)
23
- Rails.logger.debug { "#{task}: waiting until #{@name} with #{data}" }
24
- condition = @conditions.find { |condition| data.instance_eval(&condition) }
25
- next_state = (condition.nil? || @conditions.index(condition).nil?) ? task.state : @destinations[@conditions.index(condition)]
26
- 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
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
- attribute :state, :string
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.to_sym
9
+ def starts_with(value) = @initial_state = value.to_s
11
10
 
12
- def initial_state = @initial_state
11
+ def action(name, &handler) = state_handlers[name.to_s] = ActionHandler.new(name, &handler)
13
12
 
14
- def decision(name, &config) = state_handlers[name.to_sym] = DecisionHandler.new(name, &config)
13
+ def decision(name, &config) = state_handlers[name.to_s] = DecisionHandler.new(name, &config)
15
14
 
16
- def action(name, &handler) = state_handlers[name.to_sym] = ActionHandler.new(name, &handler)
15
+ def wait_until(name, &config) = state_handlers[name.to_s] = WaitHandler.new(name, &config)
17
16
 
18
- def result(name, inputs: [], optional: [], &results) = state_handlers[name.to_sym] = ResultHandler.new(name, inputs, optional, &results)
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.to_sym]
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
- private def handler_for(state) = self.class.handler_for(state.to_sym)
34
- private def state_is_valid
35
- errors.add :state, :invalid if state.blank? || handler_for(state.to_sym).nil?
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 Agent::Runner
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
- process_timed_out_agents
13
- process_waiting_agents
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 }
@@ -1,104 +1,70 @@
1
1
  module Operations
2
2
  class Task < ApplicationRecord
3
+ include HasAttributes
3
4
  include Plan
4
- include Deletion
5
- include Testing
6
- include Exports
7
- include HasDataAttributes
8
- extend InputValidation
9
-
10
- enum :status, in_progress: 0, waiting: 10, completed: 100, failed: -1
11
- scope :active, -> { where(status: %w[in_progress waiting]) }
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
12
35
 
13
- serialize :data, coder: GlobalIdSerialiser, type: Hash, default: {}
14
- serialize :results, coder: GlobalIdSerialiser, type: Hash, default: {}
36
+ def go_to(next_state) = update! current_state: next_state
15
37
 
16
- has_many :task_participants, class_name: "Operations::TaskParticipant", dependent: :destroy
17
- after_save :record_participants
38
+ def wake_up! = timeout_expired? ? call_timeout_handler : activate_and_call
18
39
 
19
- def to_s = "#{model_name.human}:#{id}"
40
+ def start(task_class, **attributes) = task_class.later(**attributes.merge(parent: self))
20
41
 
21
- def is?(state) = self.state.to_s == state.to_s
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)
22
43
 
23
- def active? = in_progress? || waiting?
44
+ private def go_to_sleep! = update!(default_times.merge(task_status: "waiting"))
24
45
 
25
- def call sub_task_class, **data, &result_handler
26
- Rails.logger.debug { "#{self}: call #{sub_task_class}" }
27
- sub_task = sub_task_class.call(**data)
28
- result_handler&.call(sub_task.results)
29
- sub_task
46
+ private def activate_and_call
47
+ active!
48
+ call(immediate: true)
30
49
  end
31
50
 
32
- def perform
33
- return if failed?
34
- in_progress!
35
- Rails.logger.debug { "#{self}: performing #{state} with #{data}" }
36
- 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
37
53
  rescue => ex
38
- record_exception(ex)
54
+ record_error! ex
39
55
  raise ex
40
56
  end
41
57
 
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
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
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 }
66
59
 
67
- def fail_with(message)
68
- Rails.logger.error { "#{self}: failed #{message}" }
69
- raise Operations::Failure.new(message, self)
70
- end
60
+ def self.later(**attributes) = call(task_status: "waiting", **attributes)
71
61
 
72
- def complete(results)
73
- Rails.logger.debug { "#{self}: completed #{results}" }
74
- update!(status: "completed", status_message: "completed", results: results.to_h)
75
- end
62
+ def self.perform_now(...) = call(...)
76
63
 
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
64
+ def self.perform_later(...) = later(...)
82
65
 
83
- private def carrier_for(data) = data.is_a?(DataCarrier) ? data : DataCarrier.new(data.merge(task: self))
66
+ def self.wake_sleeping = Task.ready_to_wake.find_each { |t| Operations::WakeTaskJob.perform_later(t) }
84
67
 
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
-
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
68
+ def self.delete_old = Task.ready_to_delete.find_each { |t| Operations::DeleteOldTaskJob.perform_later(t) }
103
69
  end
104
70
  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 :role, presence: true
9
- validates :context, presence: true
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
@@ -1,3 +1,3 @@
1
1
  module Operations
2
- VERSION = "0.6.0"
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
@@ -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.6.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-05-29 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
  - - ">="
@@ -47,39 +47,27 @@ 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/data_carrier.rb
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
70
- - app/models/operations/task/testing.rb
61
+ - app/models/operations/task/plan/wait_handler.rb
62
+ - app/models/operations/task/runner.rb
71
63
  - app/models/operations/task_participant.rb
72
64
  - config/routes.rb
73
- - db/migrate/20250127160616_create_operations_tasks.rb
74
- - db/migrate/20250309160616_create_operations_task_participants.rb
75
- - db/migrate/20250404085321_add_becomes_zombie_at_field.operations.rb
76
- - db/migrate/20250407143513_agent_fields.rb
77
- - db/migrate/20250408124423_add_task_participant_indexes.rb
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
78
68
  - lib/operations.rb
79
69
  - lib/operations/engine.rb
80
- - lib/operations/exporters/svg.rb
81
70
  - lib/operations/failure.rb
82
- - lib/operations/has_data_attributes.rb
83
71
  - lib/operations/invalid_state.rb
84
72
  - lib/operations/matchers.rb
85
73
  - lib/operations/no_decision.rb
@@ -1,5 +0,0 @@
1
- class Operations::Agent::FindTimeoutsJob < ApplicationJob
2
- queue_as :default
3
-
4
- def perform = Operations::Agent.active.timed_out.find_each { |agent| Operations::AgentTimeoutJob.perform_later(agent) }
5
- end
@@ -1,5 +0,0 @@
1
- class Operations::Agent::RunnerJob < ApplicationJob
2
- queue_as :default
3
-
4
- def perform(agent) = agent.perform!
5
- end
@@ -1,5 +0,0 @@
1
- class Operations::Agent::TimeoutJob < ApplicationJob
2
- queue_as :default
3
-
4
- def perform(agent) = agent.timeout!
5
- end
@@ -1,5 +0,0 @@
1
- class Operations::Agent::WakeAgentsJob < ApplicationJob
2
- queue_as :default
3
-
4
- def perform = Operations::Agent.waiting.ready_to_wake.find_each { |agent| Operations::Agent::RunnerJob.perform_later(agent) }
5
- end
@@ -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
@@ -1,31 +0,0 @@
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
@@ -1,16 +0,0 @@
1
- class Operations::Task::DataCarrier < OpenStruct
2
- def fail_with(message) = task.fail_with(message)
3
-
4
- def call(sub_task_class, **data, &result_handler) = task.call(sub_task_class, **data, &result_handler)
5
-
6
- def go_to(state, data = nil) = task.go_to state, data || self
7
-
8
- def complete(results) = task.complete(results)
9
-
10
- def inputs(*names)
11
- missing_inputs = (names.map(&:to_sym) - to_h.keys)
12
- raise ArgumentError.new("Missing inputs: #{missing_inputs.join(", ")}") if missing_inputs.any?
13
- end
14
-
15
- def optional(*names) = nil
16
- end
@@ -1,17 +0,0 @@
1
- module Operations::Task::Deletion
2
- extend ActiveSupport::Concern
3
-
4
- included do
5
- scope :for_deletion, -> { where(delete_at: ..Time.now.utc) }
6
- attribute :delete_at, :datetime, default: -> { deletes_after.from_now.utc }
7
- validates :delete_at, presence: true
8
- end
9
-
10
- class_methods do
11
- def delete_after(value) = @@deletes_after = value
12
-
13
- def deletes_after = @@deletes_after ||= 90.days
14
-
15
- def delete_expired = for_deletion.destroy_all
16
- end
17
- end