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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +12 -0
  5. data/Gemfile.lock +42 -0
  6. data/LICENSE +21 -0
  7. data/README.md +2 -0
  8. data/app_test/job_test.rb +20 -0
  9. data/app_test/run.rb +10 -0
  10. data/lib/server.rb +13 -0
  11. data/lib/workerholic.rb +47 -0
  12. data/lib/workerholic/adapters/active_job_adapter.rb +24 -0
  13. data/lib/workerholic/job.rb +49 -0
  14. data/lib/workerholic/job_processor.rb +29 -0
  15. data/lib/workerholic/job_retry.rb +32 -0
  16. data/lib/workerholic/job_scheduler.rb +47 -0
  17. data/lib/workerholic/job_serializer.rb +12 -0
  18. data/lib/workerholic/job_wrapper.rb +32 -0
  19. data/lib/workerholic/log_manager.rb +17 -0
  20. data/lib/workerholic/manager.rb +40 -0
  21. data/lib/workerholic/queue.rb +30 -0
  22. data/lib/workerholic/sorted_set.rb +26 -0
  23. data/lib/workerholic/statistics.rb +21 -0
  24. data/lib/workerholic/storage.rb +80 -0
  25. data/lib/workerholic/worker.rb +43 -0
  26. data/lib/workerholic/worker_balancer.rb +128 -0
  27. data/spec/helpers/helper_methods.rb +15 -0
  28. data/spec/helpers/job_tests.rb +17 -0
  29. data/spec/integration/dequeuing_and_job_processing_spec.rb +24 -0
  30. data/spec/integration/enqueuing_jobs_spec.rb +53 -0
  31. data/spec/job_processor_spec.rb +62 -0
  32. data/spec/job_retry_spec.rb +59 -0
  33. data/spec/job_scheduler_spec.rb +66 -0
  34. data/spec/job_serializer_spec.rb +28 -0
  35. data/spec/job_wrapper_spec.rb +27 -0
  36. data/spec/manager_spec.rb +26 -0
  37. data/spec/queue_spec.rb +16 -0
  38. data/spec/sorted_set.rb +25 -0
  39. data/spec/spec_helper.rb +18 -0
  40. data/spec/statistics_spec.rb +25 -0
  41. data/spec/storage_spec.rb +17 -0
  42. data/spec/worker_spec.rb +59 -0
  43. data/workerholic.gemspec +26 -0
  44. 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
@@ -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
@@ -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
@@ -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
@@ -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