litestack 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile +8 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +166 -0
  6. data/Rakefile +12 -0
  7. data/WHYLITESTACK.md +26 -0
  8. data/assets/litecache_logo_teal.png +0 -0
  9. data/assets/litedb_logo_teal.png +0 -0
  10. data/assets/litejob_logo_teal.png +0 -0
  11. data/assets/litestack_logo_teal.png +0 -0
  12. data/assets/litestack_logo_teal_large.png +0 -0
  13. data/bench/bench.rb +23 -0
  14. data/bench/bench_cache_rails.rb +67 -0
  15. data/bench/bench_cache_raw.rb +68 -0
  16. data/bench/bench_jobs_rails.rb +38 -0
  17. data/bench/bench_jobs_raw.rb +27 -0
  18. data/bench/bench_queue.rb +16 -0
  19. data/bench/bench_rails.rb +81 -0
  20. data/bench/bench_raw.rb +72 -0
  21. data/bench/rails_job.rb +18 -0
  22. data/bench/skjob.rb +13 -0
  23. data/bench/uljob.rb +15 -0
  24. data/lib/active_job/queue_adapters/litejob_adapter.rb +47 -0
  25. data/lib/active_job/queue_adapters/ultralite_adapter.rb +49 -0
  26. data/lib/active_record/connection_adapters/litedb_adapter.rb +102 -0
  27. data/lib/active_support/cache/litecache.rb +100 -0
  28. data/lib/active_support/cache/ultralite_cache_store.rb +100 -0
  29. data/lib/litestack/litecache.rb +254 -0
  30. data/lib/litestack/litedb.rb +47 -0
  31. data/lib/litestack/litejob.rb +84 -0
  32. data/lib/litestack/litejobqueue.rb +161 -0
  33. data/lib/litestack/litequeue.rb +105 -0
  34. data/lib/litestack/litesupport.rb +74 -0
  35. data/lib/litestack/version.rb +5 -0
  36. data/lib/litestack.rb +15 -0
  37. data/lib/railties/rails/commands/dbconsole.rb +87 -0
  38. data/lib/sequel/adapters/litedb.rb +43 -0
  39. data/samples/ultrajob.yaml +2 -0
  40. 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