jiggler 0.1.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE +4 -0
  4. data/README.md +423 -0
  5. data/bin/jiggler +31 -0
  6. data/lib/jiggler/cleaner.rb +130 -0
  7. data/lib/jiggler/cli.rb +263 -0
  8. data/lib/jiggler/config.rb +165 -0
  9. data/lib/jiggler/core.rb +22 -0
  10. data/lib/jiggler/errors.rb +5 -0
  11. data/lib/jiggler/job.rb +116 -0
  12. data/lib/jiggler/launcher.rb +69 -0
  13. data/lib/jiggler/manager.rb +73 -0
  14. data/lib/jiggler/redis_store.rb +55 -0
  15. data/lib/jiggler/retrier.rb +122 -0
  16. data/lib/jiggler/scheduled/enqueuer.rb +78 -0
  17. data/lib/jiggler/scheduled/poller.rb +97 -0
  18. data/lib/jiggler/stats/collection.rb +26 -0
  19. data/lib/jiggler/stats/monitor.rb +103 -0
  20. data/lib/jiggler/summary.rb +101 -0
  21. data/lib/jiggler/support/helper.rb +35 -0
  22. data/lib/jiggler/version.rb +5 -0
  23. data/lib/jiggler/web/assets/stylesheets/application.css +64 -0
  24. data/lib/jiggler/web/views/application.erb +329 -0
  25. data/lib/jiggler/web.rb +80 -0
  26. data/lib/jiggler/worker.rb +179 -0
  27. data/lib/jiggler.rb +10 -0
  28. data/spec/examples.txt +79 -0
  29. data/spec/fixtures/config/jiggler.yml +4 -0
  30. data/spec/fixtures/jobs.rb +5 -0
  31. data/spec/fixtures/my_failed_job.rb +10 -0
  32. data/spec/fixtures/my_job.rb +9 -0
  33. data/spec/fixtures/my_job_with_args.rb +18 -0
  34. data/spec/jiggler/cleaner_spec.rb +171 -0
  35. data/spec/jiggler/cli_spec.rb +87 -0
  36. data/spec/jiggler/config_spec.rb +56 -0
  37. data/spec/jiggler/core_spec.rb +34 -0
  38. data/spec/jiggler/job_spec.rb +99 -0
  39. data/spec/jiggler/launcher_spec.rb +66 -0
  40. data/spec/jiggler/manager_spec.rb +52 -0
  41. data/spec/jiggler/redis_store_spec.rb +20 -0
  42. data/spec/jiggler/retrier_spec.rb +55 -0
  43. data/spec/jiggler/scheduled/enqueuer_spec.rb +81 -0
  44. data/spec/jiggler/scheduled/poller_spec.rb +40 -0
  45. data/spec/jiggler/stats/monitor_spec.rb +40 -0
  46. data/spec/jiggler/summary_spec.rb +168 -0
  47. data/spec/jiggler/web_spec.rb +37 -0
  48. data/spec/jiggler/worker_spec.rb +110 -0
  49. data/spec/spec_helper.rb +54 -0
  50. metadata +230 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Jiggler::Scheduled::Enqueuer do
