jiggler 0.1.0.rc2

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 (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