jiggler 0.1.0.rc4 → 0.1.0.rc6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -200
  3. data/bin/jiggler +1 -1
  4. data/lib/jiggler/at_least_once/acknowledger.rb +43 -0
  5. data/lib/jiggler/at_least_once/fetcher.rb +93 -0
  6. data/lib/jiggler/at_most_once/acknowledger.rb +21 -0
  7. data/lib/jiggler/at_most_once/fetcher.rb +46 -0
  8. data/lib/jiggler/base_acknowledger.rb +11 -0
  9. data/lib/jiggler/base_fetcher.rb +14 -0
  10. data/lib/jiggler/cleaner.rb +14 -5
  11. data/lib/jiggler/cli.rb +3 -2
  12. data/lib/jiggler/config.rb +48 -4
  13. data/lib/jiggler/launcher.rb +9 -3
  14. data/lib/jiggler/manager.rb +26 -2
  15. data/lib/jiggler/retrier.rb +8 -10
  16. data/lib/jiggler/scheduled/enqueuer.rb +1 -1
  17. data/lib/jiggler/scheduled/poller.rb +38 -26
  18. data/lib/jiggler/scheduled/requeuer.rb +57 -0
  19. data/lib/jiggler/server.rb +7 -0
  20. data/lib/jiggler/stats/collection.rb +3 -2
  21. data/lib/jiggler/stats/monitor.rb +2 -2
  22. data/lib/jiggler/summary.rb +8 -1
  23. data/lib/jiggler/support/helper.rb +17 -3
  24. data/lib/jiggler/version.rb +1 -1
  25. data/lib/jiggler/web.rb +0 -2
  26. data/lib/jiggler/worker.rb +21 -36
  27. data/spec/examples.txt +96 -79
  28. data/spec/fixtures/config/jiggler.yml +2 -2
  29. data/spec/jiggler/at_least_once/acknowledger_spec.rb +30 -0
  30. data/spec/jiggler/at_least_once/fetcher_spec.rb +69 -0
  31. data/spec/jiggler/at_most_once/fetcher_spec.rb +33 -0
  32. data/spec/jiggler/cleaner_spec.rb +11 -1
  33. data/spec/jiggler/cli_spec.rb +5 -7
  34. data/spec/jiggler/config_spec.rb +45 -3
  35. data/spec/jiggler/core_spec.rb +2 -0
  36. data/spec/jiggler/job_spec.rb +4 -4
  37. data/spec/jiggler/launcher_spec.rb +60 -54
  38. data/spec/jiggler/manager_spec.rb +50 -41
  39. data/spec/jiggler/retrier_spec.rb +3 -1
  40. data/spec/jiggler/scheduled/requeuer_spec.rb +57 -0
  41. data/spec/jiggler/stats/monitor_spec.rb +3 -2
  42. data/spec/jiggler/summary_spec.rb +19 -5
  43. data/spec/jiggler/worker_spec.rb +11 -15
  44. data/spec/spec_helper.rb +7 -0
  45. metadata +38 -9
@@ -22,6 +22,7 @@ module Jiggler
22
22
 
23
23
  @done = true
24
24
  manager.suspend
25
+ logger.debug('Manager suspended')
25
26
 
26
27
  poller.terminate if config[:poller_enabled]
27
28
  monitor.terminate
@@ -30,14 +31,19 @@ module Jiggler
30
31
  def stop
31
32
  suspend
32
33
  manager.terminate
34
+ logger.debug('Manager terminated')
33
35
  end
34
36
 
35
37
  private
36
38
 
37
39
  def uuid
