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.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +91 -0
  4. data/CHANGELOG.md +23 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +47 -0
  7. data/Rakefile +55 -0
  8. data/Steepfile +10 -0
  9. data/guides/API_REFERENCE.md +112 -0
  10. data/guides/BEST_PRACTICES.md +113 -0
  11. data/guides/CACHE_STORE_INTEGRATION.md +145 -0
  12. data/guides/CONDITIONAL_EXECUTION.md +66 -0
  13. data/guides/DEPENDENCY_WAIT.md +386 -0
  14. data/guides/DRY_RUN.md +390 -0
  15. data/guides/DSL_BASICS.md +216 -0
  16. data/guides/ERROR_HANDLING.md +187 -0
  17. data/guides/GETTING_STARTED.md +524 -0
  18. data/guides/INSTRUMENTATION.md +131 -0
  19. data/guides/LIFECYCLE_HOOKS.md +415 -0
  20. data/guides/NAMESPACES.md +75 -0
  21. data/guides/OPENTELEMETRY_INTEGRATION.md +86 -0
  22. data/guides/PARALLEL_PROCESSING.md +302 -0
  23. data/guides/PRODUCTION_DEPLOYMENT.md +110 -0
  24. data/guides/QUEUE_MANAGEMENT.md +141 -0
  25. data/guides/README.md +174 -0
  26. data/guides/SCHEDULED_JOBS.md +165 -0
  27. data/guides/STRUCTURED_LOGGING.md +268 -0
  28. data/guides/TASK_OUTPUTS.md +240 -0
  29. data/guides/TESTING_STRATEGY.md +56 -0
  30. data/guides/THROTTLING.md +198 -0
  31. data/guides/TROUBLESHOOTING.md +53 -0
  32. data/guides/WORKFLOW_COMPOSITION.md +675 -0
  33. data/guides/WORKFLOW_STATUS_QUERY.md +288 -0
  34. data/lib/job-workflow.rb +3 -0
  35. data/lib/job_workflow/argument_def.rb +16 -0
  36. data/lib/job_workflow/arguments.rb +40 -0
  37. data/lib/job_workflow/auto_scaling/adapter/aws_adapter.rb +66 -0
  38. data/lib/job_workflow/auto_scaling/adapter.rb +31 -0
  39. data/lib/job_workflow/auto_scaling/configuration.rb +85 -0
  40. data/lib/job_workflow/auto_scaling/executor.rb +43 -0
  41. data/lib/job_workflow/auto_scaling.rb +69 -0
  42. data/lib/job_workflow/cache_store_adapters.rb +46 -0
  43. data/lib/job_workflow/context.rb +352 -0
  44. data/lib/job_workflow/dry_run_config.rb +31 -0
  45. data/lib/job_workflow/dsl.rb +236 -0
  46. data/lib/job_workflow/error_hook.rb +24 -0
  47. data/lib/job_workflow/hook.rb +24 -0
  48. data/lib/job_workflow/hook_registry.rb +66 -0
  49. data/lib/job_workflow/instrumentation/log_subscriber.rb +194 -0
  50. data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +221 -0
  51. data/lib/job_workflow/instrumentation.rb +257 -0
  52. data/lib/job_workflow/job_status.rb +92 -0
  53. data/lib/job_workflow/logger.rb +86 -0
  54. data/lib/job_workflow/namespace.rb +36 -0
  55. data/lib/job_workflow/output.rb +81 -0
  56. data/lib/job_workflow/output_def.rb +14 -0
  57. data/lib/job_workflow/queue.rb +74 -0
  58. data/lib/job_workflow/queue_adapter.rb +38 -0
  59. data/lib/job_workflow/queue_adapters/abstract.rb +87 -0
  60. data/lib/job_workflow/queue_adapters/null_adapter.rb +127 -0
  61. data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +224 -0
  62. data/lib/job_workflow/runner.rb +173 -0
  63. data/lib/job_workflow/schedule.rb +46 -0
  64. data/lib/job_workflow/semaphore.rb +71 -0
  65. data/lib/job_workflow/task.rb +83 -0
  66. data/lib/job_workflow/task_callable.rb +43 -0
  67. data/lib/job_workflow/task_context.rb +70 -0
  68. data/lib/job_workflow/task_dependency_wait.rb +66 -0
  69. data/lib/job_workflow/task_enqueue.rb +50 -0
  70. data/lib/job_workflow/task_graph.rb +43 -0
  71. data/lib/job_workflow/task_job_status.rb +70 -0
  72. data/lib/job_workflow/task_output.rb +51 -0
  73. data/lib/job_workflow/task_retry.rb +64 -0
  74. data/lib/job_workflow/task_throttle.rb +46 -0
  75. data/lib/job_workflow/version.rb +5 -0
  76. data/lib/job_workflow/workflow.rb +87 -0
  77. data/lib/job_workflow/workflow_status.rb +112 -0
  78. data/lib/job_workflow.rb +59 -0
  79. data/rbs_collection.lock.yaml +172 -0
  80. data/rbs_collection.yaml +14 -0
  81. data/sig/generated/job-workflow.rbs +2 -0
  82. data/sig/generated/job_workflow/argument_def.rbs +14 -0
  83. data/sig/generated/job_workflow/arguments.rbs +26 -0
  84. data/sig/generated/job_workflow/auto_scaling/adapter/aws_adapter.rbs +32 -0
  85. data/sig/generated/job_workflow/auto_scaling/adapter.rbs +22 -0
  86. data/sig/generated/job_workflow/auto_scaling/configuration.rbs +50 -0
  87. data/sig/generated/job_workflow/auto_scaling/executor.rbs +29 -0
  88. data/sig/generated/job_workflow/auto_scaling.rbs +47 -0
  89. data/sig/generated/job_workflow/cache_store_adapters.rbs +28 -0
  90. data/sig/generated/job_workflow/context.rbs +155 -0
  91. data/sig/generated/job_workflow/dry_run_config.rbs +16 -0
  92. data/sig/generated/job_workflow/dsl.rbs +117 -0
  93. data/sig/generated/job_workflow/error_hook.rbs +18 -0
  94. data/sig/generated/job_workflow/hook.rbs +18 -0
  95. data/sig/generated/job_workflow/hook_registry.rbs +47 -0
  96. data/sig/generated/job_workflow/instrumentation/log_subscriber.rbs +102 -0
  97. data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +113 -0
  98. data/sig/generated/job_workflow/instrumentation.rbs +138 -0
  99. data/sig/generated/job_workflow/job_status.rbs +46 -0
  100. data/sig/generated/job_workflow/logger.rbs +56 -0
  101. data/sig/generated/job_workflow/namespace.rbs +24 -0
  102. data/sig/generated/job_workflow/output.rbs +39 -0
  103. data/sig/generated/job_workflow/output_def.rbs +12 -0
  104. data/sig/generated/job_workflow/queue.rbs +49 -0
  105. data/sig/generated/job_workflow/queue_adapter.rbs +18 -0
  106. data/sig/generated/job_workflow/queue_adapters/abstract.rbs +56 -0
  107. data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +73 -0
  108. data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +111 -0
  109. data/sig/generated/job_workflow/runner.rbs +66 -0
  110. data/sig/generated/job_workflow/schedule.rbs +34 -0
  111. data/sig/generated/job_workflow/semaphore.rbs +37 -0
  112. data/sig/generated/job_workflow/task.rbs +60 -0
  113. data/sig/generated/job_workflow/task_callable.rbs +30 -0
  114. data/sig/generated/job_workflow/task_context.rbs +52 -0
  115. data/sig/generated/job_workflow/task_dependency_wait.rbs +42 -0
  116. data/sig/generated/job_workflow/task_enqueue.rbs +27 -0
  117. data/sig/generated/job_workflow/task_graph.rbs +27 -0
  118. data/sig/generated/job_workflow/task_job_status.rbs +42 -0
  119. data/sig/generated/job_workflow/task_output.rbs +29 -0
  120. data/sig/generated/job_workflow/task_retry.rbs +30 -0
  121. data/sig/generated/job_workflow/task_throttle.rbs +20 -0
  122. data/sig/generated/job_workflow/version.rbs +5 -0
  123. data/sig/generated/job_workflow/workflow.rbs +48 -0
  124. data/sig/generated/job_workflow/workflow_status.rbs +55 -0
  125. data/sig/generated/job_workflow.rbs +8 -0
  126. data/sig-private/activejob.rbs +35 -0
  127. data/sig-private/activesupport.rbs +23 -0
  128. data/sig-private/aws.rbs +32 -0
  129. data/sig-private/opentelemetry.rbs +40 -0
  130. data/sig-private/solid_queue.rbs +108 -0
  131. data/tmp/.keep +0 -0
  132. 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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # D = Steep::Diagnostic
4
+ #
5
+ target :lib do
6
+ signature "sig"
7
+ signature "sig-private"
8
+
9
+ check "lib"
10
+ end
@@ -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
+ ```