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.
@@ -4,6 +4,49 @@ module ChronoForge
4
4
  module WorkflowStates
5
5
  private
6
6
 
7
+ # Marks a workflow as successfully completed.
8
+ #
9
+ # This method provides durable workflow completion tracking with proper state
10
+ # management and execution logging. It ensures the workflow state is updated
11
+ # atomically and any completion side effects are properly recorded.
12
+ #
13
+ # @return [ExecutionLog] The execution log created for the completion event
14
+ #
15
+ # @raise [StandardError] If workflow completion fails for any reason
16
+ #
17
+ # @example Basic usage (typically called automatically)
18
+ # complete_workflow!
19
+ #
20
+ # @example In a workflow executor
21
+ # def execute
22
+ # process_all_steps
23
+ # complete_workflow! # Called when all steps succeed
24
+ # rescue => e
25
+ # fail_workflow!(error_log)
26
+ # end
27
+ #
28
+ # == Behavior
29
+ #
30
+ # === State Management
31
+ # - Sets workflow.completed_at to current timestamp
32
+ # - Transitions workflow state to 'completed'
33
+ # - Updates are atomic to prevent race conditions
34
+ #
35
+ # === Execution Logging
36
+ # - Creates execution log with step name: "$workflow_completion$"
37
+ # - Tracks completion attempt and timing information
38
+ # - Marks execution log as completed when successful
39
+ # - Records any errors that occur during completion
40
+ #
41
+ # === Error Handling
42
+ # - Any exceptions during completion are caught and logged
43
+ # - Execution log is marked as failed with error details
44
+ # - Original exception is re-raised after logging
45
+ #
46
+ # === Idempotency
47
+ # - Uses create_or_find_by to prevent duplicate completion logs
48
+ # - Safe to call multiple times without side effects
49
+ #
7
50
  def complete_workflow!
8
51
  # Create an execution log for workflow completion
