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

Parameters:

  • state_machine_object (Object)

    Object with a state_machine method

  • target_state (String)

    The desired target state

  • allowed_from_states (Array<String>)

    States from which transition is allowed

  • metadata (Hash) (defaults to: {})

    Optional metadata for the transition

Returns:

  • (Symbol)

    :transitioned, :already_target, :invalid_from_state



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

Parameters:

  • state_machine_object (Object)

    Object with a state_machine method

  • states (Array<String>)

    States to check against

Returns:

  • (Boolean)

    True if current state is in the provided 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

Parameters:

  • state_machine_object (Object)

    Object with a state_machine method

Returns:

  • (String, nil)

    Current state or nil if no state machine



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.

Parameters:

  • state_machine_object (Object)

    Object with a state_machine method

  • target_state (String)

    The desired target state

  • metadata (Hash) (defaults to: {})

    Optional metadata for the transition

Returns:

  • (Boolean)

    True if transition occurred, false if already in target state



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