chrono_forge 0.1.0 → 0.2.0

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: 88cee304794f2d186c2103563b8f98c08a00cc1d9a351ae206c35e34bacdcf61
4
- data.tar.gz: dee282b43b65a4846797629903141329a3f094fd009416848cb0f5b33ed3f68e
3
+ metadata.gz: f65c15313d0538a84fe916c5d3fd1966e593364d00e777aa2d1693a5bc801102
4
+ data.tar.gz: 9a3a1c025dd348d46c9169bb03a3fc26a975b5c0eb5411d032410c9f8a70e564
5
5
  SHA512:
6
- metadata.gz: ba3c5937b30c3ea0a46135a30f2cc46d59e430bb75f41bda1fc50d352a4bbe615532b1de0526eed819c824d5851cac9812b2f7de1ffb930a72a380045312692d
7
- data.tar.gz: 3db298d79b40715bc6cbcb54031b5ff37dd549c8361e70fb1584431d62010d14cffe7ca5b9e7e36e79a2513f0524882eae6ff9295fae905b342506783160a0c0
6
+ metadata.gz: 31c16f81bbfefd72f6ad4799a2ab7eeac2ffe11a8a8f2d12606632b36d843a8dc518c27a0e2f5a992184619004c7de5f3e60c4b19f55ab63b38f49e67ee32e79
7
+ data.tar.gz: 233e0badc22ad43f4084b2acfc83782d41da6b0949d6b7ee9b050b89ee4a9fbbbac46dd74029470af58dd4293767f1200ee46b3ed19dfa7eaba89c3901e8b3f8
data/README.md CHANGED
@@ -151,17 +151,6 @@ class MyWorkflow < ApplicationJob
151
151
  end
152
152
  ```
153
153
 
154
- ### Cleanup
155
-
156
- ChronoForge includes built-in cleanup methods for managing old workflow data:
157
-
158
- ```ruby
159
- # Clean up old workflows and logs
160
- ChronoForge::Workflow.cleanup_old_logs(retention_period: 30.days)
161
- ChronoForge::ExecutionLog.cleanup_old_logs(retention_period: 30.days)
162
- ChronoForge::ErrorLog.cleanup_old_logs(retention_period: 30.days)
163
- ```
164
-
165
154
  ## Testing
166
155
 
167
156
  ChronoForge 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. Here's how to set up your test environment:
@@ -186,7 +175,7 @@ require 'chaotic_job'
186
175
  Example test:
187
176
 
188
177
  ```ruby
189
- class WorkflowTest < Minitest::Test
178
+ class WorkflowTest < ActiveJob::TestCase
190
179
  include ChaoticJob::Helpers
191
180
 
192
181
  def test_workflow_completion
@@ -210,7 +199,6 @@ end
210
199
  ChaoticJob provides several helpful methods for testing workflows:
211
200
 
212
201
  - `perform_all_jobs`: Processes all enqueued jobs, including those enqueued during job execution
213
- - `enqueued_jobs`: Returns the current number of jobs in the queue
214
202
 
215
203
  For testing with specific job processing libraries like Sidekiq or Delayed Job, you can still use their respective testing modes, but ChaoticJob is recommended for testing ChronoForge workflows as it better handles the complexities of nested job scheduling and wait states.
216
204
 
data/export.json CHANGED
@@ -5,11 +5,11 @@
5
5
  },
