job-workflow 0.1.3
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +91 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +47 -0
- data/Rakefile +55 -0
- data/Steepfile +10 -0
- data/guides/API_REFERENCE.md +112 -0
- data/guides/BEST_PRACTICES.md +113 -0
- data/guides/CACHE_STORE_INTEGRATION.md +145 -0
- data/guides/CONDITIONAL_EXECUTION.md +66 -0
- data/guides/DEPENDENCY_WAIT.md +386 -0
- data/guides/DRY_RUN.md +390 -0
- data/guides/DSL_BASICS.md +216 -0
- data/guides/ERROR_HANDLING.md +187 -0
- data/guides/GETTING_STARTED.md +524 -0
- data/guides/INSTRUMENTATION.md +131 -0
- data/guides/LIFECYCLE_HOOKS.md +415 -0
- data/guides/NAMESPACES.md +75 -0
- data/guides/OPENTELEMETRY_INTEGRATION.md +86 -0
- data/guides/PARALLEL_PROCESSING.md +302 -0
- data/guides/PRODUCTION_DEPLOYMENT.md +110 -0
- data/guides/QUEUE_MANAGEMENT.md +141 -0
- data/guides/README.md +174 -0
- data/guides/SCHEDULED_JOBS.md +165 -0
- data/guides/STRUCTURED_LOGGING.md +268 -0
- data/guides/TASK_OUTPUTS.md +240 -0
- data/guides/TESTING_STRATEGY.md +56 -0
- data/guides/THROTTLING.md +198 -0
- data/guides/TROUBLESHOOTING.md +53 -0
- data/guides/WORKFLOW_COMPOSITION.md +675 -0
- data/guides/WORKFLOW_STATUS_QUERY.md +288 -0
- data/lib/job-workflow.rb +3 -0
- data/lib/job_workflow/argument_def.rb +16 -0
- data/lib/job_workflow/arguments.rb +40 -0
- data/lib/job_workflow/auto_scaling/adapter/aws_adapter.rb +66 -0
- data/lib/job_workflow/auto_scaling/adapter.rb +31 -0
- data/lib/job_workflow/auto_scaling/configuration.rb +85 -0
- data/lib/job_workflow/auto_scaling/executor.rb +43 -0
- data/lib/job_workflow/auto_scaling.rb +69 -0
- data/lib/job_workflow/cache_store_adapters.rb +46 -0
- data/lib/job_workflow/context.rb +352 -0
- data/lib/job_workflow/dry_run_config.rb +31 -0
- data/lib/job_workflow/dsl.rb +236 -0
- data/lib/job_workflow/error_hook.rb +24 -0
- data/lib/job_workflow/hook.rb +24 -0
- data/lib/job_workflow/hook_registry.rb +66 -0
- data/lib/job_workflow/instrumentation/log_subscriber.rb +194 -0
- data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +221 -0
- data/lib/job_workflow/instrumentation.rb +257 -0
- data/lib/job_workflow/job_status.rb +92 -0
- data/lib/job_workflow/logger.rb +86 -0
- data/lib/job_workflow/namespace.rb +36 -0
- data/lib/job_workflow/output.rb +81 -0
- data/lib/job_workflow/output_def.rb +14 -0
- data/lib/job_workflow/queue.rb +74 -0
- data/lib/job_workflow/queue_adapter.rb +38 -0
- data/lib/job_workflow/queue_adapters/abstract.rb +87 -0
- data/lib/job_workflow/queue_adapters/null_adapter.rb +127 -0
- data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +224 -0
- data/lib/job_workflow/runner.rb +173 -0
- data/lib/job_workflow/schedule.rb +46 -0
- data/lib/job_workflow/semaphore.rb +71 -0
- data/lib/job_workflow/task.rb +83 -0
- data/lib/job_workflow/task_callable.rb +43 -0
- data/lib/job_workflow/task_context.rb +70 -0
- data/lib/job_workflow/task_dependency_wait.rb +66 -0
- data/lib/job_workflow/task_enqueue.rb +50 -0
- data/lib/job_workflow/task_graph.rb +43 -0
- data/lib/job_workflow/task_job_status.rb +70 -0
- data/lib/job_workflow/task_output.rb +51 -0
- data/lib/job_workflow/task_retry.rb +64 -0
- data/lib/job_workflow/task_throttle.rb +46 -0
- data/lib/job_workflow/version.rb +5 -0
- data/lib/job_workflow/workflow.rb +87 -0
- data/lib/job_workflow/workflow_status.rb +112 -0
- data/lib/job_workflow.rb +59 -0
- data/rbs_collection.lock.yaml +172 -0
- data/rbs_collection.yaml +14 -0
- data/sig/generated/job-workflow.rbs +2 -0
- data/sig/generated/job_workflow/argument_def.rbs +14 -0
- data/sig/generated/job_workflow/arguments.rbs +26 -0
- data/sig/generated/job_workflow/auto_scaling/adapter/aws_adapter.rbs +32 -0
- data/sig/generated/job_workflow/auto_scaling/adapter.rbs +22 -0
- data/sig/generated/job_workflow/auto_scaling/configuration.rbs +50 -0
- data/sig/generated/job_workflow/auto_scaling/executor.rbs +29 -0
- data/sig/generated/job_workflow/auto_scaling.rbs +47 -0
- data/sig/generated/job_workflow/cache_store_adapters.rbs +28 -0
- data/sig/generated/job_workflow/context.rbs +155 -0
- data/sig/generated/job_workflow/dry_run_config.rbs +16 -0
- data/sig/generated/job_workflow/dsl.rbs +117 -0
- data/sig/generated/job_workflow/error_hook.rbs +18 -0
- data/sig/generated/job_workflow/hook.rbs +18 -0
- data/sig/generated/job_workflow/hook_registry.rbs +47 -0
- data/sig/generated/job_workflow/instrumentation/log_subscriber.rbs +102 -0
- data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +113 -0
- data/sig/generated/job_workflow/instrumentation.rbs +138 -0
- data/sig/generated/job_workflow/job_status.rbs +46 -0
- data/sig/generated/job_workflow/logger.rbs +56 -0
- data/sig/generated/job_workflow/namespace.rbs +24 -0
- data/sig/generated/job_workflow/output.rbs +39 -0
- data/sig/generated/job_workflow/output_def.rbs +12 -0
- data/sig/generated/job_workflow/queue.rbs +49 -0
- data/sig/generated/job_workflow/queue_adapter.rbs +18 -0
- data/sig/generated/job_workflow/queue_adapters/abstract.rbs +56 -0
- data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +73 -0
- data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +111 -0
- data/sig/generated/job_workflow/runner.rbs +66 -0
- data/sig/generated/job_workflow/schedule.rbs +34 -0
- data/sig/generated/job_workflow/semaphore.rbs +37 -0
- data/sig/generated/job_workflow/task.rbs +60 -0
- data/sig/generated/job_workflow/task_callable.rbs +30 -0
- data/sig/generated/job_workflow/task_context.rbs +52 -0
- data/sig/generated/job_workflow/task_dependency_wait.rbs +42 -0
- data/sig/generated/job_workflow/task_enqueue.rbs +27 -0
- data/sig/generated/job_workflow/task_graph.rbs +27 -0
- data/sig/generated/job_workflow/task_job_status.rbs +42 -0
- data/sig/generated/job_workflow/task_output.rbs +29 -0
- data/sig/generated/job_workflow/task_retry.rbs +30 -0
- data/sig/generated/job_workflow/task_throttle.rbs +20 -0
- data/sig/generated/job_workflow/version.rbs +5 -0
- data/sig/generated/job_workflow/workflow.rbs +48 -0
- data/sig/generated/job_workflow/workflow_status.rbs +55 -0
- data/sig/generated/job_workflow.rbs +8 -0
- data/sig-private/activejob.rbs +35 -0
- data/sig-private/activesupport.rbs +23 -0
- data/sig-private/aws.rbs +32 -0
- data/sig-private/opentelemetry.rbs +40 -0
- data/sig-private/solid_queue.rbs +108 -0
- data/tmp/.keep +0 -0
- metadata +190 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2ff0a254ebd8fee848ab5eb26036e0bd98392700811e0bd55ffeaf4c348e04b6
|
|
4
|
+
data.tar.gz: ada2c0170fe7776a15802416f65070911e464f98abff416f9cbb15ed207bc7a8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d406e2e4ed2671b79aac352abe0405c009d0f4b089b4ecd2fb400b2a79c7676689cdbb9992e365063f17fb7160bb45084bc18966ff101097b39c6f6e5d3ecb60
|
|
7
|
+
data.tar.gz: 8d51653d18ed1769e513c2c3b854ac21663e1a9ed168dbac1713689912fa24b0c9cb9c66469a892e946e014905fc825934b555e4d657786efd5becd99fd9a866
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-performance
|
|
3
|
+
- rubocop-rspec
|
|
4
|
+
- rubocop-rbs_inline
|
|
5
|
+
- rubocop-thread_safety
|
|
6
|
+
|
|
7
|
+
AllCops:
|
|
8
|
+
TargetRubyVersion: 3.1
|
|
9
|
+
NewCops: enable
|
|
10
|
+
SuggestExtensions: false
|
|
11
|
+
Exclude:
|
|
12
|
+
- examples/**/*
|
|
13
|
+
- tmp/**/*
|
|
14
|
+
|
|
15
|
+
Layout/LeadingCommentSpace:
|
|
16
|
+
Enabled: true
|
|
17
|
+
AllowDoxygenCommentStyle: false
|
|
18
|
+
AllowGemfileRubyComment: false
|
|
19
|
+
AllowRBSInlineAnnotation: true
|
|
20
|
+
|
|
21
|
+
Layout/LineLength:
|
|
22
|
+
Enabled: true
|
|
23
|
+
Max: 120
|
|
24
|
+
AllowHeredoc: true
|
|
25
|
+
AllowURI: true
|
|
26
|
+
AllowQualifiedName: true
|
|
27
|
+
AllowRBSInlineAnnotation: true
|
|
28
|
+
AllowCopDirectives: true
|
|
29
|
+
AllowedPatterns:
|
|
30
|
+
- "^ *#"
|
|
31
|
+
|
|
32
|
+
Metrics/BlockLength:
|
|
33
|
+
Enabled: true
|
|
34
|
+
CountComments: false
|
|
35
|
+
Max: 25
|
|
36
|
+
CountAsOne:
|
|
37
|
+
- array
|
|
38
|
+
- hash
|
|
39
|
+
- heredoc
|
|
40
|
+
|
|
41
|
+
Metrics/ClassLength:
|
|
42
|
+
Enabled: true
|
|
43
|
+
CountComments: false
|
|
44
|
+
Max: 100
|
|
45
|
+
CountAsOne:
|
|
46
|
+
- array
|
|
47
|
+
- hash
|
|
48
|
+
- heredoc
|
|
49
|
+
|
|
50
|
+
Metrics/MethodLength:
|
|
51
|
+
Enabled: true
|
|
52
|
+
CountComments: false
|
|
53
|
+
Max: 10
|
|
54
|
+
CountAsOne:
|
|
55
|
+
- array
|
|
56
|
+
- hash
|
|
57
|
+
- heredoc
|
|
58
|
+
|
|
59
|
+
Metrics/ModuleLength:
|
|
60
|
+
Enabled: true
|
|
61
|
+
CountComments: false
|
|
62
|
+
Max: 100
|
|
63
|
+
CountAsOne:
|
|
64
|
+
- array
|
|
65
|
+
- hash
|
|
66
|
+
- heredoc
|
|
67
|
+
|
|
68
|
+
Naming/FileName:
|
|
69
|
+
Exclude:
|
|
70
|
+
- lib/job-workflow.rb
|
|
71
|
+
|
|
72
|
+
RSpec/ExampleLength:
|
|
73
|
+
Enabled: true
|
|
74
|
+
Max: 5
|
|
75
|
+
CountAsOne:
|
|
76
|
+
- array
|
|
77
|
+
- hash
|
|
78
|
+
- heredoc
|
|
79
|
+
|
|
80
|
+
Style/Documentation:
|
|
81
|
+
Enabled: false
|
|
82
|
+
|
|
83
|
+
Style/StringLiterals:
|
|
84
|
+
EnforcedStyle: double_quotes
|
|
85
|
+
|
|
86
|
+
Style/StringLiteralsInInterpolation:
|
|
87
|
+
EnforcedStyle: double_quotes
|
|
88
|
+
|
|
89
|
+
ThreadSafety/ClassAndModuleAttributes:
|
|
90
|
+
Enabled: false
|
|
91
|
+
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.1.3] - 2026-01-06
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Rename library from `job-flow` to `job-workflow`
|
|
8
|
+
|
|
9
|
+
## [0.1.2] - 2026-01-05
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fix `enqueue_task` to use correct ActiveJob API `ActiveJob.perform_all_later` instead of non-existent `job.class.perform_all_later`
|
|
14
|
+
- Fix SolidQueue adapter to use correct lifecycle hook API `SolidQueue::Worker.on_stop` instead of deprecated `on_worker_stop`
|
|
15
|
+
- Fix SolidQueue job arguments extraction to handle both Hash and Array formats for compatibility with SolidQueue's serialization format
|
|
16
|
+
|
|
17
|
+
## [0.1.1] - 2026-01-04
|
|
18
|
+
|
|
19
|
+
- Added `spec.license` field to gemspec for better gem metadata
|
|
20
|
+
|
|
21
|
+
## [0.1.0] - 2026-01-04
|
|
22
|
+
|
|
23
|
+
- Initial release
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 shoma07
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# JobWorkflow
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Early Stage (v0.1.3):** This library is in active development. APIs and features may change in breaking ways without notice. Use in production at your own risk and expect potential breaking changes in future releases.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
JobWorkflow is a declarative workflow orchestration engine for Ruby on Rails applications, built on top of ActiveJob. It provides a simple DSL for defining workflows.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add this line to your application's Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
# Gemfile
|
|
15
|
+
gem 'job-workflow'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
And then execute:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Documentation
|
|
25
|
+
|
|
26
|
+
For comprehensive documentation, including step-by-step getting started instructions and in-depth feature guides, see the **[guides/](guides/README.md)** directory.
|
|
27
|
+
|
|
28
|
+
[Reference the guides →](guides/README.md)
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Rails >= 8.1.0
|
|
33
|
+
- SolidQueue >= 1.24.0
|
|
34
|
+
|
|
35
|
+
## Development
|
|
36
|
+
|
|
37
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
38
|
+
|
|
39
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
40
|
+
|
|
41
|
+
## Contributing
|
|
42
|
+
|
|
43
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/shoma07/job-workflow.
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
require "rubocop/rake_task"
|
|
9
|
+
|
|
10
|
+
RuboCop::RakeTask.new(:lint) do |t|
|
|
11
|
+
t.formatters = %w[simple]
|
|
12
|
+
t.options = ["--parallel"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
namespace :lint do
|
|
16
|
+
desc "Lint safe fix (Rubocop)"
|
|
17
|
+
task fix: :autocorrect
|
|
18
|
+
|
|
19
|
+
desc "Lint all fix (Rubocop)"
|
|
20
|
+
task fixall: :autocorrect_all
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
namespace :rbs do
|
|
24
|
+
desc "Install RBS Collection"
|
|
25
|
+
task :install do
|
|
26
|
+
require "rbs"
|
|
27
|
+
require "rbs/cli"
|
|
28
|
+
RBS::CLI.new(stdout: $stdout, stderr: $stderr).run("collection install --frozen".split)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "Update RBS Collection"
|
|
32
|
+
task :update do
|
|
33
|
+
require "rbs"
|
|
34
|
+
require "rbs/cli"
|
|
35
|
+
RBS::CLI.new(stdout: $stdout, stderr: $stderr).run("collection update".split)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
desc "Generated RBS files from rbs-inline"
|
|
39
|
+
task :inline do
|
|
40
|
+
require "rbs/inline"
|
|
41
|
+
require "rbs/inline/cli"
|
|
42
|
+
FileUtils.rm_r(File.expand_path("sig/generated", __dir__), secure: true)
|
|
43
|
+
RBS::Inline::CLI.new.run(%w[lib --output --opt-out])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
desc "Typecheck Run (Steep)"
|
|
48
|
+
task typecheck: %i[rbs:inline] do
|
|
49
|
+
require "steep"
|
|
50
|
+
require "steep/cli"
|
|
51
|
+
steep_options = { stdout: $stdout, stderr: $stderr, stdin: $stdin }
|
|
52
|
+
Steep::CLI.new(argv: ["check", "-j2"], **steep_options).run.zero? || exit(1)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
task default: %i[lint typecheck spec]
|
data/Steepfile
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Detailed reference for all DSL methods and classes in JobWorkflow.
|
|
4
|
+
|
|
5
|
+
## DSL Methods
|
|
6
|
+
|
|
7
|
+
### task
|
|
8
|
+
|
|
9
|
+
Define an individual task.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
task(name, **options, &block)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Parameters**:
|
|
16
|
+
- `name` (Symbol): Task name
|
|
17
|
+
- `options` (Hash): Task options
|
|
18
|
+
- `depends_on` (Symbol | Array[Symbol]): Dependent tasks
|
|
19
|
+
- `each` (Proc): Proc that returns an enumerable for map task execution
|
|
20
|
+
- `enqueue` (Hash | Proc | bool): Controls whether task iterations are enqueued as sub-jobs
|
|
21
|
+
- Hash format (recommended): `{ condition: Proc, queue: String, concurrency: Integer }`
|
|
22
|
+
- `condition` (Proc | bool): Determines if task should be enqueued (default: true if Hash is not empty)
|
|
23
|
+
- `queue` (String): Custom queue name for the task (optional)
|
|
24
|
+
- `concurrency` (Integer): Concurrency limit for parallel processing (default: unlimited)
|
|
25
|
+
- Proc format (legacy): Proc that returns boolean
|
|
26
|
+
- bool format: true/false for simple cases
|
|
27
|
+
- Default: nil (synchronous execution)
|
|
28
|
+
- `retry` (Integer | Hash): Retry configuration. Integer for simple retry count, Hash for advanced settings
|
|
29
|
+
- `count` (Integer): Maximum retry attempts (default: 3 when Hash)
|
|
30
|
+
- `strategy` (Symbol): `:linear` or `:exponential` (default: `:exponential`)
|
|
31
|
+
- `base_delay` (Integer): Base delay in seconds (default: 1)
|
|
32
|
+
- `jitter` (bool): Add randomness to delays (default: false)
|
|
33
|
+
- `timeout` (Numeric | nil): Execution timeout in seconds for **one attempt** (default: nil)
|
|
34
|
+
- You can pass Integer or Float seconds
|
|
35
|
+
- Timeout does **not** include retry time across attempts
|
|
36
|
+
- `nil` disables timeout
|
|
37
|
+
- `condition` (Proc): Execute only if returns true (default: `->(_ctx) { true }`)
|
|
38
|
+
- `throttle` (Hash): Throttling settings
|
|
39
|
+
- `output` (Hash): Task output definition
|
|
40
|
+
- `block` (Proc): Task implementation (always takes `|ctx|`)
|
|
41
|
+
- Without `each`: regular task execution
|
|
42
|
+
- With `each`: access current element via `ctx.each_value`
|
|
43
|
+
|
|
44
|
+
**Example**:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
argument :enabled, "bool", default: false
|
|
48
|
+
argument :data, "Hash"
|
|
49
|
+
|
|
50
|
+
task :simple, output: { result: "String" } do |ctx|
|
|
51
|
+
{ result: "simple" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
task :with_dependencies,
|
|
55
|
+
depends_on: [:simple],
|
|
56
|
+
retry: 3,
|
|
57
|
+
output: { final: "String" } do |ctx|
|
|
58
|
+
result = ctx.output[:simple].first.result
|
|
59
|
+
{ final: process(result) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
task :conditional,
|
|
63
|
+
condition: ->(ctx) { ctx.arguments.enabled },
|
|
64
|
+
output: { conditional_result: "String" } do |ctx|
|
|
65
|
+
{ conditional_result: "executed" }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
task :throttled,
|
|
69
|
+
throttle: { key: "api", limit: 10, ttl: 60 },
|
|
70
|
+
output: { response: "Hash" } do |ctx|
|
|
71
|
+
data = ctx.arguments.data
|
|
72
|
+
{ response: ExternalAPI.call(data) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Parallel processing with collection
|
|
76
|
+
task :process_items,
|
|
77
|
+
each: ->(ctx) { ctx.arguments.items },
|
|
78
|
+
enqueue: { concurrency: 5 },
|
|
79
|
+
output: { result: "String" } do |ctx|
|
|
80
|
+
item = ctx.each_value
|
|
81
|
+
{ result: ProcessService.handle(item) }
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Map Task Output**: When `each:` is specified, outputs are automatically collected as an array.
|
|
86
|
+
|
|
87
|
+
**Example**:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
argument :items, "Array[String]"
|
|
91
|
+
|
|
92
|
+
task :process_items,
|
|
93
|
+
each: ->(ctx) { ctx.arguments.items },
|
|
94
|
+
enqueue: { concurrency: 5 },
|
|
95
|
+
output: { result: "String", status: "Symbol" } do |ctx|
|
|
96
|
+
item = ctx.each_value
|
|
97
|
+
{
|
|
98
|
+
result: ProcessService.handle(item),
|
|
99
|
+
status: :success
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
task :summarize, depends_on: [:process_items] do |ctx|
|
|
104
|
+
# Access outputs as an array
|
|
105
|
+
outputs = ctx.output[:process_items]
|
|
106
|
+
puts "Processed #{outputs.size} items"
|
|
107
|
+
|
|
108
|
+
outputs.each do |output|
|
|
109
|
+
puts "Result: #{output.result}, Status: #{output.status}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Best Practices
|
|
2
|
+
|
|
3
|
+
Best practices, design patterns, and recommendations for effective JobWorkflow usage.
|
|
4
|
+
|
|
5
|
+
## Workflow Design
|
|
6
|
+
|
|
7
|
+
### Task Granularity
|
|
8
|
+
|
|
9
|
+
#### Appropriate Division
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# ✅ Recommended: Follow single responsibility principle
|
|
13
|
+
class WellDesignedWorkflowJob < ApplicationJob
|
|
14
|
+
include JobWorkflow::DSL
|
|
15
|
+
|
|
16
|
+
argument :data, "Hash"
|
|
17
|
+
|
|
18
|
+
task :validate_input do |ctx|
|
|
19
|
+
# Only validation
|
|
20
|
+
data = ctx.arguments.data
|
|
21
|
+
raise "Invalid" unless data.valid?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
task :fetch_dependencies, depends_on: [:validate_input], output: { dependencies: "Hash" } do |ctx|
|
|
25
|
+
# Only fetch data
|
|
26
|
+
{ dependencies: fetch_required_data }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
task :transform_data, depends_on: [:fetch_dependencies], output: { transformed: "Hash" } do |ctx|
|
|
30
|
+
# Only transform
|
|
31
|
+
data = ctx.arguments.data
|
|
32
|
+
dependencies = ctx.output[:fetch_dependencies].first.dependencies
|
|
33
|
+
{ transformed: transform(data, dependencies) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
task :save_result, depends_on: [:transform_data] do |ctx|
|
|
37
|
+
# Only save
|
|
38
|
+
transformed = ctx.output[:transform_data].first.transformed
|
|
39
|
+
save_to_database(transformed)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ❌ Not recommended: Multiple responsibilities in one task
|
|
44
|
+
class PoorlyDesignedWorkflowJob < ApplicationJob
|
|
45
|
+
include JobWorkflow::DSL
|
|
46
|
+
|
|
47
|
+
argument :data, "Hash"
|
|
48
|
+
|
|
49
|
+
task :do_everything do |ctx|
|
|
50
|
+
# All in one task (hard to test, not reusable)
|
|
51
|
+
data = ctx.arguments.data
|
|
52
|
+
raise "Invalid" unless data.valid?
|
|
53
|
+
deps = fetch_required_data
|
|
54
|
+
transformed = transform(data, deps)
|
|
55
|
+
save_to_database(transformed)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Explicit Dependencies
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
argument :raw_data, "String"
|
|
64
|
+
|
|
65
|
+
# ✅ Explicit dependencies
|
|
66
|
+
task :prepare_data, output: { prepared: "Hash" } do |ctx|
|
|
67
|
+
raw_data = ctx.arguments.raw_data
|
|
68
|
+
{ prepared: prepare(raw_data) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
task :process_data, depends_on: [:prepare_data], output: { result: "String" } do |ctx|
|
|
72
|
+
prepared = ctx.output[:prepare_data].first.prepared
|
|
73
|
+
{ result: process(prepared) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# ❌ Implicit dependencies (unpredictable execution order)
|
|
77
|
+
task :task1, output: { shared: "String" } do |ctx|
|
|
78
|
+
{ shared: "data" }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
task :task2 do |ctx|
|
|
82
|
+
# No guarantee task1 executes first - this may fail!
|
|
83
|
+
shared = ctx.output[:task1].first&.shared
|
|
84
|
+
use(shared)
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Future Considerations
|
|
89
|
+
|
|
90
|
+
The following features are under consideration for future releases:
|
|
91
|
+
|
|
92
|
+
### Saga Pattern
|
|
93
|
+
|
|
94
|
+
Built-in support for the Saga pattern (distributed transaction compensation) is not currently planned. For workflows requiring compensation logic, we recommend:
|
|
95
|
+
|
|
96
|
+
1. **Using Lifecycle Hooks**: Implement cleanup/rollback logic in `after` or `around` hooks
|
|
97
|
+
2. **Application-layer management**: Handle compensation in your service layer where domain logic resides
|
|
98
|
+
3. **Idempotent task design**: Design tasks to be safely retryable
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# Example: Using around hook for compensation
|
|
102
|
+
around :charge_payment do |ctx, task|
|
|
103
|
+
begin
|
|
104
|
+
task.call
|
|
105
|
+
rescue PaymentError => e
|
|
106
|
+
# Compensation logic
|
|
107
|
+
rollback_previous_reservations(ctx)
|
|
108
|
+
raise
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If there is significant demand for native Saga support, it may be reconsidered in future versions.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Cache Store Integration Guide
|
|
2
|
+
|
|
3
|
+
JobWorkflow is designed to use `ActiveSupport::Cache::Store` compatible backends. Currently, the cache store infrastructure is in place, but no features are actively using the cache yet. This guide documents the cache store abstraction layer and its planned usage patterns.
|
|
4
|
+
|
|
5
|
+
**Status**: Cache store detection and initialization are implemented. Feature implementations that utilize the cache (Workflow status persistence, Output storage, etc.) are planned for future releases.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`JobWorkflow::CacheStoreAdapters` provides automatic cache store detection and a unified access interface. This allows JobWorkflow to transparently use available cache backends like SolidCache or memory-based caches.
|
|
10
|
+
|
|
11
|
+
## Automatic Detection
|
|
12
|
+
|
|
13
|
+
`JobWorkflow::CacheStoreAdapters.current` automatically detects and instantiates an appropriate cache store with the following priority:
|
|
14
|
+
|
|
15
|
+
1. **SolidCache**: If `ActiveSupport::Cache::SolidCacheStore` is defined, a new instance is created with JobWorkflow's namespace configuration
|
|
16
|
+
2. **Memory Store**: If neither is available, `ActiveSupport::Cache::MemoryStore` is used as the default fallback
|
|
17
|
+
|
|
18
|
+
All stores are automatically namespaced with `"job_workflow"` to prevent key collisions with application-level caches.
|
|
19
|
+
|
|
20
|
+
### Example
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# Access the auto-detected cache store
|
|
24
|
+
cache = JobWorkflow::CacheStoreAdapters.current
|
|
25
|
+
|
|
26
|
+
# Write to cache
|
|
27
|
+
cache.write("my_key", { data: "value" }, expires_in: 24.hours)
|
|
28
|
+
|
|
29
|
+
# Read from cache
|
|
30
|
+
data = cache.read("my_key")
|
|
31
|
+
|
|
32
|
+
# Delete from cache
|
|
33
|
+
cache.delete("my_key")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Supported Cache Backends
|
|
37
|
+
|
|
38
|
+
### SolidCache (Recommended for Rails 8+)
|
|
39
|
+
|
|
40
|
+
SolidCache is a database-backed cache store recommended for Rails 8 applications. JobWorkflow automatically uses SolidCache if available.
|
|
41
|
+
|
|
42
|
+
For detailed SolidCache setup instructions, see the [official SolidCache documentation](https://github.com/rails/solid_cache).
|
|
43
|
+
|
|
44
|
+
### Memory Store (Default)
|
|
45
|
+
|
|
46
|
+
In environments where SolidCache is not available (development, tests, or minimal deployments), JobWorkflow uses `ActiveSupport::Cache::MemoryStore`:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
ActiveSupport::Cache::MemoryStore.new
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Design Decision: No Direct Rails.cache Usage
|
|
53
|
+
|
|
54
|
+
JobWorkflow does NOT use `Rails.cache` directly. Instead, it creates dedicated cache store instances. This design choice provides:
|
|
55
|
+
|
|
56
|
+
- **Namespace Isolation**: JobWorkflow caches are namespaced with `"job_workflow"` prefix to prevent key collisions with application-level caches
|
|
57
|
+
- **Explicit Configuration**: JobWorkflow's cache is independently configurable without affecting Rails application caching
|
|
58
|
+
- **Predictable Behavior**: No cache invalidations triggered by Rails application code
|
|
59
|
+
|
|
60
|
+
This ensures that JobWorkflow's internal caching behavior is isolated and predictable, even in complex Rails applications with sophisticated caching strategies.
|
|
61
|
+
|
|
62
|
+
## Test Environment
|
|
63
|
+
|
|
64
|
+
In test environments, JobWorkflow automatically uses `MemoryStore`. You can reset the cache between tests:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# spec/spec_helper.rb is pre-configured with:
|
|
68
|
+
config.after do
|
|
69
|
+
JobWorkflow::CacheStoreAdapters.reset!
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
To manually clear cache in tests:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
RSpec.configure do |config|
|
|
77
|
+
config.before(:each) do
|
|
78
|
+
JobWorkflow::CacheStoreAdapters.current.clear if JobWorkflow::CacheStoreAdapters.current.respond_to?(:clear)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Cache Key Format
|
|
84
|
+
|
|
85
|
+
JobWorkflow generates cache keys with the following format:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
job_workflow:<feature>:<workflow_id>:<identifier>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
- `job_workflow:task_output:wf-123:task-1`
|
|
93
|
+
- `job_workflow:dependency_wait:wf-456:dep-xyz`
|
|
94
|
+
- `job_workflow:semaphore:wf-789:sem-lock`
|
|
95
|
+
|
|
96
|
+
## Performance Considerations
|
|
97
|
+
|
|
98
|
+
### Latency by Backend
|
|
99
|
+
|
|
100
|
+
| Backend | Read/Write Latency |
|
|
101
|
+
|---------|-------------------|
|
|
102
|
+
| MemoryStore | < 1ms |
|
|
103
|
+
| SolidCache (Database) | 5-50ms |
|
|
104
|
+
| Redis | 1-10ms |
|
|
105
|
+
| Memcached | 1-5ms |
|
|
106
|
+
## Troubleshooting
|
|
107
|
+
|
|
108
|
+
### Verify Current Cache Backend
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# Check which cache store is being used
|
|
112
|
+
puts JobWorkflow::CacheStoreAdapters.current.class
|
|
113
|
+
# => ActiveSupport::Cache::SolidCacheStore or ActiveSupport::Cache::MemoryStore
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Clear Cache
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# Clear all JobWorkflow caches
|
|
120
|
+
if JobWorkflow::CacheStoreAdapters.current.respond_to?(:clear)
|
|
121
|
+
JobWorkflow::CacheStoreAdapters.current.clear
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Reset to Default Configuration
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# Reset to auto-detected cache store
|
|
129
|
+
JobWorkflow::CacheStoreAdapters.reset!
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Best Practices
|
|
133
|
+
|
|
134
|
+
1. **Production**: Use SolidCache for database-backed persistence
|
|
135
|
+
2. **Development**: Default MemoryStore is sufficient
|
|
136
|
+
3. **Testing**: MemoryStore is automatically used and cleared between tests
|
|
137
|
+
4. **Large Workflows**: Monitor cache size and choose appropriate backend (SolidCache for unlimited, MemoryStore for bounded)
|
|
138
|
+
|
|
139
|
+
## Out of Scope
|
|
140
|
+
|
|
141
|
+
The following are not supported or are intentionally excluded:
|
|
142
|
+
|
|
143
|
+
- Custom adapter implementations by users
|
|
144
|
+
- Automatic cache key prefixing (beyond JobWorkflow's internal namespace)
|
|
145
|
+
- AWS S3 or other external storage backends (planned for future LargeStorageAdapters)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Conditional Execution
|
|
2
|
+
|
|
3
|
+
JobWorkflow provides conditional execution features to selectively execute tasks based on runtime state.
|
|
4
|
+
|
|
5
|
+
## Basic Conditional Execution
|
|
6
|
+
|
|
7
|
+
### condition: Option
|
|
8
|
+
|
|
9
|
+
Execute task only if condition returns true.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class UserNotificationJob < ApplicationJob
|
|
13
|
+
include JobWorkflow::DSL
|
|
14
|
+
|
|
15
|
+
argument :user, "User"
|
|
16
|
+
argument :notification_type, "String"
|
|
17
|
+
|
|
18
|
+
task :load_user_preferences, output: { preferences: "Hash" } do |ctx|
|
|
19
|
+
user = ctx.arguments.user
|
|
20
|
+
{ preferences: user.notification_preferences }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Execute only for premium users
|
|
24
|
+
task :send_premium_notification,
|
|
25
|
+
depends_on: [:load_user_preferences],
|
|
26
|
+
condition: ->(ctx) { ctx.arguments.user.premium? } do |ctx|
|
|
27
|
+
user = ctx.arguments.user
|
|
28
|
+
notification_type = ctx.arguments.notification_type
|
|
29
|
+
PremiumNotificationService.send(user, notification_type)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Send simple notification to standard users
|
|
33
|
+
task :send_standard_notification,
|
|
34
|
+
depends_on: [:load_user_preferences],
|
|
35
|
+
condition: ->(ctx) { !ctx.arguments.user.premium? } do |ctx|
|
|
36
|
+
user = ctx.arguments.user
|
|
37
|
+
notification_type = ctx.arguments.notification_type
|
|
38
|
+
StandardNotificationService.send(user, notification_type)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Complex Conditions
|
|
44
|
+
|
|
45
|
+
You can use any Ruby expression in the condition lambda.
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
class DataSyncJob < ApplicationJob
|
|
49
|
+
include JobWorkflow::DSL
|
|
50
|
+
|
|
51
|
+
argument :force_sync, "TrueClass | FalseClass", default: false
|
|
52
|
+
argument :last_sync_at, "Time", default: nil
|
|
53
|
+
|
|
54
|
+
# Execute only if more than 1 hour since last sync
|
|
55
|
+
task :sync_data,
|
|
56
|
+
condition: ->(ctx) {
|
|
57
|
+
return true if ctx.arguments.force_sync # Always execute if force_sync is true
|
|
58
|
+
last_sync = ctx.arguments.last_sync_at
|
|
59
|
+
!last_sync || last_sync <= 1.hour.ago
|
|
60
|
+
},
|
|
61
|
+
output: { sync_time: "Time" } do |ctx|
|
|
62
|
+
SyncService.perform
|
|
63
|
+
{ sync_time: Time.current }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|