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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e6545c58aa0c8c04bbeb879c82e1a954eb0e13bb92971a148aaaaeaf17d1b0c
4
- data.tar.gz: fa12547490937572301bdee5024988c233d363f574cbf92467e3bfa9fe16b8ff
3
+ metadata.gz: 215183a38b3a7a32e692478a7aa6126fb0f78f36891708ab95e657c85e0a0c6f
4
+ data.tar.gz: bfc6e357e0917dd2175aa5bb2454a3243685f383199f69c4c58354502a460bfd
5
5
  SHA512:
6
- metadata.gz: 464c1b31e6d0b200fdaad5c6a1da857a0a9ecabc7e239202d483178a15293d96f27b866d86287f9c91576294f794dd799393eaf11694858a0d664b3b96138dfb
7
- data.tar.gz: 5e407cea521fc909160dbc637e9716f09026cc98c98e0397c129cc62dd891c20d6b582947c3944dadd2ff6165d561fd34feffc4988af7202bcc8410c87beb07c
6
+ metadata.gz: 006d0b3cc279afcf64f8abc2f75e46b4ff62a2668ee34a2fdb61bd8f4cd455b92593073697e27583d840a46b676cf90523dd470fe8dd4467c93e7f922d7a56c9
7
+ data.tar.gz: 49c1bdb550fd4721980130f7f63ffbc0b628d3a373123b52ac248de99b747cd67d2b7addf8bb7175b33b862f0e51a6b06031f62c70bf3dd7e76b2d53f13675c1
data/README.md CHANGED
@@ -7,7 +7,7 @@ Jiggler is a [Sidekiq](https://github.com/mperham/sidekiq)-inspired background j
7
7
 
8
8
  *Jiggler is based on Sidekiq implementation, and re-uses most of its concepts and ideas.*
9
9
 
10
- **NOTE**: Altrough some performance results may look interesting, it's absolutly not recommended to switch to it from well-tested stable solutions. Jiggler has a meager set of features and a very basic monitoring. It's a small indie gem made purely for fun and to gain some hand-on experience with async and fibers. It isn't tested with production projects and might have not-yet-discovered issues. \
10
+ **NOTE**: Jiggler is a small gem made purely for fun and to gain some hand-on experience with async and fibers. It isn't tested with production projects and might have not-yet-discovered issues. Use at your own risk. \
11
11
  However, it's good to play around and/or to try it in the name of science.
12
12
 
13
13
  ### Installation
@@ -17,6 +17,11 @@ Install the gem:
17
17
  gem install jiggler
18
18
  ```
19
19
 
20
+ Use `--pre` for release candidates:
21
+ ```
22
+ gem install jiggler --pre
23
+ ```
24
+
20
25
  Start Jiggler server as a separate process with bin command:
21
26
  ```
22
27
  jiggler -r <FILE_PATH>
@@ -28,204 +33,7 @@ Run `jiggler --help` to see the list of command line arguments.
28
33
 
29
34
  ### Performance
30
35
 
31
- The tests were run on local (Ubuntu 22.04, Intel(R) Core(TM) i7 6700HQ 2.60GHz). \
32
- On the other configurations the results may differ significantly, f.e. with Apple M1 Max chip it treats some IO operations as blocking and shows a poor performance ಠ_ಥ.
33
-
34
- Ruby `3.2.0` \
35
- Redis `7.0.7` \
36
- Poller interval `5s` \
37
- Monitoring interval `10s` \
38
- Logging level `WARN`
39
-
40
- #### Noop task measures
41
-
42
- ```ruby
43
- def perform
44
- # just an empty job doing nothing
45
- end
46
- ```
47
-
48
- The parent process enqueues the jobs, starts the monitoring, and then forks the child job-processor-process. Thus, `RSS` value is affected by the number of jobs uploaded in the parent process. See `bin/jigglerload` to see the load test structure and measuring. \
49
- 1_000_000 jobs were enqueued in 100k batches x10.
50
-
51
- | Job Processor | Concurrency | Jobs | Time | Start RSS | Finish RSS |
52
- |------------------|-------------|-----------|-----------|------------|---------------|
53
- | Sidekiq 7.0.2 | 5 | 100_000 | 20.01 sec | 132_080 kb | 103_168 kb (GC) |
54
- | Jiggler 0.1.0 | 5 | 100_000 | 14.25 sec | 87_532 kb | 91_464 kb |
55
- | - | | | | | |
56
- | Sidekiq 7.0.2 | 10 | 100_000 | 20.49 sec | 132_164 kb | 125_768 kb (GC) |
57
- | Jiggler 0.1.0 | 10 | 100_000 | 13.25 sec | 87_688 kb | 91_625 kb |
58
- | - | | | | | |
59
- | Sidekiq 7.0.2 | 5 | 1_000_000 | 186.90 sec | 159_712 kb | 186_224 kb |
60
- | Jiggler 0.1.0 | 5 | 1_000_000 | 123.13 sec | 113_212 kb | 116_336 kb |
61
- | - | | | | | |
62
- | Sidekiq 7.0.2 | 10 | 1_000_000 | 186.94 sec | 159_000 kb | 192_780 kb |
63
- | Jiggler 0.1.0 | 10 | 1_000_000 | 115.56 sec | 113_656 kb | 116_896 kb |
64
-
65
-
66
- #### IO tests
67
-
68
- The idea of the next tests is to simulate jobs with different kinds of IO tasks. \
69
- Ruby 3 has introduced fiber scheduler interface, which allows to implement hooks related to IO/blocking operations.\
70
- The context switching won't work well in case IO is performed by C-extentions which are not aware of Ruby scheduler.
71
-
72
- ##### NET/HTTP requests
73
-
74
- Spin-up a local sinatra app to exclude network issues while testing HTTP requests (it uses `falcon` web server).
75
-
76
- ```ruby
77
- require "sinatra"
78
-
79
- class MyApp < Sinatra::Base
80
- get "/hello" do
81
- sleep(0.2)
82
- "Hello World!"
83
- end
84
- end
85
- ```
86
-
87
- Then, the code which is going to be performed within the workers should make a `net/http` request to the local endpoint.
88
-
89
- ```ruby
90
- # a single job takes ~0.21s to perform
91
- def perform
92
- uri = URI("http://127.0.0.1:9292/hello")
93
- res = Net::HTTP.get_response(uri)
94
- puts "Request Error!!!" unless res.is_a?(Net::HTTPSuccess)
95
- end
96
- ```
97
-
98
- It's not recommended to run sidekiq with high concurrency values, setting it for the sake of test. \
99
- The time difference for these samples is small-ish, however the memory consumption is less with the fibers. \
100
- Since fibers have relatively small memory foot-print and context switching is also relatively cheap, it's possible to set concurrency to higher values within Jiggler without too much trade-offs.
101
-
102
- | Job Processor | Concurrency | Jobs | Time to complete | Start RSS | Finish RSS | %CPU |
103
- |------------------|-------------|-------|-------------------|-----------|------------|------|
104
- | Sidekiq 7.0.2 | 5 | 1_000 | 43.74 sec | 30_444 kb | 45_124 kb | 5.9 |
105
- | Jiggler 0.1.0 | 5 | 1_000 | 43.65 sec | 33_476 kb | 34_144 kb | 2.9 |
106
- | - | | | | | | |
107
- | Sidekiq 7.0.2 | 10 | 1_000 | 23.05 sec | 30_604 kb | 50_292 kb | 10.93 |
108
- | Jiggler 0.1.0 | 10 | 1_000 | 22.86 sec | 32_416 kb | 34_128 kb | 5.69 |
109
- | - | | | | | | |
110
- | Sidekiq 7.0.2 | 15 | 1_000 | 16.17 sec | 30_636 kb | 55_144 kb | 16.47 |
111
- | Jiggler 0.1.0 | 15 | 1_000 | 15.87 sec | 33_328 kb | 34_548 kb | 8.25 |
112
-
113
- **NOTE**: Jiggler has more dependencies, so with small load `start RSS` takes more space.
114
-
115
- ##### PostgreSQL connection/queries
116
-
117
- `pg` gem supports Ruby's `Fiber.scheduler` starting from 1.3.0 version. Make sure yours DB-adapter supports it.
118
-
119
- ```ruby
120
- ### global namespace
121
- require "pg"
122
-
123
- $pg_pool = ConnectionPool.new(size: CONCURRENCY) do
124
- PG.connect({ dbname: "test", password: "test", user: "test" })
125
- end
126
-
127
- ### worker context
128
- # a single job takes ~0.102s to perform
129
- def perform
130
- $pg_pool.with do |conn|
131
- conn.exec("SELECT pg_sleep(0.1)")
132
- end
133
- end
134
- ```
135
-
136
- | Job Processor | Concurrency | Jobs | Time | Start RSS | Finish RSS | %CPU |
137
- |------------------|-------------|-------|-----------|-----------|------------|------|
138
- | Sidekiq 7.0.2 | 5 | 1_000 | 23.44 sec | 31_436 kb | 48_856 kb | 7.56 |
139
- | Jiggler 0.1.0 | 5 | 1_000 | 23.20 sec | 35_312 kb | 38_592 kb | 2.91 |
140
- | - | | | | | | |
141
- | Sidekiq 7.0.2 | 10 | 1_000 | 13.15 sec | 31_272 kb | 52_808 kb | 13.76 |
142
- | Jiggler 0.1.0 | 10 | 1_000 | 12.65 sec | 35_296 kb | 38_784 kb | 6.11 |
143
- | - | | | | | | |
144
- | Sidekiq 7.0.2 | 15 | 1_000 | 9.63 sec | 31_016 kb | 59_868 kb | 20.32 |
145
- | Jiggler 0.1.0 | 15 | 1_000 | 9.17 sec | 35_188 kb | 38_948 kb | 9.26 |
146
-
147
- ##### File IO
148
-
149
- ```ruby
150
- def perform(file_name, id)
151
- File.open(file_name, "a") { |f| f.write("#{id}\n") }
152
- end
153
- ```
154
-
155
- | Job Processor | Concurrency | Jobs | Time | Start RSS | Finish RSS | %CPU |
156
- |------------------|-------------|--------|-----------|--------------|------------|-------|
157
- | Sidekiq 7.0.2 | 5 | 30_000 | 11.94 sec | 61_944 kb | 71_948 kb | 94.34 |
158
- | Jiggler 0.1.0 | 5 | 30_000 | 7.87 sec | 50_140 kb | 51_272 kb | 61.7 |
159
- | - | | | | | | |
160
- | Sidekiq 7.0.2 | 10 | 30_000 | 11.6 sec | 62_020 kb | 78_952 kb | 94.44 |
161
- | Jiggler 0.1.0 | 10 | 30_000 | 7.17 sec | 50_060 kb | 51_464 kb | 69.25 |
162
- | - | | | | | | |
163
- | Sidekiq 7.0.2 | 15 | 30_000 | 11.24 sec | 62_016 kb | 83_808 kb | 94.16 |
164
- | Jiggler 0.1.0 | 15 | 30_000 | 7.02 sec | 49_988 kb | 51_428 kb | 70.3 |
165
-
166
-
167
- Jiggler is effective only for tasks with a lot of IO. You must test the concurrency setting with your jobs to find out what configuration works best for your payload.
168
-
169
- #### Simulate CPU-only job
170
-
171
- With CPU-heavy jobs Jiggler has poor performance. Just to make sure it's generally able to work with CPU-only payloads:
172
-
173
- ```ruby
174
- def fib(n)
175
- if n <= 1
176
- 1
177
- else
178
- (fib(n-1) + fib(n-2))
179
- end
180
- end
181
-
182
- # a single job takes ~0.035s to perform
183
- def perform(_idx)
184
- fib(25)
185
- end
186
- ```
187
-
188
- | Job Processor | Concurrency | Jobs | Time | Start RSS | Finish RSS |
189
- |------------------|-------------|------|----------|------------|------------|
190
- | Sidekiq 7.0.2 | 5 | 100 | 5.81 sec | 27_792 kb | 42_464 kb |
191
- | Jiggler 0.1.0 | 5 | 100 | 5.29 sec | 31_304 kb | 32_320 kb |
192
- | - | | | | | |
193
- | Sidekiq 7.0.2 | 10 | 100 | 5.63 sec | 28_044 kb | 47_640 kb |
194
- | Jiggler 0.1.0 | 10 | 100 | 5.43 sec | 32_316 kb | 32_548 kb |
195
-
196
- #### IO Event selector
197
-
198
- `IO_EVENT_SELECTOR` is an env variable which allows to specify the event selector used by the Ruby scheduler. \
199
- On default it uses `Epoll` (`IO_EVENT_SELECTOR=EPoll`). \
200
- Another available option is `URing` (`IO_EVENT_SELECTOR=URing`). Underneath it uses `io_uring` library. It is a Linux kernel library that provides a high-performance interface for asynchronous I/O operations. It was introduced in Linux kernel version 5.1 and aims to address some of the limitations and scalability issues of the existing AIO (Asynchronous I/O) interface.
201
- In the future it might bring a lot of performance boost into Ruby fibers world (once `async` project fully adopts it), but at the moment in the most cases its performance is similar to `EPoll`, yet it could give some boost with File IO.
202
-
203
- #### Socketry stack
204
-
205
- The gem allows to use libs from `socketry` stack (https://github.com/socketry) within workers. \
206
- F.e. when making HTTP requests using `async/http/internet` to the Sinatra app described above:
207
-
208
- ```ruby
209
- ### global namespace
210
- require "async/http/internet"
211
- $internet = Async::HTTP::Internet.new
212
-
213
- ### worker context
214
- def perform
215
- uri = "https://127.0.0.1/hello"
216
- res = $internet.get(uri)
217
- res.finish
218
- puts "Request Error!!!" unless res.status == 200
219
- end
220
- ```
221
-
222
- | Job Processor | Concurrency | Jobs | Time | Start RSS | Finish RSS | %CPU |
223
- |------------------|-------------|-------|-----------|-----------|------------|------|
224
- | Jiggler 0.1.0 | 5 | 1_000 | 43.23 sec | 34_340 kb | 38_488 kb | 1.51 |
225
- | - | | | | | | |
226
- | Jiggler 0.1.0 | 10 | 1_000 | 22.67 sec | 34_552 kb | 38_600 kb | 2.75 |
227
- | - | | | | | | |
228
- | Jiggler 0.1.0 | 15 | 1_000 | 15.88 sec | 34_332 kb | 38_544 kb | 4.06 |
36
+ [Jiggler 0.1.0rc4 performance results](/docs/perf_results_0.1.0rc4.md)
229
37
 
230
38
  ### Getting Started
231
39
 
@@ -255,9 +63,34 @@ Jiggler.configure do |config|
255
63
  config[:redis_url] = ENV["REDIS_URL"] # On default fetches the value from ENV["REDIS_URL"]
256
64
  config[:queues] = ["shippers"] # An array of queue names the server is going to listen to. On default uses ["default"]
257
65
  config[:config_file] = "./jiggler.yml" # .yml file with Jiggler settings
66
+ config[:mode] = :at_most_once # at_most_once and at_least_once modes supported. Defaults to :at_least_once
67
+ end
68
+ ```
69
+
70
+ `at_least_once` mode grants reliability for the regular enqueued jobs which are going to be executed by workers. The scheduled jobs (the ones planned to be executed at a specific time, or which failed and going to be retried) still support only `at_most_once` strategy. The support for them is going to be added in the upcoming versions.
71
+
72
+ On default all queues have the same priority (equals to 0). Higher number means higher prio. \
73
+ It's possible to specify custom priorities as follows:
74
+
75
+ ```ruby
76
+ Jiggler.configure do |config|
77
+ config[:queues] = [["shippers", 0], ["shipments", 1], ["delivery", 2]]
258
78
  end
259
79
  ```
260
80
 
81
+ #### IO Event selector
82
+
83
+ `IO_EVENT_SELECTOR` is an env variable which allows to specify the event selector used by the Ruby scheduler. \
84
+ On default it uses `Epoll` (`IO_EVENT_SELECTOR=EPoll`). \
85
+ Another available option is `URing` (`IO_EVENT_SELECTOR=URing`). Underneath it uses `io_uring` library. It is a Linux kernel library that provides a high-performance interface for asynchronous I/O operations. It was introduced in Linux kernel version 5.1 and aims to address some of the limitations and scalability issues of the existing AIO (Asynchronous I/O) interface.
86
+ In the future it might bring a lot of performance boost into Ruby fibers world (once `async` project fully adopts it), but at the moment in the most cases its performance is similar to `EPoll`, yet it could give some boost with File IO.
87
+
88
+ #### Socketry stack
89
+
90
+ The gem allows to use libs from `socketry` stack (https://github.com/socketry) within workers.
91
+
92
+ #### Core concepts
93
+
261
94
  Internally Jiggler server consists of 3 parts: `Manager`, `Poller`, `Monitor`. \
262
95
  `Manager` is responsible for workers. \
263
96
  `Poller` fetches data for retries and scheduled jobs. \
@@ -377,7 +210,7 @@ Jiggler.configure_client do |config|
377
210
  config[:client_redis_pool] = my_async_redis_pool
378
211
  end
379
212
 
380
- # or use build-in async pool with
213
+ # or use built-in async pool with
381
214
  require "async/pool"
382
215
 
383
216
  Jiggler.configure_client do |config|
@@ -416,3 +249,7 @@ docker-compose run --rm web -- bundle exec rspec
416
249
  ```
417
250
 
418
251
  To run the load tests modify the `docker-compose.yml` to point to `bin/jigglerload`
252
+
253
+ ### Contributing
254
+
255
+ Fork & Pull Request.
data/bin/jiggler CHANGED
@@ -9,7 +9,7 @@ begin
9
9
  cli.parse_and_init
10
10
 
11
11
  cli.config.logger.info("Jiggler is starting in #{Jiggler.config[:environment].upcase} ✯⸜(*❛‿❛)⸝✯")
12
- cli.config.logger.info("Jiggler version=#{Jiggler::VERSION} pid=#{Process.pid} concurrency=#{cli.config[:concurrency]} queues=#{cli.config[:queues].join(',')}")
12
+ cli.config.logger.info("Jiggler version=#{Jiggler::VERSION} pid=#{Process.pid} concurrency=#{cli.config[:concurrency]} queues=#{cli.config.sorted_queues.join(',')}")
13
13
 
14
14
  cli.start
15
15
  rescue => e
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiggler
4
+ module AtLeastOnce
5
+ class Acknowledger < BaseAcknowledger
6
+ def initialize(config)
7
+ super
8
+ @runners = []
9
+ @queue = Queue.new
10
+ end
11
+
12
+ def ack(job)
13
+ @queue.push(job)
14
+ end
15
+
16
+ def start
17
+ @config[:concurrency].times do
18
+ @runners << safe_async('Acknowledger') do
19
+ while (job = @queue.pop) != nil
20
+ begin
21
+ job.ack
22
+ rescue StandardError => err
23
+ log_error(err, context: '\'Could not acknowledge a job\'', job: job)
24
+ end
25
+ end
26
+ logger.debug('Acknowledger exits')
27
+ rescue Async::Stop
28
+ logger.debug('Acknowledger received stop signal')
29
+ end
30
+ end
31
+ end
32
+
33
+ def wait
34
+ @runners.each(&:wait)
35
+ end
36
+
37
+ def terminate
38
+ logger.debug('Suspending the acknowledger')
39
+ @queue.close
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fc'
4
+
5
+ module Jiggler
6
+ module AtLeastOnce
7
+ class Fetcher < BaseFetcher
8
+ TIMEOUT = 2.0 # 2 seconds of waiting for brpoplpush
9
+ RESERVE_QUEUE_SUFFIX = 'in_progress'
10
+
11
+ attr_reader :producers
12
+
13
+ def initialize(config, collection)
14
+ super
15
+ @tasks_queue = FastContainers::PriorityQueue.new(:min)
16
+ @condition = Async::Notification.new
17
+ @consumers_queue = Queue.new
18
+ end
19
+
20
+ CurrentJob = Struct.new(:queue, :args, :reserve_queue, :config, keyword_init: true) do
21
+ def ack
22
+ config.with_sync_redis do |conn|
23
+ conn.call('LREM', reserve_queue, 1, args)
24
+ end
25
+ end
26
+ end
27
+
28
+ def start
29
+ config.sorted_queues_data.each do |queue, data|
30
+ config[:fetchers_concurrency].times do
31
+ safe_async("'Fetcher for #{queue}'") do
32
+ list = data[:list]
33
+ rlist = in_process_queue(list)
34
+ loop do
35
+ if @consumers_queue.num_waiting.zero? && !@done
36
+ @condition.wait # supposed to block here until consumers notify
37
+ end
38
+ break if @done
39
+
40
+ args = config.with_sync_redis do |conn|
41
+ conn.blocking_call(false, 'BRPOPLPUSH', list, rlist, TIMEOUT)
42
+ end
43
+ # no requeue logic rn as we expect monitor to handle
44
+ # in-process-tasks list for this process
45
+ break if @done
46
+
47
+ next if args.nil?
48
+
49
+ @tasks_queue.push(job(list, args, rlist), data[:priority])
50
+ @consumers_queue.push('') # to unblock any waiting consumer
51
+ end
52
+ logger.debug("Fetcher for #{queue} stopped")
53
+ rescue Async::Stop
54
+ logger.debug("Fetcher for #{queue} received stop signal")
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def fetch
61
+ @condition.signal if signal?
62
+ return :done if @consumers_queue.pop.nil?
63
+
64
+ @tasks_queue.pop
65
+ end
66
+
67
+ def suspend
68
+ logger.debug("Suspending the fetcher")
69
+ @done = true
70
+ @condition.signal
71
+ @consumers_queue.close # unblocks awaiting consumers
72
+ end
73
+
74
+ private
75
+
76
+ def queue_signaling_limit
77
+ @queue_signaling_limit ||= [config[:concurrency] / 2, 1].max
78
+ end
79
+
80
+ def signal?
81
+ @consumers_queue.size < config[:concurrency]
82
+ end
83
+
84
+ def in_process_queue(queue)
85
+ "#{queue}:#{RESERVE_QUEUE_SUFFIX}:#{collection.uuid}"
86
+ end
87
+
88
+ def job(queue, args, reserve_queue)
89
+ CurrentJob.new(queue:, args:, reserve_queue:, config:)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,21 @@
1
+ module Jiggler
2
+ module AtMostOnce
3
+ class Acknowledger < BaseAcknowledger
4
+ def ack(job)
5
+ # noop
6
+ end
7
+
8
+ def start
9
+ # noop
10
+ end
11
+
12
+ def wait
13
+ # noop
14
+ end
15
+
16
+ def terminate
17
+ # noop
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiggler
4
+ module AtMostOnce
5
+ class Fetcher < BaseFetcher
6
+ TIMEOUT = 2.0 # 2 seconds of waiting for brpop
7
+
8
+ CurrentJob = Struct.new(:queue, :args)
9
+
10
+ def start
11
+ # noop, we just block directly during the fetch
12
+ end
13
+
14
+ def fetch
15
+ return :done if @done
16
+
17
+ q, args = config.with_sync_redis do |conn|
18
+ conn.blocking_call(false, 'BRPOP', *config.sorted_lists, TIMEOUT)
19
+ end
20
+
21
+ if @done
22
+ requeue(q, args) unless q.nil?
23
+ return :done
24
+ end
25
+
26
+ job(q, args) unless q.nil?
27
+ end
28
+
29
+ def suspend
30
+ @done = true
31
+ end
32
+
33
+ private
34
+
35
+ def requeue(queue, args)
36
+ config.with_sync_redis do |conn|
37
+ conn.call('RPUSH', queue, args)
38
+ end
39
+ end
40
+
41
+ def job(queue, args)
42
+ CurrentJob.new(queue, args)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiggler
4
+ class BaseAcknowledger
5
+ include Support::Helper
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiggler
4
+ class BaseFetcher
5
+ include Support::Helper
6
+
7
+ attr_reader :config, :collection
8
+
9
+ def initialize(config, collection)
10
+ @config = config
11
+ @collection = collection
12
+ end
13
+ end
14
+ end
@@ -108,15 +108,24 @@ module Jiggler
108
108
  def prn_dead_set(conn)
109
109
  conn.call('DEL', config.dead_set)
110
110
  end
111
-
111
+
112
+ # it deletes in_progress queues as well
112
113
  def prn_all_queues(conn)
113
- queues = conn.call('SCAN', '0', 'MATCH', config.queue_scan_key).last
114
- conn.call('DEL', *queues) unless queues.empty?
114
+ cursor = '0'
115
+ loop do
116
+ cursor, queues = conn.call('SCAN', cursor, 'MATCH', config.queue_scan_key)
117
+ conn.call('DEL', *queues) unless queues.empty?
118
+ break if cursor == '0'
119
+ end
115
120
  end
116
121
 
117
122
  def prn_all_processes(conn)
118
- processes = conn.call('SCAN', '0', 'MATCH', config.process_scan_key).last
119
- conn.call('DEL', *processes) unless processes.empty?
123
+ cursor = '0'
124
+ loop do
125
+ cursor, processes = conn.call('SCAN', cursor, 'MATCH', config.process_scan_key)
126
+ conn.call('DEL', *processes) unless processes.empty?
127
+ break if cursor == '0'
128
+ end
120
129
  end
121
130
 
122
131
  def prn_failures_counter(conn)
data/lib/jiggler/cli.rb CHANGED
@@ -3,10 +3,11 @@
3
3
  require 'singleton'
4
4
  require 'optparse'
5
5
  require 'yaml'
6
- require 'set'
6
+ require 'erb'
7
7
  require 'async'
8
8
  require 'async/io/trap'
9
9
  require 'async/pool'
10
+ require 'debug'
10
11
 
11
12
  module Jiggler
12
13
  class CLI
@@ -45,7 +46,7 @@ module Jiggler
45
46
  def start
46
47
  return unless ping_redis
47
48
  @cond = Async::Condition.new
48
- Async do
49
+ Async do |task|
49
50
  setup_signal_handlers
50
51
  patch_scheduler
51
52
  @launcher = Launcher.new(config)
@@ -25,11 +25,15 @@ module Jiggler
25
25
  stats_interval: 10,
26
26
  poller_enabled: true,
27
27
  poll_interval: 5,
28
+ # used in scheduled/requeuer
29
+ in_process_interval: 120,
28
30
  dead_timeout: 180 * 24 * 60 * 60, # 6 months in seconds
31
+ mode: :at_least_once,
29
32
  # client settings
30
33
  client_concurrency: 10,
31
34
  client_redis_pool: nil,
32
- client_async: false,
35
+ fetchers_concurrency: 1,
36
+ client_async: false
33
37
  }
34
38
 
35
39
  def initialize(options = {})
@@ -80,11 +84,42 @@ module Jiggler
80
84
  @queue_scan_key ||= "#{queue_prefix}*"
81
85
  end
82
86
 
83
- def prefixed_queues
84
- @prefixed_queues ||= @options[:queues].map do |name|
85
- "#{QUEUE_PREFIX}#{name}"
87
+ def at_least_once?
88
+ @options[:mode] == :at_least_once
89
+ end
90
+
91
+ def queues_data
92
+ @queues_data ||= begin
93
+ queues = {}
94
+
95
+ @options[:queues].each do |queue|
96
+ name, priority = queue
97
+ # by default all queues have the same priority
98
+ priority ||= 0
99
+
100
+ queues[name] = {
101
+ priority: priority,
102
+ # list is a redis list key for a queue
103
+ list: "#{QUEUE_PREFIX}#{name}",
104
+ }
105
+ end
106
+
107
+ queues
86
108
  end
87
109
  end
110
+
111
+ # sort in descending order (higher priority first)
112
+ def sorted_queues_data
113
+ @sorted_queues_data ||= queues_data.sort_by { |_, v| -v[:priority] }
114
+ end
115
+
116
+ def sorted_lists
117
+ @sorted_lists ||= sorted_queues_data.map { |_, v| v[:list] }
118
+ end
119
+
120
+ def sorted_queues
121
+ @sorted_queues ||= sorted_queues_data.map { |k, _| k }
122
+ end
88
123
 
89
124
  def with_async_redis
90
125
  Async do
@@ -109,6 +144,15 @@ module Jiggler
109
144
  :redis_url
110
145
  )
111
146
 
147
+ if at_least_once?
148
+ # for acknowledgers
149
+ opts[:concurrency] *= 2
150
+ # for queue fetchers
151
+ opts[:concurrency] += @options[:fetchers_concurrency] * sorted_queues.count
152
+ # extra poller task to cleanup leftover in-process queues
153
+ opts[:concurrency] += 1
154
+ end
155
+
112
156
  opts[:concurrency] += 2 # monitor + safety margin
113
157
  opts[:concurrency] += 1 if @options[:poller_enabled]
114
158
  opts[:async] = true