Module: Tasker::Concerns::IdempotentStateTransitions

Extended by:
ActiveSupport::Concern
Included in:
Events::Publisher, Orchestration::Coordinator, Orchestration::StepExecutor, Orchestration::TaskFinalizer, Orchestration::TaskInitializer, Orchestration::TaskReenqueuer
Defined in:
lib/tasker/concerns/idempotent_state_transitions.rb

Overview

IdempotentStateTransitions provides helper methods for safely transitioning state machine objects without throwing errors on same-state transitions.

This concern extracts the common pattern of checking current state before attempting transitions to handle Statesman’s restriction on same-state transitions.

Instance Method Summary collapse

Instance Method Details

#conditional_transition_to(state_machine_object, target_state, allowed_from_states, metadata = {}) ⇒ Symbol

Safely transition to target state only if current state matches one of the allowed from states



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/tasker/concerns/idempotent_state_transitions.rb', line 67

def conditional_transition_to(state_machine_object, target_state, allowed_from_states,  = {})
  current_state = state_machine_object.state_machine.current_state

  # Already in target state - idempotent
  return :already_target if current_state == target_state

  # Check if current state allows this transition
  unless allowed_from_states.include?(current_state)
    allowed_states_list = allowed_from_states.join(', ')
    Rails.logger.debug do
      "#{self.class.name}: Cannot transition #{state_machine_object.class.name} #{state_machine_object.id} " \
        "from #{current_state} to #{target_state}. Allowed from states: #{allowed_states_list}"
    end
    return :invalid_from_state
  end

  # Perform the transition
  state_machine_object.state_machine.transition_to!(target_state, )
  Rails.logger.debug do
    "#{self.class.name}: Successfully transitioned" \
      "#{state_machine_object.class.name} #{state_machine_object.id} " \
      "from #{current_state} to #{target_state}"
  end
  :transitioned
rescue StandardError => e
  Rails.logger.error do
    "#{self.class.name}: Failed to transition #{state_machine_object.class.name} #{state_machine_object.id} " \
      "to #{target_state}: #{e.message}"
  end
  raise
end

#in_any_state?(state_machine_object, states) ⇒ Boolean

Check if an object is in any of the specified states



120
121
122
123
124
125
# File 'lib/tasker/concerns/idempotent_state_transitions.rb', line 120

def in_any_state?(state_machine_object, states)
  current_state = safe_current_state(state_machine_object)
  return false if current_state.nil?

  states.include?(current_state)
end

#safe_current_state(state_machine_object) ⇒ String?

Get current state safely, handling cases where state machine might not exist



103
104
105
106
107
108
109
110
111
112
113
# File 'lib/tasker/concerns/idempotent_state_transitions.rb', line 103

def safe_current_state(state_machine_object)
  return nil unless state_machine_object.respond_to?(:state_machine)

  state_machine_object.state_machine.current_state
rescue StandardError => e
  Rails.logger.warn do
    "#{self.class.name}: Could not get current state for #{state_machine_object.class.name} " \
      "#{state_machine_object.id}: #{e.message}"
  end
  nil
end

#safe_transition_to(state_machine_object, target_state, metadata = {}) ⇒ Boolean

Safely transition a state machine object to a target state

Checks the current state first and only attempts transition if different. This prevents Statesman’s “Cannot transition from X to X” errors.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/tasker/concerns/idempotent_state_transitions.rb', line 22

def safe_transition_to(state_machine_object, target_state,  = {})
  current_state = state_machine_object.state_machine.current_state

  if current_state == target_state
    Rails.logger.debug do
      "#{self.class.name}: #{state_machine_object.class.name} #{state_machine_object.id} " \
        "already in #{target_state}, skipping transition"
    end
    return false
  end

  Rails.logger.debug do
    "#{self.class.name}: Transitioning #{state_machine_object.class.name} #{state_machine_object.id} " \
      "from #{current_state} to #{target_state}"
  end

  state_machine_object.state_machine.transition_to!(target_state, )
  true
rescue Statesman::GuardFailedError => e
  Rails.logger.debug do
    "#{self.class.name}: Guard clause prevented transition of #{state_machine_object.class.name} " \
      "#{state_machine_object.id} from '#{current_state}' to '#{target_state}': #{e.message}"
  end
  false
rescue Statesman::TransitionFailedError => e
  Rails.logger.warn do
    "#{self.class.name}: Invalid transition for #{state_machine_object.class.name} " \
      "#{state_machine_object.id} from '#{current_state}' to '#{target_state}': #{e.message}"
  end
  false
rescue StandardError => e
  Rails.logger.error do
    "#{self.class.name}: Failed to transition #{state_machine_object.class.name} #{state_machine_object.id} " \
      "to #{target_state}: #{e.message}"
  end
  raise
end