Skip to content

Commit

Permalink
Enable per job class max_job_runtime
Browse files Browse the repository at this point in the history
This allows incremental adoption of the setting, without applying the setting
globally. Alternatively, it allows applications to set a conservative global
setting, and a more aggressive setting per jobs.

In order to prevent rogue jobs from causing trouble, the per-job override can
only be set to a value less than the inherited value.
  • Loading branch information
sambostock committed Jun 29, 2022
1 parent a68ae83 commit 7098006
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- [241](https://github.com/Shopify/job-iteration/pull/241) - Require Ruby 2.7+, dropping 2.6 support
- [241](https://github.com/Shopify/job-iteration/pull/241) - Require Rails 6.0+, dropping 5.2 support
- [240](https://github.com/Shopify/job-iteration/pull/240) - Allow setting inheritable per-job `max_job_runtime`

## v1.3.6 (Mar 9, 2022)

Expand Down
15 changes: 15 additions & 0 deletions guides/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,18 @@ JobIteration.max_job_runtime = 5.minutes # nil by default
```

Use this accessor to tweak how often you'd like the job to interrupt itself.

### Per job max job runtime

For more granular control, `max_job_runtime` can be set **per-job class**. This allows both incremental adoption, as well as using a conservative global setting, and an aggressive setting on a per-job basis.

```ruby
class MyJob < ApplicationJob
include JobIteration::Iteration

self.max_job_runtime = 3.minutes

# ...
```

This setting will be inherited by any child classes, although it can be further overridden. Note that no class can **increase** the `max_job_runtime` it has inherited; it can only be **decreased**.
23 changes: 22 additions & 1 deletion lib/job-iteration/iteration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,27 @@ def inspected_cursor
define_callbacks :start
define_callbacks :shutdown
define_callbacks :complete

class_attribute :max_job_runtime, instance_writer: false, instance_predicate: false, default: JobIteration.max_job_runtime

singleton_class.prepend PrependedClassMethods
end

module PrependedClassMethods
def max_job_runtime=(new)
existing = max_job_runtime

if existing && (!new || new > existing)
existing_label = existing.inspect
new_label = new ? new.inspect : "#{new.inspect} (no limit)"
raise(
ArgumentError,
"max_job_runtime may only decrease; #{self} tried to increase it from #{existing_label} to #{new_label}",
)
end

super
end
end

module ClassMethods
Expand Down Expand Up @@ -262,7 +283,7 @@ def output_interrupt_summary
end

def job_should_exit?
if ::JobIteration.max_job_runtime && start_time && (Time.now.utc - start_time) > ::JobIteration.max_job_runtime
if max_job_runtime && start_time && (Time.now.utc - start_time) > max_job_runtime
return true
end

Expand Down
69 changes: 69 additions & 0 deletions test/unit/iteration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,75 @@ def test_global_max_job_runtime
end
end

def test_per_class_max_job_runtime_with_no_global_set
freeze_time
with_global_max_job_runtime(nil) do
parent = build_slow_job_class(iterations: 3, iteration_duration: 30.seconds)
child = Class.new(parent) do
self.max_job_runtime = 1.minute
end

parent.perform_now
assert_no_enqueued_jobs

child.perform_now
assert_partially_completed_job(cursor_position: 2)
end
end

def test_per_class_max_job_runtime_with_global_set
freeze_time
with_global_max_job_runtime(1.minute) do
parent = build_slow_job_class(iterations: 3, iteration_duration: 30.seconds)
child = Class.new(parent) do
self.max_job_runtime = 30.seconds
end

parent.perform_now
assert_partially_completed_job(cursor_position: 2)
clear_enqueued_jobs

child.perform_now
assert_partially_completed_job(cursor_position: 1)
end
end

def test_max_job_runtime_cannot_be_higher_than_global
with_global_max_job_runtime(30.seconds) do
klass = Class.new(ActiveJob::Base) do
include JobIteration::Iteration
end

error = assert_raises(ArgumentError) do
klass.max_job_runtime = 1.minute
end

assert_equal(
"max_job_runtime may only decrease; #{klass} tried to increase it from 30 seconds to 1 minute",
error.message,
)
end
end

def test_max_job_runtime_cannot_be_higher_than_parent
with_global_max_job_runtime(1.minute) do
parent = Class.new(ActiveJob::Base) do
include JobIteration::Iteration
self.max_job_runtime = 30.seconds
end
child = Class.new(parent)

error = assert_raises(ArgumentError) do
child.max_job_runtime = 45.seconds
end

assert_equal(
"max_job_runtime may only decrease; #{child} tried to increase it from 30 seconds to 45 seconds",
error.message,
)
end
end

private

# Allows building job classes that read max_job_runtime during the test,
Expand Down

0 comments on commit 7098006

Please sign in to comment.