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



199
200
201
# File 'lib/litestack/litecache.rb', line 199

def clear
  run_sql("delete FROM data")
end

#closeObject

close the connection to the cache file



204
205
206
207
# File 'lib/litestack/litecache.rb', line 204

def close
  @running = false
  super
end

#countObject

return the number of key, value pairs in the cache



189
190
191
# File 'lib/litestack/litecache.rb', line 189

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

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

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



171
172
173
# File 'lib/litestack/litecache.rb', line 171

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

#delete(key) ⇒ Object

delete a key, value pair from the cache



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

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



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

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,



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

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, expires_in = nil) ⇒ Object

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



165
166
167
168
# File 'lib/litestack/litecache.rb', line 165

def increment(key, amount, 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



210
211
212
# File 'lib/litestack/litecache.rb', line 210

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



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

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
102
103
# 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|
      begin
        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
  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)



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

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



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

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



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

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