6
6
  {
7
7
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/README.md",
8
- "contents": "# ChronoForge\n\nChronoForge is a Ruby gem that provides a robust framework for building durable, distributed workflows in Ruby on Rails applications. It offers a reliable way to handle long-running processes, state management, and error recovery.\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 support for custom contexts\n- **Concurrency Control**: Advanced locking mechanisms to prevent concurrent execution of the same workflow\n- **Error Handling**: Comprehensive error tracking and retry strategies\n- **Execution Logging**: Detailed logging of workflow execution steps and errors\n- **Wait States**: Support for time-based waits and condition-based waiting\n- **Database-Backed**: All workflow state is persisted to the database for durability\n- **ActiveJob Integration**: Seamlessly works with any ActiveJob backend\n\n## Installation\n\nAdd this line 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 it 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### Basic Workflow Example\n\nHere's a complete example of a durable order processing workflow:\n\n```ruby\nclass OrderProcessingWorkflow < ApplicationJob\n include ChronoForge::Executor\n\n def perform\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\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?(context[\"order_id\"])\n end\n\n def process_order\n context[\"processed_at\"] = Time.current.iso8601\n OrderProcessor.process(context[\"order_id\"])\n end\n\n def complete_order\n context[\"completed_at\"] = Time.current.iso8601\n OrderCompletionService.complete(context[\"order_id\"])\n end\nend\n```\n\n### Workflow Features\n\n#### Durable Execution\n\nThe `durably_execute` method ensures operations are executed exactly once:\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:\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\n### Error Handling\n\nChronoForge automatically tracks errors and provides retry capabilities:\n\n```ruby\nclass MyWorkflow < ApplicationJob\n include ChronoForge::Executor\n\n private\n\n def should_retry?(error, attempt_count)\n case error\n when NetworkError\n attempt_count < 5\n when ValidationError\n false # Don't retry validation errors\n else\n attempt_count < 3\n end\n end\nend\n```\n\n### Cleanup\n\nChronoForge includes built-in cleanup methods for managing old workflow data:\n\n```ruby\n# Clean up old workflows and logs\nChronoForge::Workflow.cleanup_old_logs(retention_period: 30.days)\nChronoForge::ExecutionLog.cleanup_old_logs(retention_period: 30.days)\nChronoForge::ErrorLog.cleanup_old_logs(retention_period: 30.days)\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. Here's how to set up your test environment:\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 < Minitest::Test\n include ChaoticJob::Helpers\n\n def test_workflow_completion\n # Enqueue the job\n OrderProcessingWorkflow.perform_later(\"order_123\")\n \n # Perform all enqueued jobs\n perform_all_jobs\n \n # Assert workflow completed successfully\n workflow = ChronoForge::Workflow.last\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\nChaoticJob provides several helpful methods for testing workflows:\n\n- `perform_all_jobs`: Processes all enqueued jobs, including those enqueued during job execution\n- `enqueued_jobs`: Returns the current number of jobs in the queue\n\nFor testing with specific job processing libraries like Sidekiq or Delayed Job, you can still use their respective testing modes, but ChaoticJob is recommended for testing ChronoForge workflows as it better handles the complexities of nested job scheduling and wait states.\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## 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"
8
+ "contents": "# ChronoForge\n\nChronoForge is a Ruby gem that provides a robust framework for building durable, distributed workflows in Ruby on Rails applications. It offers a reliable way to handle long-running processes, state management, and error recovery.\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 support for custom contexts\n- **Concurrency Control**: Advanced locking mechanisms to prevent concurrent execution of the same workflow\n- **Error Handling**: Comprehensive error tracking and retry strategies\n- **Execution Logging**: Detailed logging of workflow execution steps and errors\n- **Wait States**: Support for time-based waits and condition-based waiting\n- **Database-Backed**: All workflow state is persisted to the database for durability\n- **ActiveJob Integration**: Seamlessly works with any ActiveJob backend\n\n## Installation\n\nAdd this line 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 it 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### Basic Workflow Example\n\nHere's a complete example of a durable order processing workflow:\n\n```ruby\nclass OrderProcessingWorkflow < ApplicationJob\n include ChronoForge::Executor\n\n def perform\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\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?(context[\"order_id\"])\n end\n\n def process_order\n context[\"processed_at\"] = Time.current.iso8601\n OrderProcessor.process(context[\"order_id\"])\n end\n\n def complete_order\n context[\"completed_at\"] = Time.current.iso8601\n OrderCompletionService.complete(context[\"order_id\"])\n end\nend\n```\n\n### Workflow Features\n\n#### Durable Execution\n\nThe `durably_execute` method ensures operations are executed exactly once:\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:\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\n### Error Handling\n\nChronoForge automatically tracks errors and provides retry capabilities:\n\n```ruby\nclass MyWorkflow < ApplicationJob\n include ChronoForge::Executor\n\n private\n\n def should_retry?(error, attempt_count)\n case error\n when NetworkError\n attempt_count < 5\n when ValidationError\n false # Don't retry validation errors\n else\n attempt_count < 3\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. Here's how to set up your test environment:\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\n OrderProcessingWorkflow.perform_later(\"order_123\")\n \n # Perform all enqueued jobs\n perform_all_jobs\n \n # Assert workflow completed successfully\n workflow = ChronoForge::Workflow.last\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\nChaoticJob provides several helpful methods for testing workflows:\n\n- `perform_all_jobs`: Processes all enqueued jobs, including those enqueued during job execution\n\nFor testing with specific job processing libraries like Sidekiq or Delayed Job, you can still use their respective testing modes, but ChaoticJob is recommended for testing ChronoForge workflows as it better handles the complexities of nested job scheduling and wait states.\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## 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
9
  },
10
10
  {
11
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 = \"Base fields for the Phlexi libraries\"\n spec.description = \"Base fields for the Phlexi libraries\"\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"
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
13
  },
