scatter_gather 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.cursor/rules/instructions.mdc +21 -0
- data/.github/dependabot.yml +12 -0
- data/.github/workflows/ci.yml +55 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +11 -0
- data/LICENSE.md +21 -0
- data/README.md +86 -0
- data/Rakefile +32 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/bin/test +5 -0
- data/lib/generators/install_generator.rb +33 -0
- data/lib/generators/scatter_gather_migration_001.rb.erb +16 -0
- data/lib/scatter_gather/version.rb +5 -0
- data/lib/scatter_gather.rb +196 -0
- data/lib/tasks/scatter_gather_tasks.rake +8 -0
- data/rbi/scatter_gather.rbi +135 -0
- data/scatter_gather-0.1.19.gem +0 -0
- data/scatter_gather.gemspec +46 -0
- data/sig/scatter_gather.rbs +116 -0
- data/test/dummy/Rakefile +8 -0
- data/test/dummy/app/assets/stylesheets/application.css +1 -0
- data/test/dummy/app/controllers/application_controller.rb +6 -0
- data/test/dummy/app/helpers/application_helper.rb +4 -0
- data/test/dummy/app/jobs/application_job.rb +9 -0
- data/test/dummy/app/mailers/application_mailer.rb +6 -0
- data/test/dummy/app/models/application_record.rb +5 -0
- data/test/dummy/app/views/layouts/application.html.erb +27 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
- data/test/dummy/app/views/pwa/service-worker.js +26 -0
- data/test/dummy/bin/dev +2 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +34 -0
- data/test/dummy/config/application.rb +28 -0
- data/test/dummy/config/boot.rb +7 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/database.sqlite3.yml +32 -0
- data/test/dummy/config/database.yml +32 -0
- data/test/dummy/config/environment.rb +7 -0
- data/test/dummy/config/environments/development.rb +71 -0
- data/test/dummy/config/environments/production.rb +91 -0
- data/test/dummy/config/environments/test.rb +55 -0
- data/test/dummy/config/initializers/content_security_policy.rb +27 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +10 -0
- data/test/dummy/config/initializers/inflections.rb +18 -0
- data/test/dummy/config/locales/en.yml +31 -0
- data/test/dummy/config/puma.rb +40 -0
- data/test/dummy/config/routes.rb +16 -0
- data/test/dummy/config/storage.yml +34 -0
- data/test/dummy/config.ru +8 -0
- data/test/dummy/db/migrate/20250101000001_add_scatter_gather_completions.rb +16 -0
- data/test/dummy/db/schema.rb +23 -0
- data/test/dummy/public/400.html +114 -0
- data/test/dummy/public/404.html +114 -0
- data/test/dummy/public/406-unsupported-browser.html +114 -0
- data/test/dummy/public/422.html +114 -0
- data/test/dummy/public/500.html +114 -0
- data/test/dummy/public/icon.png +0 -0
- data/test/dummy/public/icon.svg +3 -0
- data/test/scatter_gather_test.rb +180 -0
- data/test/test_helper.rb +17 -0
- metadata +285 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 423f509998336fa071a30240b18544524dbb9ee711c1250e3daf597c10bd9993
|
4
|
+
data.tar.gz: 14b5c6c4084f4c9c9dc440ab4c015e85f67695b6512ba1a23dc94e477bdaca6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: df506e118824334796d6f2da4464672740bbfc376a58a29e754f7224958bf369983ca253f7e38887bbcbdab1f1159616d31790bbdefe6f0ed4f216f8b7d522dd
|
7
|
+
data.tar.gz: a47b8afba198ff4ab3428ed11fdbb4469f83ab45139e8f8dcf2f29261c0c144f54d38c26181eff9a74c6d694d72d01f6e505c744a1451a867543102a13cc838b
|
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
alwaysApply: true
|
3
|
+
---
|
4
|
+
|
5
|
+
## Linting
|
6
|
+
|
7
|
+
* Run standardrb --fix-unsafely on any .rb file you create or make edits to
|
8
|
+
|
9
|
+
|
10
|
+
## Commit message titles
|
11
|
+
|
12
|
+
* Do not use feat/chore prefixes for commit message titles
|
13
|
+
|
14
|
+
## Type definitions
|
15
|
+
|
16
|
+
* Do not update type definitions (rbi and rbs files) they get updated automatically on every gem build
|
17
|
+
|
18
|
+
## Comments
|
19
|
+
|
20
|
+
* Add generous YARD comments to the public methods you add. Add type definitions for parameters and return values
|
21
|
+
* Specify @return [void] for methods that return no value / undefined value
|
@@ -0,0 +1,55 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request:
|
5
|
+
push:
|
6
|
+
branches: [ main ]
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
lint:
|
10
|
+
name: "Lint (standardrb)"
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
steps:
|
13
|
+
- name: Checkout code
|
14
|
+
uses: actions/checkout@v5
|
15
|
+
|
16
|
+
- name: Set up Ruby
|
17
|
+
uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: 3.2.2
|
20
|
+
bundler-cache: true
|
21
|
+
|
22
|
+
- name: Lint code for consistent style
|
23
|
+
run: bundle exec standardrb
|
24
|
+
|
25
|
+
test:
|
26
|
+
name: "Tests (SQLite)"
|
27
|
+
runs-on: ubuntu-latest
|
28
|
+
steps:
|
29
|
+
- name: Checkout code
|
30
|
+
uses: actions/checkout@v5
|
31
|
+
|
32
|
+
- name: Set up Ruby
|
33
|
+
uses: ruby/setup-ruby@v1
|
34
|
+
with:
|
35
|
+
ruby-version: 3.2.2
|
36
|
+
bundler-cache: true
|
37
|
+
|
38
|
+
- name: Remove existing schema.rb
|
39
|
+
run: rm -f test/dummy/db/schema.rb
|
40
|
+
|
41
|
+
- name: Setup database
|
42
|
+
env:
|
43
|
+
RAILS_ENV: test
|
44
|
+
DATABASE_URL: sqlite3:db/test.sqlite3
|
45
|
+
run: |
|
46
|
+
cd test/dummy
|
47
|
+
bundle exec rails db:create
|
48
|
+
bundle exec rails db:migrate
|
49
|
+
cd ../..
|
50
|
+
|
51
|
+
- name: Run tests
|
52
|
+
env:
|
53
|
+
RAILS_ENV: test
|
54
|
+
DATABASE_URL: sqlite3:db/test.sqlite3
|
55
|
+
run: bin/test
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Julik Tarkhanov
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# scatter_gather
|
2
|
+
|
3
|
+
A Ruby gem that provides a scatter-gather pattern for coordinating ActiveJob execution. Jobs can wait for other jobs to complete before executing, with configurable polling, retry, and timeout behavior.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
Start some scatter jobs and create a gather job that waits for all dependencies to complete:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class EmailProcessorJob < ActiveJob::Base
|
11
|
+
include ScatterGather
|
12
|
+
|
13
|
+
def perform(email_id)
|
14
|
+
# Process email
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class AttachmentProcessorJob < ActiveJob::Base
|
19
|
+
include ScatterGather
|
20
|
+
|
21
|
+
def perform(email_id)
|
22
|
+
# Process attachments
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class AICategorizerJob < ActiveJob::Base
|
27
|
+
include ScatterGather
|
28
|
+
|
29
|
+
def perform(email_id)
|
30
|
+
# Categorize email with AI
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class NotifyCompleteJob < ActiveJob::Base
|
35
|
+
include ScatterGather
|
36
|
+
|
37
|
+
def perform(email_id)
|
38
|
+
# Notify that all processing is complete
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Start the scatter jobs
|
43
|
+
email_parser_job = EmailProcessorJob.perform_later(email_id: 123)
|
44
|
+
attachment_processor_job = AttachmentProcessorJob.perform_later(email_id: 123)
|
45
|
+
ai_categorizer_job = AICategorizerJob.perform_later(email_id: 123)
|
46
|
+
|
47
|
+
# Create a gather job that waits for all dependencies to complete
|
48
|
+
NotifyCompleteJob.gather(email_parser_job, attachment_processor_job, ai_categorizer_job).perform_later(email_id: 123)
|
49
|
+
```
|
50
|
+
|
51
|
+
The gather job will:
|
52
|
+
- Check if all dependencies are complete
|
53
|
+
- If complete: enqueue the target job immediately
|
54
|
+
- If not complete: poll every 2 seconds (configurable), re-enqueuing itself
|
55
|
+
- After 10 attempts (configurable): discard with error reporting
|
56
|
+
|
57
|
+
### Configuration Options
|
58
|
+
|
59
|
+
- `max_attempts`: Number of polling attempts before giving up (default: 10)
|
60
|
+
- `poll_interval`: Time between polling attempts (default: 2.seconds)
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
# Example with custom configuration
|
64
|
+
TouchingJob.gather(jobs, poll_interval: 0.2.seconds, max_attempts: 4).perform_later(final_path)
|
65
|
+
```
|
66
|
+
|
67
|
+
## Installation
|
68
|
+
|
69
|
+
Add the gem to the application's Gemfile, and then generate and run the migration:
|
70
|
+
|
71
|
+
$ bundle add scatter_gather
|
72
|
+
$ bundle install
|
73
|
+
$ bin/rails g scatter_gather:install
|
74
|
+
$ bin/rails db:migrate
|
75
|
+
|
76
|
+
## Development
|
77
|
+
|
78
|
+
After checking out the repo, run `bundle` to install dependencies. The development process from there on is like any other gem.
|
79
|
+
|
80
|
+
## License
|
81
|
+
|
82
|
+
This gem is made available under the MIT license
|
83
|
+
|
84
|
+
## Contributing
|
85
|
+
|
86
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/julik/scatter_gather.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
Bundler::GemHelper.install_tasks(name: "scatter_gather")
|
5
|
+
require "rake/testtask"
|
6
|
+
require "standard/rake"
|
7
|
+
require "yard"
|
8
|
+
|
9
|
+
YARD::Rake::YardocTask.new(:doc)
|
10
|
+
|
11
|
+
Rake::TestTask.new(:test) do |t|
|
12
|
+
t.libs << "test"
|
13
|
+
t.libs << "lib"
|
14
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
15
|
+
t.warning = false # To avoid any warnings from dependencies
|
16
|
+
end
|
17
|
+
|
18
|
+
task :format do
|
19
|
+
`bundle exec standardrb --fix`
|
20
|
+
`bundle exec magic_frozen_string_literal .`
|
21
|
+
end
|
22
|
+
|
23
|
+
task :generate_typedefs do
|
24
|
+
`bundle exec sord rbi/scatter_gather.rbi`
|
25
|
+
`bundle exec sord sig/scatter_gather.rbs`
|
26
|
+
end
|
27
|
+
|
28
|
+
task default: [:test, :standard, :generate_typedefs]
|
29
|
+
|
30
|
+
# When building the gem, generate typedefs beforehand,
|
31
|
+
# so that they get included
|
32
|
+
Rake::Task["build"].enhance(["generate_typedefs"])
|
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "scatter_gather"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require "irb"
|
11
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/bin/test
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/active_record"
|
5
|
+
|
6
|
+
module ScatterGather
|
7
|
+
# The generator is used to install ScatterGather. It adds the migration that creates
|
8
|
+
# the scatter_gather_completions table.
|
9
|
+
# Run it with `bin/rails g scatter_gather:install` in your console.
|
10
|
+
class InstallGenerator < Rails::Generators::Base
|
11
|
+
include ActiveRecord::Generators::Migration
|
12
|
+
|
13
|
+
source_paths << File.join(File.dirname(__FILE__, 2))
|
14
|
+
|
15
|
+
# Generates migration file that creates the scatter_gather_completions table.
|
16
|
+
def create_migration_file
|
17
|
+
# Migration files are named "...migration_001.rb" etc. This allows them to be emitted
|
18
|
+
# as they get added, and the order of the migrations can be controlled using predictable sorting.
|
19
|
+
# Adding a new migration to the gem is then just adding a file.
|
20
|
+
migration_file_paths_in_order = Dir.glob(__dir__ + "/*_migration_*.rb.erb").sort
|
21
|
+
migration_file_paths_in_order.each do |migration_template_path|
|
22
|
+
untemplated_migration_filename = File.basename(migration_template_path).gsub(/\.erb$/, "")
|
23
|
+
migration_template(migration_template_path, File.join(db_migrate_path, untemplated_migration_filename))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def migration_version
|
30
|
+
ActiveRecord::VERSION::STRING.split(".").take(2).join(".")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class AddScatterGatherCompletions < ActiveRecord::Migration[<%= migration_version %>]
|
2
|
+
def up
|
3
|
+
create_table :scatter_gather_completions do |t|
|
4
|
+
t.string :active_job_id, null: false
|
5
|
+
t.string :active_job_class_name
|
6
|
+
t.string :status, default: "unknown"
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
add_index :scatter_gather_completions, [:active_job_id], unique: true # For lookups
|
10
|
+
add_index :scatter_gather_completions, [:created_at] # For cleanup
|
11
|
+
end
|
12
|
+
|
13
|
+
def down
|
14
|
+
drop_table :scatter_gather_completions
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_record"
|
5
|
+
require "active_job"
|
6
|
+
require "json"
|
7
|
+
|
8
|
+
# Scatter-Gather Pattern for ActiveJob
|
9
|
+
#
|
10
|
+
# This module provides a scatter-gather pattern for coordinating job execution.
|
11
|
+
# Jobs can wait for other jobs to complete before executing, with configurable
|
12
|
+
# polling, retry, and timeout behavior.
|
13
|
+
#
|
14
|
+
# Example workflow:
|
15
|
+
# # Start some scatter jobs
|
16
|
+
# email_parser_job = EmailParserJob.perform_later(email_id: 123)
|
17
|
+
# attachment_processor_job = AttachmentProcessorJob.perform_later(email_id: 123)
|
18
|
+
# ai_categorizer_job = AICategorizerJob.perform_later(email_id: 123)
|
19
|
+
#
|
20
|
+
# # Create a gather job that waits for all dependencies to complete
|
21
|
+
# NotifyCompleteJob.gather(email_parser_job, attachment_processor_job, ai_categorizer_job).perform_later
|
22
|
+
#
|
23
|
+
# The gather job will:
|
24
|
+
# - Check if all dependencies are complete
|
25
|
+
# - If complete: enqueue the target job immediately
|
26
|
+
# - If not complete: poll every 2 seconds (configurable), re-enqueuing itself
|
27
|
+
# - After 10 attempts (configurable): discard with error reporting
|
28
|
+
#
|
29
|
+
# Configuration options:
|
30
|
+
# - max_attempts: Number of polling attempts before giving up (default: 10)
|
31
|
+
# - poll_interval: Time between polling attempts (default: 2.seconds)
|
32
|
+
#
|
33
|
+
# Example with custom configuration:
|
34
|
+
# TouchingJob.gather(jobs, poll_interval: 0.2.seconds, max_attempts: 4).perform_later(final_path)
|
35
|
+
module ScatterGather
|
36
|
+
extend ActiveSupport::Concern
|
37
|
+
|
38
|
+
class Completion < ActiveRecord::Base
|
39
|
+
self.table_name = "scatter_gather_completions"
|
40
|
+
|
41
|
+
def self.collect_statuses(active_job_ids)
|
42
|
+
statuses = active_job_ids.map { |it| [it, :unknown] }.to_h
|
43
|
+
statuses_from_completions = where(active_job_id: active_job_ids)
|
44
|
+
.pluck(:active_job_id, :status)
|
45
|
+
.map do |(id, st)|
|
46
|
+
[id, st.to_sym]
|
47
|
+
end.to_h
|
48
|
+
statuses.merge!(statuses_from_completions)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Default configuration for gather jobs
|
53
|
+
DEFAULT_GATHER_CONFIG = {
|
54
|
+
max_attempts: 10,
|
55
|
+
poll_interval: 2.seconds
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
# Proxy class that mimics ActiveJob behavior for gather jobs
|
59
|
+
class GatherJobProxy
|
60
|
+
def initialize(target_class, ids, config)
|
61
|
+
@target_class = target_class
|
62
|
+
@ids = ids
|
63
|
+
@config = config.with_indifferent_access
|
64
|
+
end
|
65
|
+
|
66
|
+
# Mimic ActiveJob's perform_later method
|
67
|
+
# @param args [Array] Positional arguments to pass to the target job's perform method
|
68
|
+
# @param kwargs [Hash] Keyword arguments to pass to the target job's perform method
|
69
|
+
# @return [void] Enqueues the gather job
|
70
|
+
def perform_later(*args, **kwargs)
|
71
|
+
job_arguments = {cn: @target_class.name, p: args, k: kwargs}
|
72
|
+
gather_job_params = {
|
73
|
+
wait_for_active_job_ids: @ids,
|
74
|
+
target_job: job_arguments,
|
75
|
+
gather_config: @config,
|
76
|
+
remaining_attempts: @config.fetch(:max_attempts) - 1
|
77
|
+
}
|
78
|
+
tagged = ActiveSupport::TaggedLogging.new(Rails.logger).tagged("ScatterGather")
|
79
|
+
tagged.info { "Enqueueing gather job waiting for #{@ids.inspect} to run a #{@target_class.name} after" }
|
80
|
+
GatherJob.perform_later(**gather_job_params)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Custom exception for when gather job exhausts attempts
|
85
|
+
class DependencyTimeoutError < StandardError
|
86
|
+
attr_reader :dependency_status
|
87
|
+
|
88
|
+
def initialize(max_attempts, dependency_status)
|
89
|
+
@dependency_status = dependency_status
|
90
|
+
super(<<~MSG)
|
91
|
+
Gather failed after #{max_attempts} attempts. Dependencies:
|
92
|
+
|
93
|
+
#{JSON.pretty_generate(dependency_status)}
|
94
|
+
MSG
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Internal job class for polling and coordinating gather operations
|
99
|
+
class GatherJob < ActiveJob::Base
|
100
|
+
include ScatterGather
|
101
|
+
discard_on DependencyTimeoutError
|
102
|
+
|
103
|
+
def logger = ActiveSupport::TaggedLogging.new(super).tagged("ScatterGather")
|
104
|
+
|
105
|
+
def perform(wait_for_active_job_ids:, target_job:, gather_config:, remaining_attempts:)
|
106
|
+
deps = ScatterGather::Completion.collect_statuses(wait_for_active_job_ids)
|
107
|
+
logger.info { "Gathered completions #{tally_in_logger_format(deps)}" }
|
108
|
+
|
109
|
+
all_done = deps.values.all? { |it| it == :completed }
|
110
|
+
if all_done
|
111
|
+
logger.info { "Dependencies done, enqueueing #{target_job.fetch(:cn)}" }
|
112
|
+
perform_target_later_from_args(target_job)
|
113
|
+
Completion.where(active_job_id: wait_for_active_job_ids).delete_all
|
114
|
+
elsif remaining_attempts < 1
|
115
|
+
max_attempts = gather_config.fetch(:max_attempts)
|
116
|
+
error = DependencyTimeoutError.new(max_attempts, deps)
|
117
|
+
logger.warn { "Failed to gather dependencies after #{max_attempts} attempts" }
|
118
|
+
Completion.where(active_job_id: wait_for_active_job_ids).delete_all
|
119
|
+
|
120
|
+
# We configure our job to discard on timeout, and discard does not report the error by default
|
121
|
+
Rails.error.report(error)
|
122
|
+
raise error
|
123
|
+
else
|
124
|
+
# Re-enqueue with delay. We could poll only for dependencies which are still remaining,
|
125
|
+
# but for debugging this is actually worse because for hanging stuff there will be one
|
126
|
+
# job that hangs in the end. Knowing which jobs were part of the batch is useful!
|
127
|
+
args = {
|
128
|
+
wait_for_active_job_ids:,
|
129
|
+
target_job:,
|
130
|
+
gather_config:,
|
131
|
+
remaining_attempts: remaining_attempts - 1
|
132
|
+
}
|
133
|
+
wait = gather_config.fetch(:poll_interval)
|
134
|
+
self.class.set(wait:).perform_later(**args)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def tally_in_logger_format(hash)
|
141
|
+
hash.values.tally.map do |k, count|
|
142
|
+
"#{k}=#{count}"
|
143
|
+
end.join(" ")
|
144
|
+
end
|
145
|
+
|
146
|
+
def perform_target_later_from_args(target_job)
|
147
|
+
# The only purpose of this is to pass all variations
|
148
|
+
# of `perform_later` argument shapes correctly
|
149
|
+
job_class = target_job.fetch(:cn).constantize
|
150
|
+
if target_job[:p].any? && target_job[:k] # Both
|
151
|
+
job_class.perform_later(*target_job[:p], **target_job[:k])
|
152
|
+
elsif target_job[:k] # Just kwargs
|
153
|
+
job_class.perform_later(**target_job[:k])
|
154
|
+
elsif target_job[:p] # Just posargs
|
155
|
+
job_class.perform_later(*target_job[:p])
|
156
|
+
else
|
157
|
+
job_class.perform_later # No args
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
included do
|
163
|
+
after_perform :register_completion_for_gathering
|
164
|
+
discard_on ScatterGather::DependencyTimeoutError
|
165
|
+
|
166
|
+
def self.gather(*active_jobs, **gather_config_options)
|
167
|
+
active_jobs = Array(active_jobs).flatten
|
168
|
+
config = DEFAULT_GATHER_CONFIG.merge(gather_config_options)
|
169
|
+
|
170
|
+
# Pre-insert IDs to wait for
|
171
|
+
t = Time.current
|
172
|
+
attrs = active_jobs.map do |aj|
|
173
|
+
{
|
174
|
+
active_job_id: aj.job_id,
|
175
|
+
active_job_class_name: aj.class.name,
|
176
|
+
status: "pending",
|
177
|
+
created_at: t,
|
178
|
+
updated_at: t
|
179
|
+
}
|
180
|
+
end
|
181
|
+
ScatterGather::Completion.insert_all(attrs)
|
182
|
+
ScatterGather::Completion.where("created_at < ?", 1.week.ago).delete_all
|
183
|
+
|
184
|
+
# Return a proxy object that behaves like an ActiveJob proxy
|
185
|
+
GatherJobProxy.new(self, active_jobs.map(&:job_id), config)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Updates the completions table with the status of this job
|
190
|
+
def register_completion_for_gathering
|
191
|
+
n_updated = ScatterGather::Completion.where(active_job_id: job_id).update_all(status: "completed", updated_at: Time.current)
|
192
|
+
if n_updated > 0
|
193
|
+
logger.tagged("ScatterGather").info { "Registered completion of #{self.class.name} id=#{job_id} since it will be gathered" }
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|