4
+ let(:config) do
5
+ Jiggler::Config.new(
6
+ concurrency: 1,
7
+ timeout: 1,
8
+ queues: ['default', 'mine'],
9
+ server_mode: true
10
+ )
11
+ end
12
+ let(:enqueuer) { described_class.new(config) }
13
+
14
+ describe '#push_job' do
15
+ it 'pushes an empty job to default queue' do
16
+ expect do
17
+ config.with_sync_redis do |conn|
18
+ enqueuer.push_job(conn, '{ "name": "MyJob", "jid": "001" }')
19
+ end
20
+ end.to change {
21
+ config.with_sync_redis { |conn| conn.call('LLEN', 'jiggler:list:default') }
22
+ }.by(1)
23
+ end
24
+
25
+ it 'pushes job to queue from args' do
26
+ expect do
27
+ config.with_sync_redis do |conn|
28
+ enqueuer.push_job(
29
+ conn,
30
+ '{ "name": "MyJob", "queue": "mine", "jid": "111" }'
31
+ )
32
+ end
33
+ end.to change {
34
+ config.with_sync_redis { |conn| conn.call('LLEN', 'jiggler:list:mine') }
35
+ }.by(1)
36
+ config.with_sync_redis { |conn| conn.call('DEL', 'jiggler:list:mine') }
37
+ end
38
+
39
+ it 'does not drop job if queue is not configured' do
40
+ expect do
41
+ config.with_sync_redis do |conn|
42
+ enqueuer.push_job(
43
+ conn,
44
+ '{ "name": "MyJob", "queue": "unknown", "jid": "011" }'
45
+ )
46
+ end
47
+ end.to change {
48
+ config.with_sync_redis { |conn| conn.call('LLEN', 'jiggler:list:unknown') }
49
+ }.by(1)
50
+ end
51
+ end
52
+
53
+ describe '#enqueue_jobs' do
54
+ it 'enqueues jobs if their zrange is in the past' do
55
+ expect do
56
+ config.with_sync_redis do |conn|
57
+ conn.call(
58
+ 'ZADD',
59
+ config.retries_set,
60
+ (Time.now.to_f - 10.0).to_s,
61
+ '{ "name": "MyJob", "queue": "mine", "jid": "002" }'
62
+ )
63
+ conn.call(
64
+ 'ZADD',
65
+ config.retries_set,
66
+ (Time.now.to_f + 10.0).to_s,
67
+ '{ "name": "MyFailedJob", "queue": "mine", "jid": "012" }'
68
+ )
69
+ config.logger.debug('Enqueuer Test') { conn.call('ZRANGE', config.retries_set, -5, -1) }
70
+ end
71
+ expect(enqueuer).to receive(:push_job).at_least(:once).and_call_original
72
+ Sync do
73
+ enqueuer.enqueue_jobs
74
+ end
75
+ end.to change {
76
+ config.with_sync_redis { |conn| conn.call('LLEN', 'jiggler:list:mine') }
77
+ }.by(1)
78
+ config.with_sync_redis { |conn| conn.call('DEL', 'jiggler:list:mine') }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Jiggler::Scheduled::Poller do
4
+ let(:config) do
5
+ Jiggler::Config.new(
6
+ concurrency: 1,
7
+ timeout: 1,
8
+ poll_interval: 1
9
+ )
10
+ end
11
+ let(:poller) { described_class.new(config) }
12
+
13
+ describe '#start' do
14
+ it 'starts the poller' do
15
+ task = Async do
16
+ expect(poller).to receive(:initial_wait)
17
+ expect(poller).to receive(:enqueue).at_least(:once).and_call_original
18
+ Async { poller.start }
19
+ expect(poller.instance_variable_get(:@done)).to be false
20
+ sleep(1)
21
+ poller.terminate
22
+ end
23
+ task.wait
24
+ end
25
+ end
26
+
27
+ describe '#terminate' do
28
+ it 'terminates the poller' do
29
+ expect(poller.instance_variable_get(:@enqueuer)).to receive(:terminate).
30
+ and_call_original
31
+ task = Async do
32
+ Async { poller.start }
33
+ sleep(1)
34
+ poller.terminate
35
+ expect(poller.instance_variable_get(:@done)).to be true
36
+ end
37
+ task.wait
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Jiggler::Stats::Monitor do
4
+ let(:config) do
5
+ Jiggler::Config.new(
6
+ timeout: 1,
7
+ stats_interval: 1
8
+ )
9
+ end
10
+ let(:uuid) { 'monitor-test-uuid' }
11
+ let(:collection) { Jiggler::Stats::Collection.new(uuid) }
12
+ let(:monitor) { described_class.new(config, collection) }
13
+
14
+ describe '#start' do
15
+ it 'starts the monitor' do
16
+ task = Async do
17
+ expect(monitor).to receive(:load_data_into_redis).at_least(:once).and_call_original
18
+ Async { monitor.start }
19
+ expect(monitor.instance_variable_get(:@done)).to be false
20
+ sleep(1)
21
+ monitor.terminate
22
+ end
23
+ task.wait
24
+ end
25
+ end
26
+
27
+ describe '#terminate' do
28
+ it 'terminates the monitor' do
29
+ task = Async do
30
+ Async { monitor.start }
31
+ sleep(1)
32
+ expect(monitor).to receive(:cleanup).and_call_original
33
+ monitor.terminate
34
+ expect(monitor.instance_variable_get(:@done)).to be true
35
+ expect(config.client_redis_pool.acquire { |conn| conn.call('GET', uuid) }).to be nil
36
+ end
37
+ task.wait
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Jiggler::Summary do
4
+ let(:queues) { ['default', 'queue1'] }
5
+ let(:config) do
6
+ Jiggler::Config.new(
7
+ concurrency: 1,
8
+ timeout: 1,
9
+ stats_interval: 1,
10
+ queues: queues,
11
+ poller_enabled: false
12
+ )
13
+ end
14
+ let(:collection) { Jiggler::Stats::Collection.new('summary-test-uuid') }
15
+ let(:summary) { described_class.new(config) }
16
+
17
+ describe '.all' do
18
+ it 'has correct keys and data types' do
19
+ subject = summary.all
20
+ expect(subject).to be_a Hash
21
+ expect(subject.keys).to eq Jiggler::Summary::KEYS
22
+ expect(subject['retry_jobs_count']).to be_a Integer
23
+ expect(subject['dead_jobs_count']).to be_a Integer
24
+ expect(subject['scheduled_jobs_count']).to be_a Integer
25
+ expect(subject['processed_count']).to be_a Integer
26
+ expect(subject['failures_count']).to be_a Integer
27
+ expect(subject['monitor_enabled']).to be_a(String).or be_a(NilClass)
28
+ expect(subject['processes']).to be_a Hash
29
+ expect(subject['queues']).to be_a Hash
30
+ end
31
+
32
+ it 'gets latest data' do
33
+ Sync do
34
+ config.cleaner.prune_all
35
+ launcher = Jiggler::Launcher.new(config)
36
+ uuid = launcher.send(:uuid)
37
+ MyJob.with_options(queue: 'queue1').enqueue
38
+
39
+ first_summary = summary.all
40
+ expect(first_summary['queues']).to include({
41
+ 'queue1' => 1
42
+ })
43
+ expect(first_summary['processes'].keys).to_not include(uuid)
44
+
45
+ second_summary = nil
46
+ launcher_task = Async { launcher.start }
47
+ stop_task = Async do
48
+ sleep(2)
49
+ second_summary = summary.all
50
+ launcher.stop
51
+ end
52
+ launcher_task.wait
53
+ stop_task.wait
54
+
55
+ expect(second_summary['queues']).to_not include({
56
+ 'queue1' => 1
57
+ })
58
+ expect(second_summary['processes'].keys).to include(uuid)
59
+ expect(second_summary['processes'][uuid]).to include({
60
+ 'queues' => queues.join(','),
61
+ 'hostname' => Socket.gethostname,
62
+ 'pid' => Process.pid.to_s,
63
+ 'concurrency' => '1',
64
+ 'timeout' => '1',
65
+ 'poller_enabled' => false,
66
+ 'current_jobs' => {}
67
+ })
68
+ end
69
+ end
70
+
71
+ context 'key dissasembly' do
72
+ let(:uuid) { 'jiggler:svr:2aa9:10:15:drop,bark,meow:1:1673820635:29677:dyno:1' }
73
+
74
+ it 'has correct process metadata' do
75
+ allow(summary).to receive(:fetch_processes).and_return([uuid])
76
+ subject = summary.all
77
+ expect(subject['processes'][uuid]).to include({
78
+ 'name' => 'jiggler:svr:2aa9',
79
+ 'concurrency' => '10',
80
+ 'timeout' => '15',
81
+ 'queues' => 'drop,bark,meow',
82
+ 'pid' => '29677',
83
+ 'poller_enabled' => true,
84
+ 'started_at' => '1673820635',
85
+ 'hostname' => 'dyno:1',
86
+ })
87
+ end
88
+ end
89
+ end
90
+
91
+ describe '#last_dead_jobs' do
92
+ it 'returns last n dead jobs' do
93
+ Sync do
94
+ config.cleaner.prune_all
95
+ expect(summary.last_dead_jobs(1)).to be_empty
96
+ MyFailedJob.with_options(queue: 'queue1', retries: 0).enqueue('yay')
97
+ end
98
+ worker = Jiggler::Worker.new(config, collection) do
99
+ config.logger.info('Doing some weird dead testings')
100
+ end
101
+ task = Async do
102
+ Async do
103
+ worker.run
104
+ end
105
+ sleep(1)
106
+ worker.terminate
107
+ end
108
+ task.wait
109
+ jobs = Sync { summary.last_dead_jobs(3) }
110
+ expect(jobs.count).to be 1
111
+ expect(jobs.first).to include({
112
+ 'name' => 'MyFailedJob',
113
+ 'args' => ['yay']
114
+ })
115
+ end
116
+ end
117
+
118
+ describe '#last_retry_jobs' do
119
+ it 'returns last n retry jobs' do
120
+ Sync do
121
+ config.cleaner.prune_all
122
+ expect(summary.last_retry_jobs(3)).to be_empty
123
+ 5.times do |i|
124
+ MyFailedJob.with_options(
125
+ queue: 'queue1',
126
+ retries: 1
127
+ ).enqueue("yay-#{i}")
128
+ end
129
+ end
130
+ worker = Jiggler::Worker.new(config, collection) do
131
+ config.logger.info('Doing some weird retry testings')
132
+ end
133
+ task = Async do
134
+ Async do
135
+ worker.run
136
+ end
137
+ sleep(1)
138
+ worker.terminate
139
+ end
140
+ task.wait
141
+ jobs = Sync { summary.last_retry_jobs(3) }
142
+ expect(jobs.count).to be 3
143
+ expect(jobs.first).to include({
144
+ 'name' => 'MyFailedJob',
145
+ 'args' => ['yay-4']
146
+ })
147
+ end
148
+ end
149
+
150
+ describe '#last_scheduled_jobs' do
151
+ it 'returns last n scheduled jobs' do
152
+ Sync do
153
+ config.cleaner.prune_all
154
+ expect(summary.last_scheduled_jobs(3)).to be_empty
155
+ MyFailedJob.with_options(
156
+ queue: 'queue1'
157
+ ).enqueue_in(100, 'yay-scheduled')
158
+ jobs = summary.last_scheduled_jobs(3)
159
+ expect(jobs.count).to be 1
160
+ expect(jobs.first).to include({
161
+ 'name' => 'MyFailedJob',
162
+ 'args' => ['yay-scheduled']
163
+ })
164
+ expect(jobs.first['scheduled_at']).to be_a Float
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Jiggler::Web do
4
+ let(:web) { Jiggler::Web.new }
5
+
6
+ describe '#time_ago_in_words' do
7
+ it 'returns the correct string for a timestamp' do
8
+ expect(web.time_ago_in_words(Time.now.to_i - 120)).to eq '2 minutes ago'
9
+ end
10
+
11
+ it 'returns nil for nil' do
12
+ expect(web.time_ago_in_words(nil)).to be_nil
13
+ end
14
+ end
15
+
16
+ describe '#format_memory' do
17
+ it 'returns the correct string for a memory size' do
18
+ expect(web.format_memory(1024)).to eq '1.0 MB'
19
+ end
20
+
21
+ it 'returns ? for nil' do
22
+ expect(web.format_memory(nil)).to eq '?'
23
+ end
24
+ end
25
+
26
+ describe '#outdated_heartbeat?' do
27
+ it 'returns true if the heartbeat is older than 2 stats intervals' do
28
+ expect(
29
+ web.outdated_heartbeat?(Time.now.to_i - Jiggler.config[:stats_interval] * 2 - 1)
30
+ ).to be true
31
+ end
32
+
33
+ it 'returns false if the heartbeat is newer than 2 stats intervals' do
34
+ expect(web.outdated_heartbeat?(Time.now.to_i - Jiggler.config[:stats_interval])).to be false
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Jiggler::Worker do
4
+ before(:all) do
5
+ Jiggler.instance_variable_set(:@config, Jiggler::Config.new(
6
+ concurrency: 1,
7
+ client_concurrency: 1,
8
+ timeout: 1,
9
+ queues: ['default', 'test']
10
+ ) )
11
+ end
12
+ after(:all) do
13
+ Jiggler.instance_variable_set(:@config, Jiggler::Config.new)
14
+ end
15
+
16
+ let(:config) { Jiggler.config }
17
+ let(:collection) { Jiggler::Stats::Collection.new(config) }
18
+ let(:worker) do
19
+ described_class.new(config, collection) do
20
+ config.logger.debug("Callback called: #{rand(100)}")
21
+ end
22
+ end
23
+
24
+ describe '#run' do
25
+ it 'runs the worker and performs the job' do
26
+ MyJob.enqueue
27
+ task = Async do
28
+ Async do
29
+ expect(worker).to receive(:fetch_one).at_least(:once).and_call_original
30
+ expect(worker).to receive(:execute_job).at_least(:once).and_call_original
31
+ worker.run
32
+ end
33
+ sleep(1)
34
+ worker.terminate
35
+ end
36
+ task.wait
37
+ end
38
+
39
+ it 'runs the worker and performs the job with args' do
40
+ expect do
41
+ MyJobWithArgs.enqueue('str', 1, 2.0, true, [1, 2], { a: 1, b: 2 })
42
+ task = Async do
43
+ Async do
44
+ expect(worker).to receive(:fetch_one).at_least(:once).and_call_original
45
+ expect(worker).to receive(:execute_job).at_least(:once).and_call_original
46
+ worker.run
47
+ end
48
+ sleep(1)
49
+ worker.terminate
50
+ end
51
+ task.wait
52
+ end.to_not change {
53
+ config.with_sync_redis { |conn| conn.call('ZCARD', config.retries_set) }
54
+ }
55
+ end
56
+
57
+ it 'runs the worker and adds the job to retry queue' do
58
+ expect do
59
+ MyFailedJob.enqueue
60
+ task = Async do
61
+ Async do
62
+ expect(worker).to receive(:fetch_one).at_least(:once).and_call_original
63
+ expect(worker).to receive(:execute_job).at_least(:once).and_call_original
64
+ worker.run
65
+ end
66
+ sleep(1)
67
+ worker.terminate
68
+ end
69
+ task.wait
70
+ end.to change {
71
+ config.with_sync_redis { |conn| conn.call('ZCARD', config.retries_set) }
72
+ }.by(1)
73
+ config.with_sync_redis { |conn| conn.call('DEL', 'jiggler:set:retries') }
74
+ end
75
+ end
76
+
77
+ describe '#execute_job' do
78
+ it 'executes the job' do
79
+ expect do
80
+ worker.instance_variable_set(
81
+ :@current_job,
82
+ Jiggler::Worker::CurrentJob.new(queue: 'default', args: '{ "name": "MyJob", "jid": "321" }')
83
+ )
84
+ worker.send(:execute_job)
85
+ end.to output("Hello World\n").to_stdout
86
+ end
87
+ end
88
+
89
+ describe '#terminate' do
90
+ context 'when worker is running' do
91
+ it 'terminates the worker' do
92
+ task = Async do
93
+ expect(worker.done).to be false
94
+ Async { worker.run }
95
+ worker.terminate
96
+ end
97
+ task.wait
98
+ expect(worker.done).to be true
99
+ end
100
+ end
101
+
102
+ context 'when worker is not running' do
103
+ it do
104
+ expect(worker.done).to be false
105
+ worker.terminate
106
+ expect(worker.done).to be true
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'debug'
4
+
5
+ # client
6
+ require 'jiggler'
7
+ require 'jiggler/web'
8
+
9
+ # server
10
+ require 'jiggler/support/helper'
11
+ require 'jiggler/scheduled/enqueuer'
12
+ require 'jiggler/scheduled/poller'
13
+ require 'jiggler/stats/collection'
14
+ require 'jiggler/stats/monitor'
15
+ require 'jiggler/errors'
16
+ require 'jiggler/retrier'
17
+ require 'jiggler/launcher'
18
+ require 'jiggler/manager'
19
+ require 'jiggler/worker'
20
+ require 'jiggler/cli'
21
+
22
+ require_relative './fixtures/jobs'
23
+
24
+ RSpec.configure do |config|
25
+ config.expect_with :rspec do |expectations|
26
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
27
+ end
28
+
29
+ config.mock_with :rspec do |mocks|
30
+ mocks.verify_partial_doubles = true
31
+ end
32
+
33
+ config.shared_context_metadata_behavior = :apply_to_host_groups
34
+
35
+ config.filter_run_when_matching :focus
36
+ config.example_status_persistence_file_path = 'spec/examples.txt'
37
+ config.disable_monkey_patching!
38
+ config.warnings = true
39
+
40
+ if config.files_to_run.one?
41
+ config.default_formatter = 'doc'
42
+ end
43
+
44
+ config.around(:each) do |example|
45
+ Timeout.timeout(10) do
46
+ example.run
47
+ end
48
+ end
49
+
50
+ config.profile_examples = 10
51
+ config.order = :random
52
+
53
+ Kernel.srand config.seed
54
+ end