litestack 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2662941f303da99554039370e6e53e4c13956c52070e93ad6862742881ca8063
4
- data.tar.gz: ccd5583a9e0e7f5c559f8dafdde913bba256b7b5a679fe927cd31805c1289022
3
+ metadata.gz: d08724a0b9293f55ebed24ba6d738b103795b69563a210c4454322fc382e174e
4
+ data.tar.gz: 01401bfca727b4ef9452a9efafba846c886845dd43c1d8f5f5ce84cee22ab61f
5
5
  SHA512:
6
- metadata.gz: 1a139b3c42cd3f9a327fc7d8f1487dab2e95f2c88d07e063eecc2e438c91236f301a8e3928c9a3b41030fd8c08f4c118c449e56ea4be35b73b00fe2aa240faf8
7
- data.tar.gz: 409221a087df5707bd6da477bb9091daf2ee0fc5f8114d71a7666324676bc62e5d77b6edfd1e3e7d29d601985f7936b2e3545e618b1c888b6c7a748544bbafde
6
+ metadata.gz: 729320670e62261596eabbfd8f8d931117507317127e9fd9e5a796928e7031418455ca351dd2de815d4860edc2e688c0bbdb79560d3907cb3545deb75a1b4fae
7
+ data.tar.gz: d75cc4f23694c726a361b0bc474f561b8ffc4db074ef275839917d4c82461aca7a1c4a0b0e939cd749a7e7afbb58de86f3de254a6adadfc7f6d91ea9021218f3
data/BENCHMARKS.md CHANGED
@@ -67,7 +67,7 @@ For testing the cache we attempted to try writing and reading different payload
67
67
 
68
68
  ### Write
69
69
 
70
- |Payload Size (bytes)|Redis|litecahce|
70
+ |Payload Size (bytes)|Redis|litecache|
71
71
  |-:|-:|-:|
72
72
  |10|4.2K q/s|11.0K q/s|
73
73
  |100|4.7K q/s|11.6K q/s|
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.8] - 2022-03-08
4
+
5
+ - More code cleanups, more test coverage
6
+ - Retry support for jobs in Litejob
7
+ - Job storage and garbage collection for failed jobs
8
+ - Initial graceful shutdown support for Litejob (incomplete)
9
+ - More configuration options for Litejob
10
+
3
11
  ## [0.1.7] - 2022-03-05
4
12
 
5
13
  - Code cleanup, removal of references to older name
data/README.md CHANGED
@@ -120,7 +120,7 @@ litejob is a fast and very efficient job queue processor for Ruby applications.
120
120
  require 'litestack'
121
121
  # define your job class
122
122
  class MyJob
123
- include ::litejob
123
+ include ::Litejob
124
124
 
125
125
  queue = :default
126
126
 
@@ -16,15 +16,18 @@ redis = Redis.new # default settings
16
16
 
17
17
  values = []
18
18
  keys = []
19
- count = 1000
19
+ count = 5
20
20
  count.times { keys << random_str(10) }
21
21
 
22
- [10, 100, 1000, 10000].each do |size|
22
+ [10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000].each do |size|
23
23
  count.times do
24
24
  values << random_str(size)
25
25
  end
26
26
 
27
27
  random_keys = keys.shuffle
28
+
29
+ GC.compact
30
+
28
31
  puts "Benchmarks for values of size #{size} bytes"
29
32
  puts "=========================================================="
30
33
  puts "== Writes =="
@@ -33,7 +36,7 @@ count.times { keys << random_str(10) }
33
36
  end
34
37
 
35
38
  bench("Redis writes", count) do |i|
36
- redis.set(keys[i], values[i])
39
+ #redis.set(keys[i], values[i])
37
40
  end
38
41
 
39
42
  puts "== Reads =="
@@ -42,11 +45,13 @@ count.times { keys << random_str(10) }
42
45
  end
43
46
 
44
47
  bench("Redis reads", count) do |i|
45
- redis.get(random_keys[i])
48
+ #redis.get(random_keys[i])
46
49
  end
47
50
  puts "=========================================================="
48
51
 
49
52
  values = []
53
+
54
+
50
55
  end
51
56
 
52
57
 
@@ -64,5 +69,5 @@ end
64
69
  cache.clear
65
70
  redis.flushdb
66
71
 
67
- sleep
72
+ #sleep
68
73
 