14
14
  {
15
15
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/export.rb",
@@ -17,11 +17,11 @@
17
17
  },
18
18
  {
19
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\n # Cleanup method\n def self.cleanup_old_logs(retention_period: 30.days)\n where(\"created_at < ?\", retention_period.ago).delete_all\n end\n end\nend\n"
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
21
  },
22
22
  {
23
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\n # Cleanup method\n def self.cleanup_old_logs(retention_period: 30.days)\n where(\"created_at < ?\", retention_period.ago).delete_all\n end\n end\nend\n"
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
25
  },
26
26
  {
27
27
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/context.rb",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  {
39
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 execution_log.metadata[\"result\"] 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 result = 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 metadata: {result: result}\n )\n result\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"
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 result\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
41
  },
42
42
  {
43
43
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/wait.rb",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  {
47
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] || 10.seconds\n check_interval = options[:check_interval] || 1.second\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}. Condition not met within the timeout period.\" }\n raise WaitConditionNotMet, \"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"
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
49
  },
50
50
  {
51
51
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods.rb",
@@ -53,39 +53,43 @@
53
53
  },
54
54
  {
55
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.constantize\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"
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
57
  },
58
58
  {
59
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, **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, 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 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 workflow.failed!\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 log.metadata = {\n workflow_id: workflow.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.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 setup_workflow(key, kwargs)\n @workflow = find_workflow(key, kwargs)\n @context = Context.new(@workflow)\n end\n\n def find_workflow(key, kwargs)\n Workflow.create_or_find_by!(key: key) do |workflow|\n workflow.job_klass = self.class.to_s\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"
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
61
  },
62
62
  {
63
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.1.0\"\nend\n"
64
+ "contents": "# frozen_string_literal: true\n\nmodule ChronoForge\n VERSION = \"0.1.1\"\nend\n"
65
65
  },
66
66
  {
67
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_klass :string not null\n# key :string not null\n# kwargs :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\n has_many :error_logs\n\n enum :state, %i[\n idle\n running\n completed\n failed\n stalled\n ]\n\n # Cleanup method\n def self.cleanup_old_logs(retention_period: 30.days)\n where(\"created_at < ?\", retention_period.ago).delete_all\n end\n\n # Serialization for metadata\n serialize :metadata, coder: JSON\n\n def executable?\n idle? || running?\n end\n end\nend\n"
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) }\n has_many :error_logs, -> { order(id: :asc) }\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
69
  },
70
70
  {
71
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"
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
73
  },
74
74
  {
75
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\", \"install_chrono_forge.rb\"\n end\n end\nend\n"
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
77
  },
78
78
  {
79
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: {unique: true}\n t.string :job_klass, 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 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"
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
81
  },
82
82
  {
83
83
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/chrono_forge_test.rb",
84
- "contents": "require \"test_helper\"\n\nclass ChronoForgeTest < Minitest::Test\n include ChaoticJob::Helpers\n\n def test_version\n assert ChronoForge::VERSION\n end\n\n def test_job_is_durable\n DurableJob.perform_later(:key)\n perform_all_jobs\n\n assert ChronoForge::Workflow.last.completed?\n end\nend\n"
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
85
  },
86
86
  {
87
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/app/jobs/durable_job.rb",
88
- "contents": "class DurableJob < ActiveJob::Base\n prepend ChronoForge::Executor\n\n def perform\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\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 # Final steps\n complete_order\n end\n\n private\n\n def payment_confirmed?\n [true, false].sample\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"
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"
89
93
  },
90
94
  {
91
95
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/app/models/user.rb",
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- chrono_forge (0.0.1)
4
+ chrono_forge (0.1.0)
5
5
  activejob
6
6
  activerecord
7
7
  zeitwerk
@@ -27,10 +27,5 @@ module ChronoForge
27
27
  self.table_name = "chrono_forge_error_logs"
28
28
 
29
29
  belongs_to :workflow
30
-
31
- # Cleanup method
32
- def self.cleanup_old_logs(retention_period: 30.days)
33
- where("created_at < ?", retention_period.ago).delete_all
34
- end
35
30
  end
36
31
  end
@@ -38,10 +38,5 @@ module ChronoForge
38
38
  completed
39
39
  failed
40
40
  ]
41
-
42
- # Cleanup method
43
- def self.cleanup_old_logs(retention_period: 30.days)
44
- where("created_at < ?", retention_period.ago).delete_all
45
- end
46
41
  end
47
42
  end
@@ -13,7 +13,7 @@ module ChronoForge
13
13
  end
14
14
 
15
15
  # Return if already completed
