Class: Litecache

Inherits:
Object
  • Object
show all
Includes:
Litemetric::Measurable, Litesupport::Liteconnection
Defined in:
lib/litestack/litecache.rb

Overview

Litecache is a caching library for Ruby applications that is built on top of SQLite. It is designed to be simple to use, very fast, and feature-rich, providing developers with a reliable and efficient way to cache data.

One of the main features of Litecache is automatic key expiry, which allows developers to set an expiration time for each cached item. This ensures that cached data is automatically removed from the cache after a certain amount of time has passed, reducing the risk of stale data being served to users.

In addition, Litecache supports LRU (Least Recently Used) removal, which means that if the cache reaches its capacity limit, the least recently used items will be removed first to make room for new items. This ensures that the most frequently accessed data is always available in the cache.

Litecache also supports integer value increment/decrement, which allows developers to increment or decrement the value of a cached item in a thread-safe manner. This is useful for implementing counters or other types of numerical data that need to be updated frequently.

Overall, Litecache is a powerful and flexible caching library that provides automatic key expiry, LRU removal, and integer value increment/decrement capabilities. Its fast performance and simple API make it an excellent choice for Ruby applications that need a reliable and efficient way to cache data.

Constant Summary collapse

DEFAULT_OPTIONS =

the default options for the cache can be overridden by passing new options in a hash to Litecache.new

path: "./cache.db"
expiry: 60 * 60 * 24 * 30 -> one month default expiry if none is provided
size: 128 * 1024 * 1024 -> 128MB
mmap_size: 128 * 1024 * 1024 -> 128MB to be held in memory
min_size: 32 * 1024 -> 32KB
return_full_record: false -> only return the payload
sleep_interval: 1 -> 1 second of sleep between cleanup runs
{
  path: Litesupport.root.join("cache.sqlite3"),
  config_path: "./litecache.yml",
  sync: 0,
  expiry: 60 * 60 * 24 * 30, # one month
  size: 128 * 1024 * 1024, # 128MB
  mmap_size: 128 * 1024 * 1024, # 128MB
  min_size: 8 * 1024 * 1024, # 16MB
  return_full_record: false, # only return the payload
  sleep_interval: 30, # 30 seconds
  metrics: false
}

Instance Method Summary collapse

Methods included from Litemetric::Measurable

#capture, #capture_snapshot, #collect_metrics, #create_snapshotter, #measure, #metrics_identifier

Methods included from Litesupport::Liteconnection

#journal_mode, #options, #path, #size, #synchronous

Methods included from Litesupport::Forkable

#_fork

Constructor Details

#initialize(options = {}) ⇒ Litecache

creates a new instance of Litecache can optionally receive an options hash which will be merged with the DEFAULT_OPTIONS (the new hash overrides any matching keys in the default one).

Example:

litecache = Litecache.new

litecache.set("a", "somevalue")
litecache.get("a") # =>  "somevalue"

litecache.set("b", "othervalue", 1) # expire aftre 1 second
litecache.get("b") # => "othervalue"
sleep 2
litecache.get("b") # => nil

litecache.clear # nothing remains in the cache
litecache.close # optional, you can safely kill the process


64
65
66
67
68
69
# File 'lib/litestack/litecache.rb', line 64

def initialize(options = {})
  options[:size] = DEFAULT_OPTIONS[:min_size] if options[:size] && options[:size] < DEFAULT_OPTIONS[:min_size]
  init(options)
  @expires_in = @options[:expiry] || 60 * 60 * 24 * 30
  collect_metrics if @options[:metrics]
end

Instance Method Details

#clearObject

delete all key, value pairs in the cache



197
198
199
# File 'lib/litestack/litecache.rb', line 197

def clear
  run_sql("delete FROM data")
end

#closeObject

close the connection to the cache file



202
203
204
205
# File 'lib/litestack/litecache.rb', line 202

def close
  @running = false
  super
end

#countObject

return the number of key, value pairs in the cache



187
188
189
# File 'lib/litestack/litecache.rb', line 187

def count
  run_stmt(:counter)[0][0]
end

#decrement(key, amount = 1, expires_in = nil) ⇒ Object

decrement an integer value by amount, optionally add an expiry value (in seconds)



169
170
171
# File 'lib/litestack/litecache.rb', line 169

def decrement(key, amount = 1, expires_in = nil)
  increment(key, -amount, expires_in)
end

#delete(key) ⇒ Object

delete a key, value pair from the cache



153
154
155
156
157
158
159
160
# File 'lib/litestack/litecache.rb', line 153

def delete(key)
  changes = 0
  @conn.acquire do |cache|
    cache.stmts[:deleter].execute!(key)
    changes = cache.changes
  end
  changes > 0
end

#get(key) ⇒ Object

get a value by its key if the key doesn’t exist or it is expired then null will be returned