@@ -1,22 +1,14 @@
1
1
  require './bench'
2
- require 'async/scheduler'
3
-
4
- #ActiveJob::Base.logger = Logger.new(IO::NULL)
5
2
 
3
+ count = ARGV[0].to_i rescue 1000
4
+ env = ARGV[1] || "t"
5
+ delay = ARGV[2].to_f rescue 0
6
6
 
7
- Fiber.set_scheduler Async::Scheduler.new
8
7
 
9
- require_relative '../lib/active_job/queue_adapters/litejob_adapter'
10
-
11
- ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
8
+ #ActiveJob::Base.logger = Logger.new(IO::NULL)
12
9
 
13
10
  require './rails_job.rb'
14
11
 
15
-
16
- puts Litesupport.environment
17
-
18
- count = 1000
19
-
20
12
  RailsJob.queue_adapter = :sidekiq
21
13
  t = Time.now.to_f
22
14
  puts "Make sure sidekiq is started with -c ./rails_job.rb"
@@ -26,13 +18,27 @@ end
26
18
 
27
19
  puts "Don't forget to check the sidekiq log for processing time conclusion"
28
20
 
21
+
22
+ # Litejob bench
23
+ ###############
24
+
25
+ if env == "a" # threaded
26
+ require 'async/scheduler'
27
+ Fiber.set_scheduler Async::Scheduler.new
28
+ ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
29
+ end
30
+
31
+ require_relative '../lib/active_job/queue_adapters/litejob_adapter'
32
+ puts Litesupport.environment
33
+
29
34
  RailsJob.queue_adapter = :litejob
30
35
  t = Time.now.to_f
31
36
  bench("enqueuing litejobs", count) do
32
37
  RailsJob.perform_later(count, t)
33
38
  end
34
39
 
35
- Fiber.scheduler.run
36
-
40
+ if env == "a" # threaded
41
+ Fiber.scheduler.run
42
+ end
37
43
 
38
44
  sleep
@@ -24,8 +24,6 @@ if env == "t" # threaded
24
24
  elsif env == "a" # async
25
25
  require 'async/scheduler'
26
26
  Fiber.set_scheduler Async::Scheduler.new
27
- elsif env == "p" # polyphony
28
- require 'polyphony'
29
27
  end
30
28
 
31
29
  require './uljob.rb'
@@ -16,12 +16,16 @@ module ActiveJob
16
16
  DEFAULT_OPTIONS = {
17
17
  config_path: "./config/litejob.yml",
18
18
  path: "../db/queue.db",
19
- queues: [["default", 1, "spawn"]],
20
- workers: 1
19
+ queues: [["default", 1]],
20
+ logger: nil, # Rails performs its logging already
21
+ retries: 5, # It is recommended to stop retries at the Rails level
22
+ workers: 5
21
23
  }
22
24
 
23
25
  def initialize(options={})
24
- Job.options = DEFAULT_OPTIONS.merge(options)
26
+ # we currently don't honour individual options per job class
27
+ # possible in the future?
28
+ # Job.options = DEFAULT_OPTIONS.merge(options)
25
29
  end
26
30
 
27
31
  def enqueue(job) # :nodoc:
@@ -43,7 +43,7 @@ module Litejob
43
43
  private
44
44
  def self.included(klass)
45
45
  klass.extend(ClassMethods)
46
- klass.get_jobqueue
46
+ #klass.get_jobqueue
47
47
  end
48
48
 
49
49
  module ClassMethods
@@ -52,32 +52,37 @@ module Litejob
52
52
  end
53
53
 
54
54
  def perform_at(time, *params)
55
- delay = time - Time.now.to_i
55
+ delay = time.to_i - Time.now.to_i
56
56
  get_jobqueue.push(self.name, params, delay, queue)
57
57
  end
58
58
 
59
- def perfrom_in(delay, *params)
59
+ def perform_in(delay, *params)
60
60
  get_jobqueue.push(self.name, params, delay, queue)
61
61
  end
62
-
63
- def options
64
- @@options ||= {}
62
+
63
+ def perform_after(delay, *params)
64
+ perform_in(delay, *params)
65
65
  end
66
-
67
- def options=(options)
68
- @@options = options
66
+
67
+ def process_jobs
68
+ get_jobqueue
69
69
  end
