litestack 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +166 -0
- data/Rakefile +12 -0
- data/WHYLITESTACK.md +26 -0
- data/assets/litecache_logo_teal.png +0 -0
- data/assets/litedb_logo_teal.png +0 -0
- data/assets/litejob_logo_teal.png +0 -0
- data/assets/litestack_logo_teal.png +0 -0
- data/assets/litestack_logo_teal_large.png +0 -0
- data/bench/bench.rb +23 -0
- data/bench/bench_cache_rails.rb +67 -0
- data/bench/bench_cache_raw.rb +68 -0
- data/bench/bench_jobs_rails.rb +38 -0
- data/bench/bench_jobs_raw.rb +27 -0
- data/bench/bench_queue.rb +16 -0
- data/bench/bench_rails.rb +81 -0
- data/bench/bench_raw.rb +72 -0
- data/bench/rails_job.rb +18 -0
- data/bench/skjob.rb +13 -0
- data/bench/uljob.rb +15 -0
- data/lib/active_job/queue_adapters/litejob_adapter.rb +47 -0
- data/lib/active_job/queue_adapters/ultralite_adapter.rb +49 -0
- data/lib/active_record/connection_adapters/litedb_adapter.rb +102 -0
- data/lib/active_support/cache/litecache.rb +100 -0
- data/lib/active_support/cache/ultralite_cache_store.rb +100 -0
- data/lib/litestack/litecache.rb +254 -0
- data/lib/litestack/litedb.rb +47 -0
- data/lib/litestack/litejob.rb +84 -0
- data/lib/litestack/litejobqueue.rb +161 -0
- data/lib/litestack/litequeue.rb +105 -0
- data/lib/litestack/litesupport.rb +74 -0
- data/lib/litestack/version.rb +5 -0
- data/lib/litestack.rb +15 -0
- data/lib/railties/rails/commands/dbconsole.rb +87 -0
- data/lib/sequel/adapters/litedb.rb +43 -0
- data/samples/ultrajob.yaml +2 -0
- metadata +115 -0
data/bench/skjob.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
|
3
|
+
class SidekiqJob
|
4
|
+
include Sidekiq::Job
|
5
|
+
@@count = 0
|
6
|
+
def perform(count, time)
|
7
|
+
sleep 0.01
|
8
|
+
@@count += 1
|
9
|
+
if @@count == count
|
10
|
+
puts "finished in #{Time.now.to_f - time} seconds (#{count / (Time.now.to_f - time)} jps)"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/bench/uljob.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require './bench'
|
2
|
+
require '../lib/litestack'
|
3
|
+
|
4
|
+
class MyJob
|
5
|
+
include Litejob
|
6
|
+
@@count = 0
|
7
|
+
# self.queue = :normal
|
8
|
+
def perform(count, time)
|
9
|
+
sleep 1
|
10
|
+
@@count += 1
|
11
|
+
if @@count == count
|
12
|
+
puts "UL finished in #{Time.now.to_f - time} seconds (#{count / (Time.now.to_f - time)} jps)"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../litestack/litejob.rb'
|
4
|
+
require "active_support/core_ext/enumerable"
|
5
|
+
require "active_support/core_ext/array/access"
|
6
|
+
require "active_job"
|
7
|
+
|
8
|
+
module ActiveJob
|
9
|
+
module QueueAdapters
|
10
|
+
# == Ultralite adapter for Active Job
|
11
|
+
#
|
12
|
+
#
|
13
|
+
# Rails.application.config.active_job.queue_adapter = :litejob
|
14
|
+
class LitejobAdapter
|
15
|
+
|
16
|
+
DEFAULT_OPTIONS = {
|
17
|
+
config_path: "./config/litejob.yml",
|
18
|
+
path: "../db/queue.db",
|
19
|
+
queues: [["default", 1, "spawn"]],
|
20
|
+
workers: 1
|
21
|
+
}
|
22
|
+
|
23
|
+
def initialize(options={})
|
24
|
+
Job.options = DEFAULT_OPTIONS.merge(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def enqueue(job) # :nodoc:
|
28
|
+
Job.queue = job.queue_name
|
29
|
+
Job.perform_async(job.serialize)
|
30
|
+
end
|
31
|
+
|
32
|
+
def enqueue_at(job, timestamp) # :nodoc:
|
33
|
+
Job.queue = job.queue_name
|
34
|
+
Job.perform_at(timestamp, job.serialize)
|
35
|
+
end
|
36
|
+
|
37
|
+
class Job # :nodoc:
|
38
|
+
|
39
|
+
include ::Litejob
|
40
|
+
|
41
|
+
def perform(job_data)
|
42
|
+
Base.execute job_data
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../ultralite/job.rb'
|
4
|
+
require "active_support/core_ext/enumerable"
|
5
|
+
require "active_support/core_ext/array/access"
|
6
|
+
require "active_job"
|
7
|
+
|
8
|
+
module ActiveJob
|
9
|
+
module QueueAdapters
|
10
|
+
# == Ultralite adapter for Active Job
|
11
|
+
#
|
12
|
+
#
|
13
|
+
# Rails.application.config.active_job.queue_adapter = :ultralite
|
14
|
+
class UltraliteAdapter
|
15
|
+
|
16
|
+
DEFAULT_OPTIONS = {
|
17
|
+
config_path: "./config/ultrajob.yml",
|
18
|
+
path: "../db/queue.db",
|
19
|
+
queues: [["default", 1, "spawn"]],
|
20
|
+
workers: 1
|
21
|
+
}
|
22
|
+
|
23
|
+
DEFAULT_CONFIG_PATH = "./config/ultrajob.yml"
|
24
|
+
|
25
|
+
def initialize(options={})
|
26
|
+
Job.options = DEFAULT_OPTIONS.merge(options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def enqueue(job) # :nodoc:
|
30
|
+
Job.queue = job.queue_name
|
31
|
+
Job.perform_async(job.serialize)
|
32
|
+
end
|
33
|
+
|
34
|
+
def enqueue_at(job, timestamp) # :nodoc:
|
35
|
+
Job.queue = job.queue_name
|
36
|
+
Job.perform_at(timestamp, job.serialize)
|
37
|
+
end
|
38
|
+
|
39
|
+
class Job # :nodoc:
|
40
|
+
|
41
|
+
include ::Ultralite::Job
|
42
|
+
|
43
|
+
def perform(job_data)
|
44
|
+
Base.execute job_data
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require_relative '../../litestack/litedb'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_record/connection_adapters/sqlite3_adapter'
|
4
|
+
require 'active_record/tasks/sqlite_database_tasks'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
|
8
|
+
module ConnectionHandling # :nodoc:
|
9
|
+
|
10
|
+
def litedb_connection(config)
|
11
|
+
|
12
|
+
config = config.symbolize_keys
|
13
|
+
|
14
|
+
# Require database.
|
15
|
+
unless config[:database]
|
16
|
+
raise ArgumentError, "No database file specified. Missing argument: database"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Allow database path relative to Rails.root, but only if the database
|
20
|
+
# path is not the special path that tells sqlite to build a database only
|
21
|
+
# in memory.
|
22
|
+
if ":memory:" != config[:database] && !config[:database].to_s.start_with?("file:")
|
23
|
+
config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
|
24
|
+
dirname = File.dirname(config[:database])
|
25
|
+
Dir.mkdir(dirname) unless File.directory?(dirname)
|
26
|
+
end
|
27
|
+
|
28
|
+
db = ::Litedb.new(
|
29
|
+
config[:database].to_s,
|
30
|
+
config.merge(results_as_hash: true)
|
31
|
+
)
|
32
|
+
|
33
|
+
ConnectionAdapters::LitedbAdapter.new(db, logger, nil, config)
|
34
|
+
|
35
|
+
rescue Errno::ENOENT => error
|
36
|
+
if error.message.include?("No such file or directory")
|
37
|
+
raise ActiveRecord::NoDatabaseError
|
38
|
+
else
|
39
|
+
raise
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ConnectionAdapters # :nodoc:
|
45
|
+
|
46
|
+
class LitedbAdapter < SQLite3Adapter
|
47
|
+
|
48
|
+
ADAPTER_NAME = "litedb"
|
49
|
+
|
50
|
+
class << self
|
51
|
+
|
52
|
+
def dbconsole(config, options = {})
|
53
|
+
args = []
|
54
|
+
|
55
|
+
args << "-#{options[:mode]}" if options[:mode]
|
56
|
+
args << "-header" if options[:header]
|
57
|
+
args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil)
|
58
|
+
|
59
|
+
find_cmd_and_exec("sqlite3", *args)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
NATIVE_DATABASE_TYPES = {
|
65
|
+
primary_key: "integer PRIMARY KEY NOT NULL",
|
66
|
+
string: { name: "text" },
|
67
|
+
text: { name: "text" },
|
68
|
+
integer: { name: "integer" },
|
69
|
+
float: { name: "real" },
|
70
|
+
decimal: { name: "real" },
|
71
|
+
datetime: { name: "text" },
|
72
|
+
time: { name: "integer" },
|
73
|
+
date: { name: "text" },
|
74
|
+
binary: { name: "blob" },
|
75
|
+
boolean: { name: "integer" },
|
76
|
+
json: { name: "text" },
|
77
|
+
unixtime: { name: "integer" }
|
78
|
+
}
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def connect
|
83
|
+
@raw_connection = ::Litedb.new(
|
84
|
+
@config[:database].to_s,
|
85
|
+
@config.merge(results_as_hash: true)
|
86
|
+
)
|
87
|
+
configure_connection
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
module Tasks # :nodoc:
|
94
|
+
class LitedbDatabaseTasks < SQLiteDatabaseTasks # :nodoc:
|
95
|
+
end
|
96
|
+
|
97
|
+
module DatabaseTasks
|
98
|
+
register_task(/ultralite/, "ActiveRecord::Tasks::LitedbDatabaseTasks")
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "delegate"
|
2
|
+
require "active_support/core_ext/enumerable"
|
3
|
+
require "active_support/core_ext/array/extract_options"
|
4
|
+
require "active_support/core_ext/numeric/time"
|
5
|
+
require "active_support/cache"
|
6
|
+
require_relative '../../litestack'
|
7
|
+
|
8
|
+
|
9
|
+
module ActiveSupport
|
10
|
+
module Cache
|
11
|
+
class Litecache < Store
|
12
|
+
|
13
|
+
prepend Strategy::LocalCache
|
14
|
+
|
15
|
+
def self.supports_cache_versioning?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options={})
|
20
|
+
super
|
21
|
+
@options[:return_full_record] = true
|
22
|
+
@cache = ::Litecache.new(@options) # reachout to the outer litecache class
|
23
|
+
end
|
24
|
+
|
25
|
+
def increment(key, amount = 1, options = nil)
|
26
|
+
key = key.to_s
|
27
|
+
options = merged_options(options)
|
28
|
+
@cache.transaction(:immediate) do
|
29
|
+
if value = read(key, options)
|
30
|
+
value = value.to_i + amount
|
31
|
+
write(key, value, options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def decrement(key, amount = 1, options = nil)
|
37
|
+
options = merged_options(options)
|
38
|
+
increment(key, -1 * amount, options[:expires_in])
|
39
|
+
end
|
40
|
+
|
41
|
+
def prune(limit = nil, time = nil)
|
42
|
+
@cache.prune(limit)
|
43
|
+
end
|
44
|
+
|
45
|
+
def cleanup(limit = nil, time = nil)
|
46
|
+
@cache.prune(limit)
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear()
|
50
|
+
@cache.clear
|
51
|
+
end
|
52
|
+
|
53
|
+
def count
|
54
|
+
@cache.count
|
55
|
+
end
|
56
|
+
|
57
|
+
def size
|
58
|
+
@cache.size
|
59
|
+
end
|
60
|
+
|
61
|
+
def max_size
|
62
|
+
@cache.max_size
|
63
|
+
end
|
64
|
+
|
65
|
+
def stats
|
66
|
+
@cache.stats
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Read an entry from the cache.
|
72
|
+
def read_entry(key, **options)
|
73
|
+
deserialize_entry(@cache.get(key))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Write an entry to the cache.
|
77
|
+
def write_entry(key, entry, **options)
|
78
|
+
write_serialized_entry(key, serialize_entry(entry, **options), **options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def write_serialized_entry(key, payload, **options)
|
82
|
+
expires_in = options[:expires_in].to_i
|
83
|
+
if options[:race_condition_ttl] && expires_in > 0 && !options[:raw]
|
84
|
+
expires_in += 5.minutes
|
85
|
+
end
|
86
|
+
if options[:unless_exist]
|
87
|
+
@cache.set_unless_exists(key, payload, expires_in)
|
88
|
+
else
|
89
|
+
@cache.set(key, payload, expires_in)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Delete an entry from the cache.
|
94
|
+
def delete_entry(key, **options)
|
95
|
+
return @cache.delete(key)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "delegate"
|
2
|
+
require "active_support/core_ext/enumerable"
|
3
|
+
require "active_support/core_ext/array/extract_options"
|
4
|
+
require "active_support/core_ext/numeric/time"
|
5
|
+
require "active_support/cache"
|
6
|
+
require_relative '../../ultralite/cache.rb'
|
7
|
+
|
8
|
+
|
9
|
+
module ActiveSupport
|
10
|
+
module Cache
|
11
|
+
class UltraliteCacheStore < Store
|
12
|
+
|
13
|
+
#prepend Strategy::LocalCache
|
14
|
+
|
15
|
+
def self.supports_cache_versioning?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(options={})
|
20
|
+
super
|
21
|
+
@options[:return_full_record] = true
|
22
|
+
@cache = ::Ultralite::Cache.new(@options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def increment(key, amount = 1, options = nil)
|
26
|
+
key = key.to_s
|
27
|
+
options = merged_options(options)
|
28
|
+
@cache.transaction(:immediate) do
|
29
|
+
if value = read(key, options)
|
30
|
+
value = value.to_i + amount
|
31
|
+
write(key, value, options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def decrement(key, amount = 1, options = nil)
|
37
|
+
options = merged_options(options)
|
38
|
+
increment(key, -1 * amount, options[:expires_in])
|
39
|
+
end
|
40
|
+
|
41
|
+
def prune(limit = nil, time = nil)
|
42
|
+
@cache.prune(limit)
|
43
|
+
end
|
44
|
+
|
45
|
+
def cleanup(limit = nil, time = nil)
|
46
|
+
@cache.prune(limit)
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear()
|
50
|
+
@cache.clear
|
51
|
+
end
|
52
|
+
|
53
|
+
def count
|
54
|
+
@cache.count
|
55
|
+
end
|
56
|
+
|
57
|
+
def size
|
58
|
+
@cache.size
|
59
|
+
end
|
60
|
+
|
61
|
+
def max_size
|
62
|
+
@cache.max_size
|
63
|
+
end
|
64
|
+
|
65
|
+
def stats
|
66
|
+
@cache.stats
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Read an entry from the cache.
|
72
|
+
def read_entry(key, **options)
|
73
|
+
deserialize_entry(@cache.get(key))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Write an entry to the cache.
|
77
|
+
def write_entry(key, entry, **options)
|
78
|
+
write_serialized_entry(key, serialize_entry(entry, **options), **options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def write_serialized_entry(key, payload, **options)
|
82
|
+
expires_in = options[:expires_in].to_i
|
83
|
+
if options[:race_condition_ttl] && expires_in > 0 && !options[:raw]
|
84
|
+
expires_in += 5.minutes
|
85
|
+
end
|
86
|
+
if options[:unless_exist]
|
87
|
+
@cache.set_unless_exists(key, payload, expires_in)
|
88
|
+
else
|
89
|
+
@cache.set(key, payload, expires_in)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Delete an entry from the cache.
|
94
|
+
def delete_entry(key, **options)
|
95
|
+
return @cache.delete(key)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
# frozen_stringe_literal: true
|
2
|
+
|
3
|
+
# all components should require the support module
|
4
|
+
require_relative 'litesupport'
|
5
|
+
|
6
|
+
##
|
7
|
+
#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.
|
8
|
+
#
|
9
|
+
#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.
|
10
|
+
#
|
11
|
+
#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.
|
12
|
+
#
|
13
|
+
#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.
|
14
|
+
#
|
15
|
+
#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.
|
16
|
+
|
17
|
+
class Litecache
|
18
|
+
|
19
|
+
# the default options for the cache
|
20
|
+
# can be overriden by passing new options in a hash
|
21
|
+
# to Litecache.new
|
22
|
+
# path: "./cache.db"
|
23
|
+
# expiry: 60 * 60 * 24 * 30 -> one month default expiry if none is provided
|
24
|
+
# size: 128 * 1024 * 1024 -> 128MB
|
25
|
+
# mmap_size: 128 * 1024 * 1024 -> 128MB to be held in memory
|
26
|
+
# min_size: 32 * 1024 -> 32MB
|
27
|
+
# return_full_record: false -> only return the payload
|
28
|
+
# sleep_interval: 1 -> 1 second of sleep between cleanup runs
|
29
|
+
|
30
|
+
DEFAULT_OPTIONS = {
|
31
|
+
path: "./cache.db",
|
32
|
+
expiry: 60 * 60 * 24 * 30, # one month
|
33
|
+
size: 128 * 1024 * 1024, #128MB
|
34
|
+
mmap_size: 128 * 1024 * 1024, #128MB
|
35
|
+
min_size: 32 * 1024, #32MB
|
36
|
+
return_full_record: false, #only return the payload
|
37
|
+
sleep_interval: 1 # 1 second
|
38
|
+
}
|
39
|
+
|
40
|
+
# creates a new instance of Litecache
|
41
|
+
# can optionally receive an options hash which will be merged
|
42
|
+
# with the DEFAULT_OPTIONS (the new hash overrides any matching keys in the default one).
|
43
|
+
#
|
44
|
+
# Example:
|
45
|
+
# litecache = Litecache.new
|
46
|
+
#
|
47
|
+
# litecache.set("a", "somevalue")
|
48
|
+
# litecache.get("a") # => "somevalue"
|
49
|
+
#
|
50
|
+
# litecache.set("b", "othervalue", 1) # expire aftre 1 second
|
51
|
+
# litecache.get("b") # => "othervalue"
|
52
|
+
# sleep 2
|
53
|
+
# litecache.get("b") # => nil
|
54
|
+
#
|
55
|
+
# litecache.clear # nothing remains in the cache
|
56
|
+
# litecache.close # optional, you can safely kill the process
|
57
|
+
|
58
|
+
def initialize(options = {})
|
59
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
60
|
+
@options[:size] = @options[:min_size] if @options[:size] < @options[:min_size]
|
61
|
+
@cache = create_store
|
62
|
+
@stmts = {
|
63
|
+
:pruner => @cache.prepare("DELETE FROM data WHERE expires_in <= $1"),
|
64
|
+
:extra_pruner => @cache.prepare("DELETE FROM data WHERE id IN (SELECT id FROM data ORDER BY last_used ASC LIMIT (SELECT CAST((count(*) * $1) AS int) FROM data))"),
|
65
|
+
:limited_pruner => @cache.prepare("DELETE FROM data WHERE id IN (SELECT id FROM data ORDER BY last_used asc limit $1)"),
|
66
|
+
:toucher => @cache.prepare("UPDATE data SET last_used = unixepoch('now') WHERE id = $1"),
|
67
|
+
:setter => @cache.prepare("INSERT into data (id, value, expires_in, last_used) VALUES ($1, $2, unixepoch('now') + $3, unixepoch('now')) on conflict(id) do UPDATE SET value = excluded.value, last_used = excluded.last_used, expires_in = excluded.expires_in"),
|
68
|
+
:inserter => @cache.prepare("INSERT into data (id, value, expires_in, last_used) VALUES ($1, $2, unixepoch('now') + $3, unixepoch('now')) on conflict(id) do UPDATE SET value = excluded.value, last_used = excluded.last_used, expires_in = excluded.expires_in WHERE id = $1 and expires_in <= unixepoch('now')"),
|
69
|
+
:finder => @cache.prepare("SELECT id FROM data WHERE id = $1"),
|
70
|
+
:getter => @cache.prepare("SELECT id, value, expires_in FROM data WHERE id = $1"),
|
71
|
+
:deleter => @cache.prepare("delete FROM data WHERE id = $1 returning value"),
|
72
|
+
:incrementer => @cache.prepare("INSERT into data (id, value, expires_in, last_used) VALUES ($1, $2, unixepoch('now') + $3, unixepoch('now')) on conflict(id) do UPDATE SET value = cast(value AS int) + cast(excluded.value as int), last_used = excluded.last_used, expires_in = excluded.expires_in"),
|
73
|
+
:counter => @cache.prepare("SELECT count(*) FROM data"),
|
74
|
+
:sizer => @cache.prepare("SELECT size.page_size * count.page_count FROM pragma_page_size() AS size, pragma_page_count() AS count")
|
75
|
+
}
|
76
|
+
@stats = {hit: 0, miss: 0}
|
77
|
+
@last_visited = {}
|
78
|
+
@running = true
|
79
|
+
@bgthread = spawn_worker
|
80
|
+
end
|
81
|
+
|
82
|
+
# add a key, value pair to the cache, with an optional expiry value (number of seconds)
|
83
|
+
def set(key, value, expires_in = nil)
|
84
|
+
key = key.to_s
|
85
|
+
expires_in = @options[:expires_in] if expires_in.nil? or expires_in.zero?
|
86
|
+
Litesupport.synchronize do
|
87
|
+
begin
|
88
|
+
@stmts[:setter].execute!(key, value, expires_in)
|
89
|
+
rescue SQLite3::FullException
|
90
|
+
@stmts[:extra_pruner].execute!(0.2)
|
91
|
+
@cache.execute("vacuum")
|
92
|
+
retry
|
93
|
+
end
|
94
|
+
end
|
95
|
+
return true
|
96
|
+
end
|
97
|
+
|
98
|
+
# add a key, value pair to the cache, but only if the key doesn't exist, with an optional expiry value (number of seconds)
|
99
|
+
def set_unless_exists(key, value, expires_in = nil)
|
100
|
+
key = key.to_s
|
101
|
+
expires_in = @options[:expires_in] if expires_in.nil? or expires_in.zero?
|
102
|
+
Litesupport.synchronize do
|
103
|
+
begin
|
104
|
+
transaction(:immediate) do
|
105
|
+
@stmts[:inserter].execute!(key, value, expires_in)
|
106
|
+
changes = @cache.changes
|
107
|
+
end
|
108
|
+
rescue SQLite3::FullException
|
109
|
+
@stmts[:extra_pruner].execute!(0.2)
|
110
|
+
@cache.execute("vacuum")
|
111
|
+
retry
|
112
|
+
end
|
113
|
+
end
|
114
|
+
return changes > 0
|
115
|
+
end
|
116
|
+
|
117
|
+
# get a value by its key
|
118
|
+
# if the key doesn't exist or it is expired then null will be returned
|
119
|
+
def get(key)
|
120
|
+
key = key.to_s
|
121
|
+
record = nil
|
122
|
+
Litesupport.synchronize do
|
123
|
+
record = @stmts[:getter].execute!(key)[0]
|
124
|
+
end
|
125
|
+
if record
|
126
|
+
@last_visited[key] = true
|
127
|
+
@stats[:hit] +=1
|
128
|
+
return record[1]
|
129
|
+
end
|
130
|
+
@stats[:miss] += 1
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# delete a key, value pair from the cache
|
135
|
+
def delete(key)
|
136
|
+
Litesupport.synchronize do
|
137
|
+
@stmts[:deleter].execute!(key)
|
138
|
+
return @cache.changes > 0
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# increment an integer value by amount, optionally add an expiry value (in seconds)
|
143
|
+
def increment(key, amount, expires_in = nil)
|
144
|
+
expires_in = @expires_in unless expires_in
|
145
|
+
Litesupport.synchronize do
|
146
|
+
@stmts[:incrementer].execute!(key.to_s, amount, expires_in)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# decrement an integer value by amount, optionally add an expiry value (in seconds)
|
151
|
+
def decrement(key, amount, expires_in = nil)
|
152
|
+
increment(key, -amount, expires_in)
|
153
|
+
end
|
154
|
+
|
155
|
+
# delete all entries in the cache up limit (ordered by LRU), if no limit is provided approximately 20% of the entries will be deleted
|
156
|
+
def prune(limit=nil)
|
157
|
+
Litesupport.synchronize do
|
158
|
+
if limit and limit.is_a? Integer
|
159
|
+
@stmts[:limited_pruner].execute!(limit)
|
160
|
+
elsif limit and limit.is_a? Float
|
161
|
+
@stmts[:extra_pruner].execute!(limit)
|
162
|
+
else
|
163
|
+
@stmts[:pruner].execute!
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# return the number of key, value pairs in the cache
|
169
|
+
def count
|
170
|
+
Litesupport.synchronize do
|
171
|
+
@stmts[:counter].execute!.to_a[0][0]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# return the actual size of the cache file
|
176
|
+
def size
|
177
|
+
Litesupport.synchronize do
|
178
|
+
@stmts[:sizer].execute!.to_a[0][0]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# delete all key, value pairs in the cache
|
183
|
+
def clear
|
184
|
+
Litesupport.synchronize do
|
185
|
+
@cache.execute("delete FROM data")
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# close the connection to the cache file
|
190
|
+
def close
|
191
|
+
@running = false
|
192
|
+
#Litesupport.synchronize do
|
193
|
+
#@cache.close
|
194
|
+
#end
|
195
|
+
end
|
196
|
+
|
197
|
+
# return the maximum size of the cache
|
198
|
+
def max_size
|
199
|
+
Litesupport.synchronize do
|
200
|
+
@cache.get_first_value("SELECT s.page_size * c.max_page_count FROM pragma_page_size() as s, pragma_max_page_count() as c")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# hits and misses for get operations performed over this particular connection (not cache wide)
|
205
|
+
#
|
206
|
+
# litecache.stats # => {hit: 543, miss: 31}
|
207
|
+
def stats
|
208
|
+
@stats
|
209
|
+
end
|
210
|
+
|
211
|
+
# low level access to SQLite transactions, use with caution
|
212
|
+
def transaction(mode)
|
213
|
+
@cache.transaction(mode) do
|
214
|
+
yield
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def spawn_worker
|
221
|
+
Litesupport.spawn do
|
222
|
+
while @running
|
223
|
+
Litesupport.synchronize do
|
224
|
+
begin
|
225
|
+
@cache.transaction(:immediate) do
|
226
|
+
@last_visited.delete_if do |k|
|
227
|
+
@stmts[:toucher].execute!(k) || true
|
228
|
+
end
|
229
|
+
@stmts[:pruner].execute!
|
230
|
+
end
|
231
|
+
rescue SQLite3::FullException
|
232
|
+
@stmts[:extra_pruner].execute!(0.2)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
sleep @options[:sleep_interval]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def create_store
|
241
|
+
db = Litesupport.create_db(@options[:path])
|
242
|
+
db.synchronous = 0
|
243
|
+
db.cache_size = 2000
|
244
|
+
db.journal_size_limit = [(@options[:size]/2).to_i, @options[:min_size]].min
|
245
|
+
db.mmap_size = @options[:mmap_size]
|
246
|
+
db.max_page_count = (@options[:size] / db.page_size).to_i
|
247
|
+
db.case_sensitive_like = true
|
248
|
+
db.execute("CREATE table if not exists data(id text primary key, value text, expires_in integer, last_used integer)")
|
249
|
+
db.execute("CREATE index if not exists expiry_index on data (expires_in)")
|
250
|
+
db.execute("CREATE index if not exists last_used_index on data (last_used)")
|
251
|
+
db
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|