Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,20 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).

### Testing

Æternitas provides a test mode to help write tests. When enabled, all cooldowns, retry delays, and sleep durations are set to zero. This prevents your test suite from having to wait for scheduled delays.

To enable test mode for a specific block of code, use the `Aeternitas::Test.test_mode` helper:

```ruby
Aeternitas::Test.test_mode do
# ...
end
```

This ensures that test mode is enabled only for the duration of the block and is automatically disabled afterward.

## Contributing

Bug reports and spec backed pull requests are welcome on GitHub at https://github.com/Dietech-Group/aeternitas. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
Expand Down
13 changes: 13 additions & 0 deletions lib/aeternitas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
require "aeternitas/poll_job"
require "aeternitas/cleanup_stale_locks_job"
require "aeternitas/cleanup_old_metrics_job"
require "aeternitas/test"

# Aeternitas
module Aeternitas
Expand All @@ -33,6 +34,18 @@ def self.configure
yield(config)
end

# Returns true if aeternitas is in test mode.
# @return [Boolean]
def self.test_mode?
@test_mode == true
end

# Sets the test mode.
# @param [Boolean] value
def self.test_mode=(value)
@test_mode = value
end

# Enqueues all active pollables for which next polling is lower than the current time
def self.enqueue_due_pollables
Aeternitas::PollableMetaData.due.find_each do |pollable_meta_data|
Expand Down
3 changes: 2 additions & 1 deletion lib/aeternitas/guard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Guard
# @return [Aeternitas::Guard] Creates a new Instance
def initialize(id, cooldown, timeout = 10.minutes)
@id = id
@cooldown = cooldown
@cooldown = Aeternitas.test_mode? ? 0.seconds : cooldown
@timeout = timeout
@token = SecureRandom.hex(10)
end
Expand Down Expand Up @@ -134,6 +134,7 @@ def acquire_lock!
# @param [Time] sleep_timeout for how long will the guard sleep
# @param [String] msg hint why the guard sleeps
def sleep(sleep_timeout, msg = nil)
sleep_timeout = Time.now if Aeternitas.test_mode?
Aeternitas::GuardLock.transaction do
lock = Aeternitas::GuardLock.where(lock_key: @id).lock.first_or_initialize

Expand Down
14 changes: 9 additions & 5 deletions lib/aeternitas/poll_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ class PollJob < AeternitasJob
meta_data.enqueue!

if pollable_config.sleep_on_guard_locked
if base_delay > 0
delay = Aeternitas.test_mode? ? 0 : base_delay
if delay > 0
ActiveJob::Base.logger.warn "[Aeternitas::PollJob] Guard locked for #{arguments.first}. Sleep for #{base_delay.round(2)}s."
sleep(base_delay)
sleep(delay)
end
retry_job
else
Expand All @@ -78,17 +79,20 @@ class PollJob < AeternitasJob
jitter = rand(0.0..2.0)
total_wait = base_delay + stagger_delay + jitter

if total_wait > 0
retry_job(wait: total_wait.seconds)
if total_wait > 0 || Aeternitas.test_mode?
wait_time = Aeternitas.test_mode? ? 0.seconds : total_wait.seconds
retry_job(wait: wait_time)
ActiveJob::Base.logger.info "[Aeternitas::PollJob] Guard locked for #{arguments.first}. Retry in #{total_wait.round(2)}s."
else
# GuardLock expired, retry with minimal delay
retry_job(wait: jitter.seconds)
wait_time = Aeternitas.test_mode? ? 0.seconds : jitter.seconds
retry_job(wait: wait_time)
end
end
end

def self.execution_wait_time(executions)
return 0.seconds if Aeternitas.test_mode?
wait_index = executions - 1
RETRY_DELAYS[wait_index] || RETRY_DELAYS.last
end
Expand Down
13 changes: 13 additions & 0 deletions lib/aeternitas/test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Aeternitas
# Provides test helpers for aeternitas
module Test
# Executes a block of code in test mode; all cooldowns and wait times are set to 0.
def self.test_mode
original_mode = Aeternitas.test_mode?
Aeternitas.test_mode = true
yield
ensure
Aeternitas.test_mode = original_mode
end
end
end
18 changes: 18 additions & 0 deletions spec/aeternitas/guard_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,22 @@
end
end
end

describe "in test mode" do
around do |example|
Aeternitas::Test.test_mode do
example.run
end
end

it "initializes with a cooldown of 0" do
expect(guard.cooldown).to eq(0.seconds)
end

it "sleeps until now" do
guard.sleep_until(1.hour.from_now)
lock_record = Aeternitas::GuardLock.find_by(lock_key: lock_key)
expect(lock_record.locked_until).to be_within(1.second).of(Time.now)
end
end
end
30 changes: 30 additions & 0 deletions spec/aeternitas/poll_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,34 @@
end
end
end

describe "in test mode" do
around do |example|
Aeternitas::Test.test_mode do
example.run
end
end

let(:guard_locked_error) { Aeternitas::Guard::GuardIsLocked.new("guard-key", 30.minutes.from_now) }

it "uses a wait time of 0 for standard retries" do
expect(described_class.execution_wait_time(1)).to eq(0.seconds)
end

context "when a guard is locked" do
before do
allow_any_instance_of(Aeternitas::Guard).to receive(:with_lock).and_raise(guard_locked_error)
end

it "retries the job with a wait time of 0" do
travel_to Time.current do
described_class.perform_later(meta_data.id)
perform_enqueued_jobs
expect(enqueued_jobs.size).to eq(1)
enqueued_job = enqueued_jobs.last
expect(Time.at(enqueued_job[:at])).to be_within(1.second).of(Time.current)
end
end
end
end
end