70
-
70
+
71
+ def delete(id, queue_name=nil)
72
+ queue_name ||= queue
73
+ get_jobqueue.delete(id, queue)
74
+ end
75
+
71
76
  def queue
72
- @@queue_name ||= "default"
77
+ @@queue ||= "default"
73
78
  end
74
79
 
75
80
  def queue=(queue_name)
76
- @@queue_name = queue_name.to_s
81
+ @@queue = queue_name.to_s
77
82
  end
78
83
 
79
84
  def get_jobqueue
80
- Litejobqueue.jobqueue(options)
85
+ Litejobqueue.jobqueue
81
86
  end
82
87
  end
83
88
 
@@ -34,12 +34,19 @@ class Litejobqueue
34
34
  path: "./queue.db",
35
35
  queues: [["default", 1]],
36
36
  workers: 5,
37
- logger: STDOUT,
37
+ retries: 5,
38
+ retry_delay: 60,
39
+ retry_delay_multiplier: 10,
40
+ dead_job_retention: 10 * 24 * 3600,
41
+ gc_sleep_interval: 7200,
42
+ logger: 'STDOUT',
38
43
  sleep_intervals: [0.001, 0.005, 0.025, 0.125, 0.625, 1.0, 2.0]
39
44
  }
40
45
 
41
46
  @@queue = nil
42
47
 
48
+ attr_reader :running
49
+
43
50
  # a method that returns a single instance of the job queue
44
51
  # for use by Litejob
45
52
  def self.jobqueue(options = {})
@@ -64,11 +71,23 @@ class Litejobqueue
64
71
  config.delete k
65
72
  end
66
73
  @options.merge!(config)
74
+ @options.merge!(options) # make sure options passed to initialize trump everything else
75
+
67
76
  @queue = Litequeue.new(@options) # create a new queue object
77
+
78
+ # create logger
68
79
  if @options[:logger].respond_to? :info
69
80
  @logger = @options[:logger]
70
- else
81
+ elsif @options[:logger] == 'STDOUT'
82
+ @logger = Logger.new(STDOUT)
83
+ elsif @options[:logger] == 'STDERR'
84
+ @logger = Logger.new(STDERR)
85
+ elsif @options[:logger].nil?
86
+ @logger = Logger.new(IO::NULL)
87
+ elsif @options[:logger].is_a? String
71
88
  @logger = Logger.new(@options[:logger])
89
+ else
90
+ @logger = Logger.new(IO::NULL)
72
91
  end
73
92
  # group and order queues according to their priority
74
93
  pgroups = {}
@@ -77,7 +96,25 @@ class Litejobqueue
77
96
  pgroups[q[1]] << [q[0], q[2] == "spawn"]
78
97
  end
79
98
  @queues = pgroups.keys.sort.reverse.collect{|p| [p, pgroups[p]]}
80
- @workers = @options[:workers].times.collect{ create_worker }
99
+ @running = true
100
+ @workers = @options[:workers].times.collect{ create_worker }
101
+
102
+ @gc = create_garbage_collector
103
+ @jobs_in_flight = 0
104
+ @mutex = Litesupport::Mutex.new
105
+
106
+ at_exit do
107
+ @running = false
108
+ puts "--- Litejob detected an exit attempt, cleaning up"
109
+ index = 0
110
+ while @jobs_in_flight > 0 and index < 5
111
+ puts "--- Waiting for #{@jobs_in_flight} jobs to finish"
112
+ sleep 1
113
+ index += 1
114
+ end
115
+ puts " --- Exiting with #{@jobs_in_flight} jobs in flight"
116
+ end
117
+
81
118
  end
82
119
 
83
120
  # push a job to the queue
@@ -89,10 +126,9 @@ class Litejobqueue
89
126
  # jobqueue = Litejobqueue.new
90
127
  # jobqueue.push(EasyJob, params) # the job will be performed asynchronously
91
128
  def push(jobclass, params, delay=0, queue=nil)
92
- payload = Oj.dump([jobclass, params])
93
- #res =
129
+ payload = Oj.dump({klass: jobclass, params: params, retries: @options[:retries], queue: queue})
94
130
  res = @queue.push(payload, delay, queue)
95
- @logger.info("[litejob]:[ENQ] id: #{res} class: #{jobclass}")
131
+ @logger.info("[litejob]:[ENQ] id: #{res} job: #{jobclass}")
96
132
  res
97
133
  end
98
134
 
