Skip to content
Open
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: 11 additions & 3 deletions lib/sidekiq/batch/extension/worker.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
module Sidekiq::Batch::Extension
module Worker
# Allow setting bid and batch directly on worker instances
# This enables compatibility with Sidekiq's perform_sync and perform_inline methods
attr_writer :bid, :batch

# Check current execution context first,
# then fall back to instance variable (for sync execution without middleware)
def bid
Thread.current[:batch].bid
Thread.current[:batch]&.bid || (defined?(@bid) ? @bid : nil)
end

# Check current execution context first,
# then fall back to instance variable (for sync execution without middleware)
def batch
Thread.current[:batch]
Thread.current[:batch] || (defined?(@batch) ? @batch : nil)
end

def valid_within_batch?
batch.valid?
batch&.valid?
end
end
end
252 changes: 252 additions & 0 deletions spec/sidekiq/batch/extension/worker_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
require 'spec_helper'

# Define test worker class at module level (not anonymous)
class BatchExtensionTestWorker
include Sidekiq::Worker

def perform(arg)
"performed with #{arg}"
end
end

describe Sidekiq::Batch::Extension::Worker do
let(:worker_class) { BatchExtensionTestWorker }
let(:worker) { worker_class.new }

describe 'attribute writers' do
it 'allows setting bid directly' do
expect { worker.bid = 'test-bid-123' }.not_to raise_error
end

it 'allows setting batch directly' do
batch = Sidekiq::Batch.new
expect { worker.batch = batch }.not_to raise_error
end

it 'allows setting bid to nil' do
expect { worker.bid = nil }.not_to raise_error
end

it 'allows setting batch to nil' do
expect { worker.batch = nil }.not_to raise_error
end
end

describe '#bid' do
context 'when instance variable is set' do
it 'returns the instance variable value' do
worker.bid = 'instance-bid-123'
expect(worker.bid).to eq('instance-bid-123')
end

it 'returns instance variable even when nil is explicitly set' do
worker.bid = nil
expect(worker.bid).to be_nil
end

it 'prioritizes thread-local over instance variable' do
Thread.current[:batch] = Sidekiq::Batch.new
thread_bid = Thread.current[:batch].bid

worker.bid = 'instance-value'
# Thread-local should win because it's the current execution context
expect(worker.bid).to eq(thread_bid)
expect(worker.bid).not_to eq('instance-value')

Thread.current[:batch] = nil
end
end

context 'when instance variable is not set' do
it 'falls back to Thread.current[:batch].bid' do
Thread.current[:batch] = Sidekiq::Batch.new
thread_bid = Thread.current[:batch].bid

expect(worker.bid).to eq(thread_bid)

Thread.current[:batch] = nil
end

it 'returns nil when no batch context exists' do
Thread.current[:batch] = nil
expect(worker.bid).to be_nil
end

it 'dynamically reflects changes in thread-local batch' do
Thread.current[:batch] = Sidekiq::Batch.new
first_bid = worker.bid

Thread.current[:batch] = Sidekiq::Batch.new
second_bid = worker.bid

expect(second_bid).not_to eq(first_bid)
expect(second_bid).to eq(Thread.current[:batch].bid)

Thread.current[:batch] = nil
end
end
end

describe '#batch' do
context 'when instance variable is set' do
it 'returns the instance variable value' do
batch_obj = Sidekiq::Batch.new
worker.batch = batch_obj
expect(worker.batch).to eq(batch_obj)
end

it 'returns instance variable even when nil is explicitly set' do
worker.batch = nil
expect(worker.batch).to be_nil
end

it 'prioritizes thread-local over instance variable' do
Thread.current[:batch] = Sidekiq::Batch.new
thread_batch = Thread.current[:batch]
instance_batch = Sidekiq::Batch.new

worker.batch = instance_batch
# Thread-local should win because it's the current execution context
expect(worker.batch).to eq(thread_batch)
expect(worker.batch).not_to eq(instance_batch)

Thread.current[:batch] = nil
end
end

context 'when instance variable is not set' do
it 'falls back to Thread.current[:batch]' do
batch_obj = Sidekiq::Batch.new
Thread.current[:batch] = batch_obj

expect(worker.batch).to eq(batch_obj)

Thread.current[:batch] = nil
end

it 'returns nil when no batch context exists' do
Thread.current[:batch] = nil
expect(worker.batch).to be_nil
end

it 'dynamically reflects changes in thread-local batch' do
first_batch = Sidekiq::Batch.new
Thread.current[:batch] = first_batch
expect(worker.batch).to eq(first_batch)

second_batch = Sidekiq::Batch.new
Thread.current[:batch] = second_batch
expect(worker.batch).to eq(second_batch)

Thread.current[:batch] = nil
end
end
end

describe '#valid_within_batch?' do
it 'returns true when batch is valid' do
batch = Sidekiq::Batch.new
Thread.current[:batch] = batch

expect(worker.valid_within_batch?).to be true

Thread.current[:batch] = nil
end

it 'returns nil when no batch context exists' do
Thread.current[:batch] = nil
expect(worker.valid_within_batch?).to be_nil
end

it 'does not raise error when batch is nil' do
Thread.current[:batch] = nil
expect { worker.valid_within_batch? }.not_to raise_error
end
end

describe 'synchronous execution compatibility' do
context 'with perform_sync' do
it 'works without raising NoMethodError' do
expect { worker_class.perform_sync('test-arg') }.not_to raise_error
end

it 'allows setting bid during sync execution' do
# Simulate what Sidekiq does internally
test_worker = worker_class.new
test_worker.bid = 'sync-test-bid'

expect(test_worker.bid).to eq('sync-test-bid')
end
end

context 'with perform_inline' do
it 'works without raising NoMethodError' do
expect { worker_class.perform_inline('test-arg') }.not_to raise_error
end

it 'allows setting bid during inline execution' do
# Simulate what Sidekiq does internally
test_worker = worker_class.new
test_worker.bid = 'inline-test-bid'

expect(test_worker.bid).to eq('inline-test-bid')
end
end
end

describe 'backward compatibility with async execution' do
it 'maintains existing behavior for async batch jobs' do
batch = Sidekiq::Batch.new

# Queue a job within the batch context - should not raise any errors
expect {
batch.jobs do
worker_class.perform_async('async-arg')
end
}.not_to raise_error

# Verify batch exists and has the job tracked
expect(batch.bid).not_to be_nil
end

it 'workers can still read from thread-local batch' do
batch = Sidekiq::Batch.new
Thread.current[:batch] = batch

test_worker = worker_class.new
expect(test_worker.bid).to eq(batch.bid)
expect(test_worker.batch).to eq(batch)

Thread.current[:batch] = nil
end
end

describe 'edge cases' do
it 'handles falsy values correctly with defined?' do
worker.bid = false
expect(worker.bid).to eq(false)

worker.bid = 0
expect(worker.bid).to eq(0)

worker.bid = ''
expect(worker.bid).to eq('')
end

it 'does not memoize thread-local values' do
# Set up a batch
Thread.current[:batch] = Sidekiq::Batch.new
first_bid = worker.bid

# Change the batch
Thread.current[:batch] = Sidekiq::Batch.new
second_bid = worker.bid

# Should get the new bid, not a memoized one
expect(second_bid).not_to eq(first_bid)

Thread.current[:batch] = nil
end
end
end