jiggler 0.1.0.rc4 → 0.1.0.rc5

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +37 -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 +18 -3
@@ -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.rc5'
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