@@ -104,16 +140,42 @@ class Litejobqueue
104
140
  # end
105
141
  # jobqueue = Litejobqueue.new
106
142
  # id = jobqueue.push(EasyJob, params, 10) # queue for processing in 10 seconds
107
- # jobqueue.delete(id)
108
- def delete(id)
109
- job = @queue.delete(id)
143
+ # jobqueue.delete(id, 'default')
144
+ def delete(id, queue=nil)
145
+ job = @queue.delete(id, queue)
110
146
  @logger.info("[litejob]:[DEL] job: #{job}")
111
- Oj.load(job) if job
147
+ job = Oj.load(job[0]) if job
112
148
  job
113
149
  end
114
150
 
151
+ # delete all jobs in a certain named queue
152
+ # or delete all jobs if the queue name is nil
153
+ def clear(queue=nil)
154
+ @queue.clear(queue)
155
+ end
156
+
157
+ # stop the queue object (does not delete the jobs in the queue)
158
+ # specifically useful for testing
159
+ def stop
160
+ @running = false
161
+ @@queue = nil
162
+ end
163
+
164
+
165
+ def count(queue=nil)
166
+ @queue.count(queue)
167
+ end
168
+
115
169
  private
116
170
 
171
+ def job_started
172
+ Litesupport.synchronize(@mutex){@jobs_in_flight += 1}
173
+ end
174
+
175
+ def job_finished
176
+ Litesupport.synchronize(@mutex){@jobs_in_flight -= 1}
177
+ end
178
+
117
179
  # optionally run a job in its own context
118
180
  def schedule(spawn = false, &block)
119
181
  if spawn
@@ -126,14 +188,8 @@ class Litejobqueue
126
188
  # create a worker according to environment
127
189
  def create_worker
128
190
  Litesupport.spawn do
129
- if @options[:logger].respond_to? :info
130
- logger = @options[:logger]
131
- else
132
- logger = Logger.new(@options[:logger])
133
- end
134
191
  worker_sleep_index = 0
135
- i = 0
136
- loop do
192
+ while @running do
137
193
  processed = 0
138
194
  @queues.each do |level| # iterate through the levels
139
195
  level[1].each do |q| # iterate through the queues in the level
@@ -145,22 +201,34 @@ class Litejobqueue
145
201
  begin
146
202
  id, job = payload[0], payload[1]
147
203
  job = Oj.load(job)
148
- logger.info "[litejob]:[DEQ] id: #{id} class: #{job[0]}"
149
- klass = eval(job[0])
204
+ # first capture the original job id
205
+ job[:id] = id if job[:retries].to_i == @options[:retries].to_i
206
+ @logger.info "[litejob]:[DEQ] job:#{job}"
207
+ klass = eval(job[:klass])
150
208
  schedule(q[1]) do # run the job in a new context
209
+ job_started #(Litesupport.current_context)
151
210
  begin
152
- klass.new.perform(*job[1])
153
- logger.info "[litejob]:[END] id: #{id} class: #{job[0]}"
211
+ klass.new.perform(*job[:params])
212
+ @logger.info "[litejob]:[END] job:#{job}"
154
213
  rescue Exception => e
155
- puts e
156
- puts e.message
157
- puts e.backtrace
214
+ # we can retry the failed job now
215
+ if job[:retries] == 0
216
+ @logger.error "[litejob]:[ERR] job: #{job} failed with #{e}:#{e.message}, retries exhausted, moved to _dead queue"
217
+ @queue.push(Oj.dump(job), @options[:dead_job_retention], '_dead')
218
+ else
219
+ retry_delay = @options[:retry_delay_multiplier].pow(@options[:retries] - job[:retries]) * @options[:retry_delay]
220
+ job[:retries] -= 1
221
+ @logger.error "[litejob]:[ERR] job: #{job} failed with #{e}:#{e.message}, retrying in #{retry_delay}"
222
+ @queue.push(Oj.dump(job), retry_delay, q[0])
223
+ @logger.info "[litejob]:[ENQ] job: #{job} enqueued"
224
+ end
158
225
  end
226
+ job_finished #(Litesupport.current_context)
159
227
  end
160
228
  rescue Exception => e
161
- puts e
162
- puts e.message
163
- puts e.backtrace
229
+ # this is an error in the extraction of job info
230
+ # retrying here will not be useful
231
+ @logger.error "[litejob]:[ERR] failed to extract job info for: #{payload} with #{e}:#{e.message}"
164
232
  end