38
- @uuid ||= begin
40
+ @uuid ||= SecureRandom.hex(6)
41
+ end
42
+
43
+ def identity
44
+ @identity ||= begin
39
45
  data_str = [
40
- SecureRandom.hex(6),
46
+ uuid,
41
47
  config[:concurrency],
42
48
  config[:timeout],
43
49
  config[:queues].join(','),
@@ -51,7 +57,7 @@ module Jiggler
51
57
  end
52
58
 
53
59
  def collection
54
- @collection ||= Stats::Collection.new(uuid)
60
+ @collection ||= Stats::Collection.new(uuid, identity)
55
61
  end
56
62
 
57
63
  def manager
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  # This class manages the workers lifecycle
4
6
  module Jiggler
5
7
  class Manager
@@ -7,16 +9,21 @@ module Jiggler
7
9
 
8
10
  def initialize(config, collection)
9
11
  @workers = Set.new
12
+
10
13
  @done = false
11
14
  @config = config
12
15
  @timeout = @config[:timeout]
13
16
  @collection = collection
17
+ init_acknowledger_and_fetcher
18
+
14
19
  @config[:concurrency].times do
15
20
  @workers << init_worker
16
21
  end
17
22
  end
18
23
 
19
24
  def start
25
+ @acknowledger.start
26
+ @fetcher.start
20
27
  @workers.each(&:run)
21
28
  end
22
29
 
@@ -24,17 +31,34 @@ module Jiggler
24
31
  return if @done
25
32
 
26
33
  @done = true
27
- @workers.each(&:suspend)
34
+ @fetcher.suspend
28
35
  end
29
36
 
30
37
  def terminate
31
38
  suspend
32
39
  schedule_shutdown
33
40
  wait_for_workers
41
+ wait_for_acknowledger
34
42
  end
35
43
 
36
44
  private
37
45
 
46
+ def init_acknowledger_and_fetcher
47
+ if @config.at_least_once?
48
+ @fetcher = AtLeastOnce::Fetcher.new(@config, @collection)
49
+ @acknowledger = AtLeastOnce::Acknowledger.new(@config)
50
+ else
51
+ @fetcher = AtMostOnce::Fetcher.new(@config, @collection)
52
+ @acknowledger = AtMostOnce::Acknowledger.new(@config)
53
+ end
54
+ end
55
+
56
+ def wait_for_acknowledger
57
+ logger.info('Waiting for the finished jobs to acknowledge...')
58
+ @acknowledger.terminate
59
+ @acknowledger.wait
60
+ end
61
+
38
62
  def wait_for_workers
39
63
  logger.info('Waiting for workers to finish...')
40
64
  @workers.each(&:wait)
@@ -53,7 +77,7 @@ module Jiggler
53
77
 
54
78
  def init_worker
55
79
  Jiggler::Worker.new(
56
- @config, @collection, &method(:process_worker_result)
80
+ @config, @collection, @acknowledger, @fetcher, &method(:process_worker_result)
57
81
  )
58
82
  end
59
83
 
@@ -30,16 +30,14 @@ module Jiggler
30
30
 
31
31
  log_error(
32
32
  err,
33
- {
34
- context: '\'Job raised exception\'',
35
- error_class: err.class.name,
36
- name: parsed_job['name'],
37
- queue: parsed_job['queue'],
38
- args: parsed_job['args'],
39
- attempt: parsed_job['attempt'],
40
- tid: @tid,
41
- jid: parsed_job['jid']
42
- }
33
+ context: '\'Job raised exception\'',
34
+ error_class: err.class.name,
35
+ name: parsed_job['name'],
36
+ queue: parsed_job['queue'],
37
+ args: parsed_job['args'],
38
+ attempt: parsed_job['attempt'],
39
+ tid: @tid,
40
+ jid: parsed_job['jid']
43
41
  )
44
42
  end
45
43
 
@@ -32,7 +32,7 @@ module Jiggler
32
32
  end
33
33
  end
34
34
  rescue => err
35
- log_error_short(err, { context: '\'Enqueuing jobs error\'', tid: @tid })
35
+ log_error_short(err, context: '\'Enqueuing jobs error\'', tid: @tid)
36
36
  end
37
37
  end
38
38
 
@@ -12,46 +12,67 @@ module Jiggler
12
12
  def initialize(config)
13
13
  @config = config
14
14
  @enqueuer = Jiggler::Scheduled::Enqueuer.new(config)
15
+ @requeuer = Jiggler::Scheduled::Requeuer.new(config)
15
16
  @done = false
16
17
  @job = nil
17
18
  @count_calls = 0
18
- @condition = Async::Condition.new
19
+ @requeuer_condition = Async::Condition.new
20
+ @enqueuer_condition = Async::Condition.new
19
21
  end
20
22
 
21
23
  def terminate
22
24
  @done = true
23
25
  @enqueuer.terminate
26
+ @requeuer.terminate
24
27
 
25
28
  Async do
26
- @condition.signal
29
+ @requeuer_condition.signal
30
+ @enqueuer_condition.signal
27
31
  @job&.wait
28
32
  end
29
33
  end
30
34
 
31
35
  def start
32
- @job = safe_async('Poller') do
36
+ @job = Async do
33
37
  @tid = tid
34
38
  initial_wait
35
- until @done
36
- enqueue
37
- wait unless @done
39
+ safe_async('Poller') do
40
+ until @done
41
+ enqueue
42
+ wait(@enqueuer_condition) unless @done
43
+ end
38
44
  end
45
+ safe_async('Requeuer') do
46
+ until @done
47
+ handle_stale_in_process_queues
48
+ logger.debug('Executing requeuer')
49
+ wait(@requeuer_condition, in_process_interval) unless @done
50
+ end
51
+ end if @config.at_least_once?
39
52
  end
40
53
  end
41
54
 
42
55
  def enqueue
43
- # logger.warn('Poller runs')
44
56
  @enqueuer.enqueue_jobs
45
57
  end
46
58
 
59
+ def handle_stale_in_process_queues
60
+ @requeuer.handle_stale
61
+ end
62
+
47
63
  private
48
64
 
49
- def wait
65
+ def wait(condition, interval = random_poll_interval)
50
66
  Async(transient: true) do
51
- sleep(random_poll_interval)
52
- @condition.signal
67
+ sleep(interval)
68
+ condition.signal
53
69
  end
54
- @condition.wait
70
+ condition.wait
71
+ end
72
+
73
+ def in_process_interval
74
+ # 60 to 120 seconds by default
75
+ [@config[:in_process_interval] * rand, 60].max
55
76
  end
56
77
 
57
78
  def random_poll_interval
@@ -66,31 +87,22 @@ module Jiggler
66
87
  end
67
88
 
68
89
  def fetch_count
69
- @config.with_sync_redis do |conn|
70
- conn.call('SCAN', '0', 'MATCH', @config.process_scan_key).last.size
71
- rescue => err
72
- log_error_short(err, { context: '\'Poller getting processes error\'', tid: @tid })
73
- 1
74
- end
90
+ scan_all(@config.process_scan_key).size
91
+ rescue => err
92
+ log_error_short(err, { context: '\'Poller getting processes error\'', tid: @tid })
93
+ 1
75
94
  end
76
95
 
77
96
  def process_count
78
97
  count = fetch_count
79
- count = 1 if count == 0
98
+ count = 1 if count.zero?
80
99
  count
81
100
  end
82
101
 
83
102
  # wait a random amount of time so in case of multiple processes
84
103
  # their pollers won't be synchronized
85
104
  def initial_wait
86
- total = INITIAL_WAIT + (12 * rand)
87
-
88
- # in case of an early exit skip the initial wait
89
- Async(transient: true) do
90
- sleep(total)
91
- @condition.signal
92
- end
93
- @condition.wait
105
+ wait(@enqueuer_condition, INITIAL_WAIT + (12 * rand))
94
106
  end
95
107
  end
96
108
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiggler
4
+ module Scheduled
5
+ class Requeuer
6
+ include Support::Helper
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ @done = false
11
+ @tid = tid
12
+ end
13
+
14
+ def handle_stale
15
+ requeue_data.each do |(rqueue, queue, _)|
16
+ @config.with_async_redis do |conn|
17
+ loop do
18
+ return if @done
19
+ # reprocessing is prioritised so we push at the right side
20
+ break if conn.call('LMOVE', rqueue, queue, 'RIGHT', 'RIGHT').nil?
21
+ end
22
+ end
23
+ end
24
+ rescue => err
25
+ log_error_short(err, context: '\'Requeuing jobs error\'', tid: @tid)
26
+ end
27
+
28
+ def terminate
29
+ @done = true
30
+ end
31
+
32
+ private
33
+
34
+ def requeue_data
35
+ grouped_queues = in_progress_queues.map do |queue|
36
+ [queue, *queue.split(":#{AtLeastOnce::Fetcher::RESERVE_QUEUE_SUFFIX}:")]
37
+ end.group_by(&:last)
38
+ # returns [[queue_in_progress, queue, uuid]] for non-running processes
39
+ grouped_queues.except(*running_processes_uuid).values.flatten(1)
40
+ end
41
+
42
+ def running_processes_uuid
43
+ scan_all(@config.process_scan_key).map do |process|
44
+ process.split(':')[2]
45
+ end
46
+ end
47
+
48
+ def in_progress_queues
49
+ scan_all(in_progress_wildcard)
50
+ end
51
+
52
+ def in_progress_wildcard
53
+ "*#{AtLeastOnce::Fetcher::RESERVE_QUEUE_SUFFIX}*"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -4,6 +4,7 @@
4
4
  require 'jiggler/support/helper'
5
5
  require 'jiggler/scheduled/enqueuer'
6
6
  require 'jiggler/scheduled/poller'
7
+ require 'jiggler/scheduled/requeuer'
7
8
  require 'jiggler/stats/collection'
8
9
  require 'jiggler/stats/monitor'
9
10
  require 'jiggler/errors'
@@ -11,4 +12,10 @@ require 'jiggler/retrier'
11
12
  require 'jiggler/launcher'
12
13
  require 'jiggler/manager'
13
14
  require 'jiggler/worker'
15
+ require 'jiggler/base_acknowledger'
16
+ require 'jiggler/base_fetcher'
17
+ require 'jiggler/at_least_once/acknowledger'
18
+ require 'jiggler/at_least_once/fetcher'
19
+ require 'jiggler/at_most_once/acknowledger'
20
+ require 'jiggler/at_most_once/fetcher'
14
21
  require 'jiggler/cli'
@@ -3,10 +3,11 @@
3
3
  module Jiggler
4
4
  module Stats
5
5
  class Collection
6
- attr_reader :uuid, :data
6
+ attr_reader :uuid, :identity, :data
7
7
 
8
- def initialize(uuid)
8
+ def initialize(uuid, identity)
9
9
  @uuid = uuid
10
+ @identity = identity
10
11
  @data = {
11
12
  processed: 0,
12
13
  failures: 0,
@@ -52,7 +52,7 @@ module Jiggler
52
52
 
53
53
  config.with_async_redis do |conn|
54
54
  conn.pipelined do |pipeline|
55
- pipeline.call('SET', collection.uuid, process_data, ex: exp)
55
+ pipeline.call('SET', collection.identity, process_data, ex: exp)
56
56
  pipeline.call('INCRBY', config.processed_counter, processed_jobs)
57
57
  pipeline.call('INCRBY', config.failures_counter, failed_jobs)
58
58
  end
@@ -96,7 +96,7 @@ module Jiggler
96
96
  private
97
97
 
98
98
  def cleanup_with(conn)
99
- conn.call('DEL', collection.uuid)
99
+ conn.call('DEL', collection.identity)
100
100
  end
101
101
  end
102
102
  end
@@ -84,7 +84,14 @@ module Jiggler
84
84
  end
85
85
 
86
86
  def fetch_and_format_queues(conn)
87
- lists = conn.call('SCAN', '0', 'MATCH', config.queue_scan_key).last
87
+ cursor = '0'
88
+ lists = []
89
+
90
+ loop do
91
+ cursor, current_lists = conn.call('SCAN', cursor, 'MATCH', config.queue_scan_key)
92
+ lists += current_lists.select { |list| !list.include?(':in_progress:') } # TODO: replace with constant
93
+ break if cursor == '0'
94
+ end
88
95
  lists_data = {}
89
96
 
90
97
  collected_data = conn.pipelined do |pipeline|
@@ -7,17 +7,17 @@ module Jiggler
7
7
  Async do
8
8
  yield
9
9
  rescue Exception => ex
10
- log_error(ex, { context: name, tid: tid })
10
+ log_error(ex, context: name, tid: tid)
11
11
  end
12
12
  end
13
13
 
14
- def log_error(ex, ctx = {})
14
+ def log_error(ex, **ctx)
15
15
  err_context = ctx.compact.map { |k, v| "#{k}=#{v}" }.join(' ')
16
16
  logger.error("error_message='#{ex.message}' #{err_context}")
17
17
  logger.error(ex.backtrace.first(12).join("\n")) unless ex.backtrace.nil?
18
18
  end
19
19
 
20
- def log_error_short(err, ctx = {})
20
+ def log_error_short(err, **ctx)
21
21
  err_context = ctx.compact.map { |k, v| "#{k}=#{v}" }.join(' ')
22
22
  logger.error("error_message='#{err.message}' #{err_context}")
23
23
  end
@@ -25,6 +25,20 @@ module Jiggler
25
25
  def logger
26
26
  @config.logger
27
27
  end
28
+
29
+ def scan_all(mask)
30
+ @config.with_sync_redis do |conn|
31
+ start = '0'
32
+ all_keys = []
33
+ loop do
34
+ start, keys = conn.call('SCAN', start, 'MATCH', mask)
35
+ break if keys.empty?
36
+ all_keys += keys
37
+ break if start == '0'
38
+ end
39
+ all_keys
40
+ end
41
+ end
28
42
 
29
43
  def tid
30
44
  return unless Async::Task.current?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jiggler
4
- VERSION = '0.1.0.rc4'
4
+ VERSION = '0.1.0.rc6'
5
5
  end
data/lib/jiggler/web.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'erb'
4
-
5
3
  module Jiggler
6
4
  class Web
7
5
  WEB_PATH = File.expand_path("#{File.dirname(__FILE__)}/web")
@@ -3,18 +3,19 @@
3
3
  module Jiggler
4
4
  class Worker
5
5
  include Support::Helper
6
- TIMEOUT = 2 # timeout for brpop
7
6
 
8
7
  CurrentJob = Struct.new(:queue, :args, keyword_init: true)
9
8
 
10
- attr_reader :current_job, :config, :done, :collection
9
+ attr_reader :current_job, :config, :collection, :acknowledger, :fetcher
11
10
 
12
- def initialize(config, collection, &callback)
11
+ def initialize(config, collection, acknowledger, fetcher, &callback)
13
12
  @done = false
14
13
  @current_job = nil
15
14
  @callback = callback
16
15
  @config = config
17
16
  @collection = collection
17
+ @acknowledger = acknowledger
18
+ @fetcher = fetcher
18
19
  end
19
20
 
20
21
  def run
@@ -43,14 +44,9 @@ module Jiggler
43
44
  end
44
45
 
45
46
  def terminate
46
- @done = true
47
47
  @runner&.stop
48
48
  end
49
49
 
50
- def suspend
51
- @done = true
52
- end
53
-
54
50
  def wait
55
51
  @runner&.wait
56
52
  end
@@ -59,21 +55,20 @@ module Jiggler
59
55
 
60
56
  def process_job
61
57
  @current_job = fetch_one
62
- return if current_job.nil? # timed out brpop or done
58
+ return if current_job.nil? # done
59
+
63
60
  execute_job
64
61
  @current_job = nil
65
62
  end
66
63
 
67
64
  def fetch_one
68
- queue, args = config.with_sync_redis { |conn| conn.blocking_call(false, 'BRPOP', *queues, TIMEOUT) }
69
- return nil unless queue
70
-
71
- if @done
72
- requeue(queue, args)
73
- nil
74
- else
75
- CurrentJob.new(queue: queue, args: args)
65
+ job = fetcher.fetch
66
+ if job == :done
67
+ logger.debug('Suspending the worker')
68
+ @done = true
69
+ return
76
70
  end
71
+ job
77
72
  rescue Async::Stop => err
78
73
  raise err
79
74
  rescue => err
@@ -86,17 +81,16 @@ module Jiggler
86
81
  def execute_job
87
82
  parsed_args = Oj.load(current_job.args, mode: :compat)
88
83
  execute(parsed_args, current_job.queue)
84
+ acknowledger.ack(current_job)
89
85
  rescue Async::Stop => err
90
86
  raise err
91
87
  rescue UnknownJobError => err
92
88
  collection.incr_failures
93
89
  log_error_short(
94
90
  err,
95
- {
96
- error_class: err.class.name,
97
- job: parsed_args,
98
- tid: @tid
99
- }
91
+ error_class: err.class.name,
92
+ job: parsed_args,
93
+ tid: @tid
100
94
  )
101
95
  rescue JSON::ParserError => err
102
96
  collection.incr_failures
@@ -104,13 +98,10 @@ module Jiggler
104
98
  rescue Exception => ex
105
99
  log_error(
106
100
  ex,
107
- {
108
- context: '\'Internal exception\'',
109
- tid: @tid,
110
- jid: parsed_args['jid']
111
- }
101
+ context: '\'Internal exception\'',
102
+ tid: @tid,
103
+ jid: parsed_args['jid']
112
104
  )
113
- # raise ex
114
105
  end
115
106
 
116
107
  def execute(parsed_job, queue)
@@ -139,10 +130,8 @@ module Jiggler
139
130
  def handle_fetch_error(ex)
140
131
  log_error_short(
141
132
  ex,
142
- {
143
- context: '\'Fetch error\'',
144
- tid: @tid
145
- }
133
+ context: '\'Fetch error\'',
134
+ tid: @tid
146
135
  )
147
136
  sleep(TIMEOUT + rand(5) * config[:concurrency]) # sleep for a while before retrying
148
137
  end
@@ -159,10 +148,6 @@ module Jiggler
159
148
  collection.data[:current_jobs].delete(@tid)
160
149
  end
161
150
 
162
- def queues
163
- @queues ||= config.prefixed_queues
164
- end
165
-
166
151
  def constantize(str)
167
152
  return Object.const_get(str) unless str.include?('::')
168
153