litestack 0.1.5 → 0.1.6

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: 88034778fbac75441d1a9ed2e066ad6ba1aa4962eb48629de1d9ba7ae85f3468
4
- data.tar.gz: d6ac66ffd1e78856bd74aa1d88163b95581494aaa481de88b673d84b5a8b8ff6
3
+ metadata.gz: 01c10017cec21bcabea357aecb8746779060718bb2e6b6e36f250c37d80656f1
4
+ data.tar.gz: 863b319c7a658f2a19410c614035068f8a2dec72b189698a5a081f595ecf5853
5
5
  SHA512:
6
- metadata.gz: 37043f1eab519ea41e81e5b9cb6b20798ea5f6e7ed7ce99d8105454067129e7f49f4b8357b0855948e6df51bf6d466966802085b67943b1f9416ee90f443502a
7
- data.tar.gz: 602d0e03d53eeb8e780c31b2f9bb1b3add550035c3cb3fd07f1d14fe94d94aea50a93dea19ab819454465c7824249bb40f7768ec4eebf6662af713218175071d
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)
@@ -9,7 +9,7 @@ Fiber.set_scheduler Async::Scheduler.new
9
9
  Fiber.scheduler.run
10
10
 
11
11
  require_relative '../lib/litestack'
12
-
12
+ #require 'litestack'
13
13
 
14
14
  cache = Litecache.new({path: '../db/cache.db'}) # default settings
15
15
  redis = Redis.new # default settings
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
- @cache.transaction(:immediate) do
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)
@@ -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 = create_store
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
- begin
89
- @stmts[:setter].execute!(key, value, expires_in)
90
- rescue SQLite3::FullException
91
- @stmts[:extra_pruner].execute!(0.2)
92
- @cache.execute("vacuum")
93
- retry
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
- begin
103
- transaction(:immediate) do
104
- @stmts[:inserter].execute!(key, value, expires_in)
105
- changes = @cache.changes
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
- @stmts[:deleter].execute!(key)
131
- return @cache.changes > 0
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
- if limit and limit.is_a? Integer
148
- @stmts[:limited_pruner].execute!(limit)
149
- elsif limit and limit.is_a? Float
150
- @stmts[:extra_pruner].execute!(limit)
151
- else
152
- @stmts[:pruner].execute!
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
- Litesupport.synchronize do
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.transaction(mode) do
196
- yield
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
- Litesupport.synchronize do
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 create_store
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
@@ -34,15 +34,18 @@ class Litequeue
34
34
 
35
35
  def initialize(options = {})
36
36
  @options = DEFAULT_OPTIONS.merge(options)
37
- @queue = create_db #(@options[:path])
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
- result = @push.execute!(queue, delay, value)[0]
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 = @deleter.execute!(queue, fire_at.to_i, id)[0]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Litestack
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.6"
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.5
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-02-27 00:00:00.000000000 Z
11
+ date: 2023-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3