165
233
  Litesupport.switch #give other contexts a chance to run here
166
234
  end
@@ -176,4 +244,20 @@ class Litejobqueue
176
244
  end
177
245
  end
178
246
 
247
+ # create a gc for dead jobs
248
+ def create_garbage_collector
249
+ Litesupport.spawn do
250
+ while @running do
251
+ while jobs = @queue.pop('_dead', 100)
252
+ if jobs[0].is_a? Array
253
+ @logger.info "[litejob]:[DEL] garbage collector deleted #{jobs.length} dead jobs"
254
+ else
255
+ @logger.info "[litejob]:[DEL] garbage collector deleted 1 dead job"
256
+ end
257
+ end
258
+ sleep @options[:gc_sleep_interval]
259
+ end
260
+ end
261
+ end
262
+
179
263
  end
@@ -53,9 +53,10 @@ class Litequeue
53
53
 
54
54
  # pop an item from the queue, optionally with a specific queue name (default queue name is 'default')
55
55
  def pop(queue='default', limit = 1)
56
- res = @queue.acquire {|q| res = q.stmts[:pop].execute!(queue, limit)[0] }
57
- #return res[0] if res.length == 1
58
- #res
56
+ res = @queue.acquire {|q| res = q.stmts[:pop].execute!(queue, limit) }
57
+ return res[0] if res.length == 1
58
+ return nil if res.empty?
59
+ res
59
60
  end
60
61
 
61
62
  # delete an item from the queue
@@ -64,7 +65,7 @@ class Litequeue
64
65
  # queue.delete(id) # => "somevalue"
65
66
  # queue.pop # => nil
66
67
  def delete(id, queue='default')
67
- fire_at, id = id.split("_")
68
+ fire_at, id = id.split("-")
68
69
  result = @queue.acquire{|q| q.stmts[:delete].execute!(queue, fire_at.to_i, id)[0] }
69
70
  end
70
71
 
@@ -82,6 +83,13 @@ class Litequeue
82
83
  def size
83
84
  @queue.acquire{|q| q.get_first_value("SELECT size.page_size * count.page_count FROM pragma_page_size() AS size, pragma_page_count() AS count") }
84
85
  end
86
+
87
+ def close
88
+ @queue.acquire do |q|
89
+ q.stmts.each_pair {|k, v| q.stmts[k].close }
90
+ q.close
91
+ end
92
+ end
85
93
 
86
94
  private
87
95
 
@@ -1,5 +1,4 @@
1
1
  require 'sqlite3'
2
- require 'hiredis'
3
2
 
4
3
  module Litesupport
5
4
 
@@ -38,8 +37,8 @@ module Litesupport
38
37
  end
39
38
  # we should never reach here
40
39
  end
41
-
42
- def self.detect_context
40
+
41
+ def self.context
43
42
  if environment == :fiber || environment == :poylphony
44
43
  Fiber.current.storage
45
44
  else
@@ -47,8 +46,12 @@ module Litesupport
47
46
  end
48
47
  end
49
48
 
50
- def self.context
51
- @ctx ||= detect_context
49
+ def self.current_context
50
+ if environment == :fiber || environment == :poylphony
51
+ Fiber.current
52
+ else
53
+ Thread.current
54
+ end
52
55
  end
53
56
 
54
57
  # switch the execution context to allow others to run
@@ -111,6 +114,21 @@ module Litesupport
111
114
  end
112
115
 
113
116
  end
117
+
118
+ module Forkable
119
+
120
+ def _fork(*args)
121
+ ppid = Process.pid
122
+ result = super
123
+ if Process.pid != ppid
124
+ # trigger a restart of all connections owned by Litesupport::Pool
125
+ end
126
+ result
127
+ end
128
+
129
+ end
130
+
131
+ #::Process.singleton_class.prepend(::Litesupport::Forkable)
114
132
 
115
133
  class Pool
116
134
 
@@ -126,6 +144,7 @@ module Litesupport
126
144
  end
127
145
 
128
146
  def acquire
147
+ # check for pid changes
129
148
  acquired = false
130
149
  result = nil
131
150
  while !acquired do
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Litestack
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.8"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: litestack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mohamed Hassan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-05 00:00:00.000000000 Z
11
+ date: 2023-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3