chrono_forge 0.2.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 +123 -46
- data/export.json +6 -6
- data/export.rb +1 -1
- data/lib/chrono_forge/executor/context.rb +43 -11
- data/lib/chrono_forge/executor/methods/durably_execute.rb +3 -1
- data/lib/chrono_forge/version.rb +1 -1
- data/lib/chrono_forge/workflow.rb +2 -2
- metadata +2 -2
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
@@ -1,21 +1,26 @@
|
|
1
1
|
# ChronoForge
|
2
2
|
|
3
|
-
|
3
|
+

|
4
|
+
[](https://opensource.org/licenses/MIT)
|
4
5
|
|
5
|
-
|
6
|
+
> A robust framework for building durable, distributed workflows in Ruby on Rails applications
|
7
|
+
|
8
|
+
ChronoForge 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.
|
9
|
+
|
10
|
+
## 🌟 Features
|
6
11
|
|
7
12
|
- **Durable Execution**: Automatically tracks and recovers from failures during workflow execution
|
8
|
-
- **State Management**: Built-in workflow state tracking with
|
9
|
-
- **Concurrency Control**: Advanced locking mechanisms to prevent
|
10
|
-
- **Error Handling**: Comprehensive error tracking
|
11
|
-
- **Execution Logging**: Detailed logging of workflow
|
13
|
+
- **State Management**: Built-in workflow state tracking with persistent context storage
|
14
|
+
- **Concurrency Control**: Advanced locking mechanisms to prevent parallel execution of the same workflow
|
15
|
+
- **Error Handling**: Comprehensive error tracking with configurable retry strategies
|
16
|
+
- **Execution Logging**: Detailed logging of workflow steps and errors for visibility
|
12
17
|
- **Wait States**: Support for time-based waits and condition-based waiting
|
13
|
-
- **Database-Backed**: All workflow state is persisted to
|
14
|
-
- **ActiveJob Integration**:
|
18
|
+
- **Database-Backed**: All workflow state is persisted to ensure durability
|
19
|
+
- **ActiveJob Integration**: Compatible with all ActiveJob backends, though database-backed processors (like Solid Queue) provide the most reliable experience for long-running workflows
|
15
20
|
|
16
|
-
## Installation
|
21
|
+
## 📦 Installation
|
17
22
|
|
18
|
-
Add
|
23
|
+
Add to your application's Gemfile:
|
19
24
|
|
20
25
|
```ruby
|
21
26
|
gem 'chrono_forge'
|
@@ -27,7 +32,7 @@ Then execute:
|
|
27
32
|
$ bundle install
|
28
33
|
```
|
29
34
|
|
30
|
-
Or install
|
35
|
+
Or install directly:
|
31
36
|
|
32
37
|
```bash
|
33
38
|
$ gem install chrono_forge
|
@@ -40,7 +45,33 @@ $ rails generate chrono_forge:install
|
|
40
45
|
$ rails db:migrate
|
41
46
|
```
|
42
47
|
|
43
|
-
## Usage
|
48
|
+
## 📋 Usage
|
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
|
+
```
|
44
75
|
|
45
76
|
### Basic Workflow Example
|
46
77
|
|
@@ -48,11 +79,13 @@ Here's a complete example of a durable order processing workflow:
|
|
48
79
|
|
49
80
|
```ruby
|
50
81
|
class OrderProcessingWorkflow < ApplicationJob
|
51
|
-
|
82
|
+
prepend ChronoForge::Executor
|
83
|
+
|
84
|
+
def perform(order_id:)
|
85
|
+
@order_id = order_id
|
52
86
|
|
53
|
-
def perform
|
54
87
|
# Context can be used to pass and store data between executions
|
55
|
-
context
|
88
|
+
context.set_once "execution_id", SecureRandom.hex
|
56
89
|
|
57
90
|
# Wait until payment is confirmed
|
58
91
|
wait_until :payment_confirmed?
|
@@ -70,26 +103,48 @@ class OrderProcessingWorkflow < ApplicationJob
|
|
70
103
|
private
|
71
104
|
|
72
105
|
def payment_confirmed?
|
73
|
-
PaymentService.confirmed?(context["
|
106
|
+
PaymentService.confirmed?(@order_id, context["execution_id"])
|
74
107
|
end
|
75
108
|
|
76
109
|
def process_order
|
110
|
+
OrderProcessor.process(@order_id, context["execution_id"])
|
77
111
|
context["processed_at"] = Time.current.iso8601
|
78
|
-
OrderProcessor.process(context["order_id"])
|
79
112
|
end
|
80
113
|
|
81
114
|
def complete_order
|
115
|
+
OrderCompletionService.complete(@order_id, context["execution_id"])
|
82
116
|
context["completed_at"] = Time.current.iso8601
|
83
|
-
OrderCompletionService.complete(context["order_id"])
|
84
117
|
end
|
85
118
|
end
|
86
119
|
```
|
87
120
|
|
88
|
-
### Workflow Features
|
121
|
+
### Core Workflow Features
|
89
122
|
|
90
|
-
####
|
123
|
+
#### 🚀 Executing Workflows
|
91
124
|
|
92
|
-
|
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
|
+
|
145
|
+
#### ⚡ Durable Execution
|
146
|
+
|
147
|
+
The `durably_execute` method ensures operations are executed exactly once, even if the job fails and is retried:
|
93
148
|
|
94
149
|
```ruby
|
95
150
|
# Execute a method
|
@@ -101,7 +156,7 @@ durably_execute -> (ctx) {
|
|
101
156
|
}
|
102
157
|
```
|
103
158
|
|
104
|
-
#### Wait States
|
159
|
+
#### ⏱️ Wait States
|
105
160
|
|
106
161
|
ChronoForge supports both time-based and condition-based waits:
|
107
162
|
|
@@ -115,9 +170,9 @@ wait_until :payment_processed,
|
|
115
170
|
check_interval: 5.minutes
|
116
171
|
```
|
117
172
|
|
118
|
-
#### Workflow Context
|
173
|
+
#### 🔄 Workflow Context
|
119
174
|
|
120
|
-
ChronoForge provides a persistent context that survives job restarts:
|
175
|
+
ChronoForge provides a persistent context that survives job restarts. The context behaves like a Hash but with additional capabilities:
|
121
176
|
|
122
177
|
```ruby
|
123
178
|
# Set context values
|
@@ -126,34 +181,50 @@ context[:status] = "processing"
|
|
126
181
|
|
127
182
|
# Read context values
|
128
183
|
user_name = context[:user_name]
|
184
|
+
|
185
|
+
# Using the fetch method (returns default if key doesn't exist)
|
186
|
+
status = context.fetch(:status, "pending")
|
187
|
+
|
188
|
+
# Set a value with the set method (alias for []=)
|
189
|
+
context.set(:total_amount, 99.99)
|
190
|
+
|
191
|
+
# Set a value only if the key doesn't already exist
|
192
|
+
context.set_once(:created_at, Time.current.iso8601)
|
193
|
+
|
194
|
+
# Check if a key exists
|
195
|
+
if context.key?(:user_id)
|
196
|
+
# Do something with the user ID
|
197
|
+
end
|
129
198
|
```
|
130
199
|
|
131
|
-
|
200
|
+
The context supports serializable Ruby objects (Hash, Array, String, Integer, Float, Boolean, and nil) and validates types automatically.
|
201
|
+
|
202
|
+
### 🛡️ Error Handling
|
132
203
|
|
133
|
-
ChronoForge automatically tracks errors and provides retry capabilities:
|
204
|
+
ChronoForge automatically tracks errors and provides configurable retry capabilities:
|
134
205
|
|
135
206
|
```ruby
|
136
207
|
class MyWorkflow < ApplicationJob
|
137
|
-
|
208
|
+
prepend ChronoForge::Executor
|
138
209
|
|
139
210
|
private
|
140
211
|
|
141
212
|
def should_retry?(error, attempt_count)
|
142
213
|
case error
|
143
214
|
when NetworkError
|
144
|
-
attempt_count < 5
|
215
|
+
attempt_count < 5 # Retry network errors up to 5 times
|
145
216
|
when ValidationError
|
146
217
|
false # Don't retry validation errors
|
147
218
|
else
|
148
|
-
attempt_count < 3
|
219
|
+
attempt_count < 3 # Default retry policy
|
149
220
|
end
|
150
221
|
end
|
151
222
|
end
|
152
223
|
```
|
153
224
|
|
154
|
-
## Testing
|
225
|
+
## 🧪 Testing
|
155
226
|
|
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
|
227
|
+
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:
|
157
228
|
|
158
229
|
1. Add ChaoticJob to your Gemfile's test group:
|
159
230
|
|
@@ -179,14 +250,18 @@ class WorkflowTest < ActiveJob::TestCase
|
|
179
250
|
include ChaoticJob::Helpers
|
180
251
|
|
181
252
|
def test_workflow_completion
|
182
|
-
# Enqueue the job
|
183
|
-
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
|
+
)
|
184
259
|
|
185
260
|
# Perform all enqueued jobs
|
186
261
|
perform_all_jobs
|
187
262
|
|
188
263
|
# Assert workflow completed successfully
|
189
|
-
workflow = ChronoForge::Workflow.
|
264
|
+
workflow = ChronoForge::Workflow.find_by(key: "order-test-123")
|
190
265
|
assert workflow.completed?
|
191
266
|
|
192
267
|
# Check workflow context
|
@@ -196,22 +271,24 @@ class WorkflowTest < ActiveJob::TestCase
|
|
196
271
|
end
|
197
272
|
```
|
198
273
|
|
199
|
-
|
274
|
+
## 🗄️ Database Schema
|
200
275
|
|
201
|
-
|
202
|
-
|
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.
|
276
|
+
ChronoForge creates three main tables:
|
204
277
|
|
278
|
+
1. **chrono_forge_workflows**: Stores workflow state and context
|
279
|
+
2. **chrono_forge_execution_logs**: Tracks individual execution steps
|
280
|
+
3. **chrono_forge_error_logs**: Records detailed error information
|
205
281
|
|
206
|
-
##
|
282
|
+
## 🔍 When to Use ChronoForge
|
207
283
|
|
208
|
-
ChronoForge
|
284
|
+
ChronoForge is ideal for:
|
209
285
|
|
210
|
-
|
211
|
-
|
212
|
-
|
286
|
+
- **Long-running business processes** - Order processing, account registration flows
|
287
|
+
- **Processes requiring durability** - Financial transactions, data migrations
|
288
|
+
- **Multi-step workflows** - Onboarding flows, approval processes, multi-stage jobs
|
289
|
+
- **State machines with time-based transitions** - Document approval, subscription lifecycle
|
213
290
|
|
214
|
-
## Development
|
291
|
+
## 🚀 Development
|
215
292
|
|
216
293
|
After checking out the repo, run:
|
217
294
|
|
@@ -227,7 +304,7 @@ The test suite uses SQLite by default and includes:
|
|
227
304
|
- Integration tests with ActiveJob
|
228
305
|
- Example workflow implementations
|
229
306
|
|
230
|
-
## Contributing
|
307
|
+
## 👥 Contributing
|
231
308
|
|
232
309
|
1. Fork the repository
|
233
310
|
2. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
@@ -237,6 +314,6 @@ The test suite uses SQLite by default and includes:
|
|
237
314
|
|
238
315
|
Please include tests for any new features or bug fixes.
|
239
316
|
|
240
|
-
## License
|
317
|
+
## 📜 License
|
241
318
|
|
242
319
|
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
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",
|
@@ -13,7 +13,7 @@
|
|
13
13
|
},
|
14
14
|
{
|
15
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
|
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
17
|
},
|
18
18
|
{
|
19
19
|
"path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/error_log.rb",
|
@@ -25,7 +25,7 @@
|
|
25
25
|
},
|
26
26
|
{
|
27
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 #
|
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
29
|
},
|
30
30
|
{
|
31
31
|
"path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/execution_tracker.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 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
|
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
41
|
},
|
42
42
|
{
|
43
43
|
"path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/wait.rb",
|
@@ -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/export.rb
CHANGED
@@ -15,7 +15,7 @@ def export_files_to_json(directory, extensions, output_file, exceptions = [])
|
|
15
15
|
next if exceptions.any? { |exception| path.include?(exception) }
|
16
16
|
|
17
17
|
# Check if file extension matches any in our list
|
18
|
-
ext = File.extname(path).downcase[1
|
18
|
+
ext = File.extname(path).downcase[1..] # Remove the leading dot
|
19
19
|
next unless extensions.include?(ext)
|
20
20
|
|
21
21
|
puts path
|
@@ -21,21 +21,36 @@ module ChronoForge
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def []=(key, value)
|
24
|
-
|
25
|
-
|
24
|
+
set_value(key, value)
|
25
|
+
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
else
|
31
|
-
value
|
32
|
-
end
|
27
|
+
def [](key)
|
28
|
+
get_value(key)
|
29
|
+
end
|
33
30
|
|
34
|
-
|
31
|
+
# Fetches a value from the context
|
32
|
+
# Returns the value if the key exists, otherwise returns the default value
|
33
|
+
def fetch(key, default = nil)
|
34
|
+
key?(key) ? get_value(key) : default
|
35
35
|
end
|
36
36
|
|
37
|
-
|
38
|
-
|
37
|
+
# Sets a value in the context
|
38
|
+
# Alias for the []= method
|
39
|
+
def set(key, value)
|
40
|
+
set_value(key, value)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Sets a value in the context only if the key doesn't already exist
|
44
|
+
# Returns true if the value was set, false otherwise
|
45
|
+
def set_once(key, value)
|
46
|
+
return false if key?(key)
|
47
|
+
|
48
|
+
set_value(key, value)
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def key?(key)
|
53
|
+
@context.key?(key.to_s)
|
39
54
|
end
|
40
55
|
|
41
56
|
def save!
|
@@ -47,6 +62,23 @@ module ChronoForge
|
|
47
62
|
|
48
63
|
private
|
49
64
|
|
65
|
+
def set_value(key, value)
|
66
|
+
validate_value!(value)
|
67
|
+
|
68
|
+
@context[key.to_s] =
|
69
|
+
if value.is_a?(Hash) || value.is_a?(Array)
|
70
|
+
deep_dup(value)
|
71
|
+
else
|
72
|
+
value
|
73
|
+
end
|
74
|
+
|
75
|
+
@dirty = true
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_value(key)
|
79
|
+
@context[key.to_s]
|
80
|
+
end
|
81
|
+
|
50
82
|
def validate_value!(value)
|
51
83
|
unless ALLOWED_TYPES.any? { |type| value.is_a?(type) }
|
52
84
|
raise ValidationError, "Unsupported context value type: #{value.inspect}"
|
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
|
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.
|
4
|
+
version: 0.3.1
|
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-04-
|
11
|
+
date: 2025-04-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|