chrono_forge 0.6.0 → 0.7.0.rc1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ca8b29d2a6800467fd237f7fb998a243aa1f31d2de99543017e10f03f89c949
4
- data.tar.gz: 60bb959cb97d8f782348bc80b1c7578c526e62e6a4454909308150c33cc06c9e
3
+ metadata.gz: 249a16358802f143b744130ac2807cea606fe1c3d4c4f2fc20b6ab83e4aaba9b
4
+ data.tar.gz: 4fe9d09279388146179f6f636f4b9a0601aaa758e7d2c0fccf035b278d7e9288
5
5
  SHA512:
6
- metadata.gz: deb1092227fd41a1cb6ce6ac5784cbd8e571893a18294064e648bbbbaa50ca73046457860ccd93c922d44ad2cec197a6bfbdbc12829a12f209da2f978621d893
7
- data.tar.gz: 3cca8a8acff06779828e648415333d85b8ad9161c0211ee30bc11263c3ee5dc953a637a3f8d1bde56c63d1286af01be2312697559b038776dbbf12366e1d2250
6
+ metadata.gz: 7fc5b49ed0beb6082b98a16e8ca764d6430a17a67e717e48350b416e6d2a5482cc05a0bc1f97a5b274c4f232eadfc3b1f9d8298b3e6bf05b9a85cfcd5e33dd41
7
+ data.tar.gz: 5b6a0c8ab5147b689563f2ab7516e78a93159437d10c8adab6de91a851abb1259e93a26413aa17617f17809ca30aec5ac14583bc51d9f7461de3848967a70f3a
data/README.md CHANGED
@@ -146,30 +146,262 @@ OrderProcessingWorkflow.perform_later(
146
146
 
147
147
  #### ⚡ Durable Execution
148
148
 
149
- The `durably_execute` method ensures operations are executed exactly once, even if the job fails and is retried:
149
+ The `durably_execute` method ensures operations are executed exactly once with automatic retry logic and fault tolerance:
150
150
 
151
151
  ```ruby
152
- # Execute a method
153
- durably_execute(:process_payment, max_attempts: 3)
152
+ # Basic execution
153
+ durably_execute :send_welcome_email
154
154
 
155
- # Or with a block
156
- durably_execute -> (ctx) {
157
- Payment.process(ctx[:payment_id])
158
- }
155
+ # With custom retry attempts
156
+ durably_execute :critical_payment_processing, max_attempts: 5
157
+
158
+ # With custom name for tracking multiple calls to same method
159
+ durably_execute :upload_file, name: "profile_image_upload"
160
+
161
+ # Complex example with error-prone operation
162
+ class FileProcessingWorkflow < ApplicationJob
163
+ prepend ChronoForge::Executor
164
+
165
+ def perform(file_id:)
166
+ @file_id = file_id
167
+
168
+ # This might fail due to network issues, rate limits, etc.
169
+ durably_execute :upload_to_s3, max_attempts: 5
170
+
171
+ # Process file after successful upload
172
+ durably_execute :generate_thumbnails, max_attempts: 3
173
+ end
174
+
175
+ private
176
+
177
+ def upload_to_s3
178
+ file = File.find(@file_id)
179
+ S3Client.upload(file.path, bucket: 'my-bucket')
180
+ Rails.logger.info "Successfully uploaded file #{@file_id} to S3"
181
+ end
182
+
183
+ def generate_thumbnails
184
+ ThumbnailService.generate(@file_id)
185
+ end
186
+ end
159
187
  ```
160
188
 
189
+ **Key Features:**
190
+ - **Idempotent**: Same operation won't be executed twice during replays
191
+ - **Automatic Retries**: Failed executions retry with exponential backoff (2^attempt seconds, capped at 32s)
192
+ - **Error Tracking**: All failures are logged with detailed error information
193
+ - **Configurable**: Customize retry attempts and step naming
194
+
161
195
  #### ⏱️ Wait States
162
196
 
163
- ChronoForge supports both time-based and condition-based waits:
197
+ ChronoForge supports three types of wait states, each optimized for different use cases:
198
+
199
+ **1. Time-based Waits (`wait`)**
200
+
201
+ For simple delays and scheduled pauses:
202
+
203
+ ```ruby
204
+ # Simple delays
205
+ wait 30.minutes, "cooling_period"
206
+ wait 1.day, "daily_batch_interval"
207
+
208
+ # Complex workflow with multiple waits
209
+ def user_onboarding_flow
210
+ send_welcome_email
211
+ wait 1.hour, "welcome_delay"
212
+
213
+ send_tutorial_email
214
+ wait 2.days, "tutorial_followup"
215
+
216
+ send_feedback_request
217
+ end
218
+ ```
219
+
220
+ **2. Automated Condition Waits (`wait_until`)**
221
+
222
+ For conditions that can be automatically polled at regular intervals:
164
223
 
165
224
  ```ruby
166
- # Wait for a specific duration
167
- wait 1.hour, :cooling_period
225
+ # Wait for external API
226
+ wait_until :external_api_ready?,
227
+ timeout: 30.minutes,
228
+ check_interval: 1.minute
229
+
230
+ # Wait with retry on specific errors
231
+ wait_until :database_migration_complete?,
232
+ timeout: 2.hours,
233
+ check_interval: 30.seconds,
234
+ retry_on: [ActiveRecord::ConnectionNotEstablished, Net::TimeoutError]
235
+
236
+ # Complex condition example
237
+ def third_party_service_ready?
238
+ response = HTTParty.get("https://api.example.com/health")
239
+ response.code == 200 && response.body.include?("healthy")
240
+ rescue Net::TimeoutError, Net::HTTPClientException
241
+ false # Will be retried at next check interval
242
+ end
168
243
 
169
- # Wait until a condition is met
170
- wait_until :payment_processed,
244
+ wait_until :third_party_service_ready?,
171
245
  timeout: 1.hour,
172
- check_interval: 5.minutes
246
+ check_interval: 2.minutes,
247
+ retry_on: [Net::TimeoutError, Net::HTTPClientException]
248
+ ```
249
+
250
+ **3. Event-driven Waits (`continue_if`)**
251
+
252
+ For conditions that depend on external events like webhooks, requiring manual workflow continuation:
253
+
254
+ ```ruby
255
+ # Basic usage - wait for webhook-driven state change
256
+ continue_if :payment_confirmed?
257
+
258
+ # With custom name for better tracking
259
+ continue_if :payment_confirmed?, name: "stripe_webhook"
260
+
261
+ # Wait for manual approval
262
+ continue_if :document_approved?
263
+
264
+ # Wait for external file processing
265
+ continue_if :processing_complete?
266
+
267
+ # Multiple waits with same condition but different contexts
268
+ continue_if :external_system_ready?, name: "payment_gateway"
269
+ # ... other steps ...
270
+ continue_if :external_system_ready?, name: "inventory_system"
271
+
272
+ # Complete workflow example
273
+ class PaymentWorkflow < ApplicationJob
274
+ prepend ChronoForge::Executor
275
+
276
+ def perform(order_id:)
277
+ @order_id = order_id
278
+
279
+ # Initialize payment
280
+ durably_execute :create_payment_request
281
+
282
+ # Wait for external payment confirmation (webhook-driven)
283
+ continue_if :payment_confirmed?, name: "stripe_confirmation"
284
+
285
+ # Complete order after payment
286
+ durably_execute :fulfill_order
287
+ end
288
+
289
+ private
290
+
291
+ def payment_confirmed?
292
+ PaymentService.confirmed?(@order_id)
293
+ end
294
+ end
295
+
296
+ # Later, when webhook arrives:
297
+ PaymentService.mark_confirmed(order_id)
298
+ PaymentWorkflow.perform_later("order-#{order_id}", order_id: order_id)
299
+ ```
300
+
301
+ **When to Use Each Wait Type:**
302
+
303
+ | Wait Type | Use Case | Polling | Resource Usage | Response Time |
304
+ |-----------|----------|---------|----------------|---------------|
305
+ | `wait` | Fixed delays, rate limiting | None | Minimal | Exact timing |
306
+ | `wait_until` | API readiness, data processing | Automatic | Medium | Check interval |
307
+ | `continue_if` | Webhooks, user actions, file uploads | Manual only | Minimal | Immediate |
308
+
309
+ **Key Differences:**
310
+
311
+ - **`wait`**: Time-based, no condition checking, resumes automatically
312
+ - **`wait_until`**: Condition-based with automatic polling, resumes when condition becomes true or timeout
313
+ - **`continue_if`**: Condition-based without polling, requires manual workflow retry when condition might have changed
314
+
315
+ #### 🔄 Periodic Tasks
316
+
317
+ The `durably_repeat` method enables robust periodic task execution within workflows. Tasks are scheduled at regular intervals until a specified condition is met, with automatic catch-up for missed executions and configurable error handling.
318
+
319
+ ```ruby
320
+ class NotificationWorkflow < ApplicationJob
321
+ prepend ChronoForge::Executor
322
+
323
+ def perform(user_id:)
324
+ @user_id = user_id
325
+
326
+ # Send reminders every 3 days until user completes onboarding
327
+ durably_repeat :send_reminder_email,
328
+ every: 3.days,
329
+ till: :user_onboarded?
330
+
331
+ # Critical payment processing every hour - fail workflow if it fails
332
+ durably_repeat :process_pending_payments,
333
+ every: 1.hour,
334
+ till: :all_payments_processed?,
335
+ on_error: :fail_workflow
336
+ end
337
+
338
+ private
339
+
340
+ def send_reminder_email(scheduled_time = nil)
341
+ # Optional parameter receives the scheduled execution time
342
+ if scheduled_time
343
+ lateness = Time.current - scheduled_time
344
+ Rails.logger.info "Reminder scheduled for #{scheduled_time}, running #{lateness.to_i}s late"
345
+ end
346
+
347
+ UserMailer.onboarding_reminder(@user_id).deliver_now
348
+ end
349
+
350
+ def user_onboarded?
351
+ User.find(@user_id).onboarded?
352
+ end
353
+
354
+ def process_pending_payments
355
+ PaymentProcessor.process_pending_for_user(@user_id)
356
+ end
357
+
358
+ def all_payments_processed?
359
+ Payment.where(user_id: @user_id, status: :pending).empty?
360
+ end
361
+ end
362
+ ```
363
+
364
+ **Key Features:**
365
+
366
+ - **Idempotent Execution**: Each repetition gets a unique execution log, preventing duplicates during replays
367
+ - **Automatic Catch-up**: Missed executions due to downtime are automatically skipped using timeout-based fast-forwarding
368
+ - **Flexible Timing**: Support for custom start times and precise interval scheduling
369
+ - **Error Resilience**: Individual execution failures don't break the periodic schedule
370
+ - **Configurable Error Handling**: Choose between continuing despite failures or failing the entire workflow
371
+
372
+ **Advanced Options:**
373
+
374
+ ```ruby
375
+ durably_repeat :generate_daily_report,
376
+ every: 1.day, # Execution interval
377
+ till: :reports_complete?, # Stop condition
378
+ start_at: Date.tomorrow.beginning_of_day, # Custom start time (optional)
379
+ max_attempts: 5, # Retries per execution (default: 3)
380
+ timeout: 2.hours, # Catch-up timeout (default: 1.hour)
381
+ on_error: :fail_workflow, # Error handling (:continue or :fail_workflow)
382
+ name: "daily_reports" # Custom task name (optional)
383
+ ```
384
+
385
+ **Method Parameters:**
386
+
387
+ Your periodic methods can optionally receive the scheduled execution time as their first argument:
388
+
389
+ ```ruby
390
+ # Without scheduled time parameter
391
+ def cleanup_files
392
+ FileCleanupService.perform
393
+ end
394
+
395
+ # With scheduled time parameter
396
+ def cleanup_files(scheduled_time)
397
+ # Use scheduled time for business logic
398
+ cleanup_date = scheduled_time.to_date
399
+ FileCleanupService.perform(date: cleanup_date)
400
+
401
+ # Log timing information
402
+ delay = Time.current - scheduled_time
403
+ Rails.logger.info "Cleanup was #{delay.to_i} seconds late"
404
+ end
173
405
  ```
174
406
 
175
407
  #### 🔄 Workflow Context
@@ -413,3 +645,27 @@ Please include tests for any new features or bug fixes.
413
645
  ## 📜 License
414
646
 
415
647
  This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
648
+
649
+ ## 📚 API Reference
650
+
651
+ ### Core Workflow Methods
652
+
653
+ | Method | Purpose | Key Parameters |
654
+ |--------|---------|----------------|
655
+ | `durably_execute` | Execute method with retry logic | `method`, `max_attempts: 3`, `name: nil` |
656
+ | `wait` | Time-based pause | `duration`, `name` |
657
+ | `wait_until` | Condition-based waiting | `condition`, `timeout: 1.hour`, `check_interval: 15.minutes`, `retry_on: []` |
658
+ | `continue_if` | Manual continuation wait | `condition`, `name: nil` |
659
+ | `durably_repeat` | Periodic task execution | `method`, `every:`, `till:`, `start_at: nil`, `max_attempts: 3`, `timeout: 1.hour`, `on_error: :continue` |
660
+
661
+ ### Context Methods
662
+
663
+ | Method | Purpose | Example |
664
+ |--------|---------|---------|
665
+ | `context[:key] = value` | Set context value | `context[:user_id] = 123` |
666
+ | `context[:key]` | Get context value | `user_id = context[:user_id]` |
667
+ | `context.set(key, value)` | Set context value (alias) | `context.set(:status, "active")` |
668
+ | `context.set_once(key, value)` | Set only if key doesn't exist | `context.set_once(:created_at, Time.current)` |
669
+ | `context.fetch(key, default)` | Get with default value | `context.fetch(:count, 0)` |
670
+ | `context.key?(key)` | Check if key exists | `context.key?(:user_id)` |
671
+
@@ -0,0 +1,134 @@
1
+ # Example: Webhook-driven Payment Processing Workflow
2
+ #
3
+ # This example demonstrates how to use continue_if for workflows that need to wait
4
+ # for external events like webhook notifications without consuming resources on polling.
5
+
6
+ class PaymentProcessingWorkflow < ApplicationJob
7
+ prepend ChronoForge::Executor
8
+
9
+ def perform(order_id:)
10
+ @order_id = order_id
11
+
12
+ # Store order details in durable context
13
+ context[:order_id] = @order_id
14
+ context[:created_at] = Time.current.iso8601
15
+
16
+ # Step 1: Initialize payment request
17
+ durably_execute :initialize_payment
18
+
19
+ # Step 2: Wait for payment confirmation (webhook-driven)
20
+ # This will halt the workflow until manually retried when webhook arrives
21
+ continue_if :payment_confirmed?, name: "stripe_payment_webhook"
22
+
23
+ # Step 3: Process successful payment
24
+ durably_execute :process_payment_success
25
+
26
+ # Step 4: Send confirmation to customer
27
+ durably_execute :send_confirmation
28
+ end
29
+
30
+ private
31
+
32
+ def initialize_payment
33
+ # Create payment request with external payment provider
34
+ payment_request = PaymentService.create_payment_request(
35
+ order_id: @order_id,
36
+ amount: Order.find(@order_id).total_amount
37
+ )
38
+
39
+ context[:payment_request_id] = payment_request.id
40
+ context[:payment_status] = "pending"
41
+
42
+ Rails.logger.info "Payment request created for order #{@order_id}: #{payment_request.id}"
43
+ end
44
+
45
+ def payment_confirmed?
46
+ # Check if payment has been confirmed by webhook
47
+ payment_id = context[:payment_request_id]
48
+ payment = PaymentService.find_payment(payment_id)
49
+
50
+ confirmed = payment&.status == "confirmed"
51
+
52
+ if confirmed
53
+ context[:payment_status] = "confirmed"
54
+ context[:confirmed_at] = Time.current.iso8601
55
+ end
56
+
57
+ Rails.logger.debug "Payment confirmation check for #{payment_id}: #{confirmed}"
58
+ confirmed
59
+ end
60
+
61
+ def process_payment_success
62
+ # Update order status and inventory
63
+ order = Order.find(@order_id)
64
+ order.mark_as_paid!
65
+
66
+ # Update inventory
67
+ InventoryService.reserve_items(order.items)
68
+
69
+ context[:processed_at] = Time.current.iso8601
70
+ Rails.logger.info "Payment processed successfully for order #{@order_id}"
71
+ end
72
+
73
+ def send_confirmation
74
+ # Send confirmation email to customer
75
+ order = Order.find(@order_id)
76
+ OrderMailer.payment_confirmation(order).deliver_now
77
+
78
+ context[:confirmation_sent_at] = Time.current.iso8601
79
+ Rails.logger.info "Confirmation email sent for order #{@order_id}"
80
+ end
81
+ end
82
+
83
+ # Webhook handler that resumes the workflow
84
+ class PaymentWebhookController < ApplicationController
85
+ def receive
86
+ payment_id = params[:payment_id]
87
+ status = params[:status]
88
+
89
+ if status == "confirmed"
90
+ # Update payment status in your system
91
+ PaymentService.update_payment_status(payment_id, "confirmed")
92
+
93
+ # Find and continue the corresponding workflow
94
+ order_id = PaymentService.find_payment(payment_id).order_id
95
+ workflow_key = "payment-#{order_id}"
96
+
97
+ # Continue the workflow from where it left off (continue_if)
98
+ PaymentProcessingWorkflow.perform_later(workflow_key, order_id: order_id)
99
+
100
+ Rails.logger.info "Payment confirmed via webhook, continuing workflow #{workflow_key}"
101
+ end
102
+
103
+ head :ok
104
+ end
105
+ end
106
+
107
+ # Usage example:
108
+ #
109
+ # 1. Start the workflow:
110
+ # PaymentProcessingWorkflow.perform_later("payment-order-123", order_id: "order-123")
111
+ #
112
+ # 2. Workflow will:
113
+ # - Create payment request
114
+ # - Check if payment is confirmed (initially false)
115
+ # - Halt execution and wait in idle state
116
+ #
117
+ # 3. When webhook arrives:
118
+ # - PaymentWebhookController#receive processes webhook
119
+ # - Updates payment status to "confirmed"
120
+ # - Calls PaymentProcessingWorkflow.perform_later("payment-order-123", order_id: "order-123")
121
+ #
122
+ # 4. Workflow resumes:
123
+ # - Re-evaluates payment_confirmed? (now returns true)
124
+ # - Continues with processing payment success
125
+ # - Sends confirmation email
126
+ # - Completes workflow
127
+
128
+ # Key benefits of continue_if vs wait_until:
129
+ #
130
+ # 1. No resource consumption: No background polling jobs
131
+ # 2. Instant response: Resumes immediately when condition changes
132
+ # 3. Webhook-friendly: Perfect for external event-driven workflows
133
+ # 4. Durable: Survives system restarts and deployments
134
+ # 5. Traceable: All state changes are logged in execution logs
@@ -0,0 +1,159 @@
1
+ module ChronoForge
2
+ module Executor
3
+ module Methods
4
+ module ContinueIf
5
+ # Waits until a specified condition becomes true, without any automatic polling or time-based checks.
6
+ #
7
+ # This method provides a durable pause state that can only be resumed by manually retrying
8
+ # the workflow (typically triggered by external events like webhooks). Unlike wait_until,
9
+ # this method does not automatically poll the condition - it simply evaluates the condition
10
+ # once and either proceeds (if true) or halts execution (if false) until manually retried.
11
+ #
12
+ # @param condition [Symbol] The name of the instance method to evaluate as the condition.
13
+ # The method should return a truthy value when the condition is met.
14
+ # @param name [String, nil] Optional custom name for this step. If not provided, uses the condition name.
15
+ # Useful for tracking multiple calls to the same condition or providing more descriptive names.
16
+ #
17
+ # @return [true] When the condition is met
18
+ #
19
+ # @example Basic usage
20
+ # continue_if :payment_confirmed?
21
+ #
22
+ # @example With custom name
23
+ # continue_if :payment_confirmed?, name: "stripe_payment_confirmation"
24
+ #
25
+ # @example Waiting for external webhook
26
+ # continue_if :webhook_received?
27
+ #
28
+ # @example Waiting for manual approval
29
+ # continue_if :approval_granted?
30
+ #
31
+ # @example Multiple continue_if with same condition but different names
32
+ # continue_if :external_system_ready?, name: "payment_system_ready"
33
+ # # ... other workflow steps ...
34
+ # continue_if :external_system_ready?, name: "inventory_system_ready"
35
+ #
36
+ # @example Complete workflow with manual continuation
37
+ # def perform(order_id:)
38
+ # @order_id = order_id
39
+ #
40
+ # # Process initial order
41
+ # durably_execute :initialize_order
42
+ #
43
+ # # Wait for external payment confirmation (webhook-driven)
44
+ # continue_if :payment_confirmed?, name: "stripe_webhook"
45
+ #
46
+ # # Complete order processing
47
+ # durably_execute :complete_order
48
+ # end
49
+ #
50
+ # private
51
+ #
52
+ # def payment_confirmed?
53
+ # PaymentService.confirmed?(@order_id)
54
+ # end
55
+ #
56
+ # # Later, when webhook arrives:
57
+ # # PaymentService.mark_confirmed(order_id)
58
+ # # OrderProcessingWorkflow.perform_later("order-#{order_id}", order_id: order_id)
59
+ #
60
+ # == Behavior
61
+ #
62
+ # === Condition Evaluation
63
+ # The condition method is called once per workflow execution:
64
+ # - If truthy, execution continues immediately
65
+ # - If falsy, workflow execution halts until manually retried
66
+ # - No automatic polling or retry attempts are made
67
+ #
68
+ # === Manual Retry Required
69
+ # Unlike other wait states, continue_if requires external intervention:
70
+ # - Call Workflow.perform_later(key, **kwargs) to continue the workflow
71
+ # - Typically triggered by webhooks, background jobs, or manual intervention
72
+ # - No timeout or automatic resumption
73
+ #
74
+ # === Error Handling
75
+ # - Exceptions during condition evaluation cause workflow failure
76
+ # - No automatic retry on condition evaluation errors
77
+ # - Use try/catch in condition methods for error handling
78
+ #
79
+ # === Persistence and Resumability
80
+ # - Wait state is persisted in execution logs
81
+ # - Workflow can be stopped/restarted without losing wait progress
82
+ # - Condition evaluation state persists across restarts
83
+ # - Safe for system interruptions and deployments
84
+ #
85
+ # === Execution Logs
86
+ # Creates execution log with step name: `continue_if$#{name || condition}`
87
+ # - Tracks attempt count and execution times
88
+ # - Records final result (true for success)
89
+ #
90
+ # === Use Cases
91
+ # Perfect for workflows that depend on:
92
+ # - External webhook notifications
93
+ # - Manual approval processes
94
+ # - File uploads or external processing completion
95
+ # - Third-party system state changes
96
+ # - User actions or form submissions
97
+ #
98
+ def continue_if(condition, name: nil)
99
+ step_name = "continue_if$#{name || condition}"
100
+
101
+ # Find or create execution log
102
+ execution_log = ExecutionLog.create_or_find_by!(
103
+ workflow: @workflow,
104
+ step_name: step_name
105
+ ) do |log|
106
+ log.started_at = Time.current
107
+ log.metadata = {
108
+ condition: condition.to_s,
109
+ name: name
110
+ }
111
+ end
112
+
113
+ # Return if already completed
114
+ if execution_log.completed?
115
+ return execution_log.metadata["result"]
116
+ end
117
+
118
+ # Evaluate condition once
119
+ begin
120
+ execution_log.update!(
121
+ attempts: execution_log.attempts + 1,
122
+ last_executed_at: Time.current
123
+ )
124
+
125
+ condition_met = send(condition)
126
+ rescue HaltExecutionFlow
127
+ raise
128
+ rescue => e
129
+ # Log the error and fail the execution
130
+ Rails.logger.error { "Error evaluating condition #{condition}: #{e.message}" }
131
+ self.class::ExecutionTracker.track_error(workflow, e)
132
+
133
+ execution_log.update!(
134
+ state: :failed,
135
+ error_message: e.message,
136
+ error_class: e.class.name
137
+ )
138
+ raise ExecutionFailedError, "#{step_name} failed with an error: #{e.message}"
139
+ end
140
+
141
+ # Handle condition met
142
+ if condition_met
143
+ execution_log.update!(
144
+ state: :completed,
145
+ completed_at: Time.current,
146
+ metadata: execution_log.metadata.merge("result" => true)
147
+ )
148
+ return true
149
+ end
150
+
151
+ # Condition not met - halt execution without scheduling any retry
152
+ # Workflow will remain in idle state until manually retried
153
+ Rails.logger.debug { "Condition not met for #{step_name}, workflow will wait for manual retry" }
154
+ halt_execution!
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end