Class: GenevaDrive::StepExecution
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- GenevaDrive::StepExecution
- Includes:
- MetadataAccessor
- Defined in:
- lib/geneva_drive/step_execution.rb
Overview
Represents a single step execution attempt within a workflow. Serves as both an audit record and an idempotency key.
Each step execution tracks:
- The step being executed
- The current state of execution
- When the step was scheduled, started, and completed
- Any errors that occurred
- The outcome of the execution
Defined Under Namespace
Modules: MetadataAccessor
Constant Summary collapse
- OUTCOMES =
Outcome values for audit purposes
%w[ success continued reattempted skipped canceled failed recovered workflow_paused ].freeze
Class Method Summary collapse
-
.reset_resumable_columns_cache! ⇒ void
Clears the cached detection result.
-
.reset_signal_columns_cache! ⇒ void
Clears the cached signal column detection result.
-
.resumable_columns? ⇒ Boolean
Lazily checks whether the resumable-step columns (cursor and continues_from_id) have been migrated.
-
.serialize_cursor(value) ⇒ Object?
Serializes a cursor value using ActiveJob serializers (handles Date, Time, and other types) and enforces GenevaDrive.max_cursor_size on the serialized JSON.
-
.signal_columns? ⇒ Boolean
Lazily checks whether the signal columns (signal_id and waiting_since) have been migrated, mirroring resumable_columns?.
Instance Method Summary collapse
-
#cursor_value ⇒ Object?
Returns the deserialized cursor value for resumable steps.
-
#cursor_value=(value) ⇒ void
Sets the cursor value for resumable steps.
-
#execute!(interruptible: true, max_iterations: nil) ⇒ void
Executes this step using the Executor.
-
#logger ⇒ Logger
Same as ActiveRecord::Base#logger but supplemented with tags for step and workflow.
-
#mark_canceled!(outcome: "canceled") ⇒ void
Marks the step execution as canceled.
-
#mark_completed!(outcome: "success") ⇒ void
Marks the step execution as completed.
-
#mark_failed!(error, outcome: "failed") ⇒ void
Marks the step execution as failed and records the error.
-
#mark_skipped!(outcome: "skipped") ⇒ void
Marks the step execution as skipped.
-
#resuming? ⇒ Boolean
Returns true if this execution is resuming from a prior execution.
-
#start! ⇒ Boolean
Transitions the step execution to 'in_progress' state.
-
#step_definition ⇒ StepDefinition?
Returns the step definition for this execution.
-
#with_logger(logger) { ... } ⇒ Object
private
Temporarily overrides the step execution's logger for the duration of the block.
Methods included from MetadataAccessor
#exception_info, #read_metadata, #reattempt_reason, #write_metadata
Class Method Details
.reset_resumable_columns_cache! ⇒ void
This method returns an undefined value.
Clears the cached detection result. Call this in tests or after running migrations in-process so the next access re-checks.
110 111 112 |
# File 'lib/geneva_drive/step_execution.rb', line 110 def reset_resumable_columns_cache! remove_instance_variable(:@_resumable_columns) if defined?(@_resumable_columns) end |
.reset_signal_columns_cache! ⇒ void
This method returns an undefined value.
Clears the cached signal column detection result. Call this in tests or after running migrations in-process so the next access re-checks.
137 138 139 |
# File 'lib/geneva_drive/step_execution.rb', line 137 def reset_signal_columns_cache! remove_instance_variable(:@_signal_columns) if defined?(@_signal_columns) end |
.resumable_columns? ⇒ Boolean
Lazily checks whether the resumable-step columns (cursor and continues_from_id) have been migrated. Never hits the database at class definition time — only on the first runtime call.
Deployments usually ship the gem update before running migrations, so all cursor/chaining behavior must degrade safely when the columns are absent: reads return nil, writes are no-ops, and executing an actual resumable_step raises a clear configuration error instead.
96 97 98 99 100 101 102 103 104 |
# File 'lib/geneva_drive/step_execution.rb', line 96 def resumable_columns? if defined?(@_resumable_columns) return @_resumable_columns end @_resumable_columns = table_exists? && column_names.include?("cursor") && column_names.include?("continues_from_id") end |
.serialize_cursor(value) ⇒ Object?
Serializes a cursor value using ActiveJob serializers (handles Date, Time, and other types) and enforces GenevaDrive.max_cursor_size on the serialized JSON. The single serialization path for cursor writes.
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/geneva_drive/step_execution.rb', line 148 def serialize_cursor(value) return nil if value.nil? serialized = ActiveJob::Arguments.serialize([value]).first limit = GenevaDrive.max_cursor_size if limit bytesize = JSON.generate(serialized).bytesize if bytesize > limit raise GenevaDrive::CursorTooLargeError, "Serialized cursor is #{bytesize} bytes, exceeding GenevaDrive.max_cursor_size " \ "(#{limit} bytes). The cursor is a position marker (an id, page number, or token), " \ "not a place to store the data being processed. Set GenevaDrive.max_cursor_size to " \ "nil to disable this check." end end serialized end |
.signal_columns? ⇒ Boolean
Lazily checks whether the signal columns (signal_id and waiting_since) have been migrated, mirroring resumable_columns?. Never hits the database at class definition time.
Everything unrelated to signals keeps working without them; a step declaring wait_for: fails loudly with a "run the install generator" message instead of silently running without its event.
123 124 125 126 127 128 129 130 131 |
# File 'lib/geneva_drive/step_execution.rb', line 123 def signal_columns? if defined?(@_signal_columns) return @_signal_columns end @_signal_columns = table_exists? && column_names.include?("signal_id") && column_names.include?("waiting_since") end |
Instance Method Details
#cursor_value ⇒ Object?
Returns the deserialized cursor value for resumable steps. Uses ActiveJob serializers to handle Date, Time, and other types. Returns nil when the cursor column has not been migrated yet.
250 251 252 253 254 |
# File 'lib/geneva_drive/step_execution.rb', line 250 def cursor_value return nil unless self.class.resumable_columns? return nil if cursor.blank? ActiveJob::Arguments.deserialize([cursor]).first end |
#cursor_value=(value) ⇒ void
This method returns an undefined value.
Sets the cursor value for resumable steps. Uses ActiveJob serializers to handle Date, Time, and other types. Silent no-op when the cursor column has not been migrated yet.
263 264 265 266 |
# File 'lib/geneva_drive/step_execution.rb', line 263 def cursor_value=(value) return unless self.class.resumable_columns? self.cursor = self.class.serialize_cursor(value) end |
#execute!(interruptible: true, max_iterations: nil) ⇒ void
This method returns an undefined value.
Executes this step using the Executor.
288 289 290 |
# File 'lib/geneva_drive/step_execution.rb', line 288 def execute!(interruptible: true, max_iterations: nil) GenevaDrive::Executor.execute!(self, interruptible: interruptible, max_iterations: max_iterations) end |
#logger ⇒ Logger
Same as ActiveRecord::Base#logger but supplemented with tags for step and workflow
295 296 297 298 299 300 |
# File 'lib/geneva_drive/step_execution.rb', line 295 def logger @logger ||= begin workflow_tagged_logger = workflow.logger workflow_tagged_logger.tagged("execution_id=#{id} step_name=#{step_name}") end end |
#mark_canceled!(outcome: "canceled") ⇒ void
This method returns an undefined value.
Marks the step execution as canceled.
235 236 237 238 239 240 241 242 243 |
# File 'lib/geneva_drive/step_execution.rb', line 235 def mark_canceled!(outcome: "canceled") with_lock do update!( state: "canceled", canceled_at: Time.current, outcome: outcome ) end end |
#mark_completed!(outcome: "success") ⇒ void
This method returns an undefined value.
Marks the step execution as completed.
188 189 190 191 192 193 194 195 196 |
# File 'lib/geneva_drive/step_execution.rb', line 188 def mark_completed!(outcome: "success") with_lock do update!( state: "completed", completed_at: Time.current, outcome: outcome ) end end |
#mark_failed!(error, outcome: "failed") ⇒ void
This method returns an undefined value.
Marks the step execution as failed and records the error.
203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/geneva_drive/step_execution.rb', line 203 def mark_failed!(error, outcome: "failed") with_lock do attrs = { state: "failed", failed_at: Time.current, outcome: outcome, error_message: error., error_backtrace: error.backtrace&.join("\n") } attrs[:error_class_name] = error.class.name if has_attribute?(:error_class_name) update!(attrs) end end |
#mark_skipped!(outcome: "skipped") ⇒ void
This method returns an undefined value.
Marks the step execution as skipped.
221 222 223 224 225 226 227 228 229 |
# File 'lib/geneva_drive/step_execution.rb', line 221 def mark_skipped!(outcome: "skipped") with_lock do update!( state: "skipped", skipped_at: Time.current, outcome: outcome ) end end |
#resuming? ⇒ Boolean
Returns true if this execution is resuming from a prior execution. Always false when the continues_from_id column has not been migrated yet.
272 273 274 |
# File 'lib/geneva_drive/step_execution.rb', line 272 def resuming? self.class.resumable_columns? && continues_from_id.present? end |
#start! ⇒ Boolean
Transitions the step execution to 'in_progress' state. Uses pessimistic locking to prevent double execution.
173 174 175 176 177 178 179 180 181 182 |
# File 'lib/geneva_drive/step_execution.rb', line 173 def start! with_lock do return false unless scheduled? update!( state: "in_progress", started_at: Time.current ) end true end |
#step_definition ⇒ StepDefinition?
Returns the step definition for this execution.
279 280 281 |
# File 'lib/geneva_drive/step_execution.rb', line 279 def step_definition workflow.class.steps.named(step_name) end |
#with_logger(logger) { ... } ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Temporarily overrides the step execution's logger for the duration of the block. The passed logger is used directly - it should already include step-specific tags. The original logger is restored after the block completes.
310 311 312 313 314 315 316 |
# File 'lib/geneva_drive/step_execution.rb', line 310 def with_logger(logger) previous_logger = @logger @logger = logger yield ensure @logger = previous_logger end |