16
- return execution_log.metadata["result"] if execution_log.completed?
16
+ return if execution_log.completed?
17
17
 
18
18
  # Execute with error handling
19
19
  begin
@@ -24,7 +24,7 @@ module ChronoForge
24
24
  )
25
25
 
26
26
  # Execute the method
27
- result = if method.is_a?(Symbol)
27
+ if method.is_a?(Symbol)
28
28
  send(method)
29
29
  else
30
30
  method.call(@context)
@@ -33,8 +33,7 @@ module ChronoForge
33
33
  # Complete the execution
34
34
  execution_log.update!(
35
35
  state: :completed,
36
- completed_at: Time.current,
37
- metadata: {result: result}
36
+ completed_at: Time.current
38
37
  )
39
38
  result
40
39
  rescue HaltExecutionFlow
@@ -6,8 +6,8 @@ module ChronoForge
6
6
  module WaitUntil
7
7
  def wait_until(condition, **options)
8
8
  # Default timeout and check interval
9
- timeout = options[:timeout] || 10.seconds
10
- check_interval = options[:check_interval] || 1.second
9
+ timeout = options[:timeout] || 1.hour
10
+ check_interval = options[:check_interval] || 15.minutes
11
11
 
12
12
  # Find or create execution log
13
13
  step_name = "wait_until$#{condition}"
@@ -89,8 +89,8 @@ module ChronoForge
89
89
  state: :failed,
90
90
  metadata: metadata.merge("result" => nil)
91
91
  )
92
- Rails.logger.warn { "Timeout reached for condition #{condition}. Condition not met within the timeout period." }
93
- raise WaitConditionNotMet, "Condition not met within timeout period"
92
+ Rails.logger.warn { "Timeout reached for condition '#{condition}'." }
93
+ raise WaitConditionNotMet, "Condition '#{condition}' not met within timeout period"
94
94
  end
95
95
 
96
96
  # Reschedule with delay
@@ -13,7 +13,7 @@ module ChronoForge
13
13
  wait_duration = BACKOFF_STRATEGY[attempt] || BACKOFF_STRATEGY.last
14
14
 
15
15
  # Schedule with exponential backoff
16
- workflow.job_klass.constantize
16
+ workflow.job_klass
17
17
  .set(wait: wait_duration)
18
18
  .perform_later(
19
19
  workflow.key,
@@ -10,7 +10,7 @@ module ChronoForge
10
10
 
11
11
  include Methods
12
12
 
13
- def perform(key, attempt: 0, **kwargs)
13
+ def perform(key, attempt: 0, options: {}, **kwargs)
14
14
  # Prevent excessive retries
15
15
  if attempt >= self.class::RetryStrategy.max_attempts
16
16
  Rails.logger.error { "Max attempts reached for job #{key}" }
@@ -18,7 +18,7 @@ module ChronoForge
18
18
  end
19
19
 
20
20
  # Find or create job with comprehensive tracking
21
- setup_workflow(key, kwargs)
21
+ setup_workflow(key, options, kwargs)
22
22
 
23
23
  begin
24
24
  # Skip if workflow cannot be executed
@@ -47,13 +47,13 @@ module ChronoForge
47
47
  nil
48
48
  rescue => e
49
49
  Rails.logger.error { "An error occurred during execution of #{key}" }
50
- self.class::ExecutionTracker.track_error(workflow, e)
50
+ error_log = self.class::ExecutionTracker.track_error(workflow, e)
51
51
 
52
52
  # Retry if applicable
53
53
  if should_retry?(e, attempt)
54
54
  self.class::RetryStrategy.schedule_retry(workflow, attempt: attempt)
55
55
  else
56
- workflow.failed!
56
+ fail_workflow! error_log
57
57
  end
58
58
  ensure
59
59
  context.save!
@@ -71,9 +71,6 @@ module ChronoForge
71
71
  step_name: "$workflow_completion$"
72
72
  ) do |log|
73
73
  log.started_at = Time.current
74
- log.metadata = {
75
- workflow_id: workflow.id
76
- }
77
74
  end
78
75
 
79
76
  begin
@@ -104,14 +101,53 @@ module ChronoForge
104
101
  end
105
102
  end
106
103
 