9
52
  execution_log = ExecutionLog.create_or_find_by!(
@@ -41,6 +84,62 @@ module ChronoForge
41
84
  end
42
85
  end
43
86
 
87
+ # Marks a workflow as failed due to an unrecoverable error.
88
+ #
89
+ # This method provides durable workflow failure tracking with proper state
90
+ # management and error context preservation. It ensures the workflow state
91
+ # is updated atomically and failure details are properly recorded.
92
+ #
93
+ # @param error_log [ErrorLog] The error log associated with the failure
94
+ #
95
+ # @return [ExecutionLog] The execution log created for the failure event
96
+ #
97
+ # @raise [StandardError] If workflow failure processing fails for any reason
98
+ #
99
+ # @example Basic usage
100
+ # begin
101
+ # risky_operation
102
+ # rescue => e
103
+ # error_log = log_error(e)
104
+ # fail_workflow!(error_log)
105
+ # end
106
+ #
107
+ # @example In a workflow executor with error handling
108
+ # def execute
109
+ # process_all_steps
110
+ # complete_workflow!
111
+ # rescue ExecutionFailedError => e
112
+ # error_log = ErrorLog.create!(workflow: @workflow, error: e)
113
+ # fail_workflow!(error_log)
114
+ # end
115
+ #
116
+ # == Behavior
117
+ #
118
+ # === State Management
119
+ # - Transitions workflow state to 'failed'
120
+ # - Updates are atomic to prevent race conditions
121
+ # - Preserves relationship to causing error log
122
+ #
123
+ # === Execution Logging
124
+ # - Creates execution log with step name: "$workflow_failure$#{error_log.id}"
125
+ # - Links to the error_log that caused the failure via metadata
126
+ # - Tracks failure processing attempt and timing information
127
+ # - Marks execution log as completed when failure processing succeeds
128
+ #
129
+ # === Error Context
130
+ # - Maintains reference to original error through error_log parameter
131
+ # - Enables debugging and failure analysis through error log relationship
132
+ # - Preserves error details for workflow monitoring and alerting
133
+ #
134
+ # === Error Handling
135
+ # - Any exceptions during failure processing are caught and logged
136
+ # - Execution log is marked as failed with error details
137
+ # - Original exception is re-raised after logging
138
+ #
139
+ # === Idempotency
140
+ # - Uses create_or_find_by to prevent duplicate failure logs
141
+ # - Safe to call multiple times with same error_log
142
+ #
44
143
  def fail_workflow!(error_log)
45
144
  # Create an execution log for workflow failure
46
145
  execution_log = ExecutionLog.create_or_find_by!(
@@ -80,6 +179,68 @@ module ChronoForge
80
179
  end
81
180
  end
82
181
 
182
+ # Retries a stalled or failed workflow by releasing locks and resetting state.
183
+ #
184
+ # This method provides durable workflow retry functionality with proper
185
+ # state validation and lock management. It ensures that only eligible
186
+ # workflows can be retried and tracks retry attempts for monitoring.
187
+ #
188
+ # @return [ExecutionLog] The execution log created for the retry event
189
+ #
190
+ # @raise [WorkflowNotRetryableError] If workflow is not in a retryable state
191
+ # @raise [StandardError] If retry processing fails for any reason
192
+ #
193
+ # @example Basic usage
194
+ # workflow = Workflow.find_by(key: 'stuck-workflow-123')
195
+ # workflow.retry_workflow! if workflow.stalled?
196
+ #
197
+ # @example With error handling
198
+ # begin
199
+ # retry_workflow!
200
+ # Rails.logger.info "Workflow retry initiated successfully"
201
+ # rescue WorkflowNotRetryableError => e
202
+ # Rails.logger.warn "Cannot retry workflow: #{e.message}"
203
+ # end
204
+ #
205
+ # @example In a monitoring system
206
+ # def retry_stalled_workflows
207
+ # Workflow.stalled.find_each do |workflow|
208
+ # begin
209
+ # workflow.retry_workflow!
210
+ # rescue WorkflowNotRetryableError
211
+ # # Skip non-retryable workflows
212
+ # end
213
+ # end
214
+ # end
215
+ #
216
+ # == Behavior
217
+ #
218
+ # === State Validation
219
+ # - Only allows retry of workflows in 'stalled' or 'failed' states
220
+ # - Raises WorkflowNotRetryableError for workflows in other states
221
+ # - Prevents retry of running or completed workflows
222
+ #
223
+ # === Lock Management
224
+ # - Forcibly releases any existing workflow locks using LockStrategy
225
+ # - Enables workflow to be picked up by new executor instances
226
+ # - Prevents deadlock situations during retry
227
+ #
228
+ # === Execution Logging
229
+ # - Creates execution log with step name: "$workflow_retry$#{timestamp}"
230
+ # - Records previous workflow state in metadata
231
+ # - Tracks retry request timestamp and job_id
232
+ # - Marks execution log as completed when retry succeeds
233
+ #
234
+ # === Retry Tracking
235
+ # - Preserves historical context of what state triggered the retry
236
+ # - Enables monitoring of retry frequency and patterns
237
+ # - Helps diagnose recurring workflow issues
238
+ #
239
+ # === Error Handling
240
+ # - Any exceptions during retry processing are caught and logged
241
+ # - Execution log is marked as failed with error details
242
+ # - Original exception is re-raised after logging
243
+ #
83
244
  def retry_workflow!
84
245
  # Check if the workflow is stalled or failed
85
246
  unless workflow.stalled? || workflow.failed?
@@ -3,7 +3,9 @@ module ChronoForge
3
3
  module Methods
4
4
  include Methods::Wait
5
5
  include Methods::WaitUntil
6
+ include Methods::ContinueIf
6
7
  include Methods::DurablyExecute
8
+ include Methods::DurablyRepeat
7
9
  include Methods::WorkflowStates
8
10
  end
9
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChronoForge
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0.rc1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chrono_forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-28 00:00:00.000000000 Z
11
+ date: 2025-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -181,8 +181,7 @@ files:
181
181
  - README.md
182
182
  - Rakefile
183
183
  - config.ru
184
- - export.json
185
- - export.rb
184
+ - examples/continue_if_webhook_example.rb
186
185
  - gemfiles/rails_7.1.gemfile
187
186
  - gemfiles/rails_7.1.gemfile.lock
188
187
  - lib/chrono_forge.rb
@@ -193,7 +192,9 @@ files:
193
192
  - lib/chrono_forge/executor/execution_tracker.rb
194
193
  - lib/chrono_forge/executor/lock_strategy.rb
195
194
  - lib/chrono_forge/executor/methods.rb
195
+ - lib/chrono_forge/executor/methods/continue_if.rb
196
196
  - lib/chrono_forge/executor/methods/durably_execute.rb
197
+ - lib/chrono_forge/executor/methods/durably_repeat.rb
197
198
  - lib/chrono_forge/executor/methods/wait.rb
198
199
  - lib/chrono_forge/executor/methods/wait_until.rb
199
200
  - lib/chrono_forge/executor/methods/workflow_states.rb
@@ -223,9 +224,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
223
224
  version: 3.2.2
224
225
  required_rubygems_version: !ruby/object:Gem::Requirement
225
226
  requirements:
226
- - - ">="
227
+ - - ">"
227
228
  - !ruby/object:Gem::Version
228
- version: '0'
229
+ version: 1.3.1
229
230
  requirements: []
230
231
  rubygems_version: 3.4.10
231
232
  signing_key:
data/export.json DELETED
@@ -1,118 +0,0 @@
1
- [
2
- {
3
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/CHANGELOG.md",
4
- "contents": "## [Unreleased]\n\n## [0.1.0] - 2024-12-21\n\n- Initial release\n"
5
- },
6
- {
7
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/README.md",
8
- "contents": "# ChronoForge\n\n![Version](https://img.shields.io/badge/version-0.3.0-blue.svg)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n> A robust framework for building durable, distributed workflows in Ruby on Rails applications\n\nChronoForge provides a powerful solution for handling long-running processes, managing state, and recovering from failures in your Rails applications. Built on top of ActiveJob, it ensures your critical business processes remain resilient and traceable.\n\n## 🌟 Features\n\n- **Durable Execution**: Automatically tracks and recovers from failures during workflow execution\n- **State Management**: Built-in workflow state tracking with persistent context storage\n- **Concurrency Control**: Advanced locking mechanisms to prevent parallel execution of the same workflow\n- **Error Handling**: Comprehensive error tracking with configurable retry strategies\n- **Execution Logging**: Detailed logging of workflow steps and errors for visibility\n- **Wait States**: Support for time-based waits and condition-based waiting\n- **Database-Backed**: All workflow state is persisted to ensure durability\n- **ActiveJob Integration**: Compatible with all ActiveJob backends, though database-backed processors (like Solid Queue) provide the most reliable experience for long-running workflows\n\n## 📦 Installation\n\nAdd to your application's Gemfile:\n\n```ruby\ngem 'chrono_forge'\n```\n\nThen execute:\n\n```bash\n$ bundle install\n```\n\nOr install directly:\n\n```bash\n$ gem install chrono_forge\n```\n\nAfter installation, run the generator to create the necessary database migrations:\n\n```bash\n$ rails generate chrono_forge:install\n$ rails db:migrate\n```\n\n## 📋 Usage\n\n### Creating and Executing Workflows\n\nChronoForge workflows are ActiveJob classes that prepend the `ChronoForge::Executor` module. Each workflow can **only** accept keyword arguments:\n\n```ruby\n# Define your workflow class\nclass OrderProcessingWorkflow < ApplicationJob\n prepend ChronoForge::Executor\n \n def perform(order_id:, customer_id:)\n # Workflow steps...\n end\nend\n```\n\nAll workflows require a unique identifier when executed. This identifier is used to track and manage the workflow:\n\n```ruby\n# Execute the workflow\nOrderProcessingWorkflow.perform_later(\n \"order-123\", # Unique workflow key\n order_id: \"order-134\", # Custom kwargs\n customer_id: \"customer-456\" # More custom kwargs\n)\n```\n\n### Basic Workflow Example\n\nHere's a complete example of a durable order processing workflow:\n\n```ruby\nclass OrderProcessingWorkflow < ApplicationJob\n prepend ChronoForge::Executor\n\n def perform(order_id:)\n @order_id = order_id\n\n # Context can be used to pass and store data between executions\n context.set_once \"execution_id\", SecureRandom.hex\n\n # Wait until payment is confirmed\n wait_until :payment_confirmed?\n\n # Wait for potential fraud check\n wait 1.minute, :fraud_check_delay\n\n # Durably execute order processing\n durably_execute :process_order\n\n # Final steps\n complete_order\n end\n\n private\n\n def payment_confirmed?\n PaymentService.confirmed?(@order_id, context[\"execution_id\"])\n end\n\n def process_order\n OrderProcessor.process(@order_id, context[\"execution_id\"])\n context[\"processed_at\"] = Time.current.iso8601\n end\n\n def complete_order\n OrderCompletionService.complete(@order_id, context[\"execution_id\"])\n context[\"completed_at\"] = Time.current.iso8601\n end\nend\n```\n\n### Core Workflow Features\n\n#### 🚀 Executing Workflows\n\nChronoForge workflows are executed through ActiveJob's standard interface with a specific parameter structure:\n\n```ruby\n# Perform the workflow immediately\nOrderProcessingWorkflow.perform_now(\n \"order-123\", # Unique workflow key\n order_id: \"O-123\", # Custom parameter\n customer_id: \"C-456\" # Another custom parameter\n)\n\n# Or queue it for background processing\nOrderProcessingWorkflow.perform_later(\n \"order-123-async\", # Unique workflow key\n order_id: \"O-124\",\n customer_id: \"C-457\"\n)\n```\n\n**Important:** Workflows must use keyword arguments only, not positional arguments.\n\n#### ⚡ Durable Execution\n\nThe `durably_execute` method ensures operations are executed exactly once, even if the job fails and is retried:\n\n```ruby\n# Execute a method\ndurably_execute(:process_payment, max_attempts: 3)\n\n# Or with a block\ndurably_execute -> (ctx) {\n Payment.process(ctx[:payment_id])\n}\n```\n\n#### ⏱️ Wait States\n\nChronoForge supports both time-based and condition-based waits:\n\n```ruby\n# Wait for a specific duration\nwait 1.hour, :cooling_period\n\n# Wait until a condition is met\nwait_until :payment_processed, \n timeout: 1.hour,\n check_interval: 5.minutes\n```\n\n#### 🔄 Workflow Context\n\nChronoForge provides a persistent context that survives job restarts. The context behaves like a Hash but with additional capabilities:\n\n```ruby\n# Set context values\ncontext[:user_name] = \"John Doe\"\ncontext[:status] = \"processing\"\n\n# Read context values\nuser_name = context[:user_name]\n\n# Using the fetch method (returns default if key doesn't exist)\nstatus = context.fetch(:status, \"pending\")\n\n# Set a value with the set method (alias for []=)\ncontext.set(:total_amount, 99.99)\n\n# Set a value only if the key doesn't already exist\ncontext.set_once(:created_at, Time.current.iso8601)\n\n# Check if a key exists\nif context.key?(:user_id)\n # Do something with the user ID\nend\n```\n\nThe context supports serializable Ruby objects (Hash, Array, String, Integer, Float, Boolean, and nil) and validates types automatically.\n\n### 🛡️ Error Handling\n\nChronoForge automatically tracks errors and provides configurable retry capabilities:\n\n```ruby\nclass MyWorkflow < ApplicationJob\n prepend ChronoForge::Executor\n\n private\n\n def should_retry?(error, attempt_count)\n case error\n when NetworkError\n attempt_count < 5 # Retry network errors up to 5 times\n when ValidationError\n false # Don't retry validation errors\n else\n attempt_count < 3 # Default retry policy\n end\n end\nend\n```\n\n## 🧪 Testing\n\nChronoForge is designed to be easily testable using [ChaoticJob](https://github.com/fractaledmind/chaotic_job), a testing framework that makes it simple to test complex job workflows:\n\n1. Add ChaoticJob to your Gemfile's test group:\n\n```ruby\ngroup :test do\n gem 'chaotic_job'\nend\n```\n\n2. Set up your test helper:\n\n```ruby\n# test_helper.rb\nrequire 'chrono_forge'\nrequire 'minitest/autorun'\nrequire 'chaotic_job'\n```\n\nExample test:\n\n```ruby\nclass WorkflowTest < ActiveJob::TestCase\n include ChaoticJob::Helpers\n\n def test_workflow_completion\n # Enqueue the job with a unique key and custom parameters\n OrderProcessingWorkflow.perform_later(\n \"order-test-123\",\n order_id: \"O-123\",\n customer_id: \"C-456\"\n )\n \n # Perform all enqueued jobs\n perform_all_jobs\n \n # Assert workflow completed successfully\n workflow = ChronoForge::Workflow.find_by(key: \"order-test-123\")\n assert workflow.completed?\n \n # Check workflow context\n assert workflow.context[\"processed_at\"].present?\n assert workflow.context[\"completed_at\"].present?\n end\nend\n```\n\n## 🗄️ Database Schema\n\nChronoForge creates three main tables:\n\n1. **chrono_forge_workflows**: Stores workflow state and context\n2. **chrono_forge_execution_logs**: Tracks individual execution steps\n3. **chrono_forge_error_logs**: Records detailed error information\n\n## 🔍 When to Use ChronoForge\n\nChronoForge is ideal for:\n\n- **Long-running business processes** - Order processing, account registration flows\n- **Processes requiring durability** - Financial transactions, data migrations\n- **Multi-step workflows** - Onboarding flows, approval processes, multi-stage jobs\n- **State machines with time-based transitions** - Document approval, subscription lifecycle\n\n## 🚀 Development\n\nAfter checking out the repo, run:\n\n```bash\n$ bin/setup # Install dependencies\n$ bundle exec rake test # Run the tests\n$ bin/appraise # Run the full suite of appraisals\n$ bin/console # Start an interactive console\n```\n\nThe test suite uses SQLite by default and includes:\n- Unit tests for core functionality\n- Integration tests with ActiveJob\n- Example workflow implementations\n\n## 👥 Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin feature/my-new-feature`)\n5. Create a new Pull Request\n\nPlease include tests for any new features or bug fixes.\n\n## 📜 License\n\nThis gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n"
9
- },
10
- {
11
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/chrono_forge.gemspec",
12
- "contents": "# frozen_string_literal: true\n\nrequire_relative \"lib/chrono_forge/version\"\n\nGem::Specification.new do |spec|\n spec.name = \"chrono_forge\"\n spec.version = ChronoForge::VERSION\n spec.authors = [\"Stefan Froelich\"]\n spec.email = [\"sfroelich01@gmail.com\"]\n\n spec.summary = \"A durable workflow engine for Ruby on Rails built on ActiveJob\"\n spec.description = \"ChronoForge provides a robust framework for building durable, distributed workflows in Ruby on Rails applications. It offers reliable state management, error recovery, and workflow orchestration through features like durable execution, wait states, and comprehensive error tracking.\"\n spec.homepage = \"https://github.com/radioactive-labs/chrono_forge\"\n spec.license = \"MIT\"\n spec.required_ruby_version = \">= 3.2.2\"\n\n spec.metadata[\"allowed_push_host\"] = \"https://rubygems.org\"\n\n spec.metadata[\"homepage_uri\"] = spec.homepage\n spec.metadata[\"source_code_uri\"] = spec.homepage\n spec.metadata[\"changelog_uri\"] = spec.homepage\n\n # Specify which files should be added to the gem when it is released.\n # The `git ls-files -z` loads the files in the RubyGem that have been added into git.\n gemspec = File.basename(__FILE__)\n spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|\n ls.readlines(\"\\x0\", chomp: true).reject do |f|\n (f == gemspec) ||\n f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])\n end\n end\n spec.bindir = \"exe\"\n spec.executables = spec.files.grep(%r{\\Aexe/}) { |f| File.basename(f) }\n spec.require_paths = [\"lib\"]\n\n spec.add_dependency \"activerecord\"\n spec.add_dependency \"activejob\"\n spec.add_dependency \"zeitwerk\"\n\n spec.add_development_dependency \"rake\"\n spec.add_development_dependency \"minitest\"\n spec.add_development_dependency \"minitest-reporters\"\n spec.add_development_dependency \"standard\"\n # spec.add_development_dependency \"brakeman\"\n spec.add_development_dependency \"bundle-audit\"\n spec.add_development_dependency \"appraisal\"\n spec.add_development_dependency \"combustion\"\n spec.add_development_dependency \"chaotic_job\"\nend\n"
13
- },
14
- {
15
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/export.rb",
16
- "contents": "require \"json\"\nrequire \"find\"\n\ndef export_files_to_json(directory, extensions, output_file, exceptions = [])\n # Convert extensions to lowercase for case-insensitive matching\n extensions = extensions.map(&:downcase)\n\n # Array to store file data\n files_data = []\n\n # Find all files in directory and subdirectories\n Find.find(directory) do |path|\n # Skip if not a file\n next unless File.file?(path)\n next if exceptions.any? { |exception| path.include?(exception) }\n\n # Check if file extension matches any in our list\n ext = File.extname(path).downcase[1..] # Remove the leading dot\n next unless extensions.include?(ext)\n\n puts path\n\n begin\n # Read file contents\n contents = File.read(path)\n\n # Add to our array\n files_data << {\n \"path\" => path,\n \"contents\" => contents\n }\n rescue => e\n puts \"Error reading file #{path}: #{e.message}\"\n end\n end\n\n # Write to JSON file\n File.write(output_file, JSON.pretty_generate(files_data))\n\n puts \"Successfully exported #{files_data.length} files to #{output_file}\"\nend\n\n# Example usage (uncomment and modify as needed):\ndirectory = \"/Users/stefan/Documents/plutonium/chrono_forge\"\nexceptions = [\"/.github/\", \"/.vscode/\", \"gemfiles\", \"pkg\", \"node_modules\"]\nextensions = [\"rb\", \"md\", \"yml\", \"yaml\", \"gemspec\"]\noutput_file = \"export.json\"\nexport_files_to_json(directory, extensions, output_file, exceptions)\n"
17
- },
18
- {
19
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/error_log.rb",
20
- "contents": "# frozen_string_literal: true\n\n# == Schema Information\n#\n# Table name: chrono_forge_error_logs\n#\n# id :integer not null, primary key\n# backtrace :text\n# context :json\n# error_class :string\n# error_message :text\n# created_at :datetime not null\n# updated_at :datetime not null\n# workflow_id :integer not null\n#\n# Indexes\n#\n# index_chrono_forge_error_logs_on_workflow_id (workflow_id)\n#\n# Foreign Keys\n#\n# workflow_id (workflow_id => chrono_forge_workflows.id)\n#\n\nmodule ChronoForge\n class ErrorLog < ActiveRecord::Base\n self.table_name = \"chrono_forge_error_logs\"\n\n belongs_to :workflow\n end\nend\n"
21
- },
22
- {
23
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/execution_log.rb",
24
- "contents": "# frozen_string_literal: true\n\n# == Schema Information\n#\n# Table name: chrono_forge_execution_logs\n#\n# id :integer not null, primary key\n# attempts :integer default(0), not null\n# completed_at :datetime\n# error_class :string\n# error_message :text\n# last_executed_at :datetime\n# metadata :json\n# started_at :datetime\n# state :integer default(\"pending\"), not null\n# step_name :string not null\n# created_at :datetime not null\n# updated_at :datetime not null\n# workflow_id :integer not null\n#\n# Indexes\n#\n# idx_on_workflow_id_step_name_11bea8586e (workflow_id,step_name) UNIQUE\n# index_chrono_forge_execution_logs_on_workflow_id (workflow_id)\n#\n# Foreign Keys\n#\n# workflow_id (workflow_id => chrono_forge_workflows.id)\n#\nmodule ChronoForge\n class ExecutionLog < ActiveRecord::Base\n self.table_name = \"chrono_forge_execution_logs\"\n\n belongs_to :workflow\n\n enum :state, %i[\n pending\n completed\n failed\n ]\n end\nend\n"
25
- },
26
- {
27
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/context.rb",
28
- "contents": "module ChronoForge\n module Executor\n class Context\n class ValidationError < Error; end\n\n ALLOWED_TYPES = [\n String,\n Integer,\n Float,\n TrueClass,\n FalseClass,\n NilClass,\n Hash,\n Array\n ]\n\n def initialize(workflow)\n @workflow = workflow\n @context = workflow.context || {}\n @dirty = false\n end\n\n def []=(key, value)\n set_value(key, value)\n end\n\n def [](key)\n get_value(key)\n end\n\n # Fetches a value from the context\n # Returns the value if the key exists, otherwise returns the default value\n def fetch(key, default = nil)\n key?(key) ? get_value(key) : default\n end\n\n # Sets a value in the context\n # Alias for the []= method\n def set(key, value)\n set_value(key, value)\n end\n\n # Sets a value in the context only if the key doesn't already exist\n # Returns true if the value was set, false otherwise\n def set_once(key, value)\n return false if key?(key)\n\n set_value(key, value)\n true\n end\n\n def key?(key)\n @context.key?(key.to_s)\n end\n\n def save!\n return unless @dirty\n\n @workflow.update_column(:context, @context)\n @dirty = false\n end\n\n private\n\n def set_value(key, value)\n validate_value!(value)\n\n @context[key.to_s] =\n if value.is_a?(Hash) || value.is_a?(Array)\n deep_dup(value)\n else\n value\n end\n\n @dirty = true\n end\n\n def get_value(key)\n @context[key.to_s]\n end\n\n def validate_value!(value)\n unless ALLOWED_TYPES.any? { |type| value.is_a?(type) }\n raise ValidationError, \"Unsupported context value type: #{value.inspect}\"\n end\n\n # Optional: Add size constraints\n if value.is_a?(String) && value.size > 64.kilobytes\n raise ValidationError, \"Context value too large\"\n end\n end\n\n def deep_dup(obj)\n JSON.parse(JSON.generate(obj))\n rescue\n obj.dup\n end\n end\n end\nend\n"
29
- },
30
- {
31
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/execution_tracker.rb",
32
- "contents": "module ChronoForge\n module Executor\n class ExecutionTracker\n def self.track_error(workflow, error)\n # Create a detailed error log\n ErrorLog.create!(\n workflow: workflow,\n error_class: error.class.name,\n error_message: error.message,\n backtrace: error.backtrace.join(\"\\n\"),\n context: workflow.context\n )\n end\n end\n end\nend\n"
33
- },
34
- {
35
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/lock_strategy.rb",
36
- "contents": "module ChronoForge\n module Executor\n class LongRunningConcurrentExecutionError < Error; end\n\n class ConcurrentExecutionError < Error; end\n\n class LockStrategy\n def self.acquire_lock(job_id, workflow, max_duration:)\n ActiveRecord::Base.transaction do\n # Find the workflow with a lock, considering stale locks\n workflow = workflow.lock!\n\n # Check for active execution\n if workflow.locked_at && workflow.locked_at > max_duration.ago\n raise ConcurrentExecutionError, \"Job currently in progress\"\n end\n\n # Atomic update of lock status\n workflow.update_columns(\n locked_by: job_id,\n locked_at: Time.current,\n state: :running\n )\n\n workflow\n end\n end\n\n def self.release_lock(job_id, workflow)\n workflow = workflow.reload\n if workflow.locked_by != job_id\n raise LongRunningConcurrentExecutionError,\n \"#{self.class}(#{job_id}) executed longer than specified max_duration, \" \\\n \"allowing another instance(#{workflow.locked_by}) to acquire the lock.\"\n end\n\n columns = {locked_at: nil, locked_by: nil}\n columns[:state] = :idle if workflow.running?\n\n workflow.update_columns(columns)\n end\n end\n end\nend\n"
37
- },
38
- {
39
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/durably_execute.rb",
40
- "contents": "module ChronoForge\n module Executor\n module Methods\n module DurablyExecute\n def durably_execute(method, **options)\n # Create execution log\n step_name = \"durably_execute$#{method}\"\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: @workflow,\n step_name: step_name\n ) do |log|\n log.started_at = Time.current\n end\n\n # Return if already completed\n return if execution_log.completed?\n\n # Execute with error handling\n begin\n # Update execution log with attempt\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n # Execute the method\n if method.is_a?(Symbol)\n send(method)\n else\n method.call(@context)\n end\n\n # Complete the execution\n execution_log.update!(\n state: :completed,\n completed_at: Time.current\n )\n \n # return nil\n nil\n rescue HaltExecutionFlow\n raise\n rescue => e\n # Log the error\n Rails.logger.error { \"Error while durably executing #{method}: #{e.message}\" }\n self.class::ExecutionTracker.track_error(workflow, e)\n\n # Optional retry logic\n if execution_log.attempts < (options[:max_attempts] || 3)\n # Reschedule with exponential backoff\n backoff = (2**[execution_log.attempts || 1, 5].min).seconds\n\n self.class\n .set(wait: backoff)\n .perform_later(\n @workflow.key,\n retry_method: method\n )\n\n # Halt current execution\n halt_execution!\n else\n # Max attempts reached\n execution_log.update!(\n state: :failed,\n error_message: e.message,\n error_class: e.class.name\n )\n raise ExecutionFailedError, \"#{step_name} failed after maximum attempts\"\n end\n end\n end\n end\n end\n end\nend\n"
41
- },
42
- {
43
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/wait.rb",
44
- "contents": "module ChronoForge\n module Executor\n module Methods\n module Wait\n def wait(duration, name, **options)\n # Create execution log\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: @workflow,\n step_name: \"wait$#{name}\"\n ) do |log|\n log.started_at = Time.current\n log.metadata = {\n wait_until: duration.from_now\n }\n end\n\n # Return if already completed\n return if execution_log.completed?\n\n # Check if wait period has passed\n if Time.current >= Time.parse(execution_log.metadata[\"wait_until\"])\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n state: :completed,\n completed_at: Time.current,\n last_executed_at: Time.current\n )\n return\n end\n\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n # Reschedule the job\n self.class\n .set(wait: duration)\n .perform_later(@workflow.key)\n\n # Halt current execution\n halt_execution!\n end\n end\n end\n end\nend\n"
45
- },
46
- {
47
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/wait_until.rb",
48
- "contents": "module ChronoForge\n module Executor\n class WaitConditionNotMet < ExecutionFailedError; end\n\n module Methods\n module WaitUntil\n def wait_until(condition, **options)\n # Default timeout and check interval\n timeout = options[:timeout] || 1.hour\n check_interval = options[:check_interval] || 15.minutes\n\n # Find or create execution log\n step_name = \"wait_until$#{condition}\"\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: @workflow,\n step_name: step_name\n ) do |log|\n log.started_at = Time.current\n log.metadata = {\n timeout_at: timeout.from_now,\n check_interval: check_interval,\n condition: condition.to_s\n }\n end\n\n # Return if already completed\n if execution_log.completed?\n return execution_log.metadata[\"result\"]\n end\n\n # Evaluate condition\n begin\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n condition_met = if condition.is_a?(Proc)\n condition.call(@context)\n elsif condition.is_a?(Symbol)\n send(condition)\n else\n raise ArgumentError, \"Unsupported condition type\"\n end\n rescue HaltExecutionFlow\n raise\n rescue => e\n # Log the error\n Rails.logger.error { \"Error evaluating condition #{condition}: #{e.message}\" }\n self.class::ExecutionTracker.track_error(workflow, e)\n\n # Optional retry logic\n if (options[:retry_on] || []).include?(e.class)\n # Reschedule with exponential backoff\n backoff = (2**[execution_log.attempts || 1, 5].min).seconds\n\n self.class\n .set(wait: backoff)\n .perform_later(\n @workflow.key\n )\n\n # Halt current execution\n halt_execution!\n else\n execution_log.update!(\n state: :failed,\n error_message: e.message,\n error_class: e.class.name\n )\n raise ExecutionFailedError, \"#{step_name} failed with an error: #{e.message}\"\n end\n end\n\n # Handle condition met\n if condition_met\n execution_log.update!(\n state: :completed,\n completed_at: Time.current,\n metadata: execution_log.metadata.merge(\"result\" => true)\n )\n return true\n end\n\n # Check for timeout\n metadata = execution_log.metadata\n if Time.current > metadata[\"timeout_at\"]\n execution_log.update!(\n state: :failed,\n metadata: metadata.merge(\"result\" => nil)\n )\n Rails.logger.warn { \"Timeout reached for condition '#{condition}'.\" }\n raise WaitConditionNotMet, \"Condition '#{condition}' not met within timeout period\"\n end\n\n # Reschedule with delay\n self.class\n .set(wait: check_interval)\n .perform_later(\n @workflow.key,\n wait_condition: condition\n )\n\n # Halt current execution\n halt_execution!\n end\n end\n end\n end\nend\n"
49
- },
50
- {
51
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods.rb",
52
- "contents": "module ChronoForge\n module Executor\n module Methods\n include Methods::Wait\n include Methods::WaitUntil\n include Methods::DurablyExecute\n end\n end\nend\n"
53
- },
54
- {
55
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/retry_strategy.rb",
56
- "contents": "module ChronoForge\n module Executor\n class RetryStrategy\n BACKOFF_STRATEGY = [\n 1.second, # Initial retry\n 5.seconds, # Second retry\n 30.seconds, # Third retry\n 2.minutes, # Fourth retry\n 10.minutes # Final retry\n ]\n\n def self.schedule_retry(workflow, attempt: 0)\n wait_duration = BACKOFF_STRATEGY[attempt] || BACKOFF_STRATEGY.last\n\n # Schedule with exponential backoff\n workflow.job_klass\n .set(wait: wait_duration)\n .perform_later(\n workflow.key,\n attempt: attempt + 1\n )\n end\n\n def self.max_attempts\n BACKOFF_STRATEGY.length\n end\n end\n end\nend\n"
57
- },
58
- {
59
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor.rb",
60
- "contents": "module ChronoForge\n module Executor\n class Error < StandardError; end\n\n class ExecutionFailedError < Error; end\n\n class ExecutionFlowControl < Error; end\n\n class HaltExecutionFlow < ExecutionFlowControl; end\n\n include Methods\n\n def perform(key, attempt: 0, options: {}, **kwargs)\n # Prevent excessive retries\n if attempt >= self.class::RetryStrategy.max_attempts\n Rails.logger.error { \"Max attempts reached for job #{key}\" }\n return\n end\n\n # Find or create job with comprehensive tracking\n setup_workflow(key, options, kwargs)\n\n begin\n # Skip if workflow cannot be executed\n return unless workflow.executable?\n\n # Acquire lock with advanced concurrency protection\n self.class::LockStrategy.acquire_lock(job_id, workflow, max_duration: max_duration)\n\n # Execute core job logic\n super(**workflow.kwargs.symbolize_keys)\n\n # Mark as complete\n complete_workflow!\n rescue ExecutionFailedError => e\n Rails.logger.error { \"Execution step failed for #{key}\" }\n self.class::ExecutionTracker.track_error(workflow, e)\n workflow.stalled!\n nil\n rescue HaltExecutionFlow\n # Halt execution\n Rails.logger.debug { \"Execution halted for #{key}\" }\n nil\n rescue ConcurrentExecutionError\n # Graceful handling of concurrent execution\n Rails.logger.warn { \"Concurrent execution detected for job #{key}\" }\n nil\n rescue => e\n Rails.logger.error { \"An error occurred during execution of #{key}\" }\n error_log = self.class::ExecutionTracker.track_error(workflow, e)\n\n # Retry if applicable\n if should_retry?(e, attempt)\n self.class::RetryStrategy.schedule_retry(workflow, attempt: attempt)\n else\n fail_workflow! error_log\n end\n ensure\n context.save!\n # Always release the lock\n self.class::LockStrategy.release_lock(job_id, workflow)\n end\n end\n\n private\n\n def complete_workflow!\n # Create an execution log for workflow completion\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: workflow,\n step_name: \"$workflow_completion$\"\n ) do |log|\n log.started_at = Time.current\n end\n\n begin\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n workflow.completed_at = Time.current\n workflow.completed!\n\n # Mark execution log as completed\n execution_log.update!(\n state: :completed,\n completed_at: Time.current\n )\n\n # Return the execution log for tracking\n execution_log\n rescue => e\n # Log any completion errors\n execution_log.update!(\n state: :failed,\n error_message: e.message,\n error_class: e.class.name\n )\n raise\n end\n end\n\n def fail_workflow!(error_log)\n # Create an execution log for workflow failure\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: workflow,\n step_name: \"$workflow_failure$\"\n ) do |log|\n log.started_at = Time.current\n log.metadata = {\n error_log_id: error_log.id\n }\n end\n\n begin\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n workflow.failed!\n\n # Mark execution log as completed\n execution_log.update!(\n state: :completed,\n completed_at: Time.current\n )\n\n # Return the execution log for tracking\n execution_log\n rescue => e\n # Log any completion errors\n execution_log.update!(\n state: :failed,\n error_message: e.message,\n error_class: e.class.name\n )\n raise\n end\n end\n\n def setup_workflow(key, options, kwargs)\n @workflow = find_workflow(key, options, kwargs)\n @context = Context.new(@workflow)\n end\n\n def find_workflow(key, options, kwargs)\n Workflow.create_or_find_by!(job_class: self.class.to_s, key: key) do |workflow|\n workflow.options = options\n workflow.kwargs = kwargs\n workflow.started_at = Time.current\n end\n end\n\n def should_retry?(error, attempt_count)\n attempt_count < 3\n end\n\n def halt_execution!\n raise HaltExecutionFlow\n end\n\n def workflow\n @workflow\n end\n\n def context\n @context\n end\n\n def max_duration\n 10.minutes\n end\n end\nend\n"
61
- },
62
- {
63
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/version.rb",
64
- "contents": "# frozen_string_literal: true\n\nmodule ChronoForge\n VERSION = \"0.3.1\"\nend\n"
65
- },
66
- {
67
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/workflow.rb",
68
- "contents": "# frozen_string_literal: true\n\n# == Schema Information\n#\n# Table name: chrono_forge_workflows\n#\n# id :integer not null, primary key\n# completed_at :datetime\n# context :json not null\n# job_class :string not null\n# key :string not null\n# kwargs :json not null\n# options :json not null\n# locked_at :datetime\n# started_at :datetime\n# state :integer default(\"idle\"), not null\n# created_at :datetime not null\n# updated_at :datetime not null\n#\n# Indexes\n#\n# index_chrono_forge_workflows_on_key (key) UNIQUE\n#\nmodule ChronoForge\n class Workflow < ActiveRecord::Base\n self.table_name = \"chrono_forge_workflows\"\n\n has_many :execution_logs, -> { order(id: :asc) }, dependent: :destroy\n has_many :error_logs, -> { order(id: :asc) }, dependent: :destroy\n\n enum :state, %i[\n idle\n running\n completed\n failed\n stalled\n ]\n\n # Serialization for metadata\n serialize :metadata, coder: JSON\n\n def executable?\n idle? || running?\n end\n\n def job_klass\n job_class.constantize\n end\n end\nend\n"
69
- },
70
- {
71
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge.rb",
72
- "contents": "# frozen_string_literal: true\n\nrequire \"zeitwerk\"\nrequire \"active_record\"\nrequire \"active_job\"\n\nmodule ChronoForge\n Loader = Zeitwerk::Loader.for_gem.tap do |loader|\n loader.ignore(\"#{__dir__}/generators\")\n loader.setup\n end\n\n class Error < StandardError; end\nend\n"
73
- },
74
- {
75
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/generators/chrono_forge/install/install_generator.rb",
76
- "contents": "# frozen_string_literal: true\n\nrequire \"rails/generators/active_record/migration\"\n\nmodule ChronoForge\n class InstallGenerator < Rails::Generators::Base\n include ::ActiveRecord::Generators::Migration\n\n source_root File.expand_path(\"templates\", __dir__)\n\n def start\n install_migrations\n rescue => err\n say \"#{err.class}: #{err}\\n#{err.backtrace.join(\"\\n\")}\", :red\n exit 1\n end\n\n private\n\n def install_migrations\n migration_template \"install_chrono_forge.rb\", \"db/migrate/install_chrono_forge.rb\"\n end\n end\nend\n"
77
- },
78
- {
79
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/generators/chrono_forge/install/templates/install_chrono_forge.rb",
80
- "contents": "# frozen_string_literal: true\n\nclass InstallChronoForge < ActiveRecord::Migration[7.1]\n def change\n create_table :chrono_forge_workflows do |t|\n t.string :key, null: false, index: true\n t.string :job_class, null: false\n\n if t.respond_to?(:jsonb)\n t.jsonb :kwargs, null: false, default: {}\n t.jsonb :options, null: false, default: {}\n t.jsonb :context, null: false, default: {}\n else\n t.json :kwargs, null: false, default: {}\n t.json :options, null: false, default: {}\n t.json :context, null: false, default: {}\n end\n\n t.integer :state, null: false, default: 0\n t.string :locked_by\n t.datetime :locked_at\n\n t.datetime :started_at\n t.datetime :completed_at\n\n t.timestamps\n t.index %i[job_class key], unique: true\n end\n\n create_table :chrono_forge_execution_logs do |t|\n t.references :workflow, null: false, foreign_key: {to_table: :chrono_forge_workflows}\n t.string :step_name, null: false\n t.integer :attempts, null: false, default: 0\n t.datetime :started_at\n t.datetime :last_executed_at\n t.datetime :completed_at\n if t.respond_to?(:jsonb)\n t.jsonb :metadata\n else\n t.json :metadata\n end\n t.integer :state, null: false, default: 0\n t.string :error_class\n t.text :error_message\n\n t.timestamps\n t.index %i[workflow_id step_name], unique: true\n end\n\n create_table :chrono_forge_error_logs do |t|\n t.references :workflow, null: false, foreign_key: {to_table: :chrono_forge_workflows}\n t.string :error_class\n t.text :error_message\n t.text :backtrace\n if t.respond_to?(:jsonb)\n t.jsonb :context\n else\n t.json :context\n end\n\n t.timestamps\n end\n end\nend\n"
81
- },
82
- {
83
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/chrono_forge_test.rb",
84
- "contents": "require \"test_helper\"\n\nclass ChronoForgeTest < ActiveJob::TestCase\n include ChaoticJob::Helpers\n\n def test_version\n assert ChronoForge::VERSION\n end\n\n def test_kitchen_sink_runs_successfully\n KitchenSink.perform_later(\"happy_path\", kwarg: \"durable\", options: {option1: 1})\n\n perform_all_jobs\n\n workflow = ChronoForge::Workflow.last\n\n assert workflow.completed?, \"workflow should be completed\"\n\n assert_equal \"happy_path\", workflow.key\n assert_equal \"KitchenSink\", workflow.job_class\n assert_equal({\"kwarg\" => \"durable\"}, workflow.kwargs)\n assert_equal({\"option1\" => 1}, workflow.options)\n\n assert workflow.context[\"order_id\"], \"order_id should be set\"\n assert workflow.context[\"processed_at\"], \"processed_at should be set\"\n assert workflow.context[\"completed_at\"], \"completed_at should be set\"\n\n assert workflow.started_at, \"workflow tracking dates should be set: started_at\"\n assert workflow.completed_at, \"workflow tracking dates should be set: completed_at\"\n\n refute workflow.locked_at, \"workflow should be unlocked: locked_at\"\n refute workflow.locked_by, \"workflow should be unlocked: locked_by\"\n\n assert_equal 5, workflow.execution_logs.size, \"there should be 5 executions\"\n assert_equal [\n \"wait_until$payment_confirmed?\",\n \"wait$fraud_check_delay\",\n \"durably_execute$process_order\",\n \"durably_execute$complete_order\",\n \"$workflow_completion$\"\n ], workflow.execution_logs.pluck(:step_name)\n\n assert_equal 0, workflow.error_logs.size, \"no errors should have occurred\"\n end\n\n def test_kitchen_sink_experiences_a_glitch\n workflow = KitchenSink.new(\"glitched\")\n run_scenario(\n workflow,\n glitch: [\"before\", \"#{workflow.method(:process_order).source_location[0]}:17\"]\n )\n\n workflow = ChronoForge::Workflow.last\n\n assert workflow.completed?, \"workflow should be completed\"\n\n assert_equal \"glitched\", workflow.key\n\n assert workflow.context[\"order_id\"], \"order_id should be set\"\n assert workflow.context[\"processed_at\"], \"processed_at should be set\"\n assert workflow.context[\"completed_at\"], \"completed_at should be set\"\n\n assert workflow.started_at, \"workflow tracking dates should be set: started_at\"\n assert workflow.completed_at, \"workflow tracking dates should be set: completed_at\"\n\n refute workflow.locked_at, \"workflow should be unlocked: locked_at\"\n refute workflow.locked_by, \"workflow should be unlocked: locked_by\"\n\n assert_equal 5, workflow.execution_logs.size, \"there should be 5 executions\"\n assert_equal [\n \"wait_until$payment_confirmed?\",\n \"wait$fraud_check_delay\",\n \"durably_execute$process_order\",\n \"durably_execute$complete_order\",\n \"$workflow_completion$\"\n ], workflow.execution_logs.pluck(:step_name)\n\n assert_equal 1, workflow.error_logs.size, \"a single glitch should have occurred\"\n assert_equal [\"ChaoticJob::RetryableError\"], workflow.error_logs.pluck(:error_class).uniq\n end\n\n def test_kitchen_sink_permanenty_fails\n KitchenSink.perform_later(\"permanent_failed\", permanently_fail: true)\n\n perform_all_jobs\n\n workflow = ChronoForge::Workflow.last\n\n assert workflow.failed?, \"workflow should be failed\"\n\n assert_equal \"permanent_failed\", workflow.key\n assert_equal \"KitchenSink\", workflow.job_class\n assert_equal({\"permanently_fail\" => true}, workflow.kwargs)\n assert_equal({}, workflow.options)\n\n assert workflow.context[\"order_id\"], \"order_id should be set\"\n assert workflow.context[\"processed_at\"], \"processed_at should be set\"\n refute workflow.context[\"completed_at\"], \"completed_at should NOT be set\"\n\n assert workflow.started_at, \"workflow tracking dates should be set: started_at\"\n refute workflow.completed_at, \"workflow tracking dates should NOT be set: completed_at\"\n\n refute workflow.locked_at, \"workflow should be unlocked: locked_at\"\n refute workflow.locked_by, \"workflow should be unlocked: locked_by\"\n\n assert_equal 4, workflow.execution_logs.size, \"there should be 5 executions\"\n assert_equal [\n \"wait_until$payment_confirmed?\",\n \"wait$fraud_check_delay\",\n \"durably_execute$process_order\",\n \"$workflow_failure$\"\n ], workflow.execution_logs.pluck(:step_name)\n\n assert_equal 4, workflow.error_logs.size, \"workflow should have failed after 4 runs. 1 + 3 retries.\"\n assert_equal [\"Permanent Failure\"], workflow.error_logs.pluck(:error_message).uniq\n end\nend\n"
85
- },
86
- {
87
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/app/jobs/kitchen_sink.rb",
88
- "contents": "class KitchenSink < WorkflowJob\n prepend ChronoForge::Executor\n\n def perform(kwarg: nil, permanently_fail: false)\n # Context can be used to pass and store data between executions\n context[:order_id] ||= SecureRandom.hex\n\n # Wait until payment is confirmed\n wait_until :payment_confirmed?,\n timeout: 1.second,\n check_interval: 0.1.second\n\n # Wait for potential fraud check\n wait 1.seconds, :fraud_check_delay\n\n # Durably execute order processing\n durably_execute :process_order\n\n raise \"Permanent Failure\" if permanently_fail\n\n # Final steps\n durably_execute :complete_order\n end\n\n private\n\n def payment_confirmed?\n result_list = context[:payment_confirmation_results] ||= [true, false, false]\n result = result_list.pop\n context[:payment_confirmation_results] = result_list\n result\n end\n\n def process_order\n context[\"processed_at\"] = Time.current.iso8601\n end\n\n def complete_order\n context[\"completed_at\"] = Time.current.iso8601\n end\nend\n"
89
- },
90
- {
91
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/app/jobs/workflow_job.rb",
92
- "contents": "class WorkflowJob < ActiveJob::Base\nend\n"
93
- },
94
- {
95
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/app/models/user.rb",
96
- "contents": "class User < ActiveRecord::Base\n validates :name, presence: true\n validates :email, presence: true\nend\n"
97
- },
98
- {
99
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/config/database.yml",
100
- "contents": "test:\n adapter: sqlite3\n database: db/combustion_test.sqlite\n"
101
- },
102
- {
103
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/config/storage.yml",
104
- "contents": "test:\n service: Disk\n root: tmp/storage\n"
105
- },
106
- {
107
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/db/migrate/20241217100623_install_chrono_forge.rb",
108
- "contents": "# The template is our single source of truth.\nrequire File.expand_path(\"../../../../lib/generators/chrono_forge/install/templates/install_chrono_forge.rb\", __dir__)\n"
109
- },
110
- {
111
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/db/schema.rb",
112
- "contents": "# frozen_string_literal: true\n\nActiveRecord::Schema.define do\n create_table :users, force: true do |t|\n t.string :name\n t.string :email\n t.timestamps null: false\n end\nend\n"
113
- },
114
- {
115
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/test_helper.rb",
116
- "contents": "require \"chrono_forge\"\n\nrequire \"minitest/autorun\"\nrequire \"minitest/reporters\"\nMinitest::Reporters.use!\n\nrequire \"combustion\"\nCombustion.path = \"test/internal\"\nCombustion.initialize! :active_record, :active_job\n\nrequire \"chaotic_job\"\n"
117
- }
118
- ]
data/export.rb DELETED
@@ -1,48 +0,0 @@
1
- require "json"
2
- require "find"
3
-
4
- def export_files_to_json(directory, extensions, output_file, exceptions = [])
5
- # Convert extensions to lowercase for case-insensitive matching
6
- extensions = extensions.map(&:downcase)
7
-
8
- # Array to store file data
9
- files_data = []
10
-
11
- # Find all files in directory and subdirectories
12
- Find.find(directory) do |path|
13
- # Skip if not a file
14
- next unless File.file?(path)
15
- next if exceptions.any? { |exception| path.include?(exception) }
16
-
17
- # Check if file extension matches any in our list
18
- ext = File.extname(path).downcase[1..] # Remove the leading dot
19
- next unless extensions.include?(ext)
20
-
21
- puts path
22
-
23
- begin
24
- # Read file contents
25
- contents = File.read(path)
26
-
27
- # Add to our array
28
- files_data << {
29
- "path" => path,
30
- "contents" => contents
31
- }
32
- rescue => e
33
- puts "Error reading file #{path}: #{e.message}"
34
- end
35
- end
36
-
37
- # Write to JSON file
38
- File.write(output_file, JSON.pretty_generate(files_data))
39
-
40
- puts "Successfully exported #{files_data.length} files to #{output_file}"
41
- end
42
-
43
- # Example usage (uncomment and modify as needed):
44
- directory = "/Users/stefan/Documents/plutonium/chrono_forge"
45
- exceptions = ["/.github/", "/.vscode/", "gemfiles", "pkg", "node_modules"]
46
- extensions = ["rb", "md", "yml", "yaml", "gemspec"]
47
- output_file = "export.json"
48
- export_files_to_json(directory, extensions, output_file, exceptions)