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 +4 -4
- data/BENCHMARKS.md +1 -1
- data/CHANGELOG.md +8 -0
- data/README.md +1 -1
- data/bench/bench_cache_raw.rb +10 -5
- data/bench/bench_jobs_rails.rb +20 -14
- data/bench/bench_jobs_raw.rb +0 -2
- data/lib/active_job/queue_adapters/litejob_adapter.rb +7 -3
- data/lib/litestack/litejob.rb +18 -13
- data/lib/litestack/litejobqueue.rb +111 -27
- data/lib/litestack/litequeue.rb +12 -4
- data/lib/litestack/litesupport.rb +24 -5
- data/lib/litestack/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d08724a0b9293f55ebed24ba6d738b103795b69563a210c4454322fc382e174e
|
4
|
+
data.tar.gz: 01401bfca727b4ef9452a9efafba846c886845dd43c1d8f5f5ce84cee22ab61f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 729320670e62261596eabbfd8f8d931117507317127e9fd9e5a796928e7031418455ca351dd2de815d4860edc2e688c0bbdb79560d3907cb3545deb75a1b4fae
|
7
|
+
data.tar.gz: d75cc4f23694c726a361b0bc474f561b8ffc4db074ef275839917d4c82461aca7a1c4a0b0e939cd749a7e7afbb58de86f3de254a6adadfc7f6d91ea9021218f3
|
data/BENCHMARKS.md
CHANGED
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
data/bench/bench_cache_raw.rb
CHANGED
@@ -16,15 +16,18 @@ redis = Redis.new # default settings
|
|
16
16
|
|
17
17
|
values = []
|
18
18
|
keys = []
|
19
|
-
count =
|
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
|
|
data/bench/bench_jobs_rails.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
36
|
-
|
40
|
+
if env == "a" # threaded
|
41
|
+
Fiber.scheduler.run
|
42
|
+
end
|
37
43
|
|
38
44
|
sleep
|
data/bench/bench_jobs_raw.rb
CHANGED
@@ -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
|
20
|
-
|
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
|
-
|
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:
|
data/lib/litestack/litejob.rb
CHANGED
@@ -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
|
59
|
+
def perform_in(delay, *params)
|
60
60
|
get_jobqueue.push(self.name, params, delay, queue)
|
61
61
|
end
|
62
|
-
|
63
|
-
def
|
64
|
-
|
62
|
+
|
63
|
+
def perform_after(delay, *params)
|
64
|
+
perform_in(delay, *params)
|
65
65
|
end
|
66
|
-
|
67
|
-
def
|
68
|
-
|
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
|
-
@@
|
77
|
+
@@queue ||= "default"
|
73
78
|
end
|
74
79
|
|
75
80
|
def queue=(queue_name)
|
76
|
-
@@
|
81
|
+
@@queue = queue_name.to_s
|
77
82
|
end
|
78
83
|
|
79
84
|
def get_jobqueue
|
80
|
-
Litejobqueue.jobqueue
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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(
|
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}
|
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
|
-
|
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
|
-
|
149
|
-
|
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[
|
153
|
-
logger.info "[litejob]:[END]
|
211
|
+
klass.new.perform(*job[:params])
|
212
|
+
@logger.info "[litejob]:[END] job:#{job}"
|
154
213
|
rescue Exception => e
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
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
|
data/lib/litestack/litequeue.rb
CHANGED
@@ -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)
|
57
|
-
|
58
|
-
|
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.
|
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.
|
51
|
-
|
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
|
data/lib/litestack/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2023-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sqlite3
|