workerholic 0.0.2
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/.gitignore +1 -0
- data/.rspec +1 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +21 -0
- data/README.md +2 -0
- data/app_test/job_test.rb +20 -0
- data/app_test/run.rb +10 -0
- data/lib/server.rb +13 -0
- data/lib/workerholic.rb +47 -0
- data/lib/workerholic/adapters/active_job_adapter.rb +24 -0
- data/lib/workerholic/job.rb +49 -0
- data/lib/workerholic/job_processor.rb +29 -0
- data/lib/workerholic/job_retry.rb +32 -0
- data/lib/workerholic/job_scheduler.rb +47 -0
- data/lib/workerholic/job_serializer.rb +12 -0
- data/lib/workerholic/job_wrapper.rb +32 -0
- data/lib/workerholic/log_manager.rb +17 -0
- data/lib/workerholic/manager.rb +40 -0
- data/lib/workerholic/queue.rb +30 -0
- data/lib/workerholic/sorted_set.rb +26 -0
- data/lib/workerholic/statistics.rb +21 -0
- data/lib/workerholic/storage.rb +80 -0
- data/lib/workerholic/worker.rb +43 -0
- data/lib/workerholic/worker_balancer.rb +128 -0
- data/spec/helpers/helper_methods.rb +15 -0
- data/spec/helpers/job_tests.rb +17 -0
- data/spec/integration/dequeuing_and_job_processing_spec.rb +24 -0
- data/spec/integration/enqueuing_jobs_spec.rb +53 -0
- data/spec/job_processor_spec.rb +62 -0
- data/spec/job_retry_spec.rb +59 -0
- data/spec/job_scheduler_spec.rb +66 -0
- data/spec/job_serializer_spec.rb +28 -0
- data/spec/job_wrapper_spec.rb +27 -0
- data/spec/manager_spec.rb +26 -0
- data/spec/queue_spec.rb +16 -0
- data/spec/sorted_set.rb +25 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/statistics_spec.rb +25 -0
- data/spec/storage_spec.rb +17 -0
- data/spec/worker_spec.rb +59 -0
- data/workerholic.gemspec +26 -0
- metadata +180 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
class SimpleJobTestWithError
|
4
|
+
include Workerholic::Job
|
5
|
+
job_options queue_name: TEST_SCHEDULED_SORTED_SET
|
6
|
+
|
7
|
+
def perform
|
8
|
+
raise Exception
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Workerholic::JobProcessor do
|
13
|
+
it 'processes a simple job' do
|
14
|
+
job = Workerholic::JobWrapper.new(class: SimpleJobTest, arguments: ['test job'])
|
15
|
+
serialized_job = Workerholic::JobSerializer.serialize(job)
|
16
|
+
|
17
|
+
job_processor = Workerholic::JobProcessor.new(serialized_job)
|
18
|
+
simple_job_result = SimpleJobTest.new.perform('test job')
|
19
|
+
|
20
|
+
expect(job_processor.process).to eq(simple_job_result)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'processes a complex job' do
|
24
|
+
serialized_job = Workerholic::JobSerializer.serialize({
|
25
|
+
class: ComplexJobTest,
|
26
|
+
arguments: ['test job', { a: 1, b: 2 }, [1, 2, 3]],
|
27
|
+
statistics: Workerholic::Statistics.new.to_hash
|
28
|
+
})
|
29
|
+
|
30
|
+
job_processor = Workerholic::JobProcessor.new(serialized_job)
|
31
|
+
|
32
|
+
complex_job_result = ComplexJobTest.new.perform('test job', { a: 1, b: 2 }, [1, 2, 3])
|
33
|
+
expect(job_processor.process).to eq(complex_job_result)
|
34
|
+
end
|
35
|
+
|
36
|
+
# it 'raises a custom error when processing a job with error' do
|
37
|
+
# serialized_job = Workerholic::JobSerializer.serialize({
|
38
|
+
# class: SimpleJobTestWithError,
|
39
|
+
# arguments: [],
|
40
|
+
# statistics: Workerholic::Statistics.new.to_hash
|
41
|
+
# })
|
42
|
+
|
43
|
+
# job_processor = Workerholic::JobProcessor.new(serialized_job)
|
44
|
+
|
45
|
+
# expect { job_processor.process }.to raise_error(Workerholic::JobProcessingError)
|
46
|
+
# end
|
47
|
+
|
48
|
+
it 'retries job when job processing fails' do
|
49
|
+
job = {
|
50
|
+
class: SimpleJobTestWithError,
|
51
|
+
arguments: [],
|
52
|
+
statistics: Workerholic::Statistics.new.to_hash
|
53
|
+
}
|
54
|
+
serialized_job = Workerholic::JobSerializer.serialize(job)
|
55
|
+
|
56
|
+
allow(Workerholic::JobRetry).to receive(:new)
|
57
|
+
|
58
|
+
expect(Workerholic::JobRetry).to receive(:new)
|
59
|
+
|
60
|
+
Workerholic::JobProcessor.new(serialized_job).process
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
class JobWithError
|
4
|
+
def perform
|
5
|
+
raise
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Workerholic::JobRetry do
|
10
|
+
let(:redis) { Redis.new }
|
11
|
+
|
12
|
+
before { redis.del(TEST_SCHEDULED_SORTED_SET) }
|
13
|
+
|
14
|
+
it 'increments retry count' do
|
15
|
+
job = Workerholic::JobWrapper.new(class: JobWithError, arguments: [])
|
16
|
+
|
17
|
+
Workerholic::JobRetry.new(
|
18
|
+
job: job,
|
19
|
+
sorted_set: Workerholic::SortedSet.new(TEST_SCHEDULED_SORTED_SET)
|
20
|
+
)
|
21
|
+
|
22
|
+
expect(job.retry_count).to eq(1)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'schedules job by incrementing by 10 more seconds for every new retry' do
|
26
|
+
job = Workerholic::JobWrapper.new(class: JobWithError, arguments: [], retry_count: 2)
|
27
|
+
|
28
|
+
Workerholic::JobRetry.new(
|
29
|
+
job: job,
|
30
|
+
sorted_set: Workerholic::SortedSet.new(TEST_SCHEDULED_SORTED_SET)
|
31
|
+
)
|
32
|
+
|
33
|
+
expect((job.execute_at - Time.now.to_f).ceil).to eq(30)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'pushes job inside "workerholic:test:scheduled_jobs" sorted set' do
|
37
|
+
job = Workerholic::JobWrapper.new(class: JobWithError, arguments: [], retry_count: 2)
|
38
|
+
|
39
|
+
Workerholic::JobRetry.new(
|
40
|
+
job: job,
|
41
|
+
sorted_set: Workerholic::SortedSet.new(TEST_SCHEDULED_SORTED_SET)
|
42
|
+
)
|
43
|
+
|
44
|
+
serialized_job = Workerholic::JobSerializer.serialize(job)
|
45
|
+
|
46
|
+
expect(redis.zrange(TEST_SCHEDULED_SORTED_SET, 0, 0, with_scores: true).first).to eq([serialized_job, job.execute_at])
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'discards job if retry count is greater than 5' do
|
50
|
+
job = Workerholic::JobWrapper.new(class: JobWithError, arguments: [], retry_count: 5)
|
51
|
+
|
52
|
+
Workerholic::JobRetry.new(
|
53
|
+
job: job,
|
54
|
+
sorted_set: Workerholic::SortedSet.new(TEST_SCHEDULED_SORTED_SET)
|
55
|
+
)
|
56
|
+
|
57
|
+
expect(redis.exists(TEST_SCHEDULED_SORTED_SET)).to eq(false)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
class SimpleDelayedJobTest
|
4
|
+
include Workerholic::Job
|
5
|
+
job_options queue_name: TEST_SCHEDULED_SORTED_SET
|
6
|
+
|
7
|
+
def perform(n, s)
|
8
|
+
s
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Workerholic::JobScheduler do
|
13
|
+
let(:scheduler) { Workerholic::JobScheduler.new(set_name: TEST_SCHEDULED_SORTED_SET, queue_name: TEST_QUEUE) }
|
14
|
+
let(:redis) { Redis.new }
|
15
|
+
|
16
|
+
before { redis.del(TEST_SCHEDULED_SORTED_SET) }
|
17
|
+
|
18
|
+
context 'with non-empty set' do
|
19
|
+
let(:serialized_job) do
|
20
|
+
job = Workerholic::JobWrapper.new(
|
21
|
+
class: ComplexJobTest,
|
22
|
+
arguments: ['test job', { a: 1, b: 2 }, [1, 2, 3]]
|
23
|
+
)
|
24
|
+
|
25
|
+
Workerholic::JobSerializer.serialize(job)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'checks the time for scheduled job inside sorted set' do
|
29
|
+
score = Time.now.to_f
|
30
|
+
scheduler.schedule(serialized_job, score)
|
31
|
+
|
32
|
+
expect(scheduler.job_due?).to eq(true)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'fetches a job from a sorted set' do
|
36
|
+
score = Time.now.to_f
|
37
|
+
scheduler.schedule(serialized_job, score)
|
38
|
+
scheduler.enqueue_due_jobs
|
39
|
+
|
40
|
+
expect(scheduler.sorted_set.empty?).to eq(true)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'enqueues due job to the main queue' do
|
44
|
+
score = Time.now.to_f
|
45
|
+
scheduler.schedule(serialized_job, score)
|
46
|
+
scheduler.enqueue_due_jobs
|
47
|
+
|
48
|
+
expect(scheduler.queue.empty?).to eq(false)
|
49
|
+
expect(scheduler.queue.dequeue).to eq(serialized_job)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'with delayed job option specified' do
|
54
|
+
it 'adds delayed job to the scheduled sorted set' do
|
55
|
+
SimpleDelayedJobTest.new.perform_delayed(2, 'test arg')
|
56
|
+
|
57
|
+
expect(scheduler.sorted_set.empty?).to eq(false)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'raises an ArgumentError if perform_delayed first argument is not of Numeric type' do
|
61
|
+
job = SimpleDelayedJobTest.new
|
62
|
+
|
63
|
+
expect { job.perform_delayed("wrong type", 'test arg') }.to raise_error(ArgumentError)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
class JobTest; end
|
4
|
+
|
5
|
+
describe Workerholic::JobSerializer do
|
6
|
+
it 'serializes a job' do
|
7
|
+
job = Workerholic::JobWrapper.new(
|
8
|
+
class: JobTest,
|
9
|
+
arguments: ['some_args', [1, 2, 3], true, { a: 'y' }, 1, :symptom]
|
10
|
+
)
|
11
|
+
|
12
|
+
serialized_job = Workerholic::JobSerializer.serialize(job)
|
13
|
+
|
14
|
+
expect(serialized_job).to eq(YAML.dump(job.to_hash))
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'deserializes a job' do
|
18
|
+
job = Workerholic::JobWrapper.new(
|
19
|
+
class: JobTest,
|
20
|
+
arguments: ['some_args', [1, 2, 3], true, { a: 'y' }, 1, :symptom]
|
21
|
+
)
|
22
|
+
|
23
|
+
serialized_job = YAML.dump(job.to_hash)
|
24
|
+
deserialized_job = Workerholic::JobSerializer.deserialize(serialized_job)
|
25
|
+
|
26
|
+
expect(deserialized_job).to eq(job)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe Workerholic::JobWrapper do
|
4
|
+
it 'returns a hash with job meta info and job stats info' do
|
5
|
+
job = Workerholic::JobWrapper.new(class: SimpleJobTest, arguments: ['test job'])
|
6
|
+
|
7
|
+
expected_result = {
|
8
|
+
class: SimpleJobTest,
|
9
|
+
arguments: ['test job'],
|
10
|
+
retry_count: 0,
|
11
|
+
execute_at: nil,
|
12
|
+
statistics: {
|
13
|
+
enqueued_at: nil,
|
14
|
+
errors: [],
|
15
|
+
started_at: nil,
|
16
|
+
completed_at: nil
|
17
|
+
}
|
18
|
+
}
|
19
|
+
expect(job.to_hash).to eq(expected_result)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'performs the job' do
|
23
|
+
job = Workerholic::JobWrapper.new(class: SimpleJobTest, arguments: ['test job'])
|
24
|
+
|
25
|
+
expect(job.perform).to eq('test job')
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe Workerholic::Manager do
|
4
|
+
it 'creates a number of workers based on workers count' do
|
5
|
+
manager = Workerholic::Manager.new
|
6
|
+
|
7
|
+
manager.workers.each { |worker| expect(worker).to be_a(Workerholic::Worker) }
|
8
|
+
expect(manager.workers.size).to eq(Workerholic::Manager::WORKERS_COUNT)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'creates a job scheduler' do
|
12
|
+
manager = Workerholic::Manager.new
|
13
|
+
|
14
|
+
expect(manager.scheduler).to be_a(Workerholic::JobScheduler)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'starts up the workers and the scheduler' do
|
18
|
+
manager = Workerholic::Manager.new
|
19
|
+
|
20
|
+
expect(manager.workers.first).to receive(:work)
|
21
|
+
expect(manager.scheduler).to receive(:start)
|
22
|
+
Thread.new { manager.start }
|
23
|
+
|
24
|
+
sleep(0.1)
|
25
|
+
end
|
26
|
+
end
|
data/spec/queue_spec.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe Workerholic::Queue do
|
4
|
+
let(:queue) { Workerholic::Queue.new('test') }
|
5
|
+
let(:job) { 'test job' }
|
6
|
+
|
7
|
+
it 'enqueues a job' do
|
8
|
+
expect(queue.storage).to receive(:push).with(queue.name, job)
|
9
|
+
queue.enqueue(job)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'dequeues a job' do
|
13
|
+
expect(queue.storage).to receive(:pop).with(queue.name).and_return([queue.name,job])
|
14
|
+
queue.dequeue
|
15
|
+
end
|
16
|
+
end
|
data/spec/sorted_set.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe Workerholic::SortedSet do
|
4
|
+
let(:job) {{ class: SimpleJobTest, arguments: [] }}
|
5
|
+
let(:redis) { Redis.new }
|
6
|
+
let(:sorted_set) { Workerholic::SortedSet.new(TEST_SCHEDULED_SORTED_SET) }
|
7
|
+
|
8
|
+
after { redis.del(TEST_SCHEDULED_SORTED_SET) }
|
9
|
+
|
10
|
+
it 'adds a serialized job to the sorted set' do
|
11
|
+
serialized_job = Workerholic::JobSerializer.serialize(job)
|
12
|
+
score = Time.now.to_f
|
13
|
+
expect(sorted_set.add(score, serialized_job)).to eq(true)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'removes due job from the sorted set' do
|
17
|
+
serialized_job = Workerholic::JobSerializer.serialize(job)
|
18
|
+
score = Time.now.to_f
|
19
|
+
|
20
|
+
sorted_set.add(score, serialized_job)
|
21
|
+
sorted_set.remove(score)
|
22
|
+
|
23
|
+
expect(redis.zcount(TEST_SCHEDULED_SORTED_SET, 0, '+inf')).to eq(0)
|
24
|
+
end
|
25
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
$LOAD_PATH << __dir__ + '/../lib/'
|
2
|
+
|
3
|
+
require 'workerholic'
|
4
|
+
|
5
|
+
require_relative 'helpers/helper_methods'
|
6
|
+
require_relative 'helpers/job_tests'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.expect_with :rspec do |expectations|
|
10
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
11
|
+
end
|
12
|
+
|
13
|
+
config.mock_with :rspec do |mocks|
|
14
|
+
mocks.verify_partial_doubles = true
|
15
|
+
end
|
16
|
+
|
17
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Workerholic::Statistics do
|
4
|
+
it 'initializes attributes with without an argument' do
|
5
|
+
statistics = Workerholic::Statistics.new
|
6
|
+
expect(statistics.enqueued_at).to be_nil
|
7
|
+
expect(statistics.errors).to eq([])
|
8
|
+
expect(statistics.started_at).to be_nil
|
9
|
+
expect(statistics.completed_at).to be_nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'initializes attributes with arguments' do
|
13
|
+
enqueuing_time = Time.now.to_f - 86400
|
14
|
+
started_at_time = Time.now.to_f
|
15
|
+
completed_at_time = Time.now.to_f + 86400
|
16
|
+
options = {
|
17
|
+
enqueued_at: enqueuing_time,
|
18
|
+
errors: ['Your job is bad and you should feel bad'],
|
19
|
+
started_at: started_at_time,
|
20
|
+
completed_at: completed_at_time
|
21
|
+
}
|
22
|
+
|
23
|
+
expect(Workerholic::Statistics.new(options).to_hash).to eq(options)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe Workerholic::Storage do
|
4
|
+
let(:storage) { Workerholic::Storage::RedisWrapper.new }
|
5
|
+
let(:queue_name) { TEST_QUEUE }
|
6
|
+
let(:job) { 'test job' }
|
7
|
+
|
8
|
+
it 'adds a job to the test queue' do
|
9
|
+
expect(storage.redis).to receive(:rpush).with(queue_name, job)
|
10
|
+
storage.push(queue_name, job)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'pops a job from the test queue' do
|
14
|
+
expect(storage.redis).to receive(:blpop).with(queue_name, 1)
|
15
|
+
storage.pop(queue_name)
|
16
|
+
end
|
17
|
+
end
|
data/spec/worker_spec.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
class WorkerJobTest
|
4
|
+
@@job_status = 0
|
5
|
+
|
6
|
+
def self.reset
|
7
|
+
@@job_status = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.check
|
11
|
+
@@job_status
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform
|
15
|
+
@@job_status += 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe Workerholic::Worker do
|
20
|
+
let(:redis) { Redis.new }
|
21
|
+
let(:job) do
|
22
|
+
{
|
23
|
+
class: WorkerJobTest,
|
24
|
+
arguments: [],
|
25
|
+
statistics: Workerholic::Statistics.new.to_hash
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
before do
|
30
|
+
redis.del(TEST_SCHEDULED_SORTED_SET)
|
31
|
+
WorkerJobTest.reset
|
32
|
+
end
|
33
|
+
|
34
|
+
context '#work' do
|
35
|
+
it 'polls a job from a thread' do
|
36
|
+
queue = Workerholic::Queue.new(TEST_QUEUE)
|
37
|
+
worker = Workerholic::Worker.new(queue)
|
38
|
+
|
39
|
+
serialized_job = Workerholic::JobSerializer.serialize(job)
|
40
|
+
redis.rpush(TEST_QUEUE, serialized_job)
|
41
|
+
|
42
|
+
worker.work
|
43
|
+
|
44
|
+
expect_during(1, false) { redis.exists(TEST_QUEUE) }
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'processes a job from a thread' do
|
48
|
+
queue = Workerholic::Queue.new(TEST_QUEUE)
|
49
|
+
worker = Workerholic::Worker.new(queue)
|
50
|
+
|
51
|
+
serialized_job = Workerholic::JobSerializer.serialize(job)
|
52
|
+
redis.rpush(TEST_QUEUE, serialized_job)
|
53
|
+
|
54
|
+
worker.work
|
55
|
+
|
56
|
+
expect_during(1, 1) { WorkerJobTest.check }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|