107
- def setup_workflow(key, kwargs)
108
- @workflow = find_workflow(key, kwargs)
104
+ def fail_workflow!(error_log)
105
+ # Create an execution log for workflow failure
106
+ execution_log = ExecutionLog.create_or_find_by!(
107
+ workflow: workflow,
108
+ step_name: "$workflow_failure$"
109
+ ) do |log|
110
+ log.started_at = Time.current
111
+ log.metadata = {
112
+ error_log_id: error_log.id
113
+ }
114
+ end
115
+
116
+ begin
117
+ execution_log.update!(
118
+ attempts: execution_log.attempts + 1,
119
+ last_executed_at: Time.current
120
+ )
121
+
122
+ workflow.failed!
123
+
124
+ # Mark execution log as completed
125
+ execution_log.update!(
126
+ state: :completed,
127
+ completed_at: Time.current
128
+ )
129
+
130
+ # Return the execution log for tracking
131
+ execution_log
132
+ rescue => e
133
+ # Log any completion errors
134
+ execution_log.update!(
135
+ state: :failed,
136
+ error_message: e.message,
137
+ error_class: e.class.name
138
+ )
139
+ raise
140
+ end
141
+ end
142
+
143
+ def setup_workflow(key, options, kwargs)
144
+ @workflow = find_workflow(key, options, kwargs)
109
145
  @context = Context.new(@workflow)
110
146
  end
111
147
 
112
- def find_workflow(key, kwargs)
113
- Workflow.create_or_find_by!(key: key) do |workflow|
114
- workflow.job_klass = self.class.to_s
148
+ def find_workflow(key, options, kwargs)
149
+ Workflow.create_or_find_by!(job_class: self.class.to_s, key: key) do |workflow|
150
+ workflow.options = options
115
151
  workflow.kwargs = kwargs
116
152
  workflow.started_at = Time.current
117
153
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChronoForge
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -7,9 +7,10 @@
7
7
  # id :integer not null, primary key
8
8
  # completed_at :datetime
9
9
  # context :json not null
10
- # job_klass :string not null
10
+ # job_class :string not null
11
11
  # key :string not null
12
12
  # kwargs :json not null
13
+ # options :json not null
13
14
  # locked_at :datetime
14
15
  # started_at :datetime
15
16
  # state :integer default("idle"), not null
@@ -24,8 +25,8 @@ module ChronoForge
24
25
  class Workflow < ActiveRecord::Base
25
26
  self.table_name = "chrono_forge_workflows"
26
27
 
27
- has_many :execution_logs
28
- has_many :error_logs
28
+ has_many :execution_logs, -> { order(id: :asc) }
29
+ has_many :error_logs, -> { order(id: :asc) }
29
30
 
30
31
  enum :state, %i[
31
32
  idle
@@ -35,16 +36,15 @@ module ChronoForge
35
36
  stalled
36
37
  ]
37
38
 
38
- # Cleanup method
39
- def self.cleanup_old_logs(retention_period: 30.days)
40
- where("created_at < ?", retention_period.ago).delete_all
41
- end
42
-
43
39
  # Serialization for metadata
44
40
  serialize :metadata, coder: JSON
45
41
 
46
42
  def executable?
47
43
  idle? || running?
48
44
  end
45
+
46
+ def job_klass
47
+ job_class.constantize
48
+ end
49
49
  end
50
50
  end
data/lib/chrono_forge.rb CHANGED
@@ -4,7 +4,7 @@ require "zeitwerk"
4
4
  require "active_record"
5
5
  require "active_job"
6
6
 
7
- module Chronoforge
7
+ module ChronoForge
8
8
  Loader = Zeitwerk::Loader.for_gem.tap do |loader|
9
9
  loader.ignore("#{__dir__}/generators")
10
10
  loader.setup
@@ -18,7 +18,7 @@ module ChronoForge
18
18
  private
19
19
 
20
20
  def install_migrations
21
- migration_template "install_chrono_forge.rb", "install_chrono_forge.rb"
21
+ migration_template "install_chrono_forge.rb", "db/migrate/install_chrono_forge.rb"
22
22
  end
23
23
  end
24
24
  end
@@ -3,8 +3,8 @@
3
3
  class InstallChronoForge < ActiveRecord::Migration[7.1]
4
4
  def change
5
5
  create_table :chrono_forge_workflows do |t|
6
- t.string :key, null: false, index: {unique: true}
7
- t.string :job_klass, null: false
6
+ t.string :key, null: false, index: true
7
+ t.string :job_class, null: false
8
8
 
9
9
  if t.respond_to?(:jsonb)
10
10
  t.jsonb :kwargs, null: false, default: {}
@@ -24,6 +24,7 @@ class InstallChronoForge < ActiveRecord::Migration[7.1]
24
24
  t.datetime :completed_at
25
25
 
26
26
  t.timestamps
27
+ t.index %i[job_class key], unique: true
27
28
  end
28
29
 
29
30
  create_table :chrono_forge_execution_logs do |t|
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-21 00:00:00.000000000 Z
11
+ date: 2025-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord