litestack 0.1.5 → 0.1.6
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/CHANGELOG.md +7 -0
- data/README.md +3 -3
- data/bench/bench_cache_raw.rb +1 -1
- data/bench/bench_queue.rb +2 -2
- data/lib/active_support/cache/litecache.rb +4 -2
- data/lib/litestack/litecache.rb +53 -49
- data/lib/litestack/litejobqueue.rb +1 -4
- data/lib/litestack/litequeue.rb +14 -13
- data/lib/litestack/litesupport.rb +70 -0
- 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: 01c10017cec21bcabea357aecb8746779060718bb2e6b6e36f250c37d80656f1
|
4
|
+
data.tar.gz: 863b319c7a658f2a19410c614035068f8a2dec72b189698a5a081f595ecf5853
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7bb0fd84ee582eddfe32e48b0373a7545cad8581217ba56d7ac6c984b60b137b86d5196e1bc37486b88d21c28bc5ff62da573a1ba6ce6b49d66ba744ddbcabc6
|
7
|
+
data.tar.gz: 38da99eae6a37222a8689577900504ea1b131f0ec56f67531dff00f5ae4f4c62a29baca21ba2a102435cc334773f8ef4709a361dff6f28de988471eac0937e08
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.6] - 2022-03-03
|
4
|
+
|
5
|
+
- Revamped the locking model, more robust, minimal performance hit
|
6
|
+
- Introduced a new resource pooling class
|
7
|
+
- Litecache and Litejob now use the resource pool
|
8
|
+
- Much less memory usage for Litecache and Litejob
|
9
|
+
|
3
10
|
## [0.1.0] - 2022-02-26
|
4
11
|
|
5
12
|
- Initial release
|
data/README.md
CHANGED
@@ -152,9 +152,9 @@ You can add more configuration in litejob.yml (or config/litejob.yml if you are
|
|
152
152
|
|
153
153
|
```yaml
|
154
154
|
queues:
|
155
|
-
- [default 1]
|
156
|
-
- [urgent 5]
|
157
|
-
- [critical 10 "spawn"]
|
155
|
+
- [default, 1]
|
156
|
+
- [urgent, 5]
|
157
|
+
- [critical, 10, "spawn"]
|
158
158
|
```
|
159
159
|
|
160
160
|
The queues need to include a name and a priority (a number between 1 and 10) and can also optionally add the token "spawn", which means every job will run it its own concurrency context (thread or fiber)
|
data/bench/bench_cache_raw.rb
CHANGED
data/bench/bench_queue.rb
CHANGED
@@ -5,11 +5,11 @@ count = 1000
|
|
5
5
|
|
6
6
|
q = Litequeue.new({path: '../db/queue.db' })
|
7
7
|
|
8
|
-
bench("enqueue", count) do |i|
|
8
|
+
bench("Litequeue enqueue", count) do |i|
|
9
9
|
q.push i.to_s
|
10
10
|
end
|
11
11
|
|
12
|
-
bench("dequeue", count) do |i|
|
12
|
+
bench("Litequeue dequeue", count) do |i|
|
13
13
|
q.pop
|
14
14
|
end
|
15
15
|
|
@@ -25,12 +25,14 @@ module ActiveSupport
|
|
25
25
|
def increment(key, amount = 1, options = nil)
|
26
26
|
key = key.to_s
|
27
27
|
options = merged_options(options)
|
28
|
-
|
28
|
+
# todo: fix me
|
29
|
+
# this is currently a hack to avoid dealing with Rails cache encoding and decoding
|
30
|
+
#@cache.transaction(:immediate) do
|
29
31
|
if value = read(key, options)
|
30
32
|
value = value.to_i + amount
|
31
33
|
write(key, value, options)
|
32
34
|
end
|
33
|
-
end
|
35
|
+
#end
|
34
36
|
end
|
35
37
|
|
36
38
|
def decrement(key, amount = 1, options = nil)
|
data/lib/litestack/litecache.rb
CHANGED
@@ -72,9 +72,7 @@ class Litecache
|
|
72
72
|
:counter => "SELECT count(*) FROM data",
|
73
73
|
:sizer => "SELECT size.page_size * count.page_count FROM pragma_page_size() AS size, pragma_page_count() AS count"
|
74
74
|
}
|
75
|
-
@cache =
|
76
|
-
@stmts = {}
|
77
|
-
@sql.each_pair{|k, v| @stmts[k] = @cache.prepare(v)}
|
75
|
+
@cache = Litesupport::Pool.new(1){create_db}
|
78
76
|
@stats = {hit: 0, miss: 0}
|
79
77
|
@last_visited = {}
|
80
78
|
@running = true
|
@@ -85,12 +83,14 @@ class Litecache
|
|
85
83
|
def set(key, value, expires_in = nil)
|
86
84
|
key = key.to_s
|
87
85
|
expires_in = @options[:expires_in] if expires_in.nil? or expires_in.zero?
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
86
|
+
@cache.acquire do |cache|
|
87
|
+
begin
|
88
|
+
cache.stmts[:setter].execute!(key, value, expires_in)
|
89
|
+
rescue SQLite3::FullException
|
90
|
+
cache.stmts[:extra_pruner].execute!(0.2)
|
91
|
+
cache.execute("vacuum")
|
92
|
+
retry
|
93
|
+
end
|
94
94
|
end
|
95
95
|
return true
|
96
96
|
end
|
@@ -99,15 +99,18 @@ class Litecache
|
|
99
99
|
def set_unless_exists(key, value, expires_in = nil)
|
100
100
|
key = key.to_s
|
101
101
|
expires_in = @options[:expires_in] if expires_in.nil? or expires_in.zero?
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
102
|
+
changes = 0
|
103
|
+
@cache.acquire do |cache|
|
104
|
+
begin
|
105
|
+
transaction(:immediate) do
|
106
|
+
cache.stmts[:inserter].execute!(key, value, expires_in)
|
107
|
+
changes = @cache.changes
|
108
|
+
end
|
109
|
+
rescue SQLite3::FullException
|
110
|
+
cache.stmts[:extra_pruner].execute!(0.2)
|
111
|
+
cache.execute("vacuum")
|
112
|
+
retry
|
106
113
|
end
|
107
|
-
rescue SQLite3::FullException
|
108
|
-
@stmts[:extra_pruner].execute!(0.2)
|
109
|
-
@cache.execute("vacuum")
|
110
|
-
retry
|
111
114
|
end
|
112
115
|
return changes > 0
|
113
116
|
end
|
@@ -116,7 +119,7 @@ class Litecache
|
|
116
119
|
# if the key doesn't exist or it is expired then null will be returned
|
117
120
|
def get(key)
|
118
121
|
key = key.to_s
|
119
|
-
if record = @stmts[:getter].execute!(key)[0]
|
122
|
+
if record = @cache.acquire{|cache| cache.stmts[:getter].execute!(key)[0] }
|
120
123
|
@last_visited[key] = true
|
121
124
|
@stats[:hit] +=1
|
122
125
|
return record[1]
|
@@ -127,14 +130,18 @@ class Litecache
|
|
127
130
|
|
128
131
|
# delete a key, value pair from the cache
|
129
132
|
def delete(key)
|
130
|
-
|
131
|
-
|
133
|
+
changes = 0
|
134
|
+
@cache.aquire do |cache|
|
135
|
+
cache.stmts[:deleter].execute!(key)
|
136
|
+
changes = cache.changes
|
137
|
+
end
|
138
|
+
return changes > 0
|
132
139
|
end
|
133
140
|
|
134
141
|
# increment an integer value by amount, optionally add an expiry value (in seconds)
|
135
142
|
def increment(key, amount, expires_in = nil)
|
136
143
|
expires_in = @expires_in unless expires_in
|
137
|
-
@stmts[:incrementer].execute!(key.to_s, amount, expires_in)
|
144
|
+
@cache.acquire{|cache| cache.stmts[:incrementer].execute!(key.to_s, amount, expires_in) }
|
138
145
|
end
|
139
146
|
|
140
147
|
# decrement an integer value by amount, optionally add an expiry value (in seconds)
|
@@ -144,43 +151,43 @@ class Litecache
|
|
144
151
|
|
145
152
|
# delete all entries in the cache up limit (ordered by LRU), if no limit is provided approximately 20% of the entries will be deleted
|
146
153
|
def prune(limit=nil)
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
154
|
+
@cache.acquire do |cache|
|
155
|
+
if limit and limit.is_a? Integer
|
156
|
+
cache.stmts[:limited_pruner].execute!(limit)
|
157
|
+
elsif limit and limit.is_a? Float
|
158
|
+
cache.stmts[:extra_pruner].execute!(limit)
|
159
|
+
else
|
160
|
+
cache.stmts[:pruner].execute!
|
161
|
+
end
|
153
162
|
end
|
154
163
|
end
|
155
164
|
|
156
165
|
# return the number of key, value pairs in the cache
|
157
166
|
def count
|
158
|
-
@stmts[:counter].execute!.to_a[0][0]
|
167
|
+
@cache.acquire{|cache| cache.stmts[:counter].execute!.to_a[0][0] }
|
159
168
|
end
|
160
169
|
|
161
170
|
# return the actual size of the cache file
|
162
171
|
def size
|
163
|
-
@stmts[:sizer].execute!.to_a[0][0]
|
172
|
+
@cache.acquire{|cache| cache.stmts[:sizer].execute!.to_a[0][0] }
|
164
173
|
end
|
165
174
|
|
166
175
|
# delete all key, value pairs in the cache
|
167
176
|
def clear
|
168
|
-
@cache.execute("delete FROM data")
|
177
|
+
@cache.acquire{|cache| cache.execute("delete FROM data") }
|
169
178
|
end
|
170
179
|
|
171
180
|
# close the connection to the cache file
|
172
181
|
def close
|
173
182
|
@running = false
|
174
183
|
#Litesupport.synchronize do
|
175
|
-
@cache.close
|
184
|
+
@cache.acquire{|cache| cache.close }
|
176
185
|
#end
|
177
186
|
end
|
178
187
|
|
179
188
|
# return the maximum size of the cache
|
180
189
|
def max_size
|
181
|
-
|
182
|
-
@cache.get_first_value("SELECT s.page_size * c.max_page_count FROM pragma_page_size() as s, pragma_max_page_count() as c")
|
183
|
-
end
|
190
|
+
@cache.acquire{|cache| cache.get_first_value("SELECT s.page_size * c.max_page_count FROM pragma_page_size() as s, pragma_max_page_count() as c") }
|
184
191
|
end
|
185
192
|
|
186
193
|
# hits and misses for get operations performed over this particular connection (not cache wide)
|
@@ -192,8 +199,10 @@ class Litecache
|
|
192
199
|
|
193
200
|
# low level access to SQLite transactions, use with caution
|
194
201
|
def transaction(mode)
|
195
|
-
@cache.
|
196
|
-
|
202
|
+
@cache.acquire do |cache|
|
203
|
+
cache.transaction(mode) do
|
204
|
+
yield
|
205
|
+
end
|
197
206
|
end
|
198
207
|
end
|
199
208
|
|
@@ -201,35 +210,29 @@ class Litecache
|
|
201
210
|
|
202
211
|
def spawn_worker
|
203
212
|
Litesupport.spawn do
|
204
|
-
# create a specific cache instance for this worker
|
205
|
-
# to overcome SQLite3 Database is locked error
|
206
|
-
cache = create_store
|
207
|
-
stmts = {}
|
208
|
-
[:toucher, :pruner, :extra_pruner].each do |stmt|
|
209
|
-
stmts[stmt] = cache.prepare(@sql[stmt])
|
210
|
-
end
|
211
213
|
while @running
|
212
|
-
|
214
|
+
@cache.acquire do |cache|
|
213
215
|
begin
|
214
216
|
cache.transaction(:immediate) do
|
215
217
|
@last_visited.delete_if do |k| # there is a race condition here, but not a serious one
|
216
|
-
stmts[:toucher].execute!(k) || true
|
218
|
+
cache.stmts[:toucher].execute!(k) || true
|
217
219
|
end
|
218
|
-
stmts[:pruner].execute!
|
220
|
+
cache.stmts[:pruner].execute!
|
219
221
|
end
|
220
222
|
rescue SQLite3::BusyException
|
221
223
|
retry
|
222
224
|
rescue SQLite3::FullException
|
223
|
-
stmts[:extra_pruner].execute!(0.2)
|
225
|
+
cache.stmts[:extra_pruner].execute!(0.2)
|
226
|
+
rescue Exception
|
227
|
+
# database is closed
|
224
228
|
end
|
225
229
|
end
|
226
230
|
sleep @options[:sleep_interval]
|
227
231
|
end
|
228
|
-
cache.close
|
229
232
|
end
|
230
233
|
end
|
231
234
|
|
232
|
-
def
|
235
|
+
def create_db
|
233
236
|
db = Litesupport.create_db(@options[:path])
|
234
237
|
db.synchronous = 0
|
235
238
|
db.cache_size = 2000
|
@@ -240,6 +243,7 @@ class Litecache
|
|
240
243
|
db.execute("CREATE table if not exists data(id text primary key, value text, expires_in integer, last_used integer)")
|
241
244
|
db.execute("CREATE index if not exists expiry_index on data (expires_in)")
|
242
245
|
db.execute("CREATE index if not exists last_used_index on data (last_used)")
|
246
|
+
@sql.each_pair{|k, v| db.stmts[k] = db.prepare(v)}
|
243
247
|
db
|
244
248
|
end
|
245
249
|
|
@@ -116,16 +116,13 @@ class Litejobqueue
|
|
116
116
|
# create a worker according to environment
|
117
117
|
def create_worker
|
118
118
|
Litesupport.spawn do
|
119
|
-
# we create a queue object specific to this worker here
|
120
|
-
# this way we can survive potential SQLite3 Database is locked errors
|
121
|
-
queue = Litequeue.new(@options)
|
122
119
|
loop do
|
123
120
|
processed = 0
|
124
121
|
@queues.each do |level| # iterate through the levels
|
125
122
|
level[1].each do |q| # iterate through the queues in the level
|
126
123
|
index = 0
|
127
124
|
max = level[0]
|
128
|
-
while index < max && payload = queue.pop(q[0])
|
125
|
+
while index < max && payload = @queue.pop(q[0]) # fearlessly use the same queue object
|
129
126
|
processed += 1
|
130
127
|
index += 1
|
131
128
|
begin
|
data/lib/litestack/litequeue.rb
CHANGED
@@ -34,15 +34,18 @@ class Litequeue
|
|
34
34
|
|
35
35
|
def initialize(options = {})
|
36
36
|
@options = DEFAULT_OPTIONS.merge(options)
|
37
|
-
@queue = create_db #
|
38
|
-
prepare
|
37
|
+
@queue = Litesupport::Pool.new(1){create_db} # delegate the db creation to the litepool
|
39
38
|
end
|
40
39
|
|
41
40
|
# push an item to the queue, optionally specifying the queue name (defaults to default) and after how many seconds it should be ready to pop (defaults to zero)
|
42
41
|
# a unique job id is returned from this method, can be used later to delete it before it fires. You can push string, integer, float, true, false or nil values
|
43
42
|
#
|
44
43
|
def push(value, delay=0, queue='default')
|
45
|
-
|
44
|
+
# @todo - check if queue is busy, back off if it is
|
45
|
+
# also bring back the synchronize block, to prevent
|
46
|
+
# a race condition if a thread hits the busy handler
|
47
|
+
# before the current thread proceeds after a backoff
|
48
|
+
result = @queue.acquire { |q| q.stmts[:push].execute!(queue, delay, value)[0] }
|
46
49
|
return result[0] if result
|
47
50
|
end
|
48
51
|
|
@@ -50,7 +53,7 @@ class Litequeue
|
|
50
53
|
|
51
54
|
# pop an item from the queue, optionally with a specific queue name (default queue name is 'default')
|
52
55
|
def pop(queue='default')
|
53
|
-
@pop.execute!(queue)[0]
|
56
|
+
@queue.acquire {|q| q.stmts[:pop].execute!(queue)[0] }
|
54
57
|
end
|
55
58
|
|
56
59
|
# delete an item from the queue
|
@@ -60,22 +63,22 @@ class Litequeue
|
|
60
63
|
# queue.pop # => nil
|
61
64
|
def delete(id, queue='default')
|
62
65
|
fire_at, id = id.split("_")
|
63
|
-
result = @
|
66
|
+
result = @queue.acquire{|q| q.stmts[:delete].execute!(queue, fire_at.to_i, id)[0] }
|
64
67
|
end
|
65
68
|
|
66
69
|
# deletes all the entries in all queues, or if a queue name is given, deletes all entries in that specific queue
|
67
70
|
def clear(queue=nil)
|
68
|
-
@queue.execute("DELETE FROM _ul_queue_ WHERE iif(?, queue = ?, 1)", queue)
|
71
|
+
@queue.acquire{|q| q.execute("DELETE FROM _ul_queue_ WHERE iif(?, queue = ?, 1)", queue) }
|
69
72
|
end
|
70
73
|
|
71
74
|
# returns a count of entries in all queues, or if a queue name is given, reutrns the count of entries in that queue
|
72
75
|
def count(queue=nil)
|
73
|
-
@queue.get_first_value("SELECT count(*) FROM _ul_queue_ WHERE iif(?, queue = ?, 1)", queue)
|
76
|
+
@queue.acquire{|q| q.get_first_value("SELECT count(*) FROM _ul_queue_ WHERE iif(?, queue = ?, 1)", queue) }
|
74
77
|
end
|
75
78
|
|
76
79
|
# return the size of the queue file on disk
|
77
80
|
def size
|
78
|
-
@queue.get_first_value("SELECT size.page_size * count.page_count FROM pragma_page_size() AS size, pragma_page_count() AS count")
|
81
|
+
@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") }
|
79
82
|
end
|
80
83
|
|
81
84
|
private
|
@@ -86,14 +89,12 @@ class Litequeue
|
|
86
89
|
db.wal_autocheckpoint = 10000
|
87
90
|
db.mmap_size = @options[:mmap_size]
|
88
91
|
db.execute("CREATE TABLE IF NOT EXISTS _ul_queue_(queue TEXT DEFAULT('default') NOT NULL ON CONFLICT REPLACE, fire_at INTEGER DEFAULT(unixepoch()) NOT NULL ON CONFLICT REPLACE, id TEXT DEFAULT(hex(randomblob(8)) || (strftime('%f') * 100)) NOT NULL ON CONFLICT REPLACE, value TEXT, created_at INTEGER DEFAULT(unixepoch()) NOT NULL ON CONFLICT REPLACE, PRIMARY KEY(queue, fire_at ASC, id) ) WITHOUT ROWID")
|
92
|
+
db.stmts[:push] = db.prepare("INSERT INTO _ul_queue_(queue, fire_at, value) VALUES ($1, (strftime('%s') + $2), $3) RETURNING fire_at || '-' || id")
|
93
|
+
db.stmts[:pop] = db.prepare("DELETE FROM _ul_queue_ WHERE (queue, fire_at, id) = (SELECT queue, min(fire_at), id FROM _ul_queue_ WHERE queue = ifnull($1, 'default') AND fire_at <= (unixepoch()) limit 1) RETURNING fire_at || '-' || id, value")
|
94
|
+
db.stmts[:delete] = db.prepare("DELETE FROM _ul_queue_ WHERE queue = ifnull($1, 'default') AND fire_at = $2 AND id = $3 RETURNING value")
|
89
95
|
db
|
90
96
|
end
|
91
97
|
|
92
|
-
def prepare
|
93
|
-
@push = @queue.prepare("INSERT INTO _ul_queue_(queue, fire_at, value) VALUES ($1, (strftime('%s') + $2), $3) RETURNING fire_at || '-' || id")
|
94
|
-
@pop = @queue.prepare("DELETE FROM _ul_queue_ WHERE (queue, fire_at, id) = (SELECT queue, min(fire_at), id FROM _ul_queue_ WHERE queue = ifnull($1, 'default') AND fire_at <= (unixepoch()) limit 1) RETURNING fire_at || '-' || id, value")
|
95
|
-
@deleter = @queue.prepare("DELETE FROM _ul_queue_ WHERE queue = ifnull($1, 'default') AND fire_at = $2 AND id = $3 RETURNING value")
|
96
|
-
end
|
97
98
|
|
98
99
|
end
|
99
100
|
|
@@ -33,6 +33,18 @@ module Litesupport
|
|
33
33
|
# we should never reach here
|
34
34
|
end
|
35
35
|
|
36
|
+
def self.detect_context
|
37
|
+
if environment == :fiber || environment == :poylphony
|
38
|
+
Fiber.current.storage
|
39
|
+
else
|
40
|
+
Thread.current
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.context
|
45
|
+
@ctx ||= detect_context
|
46
|
+
end
|
47
|
+
|
36
48
|
# switch the execution context to allow others to run
|
37
49
|
def self.switch
|
38
50
|
if self.environment == :fiber
|
@@ -71,7 +83,65 @@ module Litesupport
|
|
71
83
|
db = SQLite3::Database.new(path)
|
72
84
|
db.busy_handler{ switch || sleep(0.001) }
|
73
85
|
db.journal_mode = "WAL"
|
86
|
+
db.instance_variable_set(:@stmts, {})
|
87
|
+
class << db
|
88
|
+
attr_reader :stmts
|
89
|
+
end
|
74
90
|
db
|
75
91
|
end
|
76
92
|
|
93
|
+
class Mutex
|
94
|
+
|
95
|
+
def initialize
|
96
|
+
@mutex = Thread::Mutex.new
|
97
|
+
end
|
98
|
+
|
99
|
+
def synchronize(&block)
|
100
|
+
if Litesupport.environment == :threaded || Litesupport.environment == :iodine
|
101
|
+
@mutex.synchronize{ block.call }
|
102
|
+
else
|
103
|
+
block.call
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
class Pool
|
110
|
+
|
111
|
+
def initialize(count, &block)
|
112
|
+
@count = count
|
113
|
+
@block = block
|
114
|
+
@resources = []
|
115
|
+
@mutex = Litesupport::Mutex.new
|
116
|
+
@count.times do
|
117
|
+
resource = @mutex.synchronize{ block.call }
|
118
|
+
@resources << [resource, :free]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def acquire
|
123
|
+
acquired = false
|
124
|
+
result = nil
|
125
|
+
while !acquired do
|
126
|
+
@mutex.synchronize do
|
127
|
+
if resource = @resources.find{|r| r[1] == :free}
|
128
|
+
resource[1] = :busy
|
129
|
+
begin
|
130
|
+
result = yield resource[0]
|
131
|
+
rescue Exception => e
|
132
|
+
raise e
|
133
|
+
ensure
|
134
|
+
resource[1] = :free
|
135
|
+
acquired = true
|
136
|
+
return nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
sleep 0.0001 unless acquired
|
141
|
+
end
|
142
|
+
result
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
77
147
|
end
|
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.6
|
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-
|
11
|
+
date: 2023-03-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sqlite3
|