litestack 0.1.7 → 0.1.8

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