124
125
126
127
128
129
130
131
132
# File 'lib/litestack/litecache.rb', line 124

def get(key)
  key = key.to_s
  if (record = @conn.acquire { |cache| cache.stmts[:getter].execute!(key)[0] })
    capture(:get, key, 1)
    return record[1]
  end
  capture(:get, key, 0)
  nil
end

#get_multi(*keys) ⇒ Object

get multiple values by their keys, a hash with values corresponding to the keys is returned,



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/litestack/litecache.rb', line 136

def get_multi(*keys)
  results = {}
  transaction(:deferred) do |conn|
    keys.length.times do |i|
      key = keys[i].to_s
      if (record = conn.stmts[:getter].execute!(key)[0])
        results[keys[i]] = record[1] # use the original key format
        capture(:get, key, 1)
      else
        capture(:get, key, 0)
      end
    end
  end
  results
end

#increment(key, amount = 1, expires_in = nil) ⇒ Object

increment an integer value by amount, optionally add an expiry value (in seconds)



163
164
165
166
# File 'lib/litestack/litecache.rb', line 163

def increment(key, amount = 1, expires_in = nil)
  expires_in ||= @expires_in
  @conn.acquire { |cache| cache.stmts[:incrementer].execute!(key.to_s, amount, expires_in) }
end

#max_sizeObject

return the maximum size of the cache



208
209
210
# File 'lib/litestack/litecache.rb', line 208

def max_size
  run_sql("SELECT s.page_size * c.max_page_count FROM pragma_page_size() as s, pragma_max_page_count() as c")[0][0].to_f / (1024 * 1024)
end

#prune(limit = nil) ⇒ Object

delete all entries in the cache up limit (ordered by LRU), if no limit is provided approximately 20% of the entries will be deleted



174
175
176
177
178
179
180
181
182
183
184
# File 'lib/litestack/litecache.rb', line 174

def prune(limit = nil)
  @conn.acquire do |cache|
    if limit&.is_a? Integer
      cache.stmts[:limited_pruner].execute!(limit)
    elsif limit&.is_a? Float
      cache.stmts[:extra_pruner].execute!(limit)
    else
      cache.stmts[:pruner].execute!
    end
  end
end

#set(key, value, expires_in = nil) ⇒ Object

add a key, value pair to the cache, with an optional expiry value (number of seconds)



72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/litestack/litecache.rb', line 72

def set(key, value, expires_in = nil)
  key = key.to_s
  expires_in ||= @expires_in
  @conn.acquire do |cache|
    cache.stmts[:setter].execute!(key, value, expires_in)
    capture(:set, key)
  rescue SQLite3::FullException
    cache.stmts[:extra_pruner].execute!(0.2)
    cache.execute("vacuum")
    retry
  end
  true
end

#set_multi(keys_and_values, expires_in = nil) ⇒ Object

set multiple keys and values in one shot set_multi({k1: v1, k2: v2, … })



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/litestack/litecache.rb', line 87

def set_multi(keys_and_values, expires_in = nil)
  expires_in ||= @expires_in
  transaction do |conn|
    keys_and_values.each_pair do |k, v|
      key = k.to_s
      conn.stmts[:setter].execute!(key, v, expires_in)
      capture(:set, key)
    rescue SQLite3::FullException
      conn.stmts[:extra_pruner].execute!(0.2)
      conn.execute("vacuum")
      retry
    end
  end
  true
end

#set_unless_exists(key, value, expires_in = nil) ⇒ Object

add a key, value pair to the cache, but only if the key doesn’t exist, with an optional expiry value (number of seconds)



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/litestack/litecache.rb', line 104

def set_unless_exists(key, value, expires_in = nil)
  key = key.to_s
  expires_in ||= @expires_in
  changes = 0
  @conn.acquire do |cache|
    cache.transaction(:immediate) do
      cache.stmts[:inserter].execute!(key, value, expires_in)
      changes = cache.changes
    end
    capture(:set, key)
  rescue SQLite3::FullException
    cache.stmts[:extra_pruner].execute!(0.2)
    cache.execute("vacuum")
    retry
  end
  changes > 0
end

#snapshotObject



212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/litestack/litecache.rb', line 212

def snapshot
  {
    summary: {
      path: path,
      journal_mode: journal_mode,
      synchronous: synchronous,
      size: size,
      max_size: max_size,
      entries: count
    }
  }
end

#transaction(mode = :immediate) ⇒ Object

low level access to SQLite transactions, use with caution



226
227
228
229
230
231
232
233
234
235
236
# File 'lib/litestack/litecache.rb', line 226

def transaction(mode = :immediate)
  @conn.acquire do |cache|
    if cache.transaction_active?
      yield
    else
      cache.transaction(mode) do
        yield cache
      end
    end
  end
end