chrono_forge 0.3.0 โ 0.3.1
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 +4 -4
- data/README.md +66 -10
- data/export.json +3 -3
- data/lib/chrono_forge/version.rb +1 -1
- data/lib/chrono_forge/workflow.rb +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c9315ad74de7245484119f385f2eb9706d1e3e532e142696d03d204e37f81ad
|
4
|
+
data.tar.gz: 4ea5ef5858c6d5fc8903f5432dd1ddc643d3cb6d26e65693c8a30664224a51c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4713568fd2d32ddc70737fb5eb2f91db90aa41ff80d446afcd91f79719938e427f87b06da69dae18fb253c751aec07a70954e2f941053cb748db7fed04304ff0
|
7
|
+
data.tar.gz: caed47e85300d073b9ee1309d33b05047f2b0e6f60e95612ca247c86f233a32c823f9d68307e77384573c250b1779407e630a2f7ecd3557d2c033482f4326263
|
data/README.md
CHANGED
@@ -47,17 +47,45 @@ $ rails db:migrate
|
|
47
47
|
|
48
48
|
## ๐ Usage
|
49
49
|
|
50
|
+
### Creating and Executing Workflows
|
51
|
+
|
52
|
+
ChronoForge workflows are ActiveJob classes that prepend the `ChronoForge::Executor` module. Each workflow can **only** accept keyword arguments:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# Define your workflow class
|
56
|
+
class OrderProcessingWorkflow < ApplicationJob
|
57
|
+
prepend ChronoForge::Executor
|
58
|
+
|
59
|
+
def perform(order_id:, customer_id:)
|
60
|
+
# Workflow steps...
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
All workflows require a unique identifier when executed. This identifier is used to track and manage the workflow:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
# Execute the workflow
|
69
|
+
OrderProcessingWorkflow.perform_later(
|
70
|
+
"order-123", # Unique workflow key
|
71
|
+
order_id: "order-134", # Custom kwargs
|
72
|
+
customer_id: "customer-456" # More custom kwargs
|
73
|
+
)
|
74
|
+
```
|
75
|
+
|
50
76
|
### Basic Workflow Example
|
51
77
|
|
52
78
|
Here's a complete example of a durable order processing workflow:
|
53
79
|
|
54
80
|
```ruby
|
55
81
|
class OrderProcessingWorkflow < ApplicationJob
|
56
|
-
|
82
|
+
prepend ChronoForge::Executor
|
83
|
+
|
84
|
+
def perform(order_id:)
|
85
|
+
@order_id = order_id
|
57
86
|
|
58
|
-
def perform
|
59
87
|
# Context can be used to pass and store data between executions
|
60
|
-
context.set_once "
|
88
|
+
context.set_once "execution_id", SecureRandom.hex
|
61
89
|
|
62
90
|
# Wait until payment is confirmed
|
63
91
|
wait_until :payment_confirmed?
|
@@ -75,16 +103,16 @@ class OrderProcessingWorkflow < ApplicationJob
|
|
75
103
|
private
|
76
104
|
|
77
105
|
def payment_confirmed?
|
78
|
-
PaymentService.confirmed?(context["
|
106
|
+
PaymentService.confirmed?(@order_id, context["execution_id"])
|
79
107
|
end
|
80
108
|
|
81
109
|
def process_order
|
82
|
-
OrderProcessor.process(context["
|
110
|
+
OrderProcessor.process(@order_id, context["execution_id"])
|
83
111
|
context["processed_at"] = Time.current.iso8601
|
84
112
|
end
|
85
113
|
|
86
114
|
def complete_order
|
87
|
-
OrderCompletionService.complete(context["
|
115
|
+
OrderCompletionService.complete(@order_id, context["execution_id"])
|
88
116
|
context["completed_at"] = Time.current.iso8601
|
89
117
|
end
|
90
118
|
end
|
@@ -92,6 +120,28 @@ end
|
|
92
120
|
|
93
121
|
### Core Workflow Features
|
94
122
|
|
123
|
+
#### ๐ Executing Workflows
|
124
|
+
|
125
|
+
ChronoForge workflows are executed through ActiveJob's standard interface with a specific parameter structure:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
# Perform the workflow immediately
|
129
|
+
OrderProcessingWorkflow.perform_now(
|
130
|
+
"order-123", # Unique workflow key
|
131
|
+
order_id: "O-123", # Custom parameter
|
132
|
+
customer_id: "C-456" # Another custom parameter
|
133
|
+
)
|
134
|
+
|
135
|
+
# Or queue it for background processing
|
136
|
+
OrderProcessingWorkflow.perform_later(
|
137
|
+
"order-123-async", # Unique workflow key
|
138
|
+
order_id: "O-124",
|
139
|
+
customer_id: "C-457"
|
140
|
+
)
|
141
|
+
```
|
142
|
+
|
143
|
+
**Important:** Workflows must use keyword arguments only, not positional arguments.
|
144
|
+
|
95
145
|
#### โก Durable Execution
|
96
146
|
|
97
147
|
The `durably_execute` method ensures operations are executed exactly once, even if the job fails and is retried:
|
@@ -147,13 +197,15 @@ if context.key?(:user_id)
|
|
147
197
|
end
|
148
198
|
```
|
149
199
|
|
200
|
+
The context supports serializable Ruby objects (Hash, Array, String, Integer, Float, Boolean, and nil) and validates types automatically.
|
201
|
+
|
150
202
|
### ๐ก๏ธ Error Handling
|
151
203
|
|
152
204
|
ChronoForge automatically tracks errors and provides configurable retry capabilities:
|
153
205
|
|
154
206
|
```ruby
|
155
207
|
class MyWorkflow < ApplicationJob
|
156
|
-
|
208
|
+
prepend ChronoForge::Executor
|
157
209
|
|
158
210
|
private
|
159
211
|
|
@@ -198,14 +250,18 @@ class WorkflowTest < ActiveJob::TestCase
|
|
198
250
|
include ChaoticJob::Helpers
|
199
251
|
|
200
252
|
def test_workflow_completion
|
201
|
-
# Enqueue the job
|
202
|
-
OrderProcessingWorkflow.perform_later(
|
253
|
+
# Enqueue the job with a unique key and custom parameters
|
254
|
+
OrderProcessingWorkflow.perform_later(
|
255
|
+
"order-test-123",
|
256
|
+
order_id: "O-123",
|
257
|
+
customer_id: "C-456"
|
258
|
+
)
|
203
259
|
|
204
260
|
# Perform all enqueued jobs
|
205
261
|
perform_all_jobs
|
206
262
|
|
207
263
|
# Assert workflow completed successfully
|
208
|
-
workflow = ChronoForge::Workflow.
|
264
|
+
workflow = ChronoForge::Workflow.find_by(key: "order-test-123")
|
209
265
|
assert workflow.completed?
|
210
266
|
|
211
267
|
# Check workflow context
|
data/export.json
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
},
|
6
6
|
{
|
7
7
|
"path": "/Users/stefan/Documents/plutonium/chrono_forge/README.md",
|
8
|
-
"contents": "# ChronoForge\n\
|
8
|
+
"contents": "# ChronoForge\n\n\n[](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
9
|
},
|
10
10
|
{
|
11
11
|
"path": "/Users/stefan/Documents/plutonium/chrono_forge/chrono_forge.gemspec",
|
@@ -61,11 +61,11 @@
|
|
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.
|
64
|
+
"contents": "# frozen_string_literal: true\n\nmodule ChronoForge\n VERSION = \"0.3.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_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"
|
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
69
|
},
|
70
70
|
{
|
71
71
|
"path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge.rb",
|
data/lib/chrono_forge/version.rb
CHANGED
@@ -25,8 +25,8 @@ module ChronoForge
|
|
25
25
|
class Workflow < ActiveRecord::Base
|
26
26
|
self.table_name = "chrono_forge_workflows"
|
27
27
|
|
28
|
-
has_many :execution_logs, -> { order(id: :asc) }
|
29
|
-
has_many :error_logs, -> { order(id: :asc) }
|
28
|
+
has_many :execution_logs, -> { order(id: :asc) }, dependent: :destroy
|
29
|
+
has_many :error_logs, -> { order(id: :asc) }, dependent: :destroy
|
30
30
|
|
31
31
|
enum :state, %i[
|
32
32
|
idle
|