sequel-unicache 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +144 -0
- data/Rakefile +11 -0
- data/lib/sequel/unicache.rb +18 -0
- data/lib/sequel/unicache/configuration.rb +111 -0
- data/lib/sequel/unicache/expire.rb +19 -0
- data/lib/sequel/unicache/finder.rb +52 -0
- data/lib/sequel/unicache/global_configuration.rb +79 -0
- data/lib/sequel/unicache/hook.rb +56 -0
- data/lib/sequel/unicache/logger.rb +15 -0
- data/lib/sequel/unicache/transaction.rb +20 -0
- data/lib/sequel/unicache/version.rb +5 -0
- data/lib/sequel/unicache/write.rb +122 -0
- data/sequel-unicache.gemspec +35 -0
- data/spec/configuration_spec.rb +121 -0
- data/spec/finder_spec.rb +173 -0
- data/spec/global_configuration_spec.rb +56 -0
- data/spec/log_spec.rb +28 -0
- data/spec/memcache.yml.example +8 -0
- data/spec/spec_helper.rb +96 -0
- data/spec/write_spec.rb +277 -0
- metadata +221 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7312873a5f6af6b72a09b5a4711198ecd8dc437a
|
4
|
+
data.tar.gz: 16fa4a4d621007f6e721161f7083a4ef0aaed761
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 87c3e2659afbacf1efe0378f7521c991e5595330e74adcfee6798aec6d54962ab741b8b7e7e2d3d7b77aa4e8550287870d0df166222c2307c991613965232eed
|
7
|
+
data.tar.gz: c4f9ce36433aa0f91dcc7822d5a04e6413ef524565aae5e442c81d0d83ea2eb10d342f4a590ece8e4fbb908de78eeccc1679a6c3abca0c9197a9b4c87a67d327
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Bachue Zhou
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
[](https://travis-ci.org/bachue/sequel-unicache)
|
2
|
+
|
3
|
+
# Sequel Unicache
|
4
|
+
|
5
|
+
Read through caching library inspired by Cache Money, support Sequel 4
|
6
|
+
|
7
|
+
Read-Through: Queries by ID or any specified unique key, like `User[params[:id]]` or `User[username: 'bachue@gmail.com']`, will first look in Memcache/Redis store and then look in the database for the results of that query. If there is a cache miss, it will populate the cache. As objects are updated and deleted, all of the caches are automatically expired.
|
8
|
+
|
9
|
+
## Dependency
|
10
|
+
|
11
|
+
Ruby >= 2.1.0
|
12
|
+
Sequel >= 4.0
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'sequel-unicache'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install sequel-unicache
|
29
|
+
|
30
|
+
## Configuration
|
31
|
+
|
32
|
+
You must configure Unicache during initialization, for Rails, create a file in config/initializers and copy the code into it will be acceptable.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
Sequel::Unicache.configure cache: Dalli::Client.new('localhost:11211'), # Required, object to manipulate memcache or redis
|
36
|
+
ttl: 60, # Expiration time, by default it's 0, means won't expire
|
37
|
+
serialize: {|values, opts| Marshal.dump(values) }, # Serialization method,
|
38
|
+
# by default it's Marshal (fast, Ruby native-supported, non-portable)
|
39
|
+
deserialize: {|cache, opts| Marshal.load(cache) }, # Deserialization method
|
40
|
+
key: {|hash, opts| "#{opts.model_class.name}/{hash[:id]}" }, # Cache key generation method
|
41
|
+
enabled: true, # Enabled on all Sequel::Model subclasses by default
|
42
|
+
logger: Logger.new(STDOUT) # Logger, needed when debug
|
43
|
+
|
44
|
+
# Read & write global configuration by key:
|
45
|
+
Sequel::Unicache.config.ttl # 60
|
46
|
+
Sequel::Unicache.config.ttl = 20
|
47
|
+
```
|
48
|
+
|
49
|
+
Unicache configuration has 3 levels, global-level, configuration-level and key-level, which is the most flexible.
|
50
|
+
|
51
|
+
## Usage
|
52
|
+
|
53
|
+
For example, cache User object:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class User < Sequel::Model
|
57
|
+
# class level configuration, for all unicache keys of the model
|
58
|
+
unicache if: {|user, opts| !user.deleted? }, # don't cache it if model is deleted
|
59
|
+
ttl: 30, # Specify the cache expiration time (unit: second), will overwrite the default configuration
|
60
|
+
cache: Dalli::Client.new('localhost:11211'), # Memcache/Redis store, will overwrite the default configuration
|
61
|
+
serialize: {|values, opts| values.to_msgpack }, # Serialization method, will overwrite the global configuration
|
62
|
+
deserialize: {|cache, opts| MessagePack.unpack(cache) }, # Deserialization method, will overwrite the global configuration
|
63
|
+
key: {|hash, opts| "users/#{hash[:id]}" }, # Cache key generation method, will overwrite the global configuration
|
64
|
+
logger: Logger.new(STDERR), # Object for log, will overwrite the global configuration
|
65
|
+
|
66
|
+
# by default primary key is always unique cache key, all settings will just follow global configuration and class configuration
|
67
|
+
# key level configuration for username
|
68
|
+
unicache :username, # username will also be an unique key (username should has unique index in database, and never be null)
|
69
|
+
ttl: 60 # will override the global and class configuration
|
70
|
+
|
71
|
+
unicache :company_name, :department, :employee_id # company_name, department, employee_id have complexed unique index
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
Then it will fetch cached object in this situations:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
User[1]
|
79
|
+
User[username: 'bachue@gmail.com']
|
80
|
+
User.find 1
|
81
|
+
User.find username: 'bachue@gmail.com'
|
82
|
+
|
83
|
+
User[company_name: 'EMC', employee_id: '12345']
|
84
|
+
User.find company_name: 'EMC', employee_id: '12345'
|
85
|
+
article.user
|
86
|
+
```
|
87
|
+
|
88
|
+
Cache expiration methods:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
user.expire_unicache
|
92
|
+
```
|
93
|
+
|
94
|
+
You can temporarily suspend / unsuspend read-through on runtime:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# These three APIs are thread-safe
|
98
|
+
Sequel::Unicache.suspend_unicache
|
99
|
+
Sequel::Unicache.unsuspend_unicache
|
100
|
+
Sequel::Unicache.suspend_unicache do
|
101
|
+
User[1] # query database directly, and won't write model into cache
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
Even if read-through is suspended, model modification or deletion will still expire the cache, don't worry about it.
|
106
|
+
|
107
|
+
Within a transaction, read-through will also be suspended, then you can query data from transaction rather than cache.
|
108
|
+
|
109
|
+
You're not supposed to enable Unicache during the testing or development. These methods can help to enable or disable all Unicache features.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# Notice: These three APIs are not thread-safe, do not call then on runtime!
|
113
|
+
Sequel::Unicache.enable
|
114
|
+
Sequel::Unicache.disable
|
115
|
+
Sequel::Unicache.enabled?
|
116
|
+
```
|
117
|
+
|
118
|
+
But if Unicache is disabled, no expiration any more, cache could be dirty because of that.
|
119
|
+
|
120
|
+
Unicache won't expire cache until you create, update or delete a model and commit the transaction successfully.
|
121
|
+
|
122
|
+
If you reload a model, cache will also be updated.
|
123
|
+
|
124
|
+
## Notice
|
125
|
+
|
126
|
+
* Database must support transaction.
|
127
|
+
|
128
|
+
* You must call Sequel APIs as the document mentioned then cache can work.
|
129
|
+
|
130
|
+
* You must set primary key before you call any Unicache DSL if you need.
|
131
|
+
|
132
|
+
* If you want to configure both class-level and key-level for a model, configure class-level first.
|
133
|
+
|
134
|
+
* Unicache use hook to expire cache.
|
135
|
+
If someone update database by SQL directly (even Sequel APIs like `User.insert`, `user.delete` or `User.db.[]` won't be supported) or by another project without unicache, then cache in Memcache/Redis won't be expired automatically.
|
136
|
+
You must expire cache manually or by another mechanism.
|
137
|
+
|
138
|
+
## Contributing
|
139
|
+
|
140
|
+
1. Fork it ( https://github.com/bachue/sequel-unicache/fork )
|
141
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
142
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
143
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
144
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
desc 'Run all test cases'
|
4
|
+
task :test do
|
5
|
+
root = File.expand_path __dir__
|
6
|
+
specs = "#{root}/spec"
|
7
|
+
spec_helper = root + '/spec/spec_helper'
|
8
|
+
exec 'bundle', 'exec', 'rspec', '-f', 'd',
|
9
|
+
'-I', root, '-r', spec_helper, '-b', '-c', specs
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'sequel/unicache/version'
|
3
|
+
require 'sequel/unicache/global_configuration'
|
4
|
+
require 'sequel/unicache/configuration'
|
5
|
+
require 'sequel/unicache/finder'
|
6
|
+
require 'sequel/unicache/expire'
|
7
|
+
|
8
|
+
module Sequel
|
9
|
+
module Unicache
|
10
|
+
extend GlobalConfiguration::ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
class Model
|
14
|
+
extend Unicache::Configuration::ClassMethods
|
15
|
+
extend Unicache::Finder::ClassMethods
|
16
|
+
include Unicache::Expire::InstanceMethods
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'sequel/unicache/global_configuration'
|
2
|
+
require 'sequel/unicache/hook'
|
3
|
+
require 'sequel/unicache/transaction'
|
4
|
+
|
5
|
+
module Sequel
|
6
|
+
module Unicache
|
7
|
+
class Configuration < GlobalConfiguration
|
8
|
+
%i(if model_class unicache_keys).each do |attr|
|
9
|
+
define_method(attr) { @opts[attr] }
|
10
|
+
define_method("#{attr}=") { |val| @opts[attr] = val }
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
private
|
15
|
+
# Configure for specfied model
|
16
|
+
def unicache *args
|
17
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
18
|
+
Utils.initialize_unicache_for_class self unless @unicache_class_configuration # Initialize class first
|
19
|
+
if args.empty? # class-level configuration
|
20
|
+
config = Unicache.config.to_h.merge opts
|
21
|
+
@unicache_class_configuration = Configuration.new config.merge model_class: self
|
22
|
+
else # key-level configuration
|
23
|
+
Utils.initialize_unicache_for_key self unless @unicache_key_configurations # Initialize key
|
24
|
+
key = Utils.normalize_key_for_unicache args
|
25
|
+
config = Unicache.config.to_h.merge @unicache_class_configuration.to_h.merge(opts)
|
26
|
+
config.merge! unicache_keys: key
|
27
|
+
@unicache_key_configurations[key] = Configuration.new config
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize_unicache
|
32
|
+
Utils.initialize_unicache_for_class self unless @unicache_class_configuration # Initialize class first
|
33
|
+
Utils.initialize_unicache_for_key self unless @unicache_key_configurations # Initialize key
|
34
|
+
end
|
35
|
+
|
36
|
+
public
|
37
|
+
# Read configuration for specified model
|
38
|
+
def unicache_for *key, fuzzy: false
|
39
|
+
initialize_unicache
|
40
|
+
if fuzzy
|
41
|
+
config = Utils.fuzzy_search_for key, @unicache_key_configurations
|
42
|
+
else
|
43
|
+
key = Utils.normalize_key_for_unicache key
|
44
|
+
config = @unicache_key_configurations[key]
|
45
|
+
end
|
46
|
+
config
|
47
|
+
end
|
48
|
+
|
49
|
+
def unicache_enabled_for? *key # config.enabled must be true, config.cache must be existed
|
50
|
+
if key.first.is_a?(Configuration)
|
51
|
+
config = key.first
|
52
|
+
else
|
53
|
+
config = unicache_for(*key)
|
54
|
+
end
|
55
|
+
config.cache && config.enabled if config
|
56
|
+
end
|
57
|
+
|
58
|
+
def unicache_class_configuration
|
59
|
+
initialize_unicache
|
60
|
+
@unicache_class_configuration
|
61
|
+
end
|
62
|
+
|
63
|
+
def unicache_configurations
|
64
|
+
initialize_unicache
|
65
|
+
@unicache_key_configurations
|
66
|
+
end
|
67
|
+
|
68
|
+
class Utils
|
69
|
+
class << self
|
70
|
+
def initialize_unicache_for_class model_class
|
71
|
+
model_class.instance_exec do
|
72
|
+
plugin :dirty
|
73
|
+
class_config = Unicache.config.to_h.merge model_class: model_class
|
74
|
+
@unicache_class_configuration = Configuration.new class_config
|
75
|
+
end
|
76
|
+
Hook.install_hooks_for_unicache
|
77
|
+
Transaction.install_hooks_for_unicache
|
78
|
+
end
|
79
|
+
|
80
|
+
def initialize_unicache_for_key model_class
|
81
|
+
model_class.instance_exec do
|
82
|
+
@unicache_key_configurations = {}
|
83
|
+
if primary_key
|
84
|
+
pk_config = @unicache_class_configuration.to_h.merge unicache_keys: model_class.primary_key
|
85
|
+
@unicache_key_configurations[primary_key] = Configuration.new pk_config
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def normalize_key_for_unicache keys
|
91
|
+
keys.size == 1 ? keys.first.to_sym : keys.sort.map(&:to_sym)
|
92
|
+
end
|
93
|
+
|
94
|
+
# fuzzy search will always search for enabled config
|
95
|
+
def fuzzy_search_for keys, configs
|
96
|
+
_, result = configs.detect do |cache_key, config|
|
97
|
+
match = if cache_key.is_a? Array
|
98
|
+
cache_key & keys == cache_key
|
99
|
+
else
|
100
|
+
keys.include? cache_key
|
101
|
+
end
|
102
|
+
match & config.cache && config.enabled
|
103
|
+
end
|
104
|
+
result
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'sequel/unicache/write'
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
module Unicache
|
5
|
+
class Expire
|
6
|
+
module InstanceMethods # Provide instance methods for Sequel::Model, to expire cache
|
7
|
+
def refresh
|
8
|
+
model = super
|
9
|
+
Write.expire model if Unicache.enabled?
|
10
|
+
model
|
11
|
+
end
|
12
|
+
|
13
|
+
def expire_unicache
|
14
|
+
Write.expire self, force: true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Unicache
|
3
|
+
module Finder # Provide class methods for Sequel::Model, to find cache by unicache keys
|
4
|
+
module ClassMethods
|
5
|
+
def primary_key_lookup pk
|
6
|
+
if !Unicache.enabled? || Unicache.unicache_suspended? ||
|
7
|
+
dataset.joined_dataset? || !@fast_pk_lookup_sql
|
8
|
+
# If it's not a simple table, simple pk,
|
9
|
+
# assign this job to parent class, which will call first_where to do that
|
10
|
+
super
|
11
|
+
else
|
12
|
+
config = unicache_for primary_key # primary key is always unicache keys, no needs to fuzzy search
|
13
|
+
if unicache_enabled_for? config
|
14
|
+
find_with_cache({primary_key => pk}, config) { super }
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def first_where hash
|
22
|
+
return primary_key_lookup hash unless hash.is_a? Hash
|
23
|
+
if Unicache.enabled? && !Unicache.unicache_suspended? && simple_table
|
24
|
+
config = unicache_for(*hash.keys, fuzzy: true) # need fuzzy search, this function always returns enabled config
|
25
|
+
if config
|
26
|
+
find_with_cache(hash, config) { super }
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Find from cache first, if failed, get fallback result from block
|
38
|
+
def find_with_cache hash, config
|
39
|
+
cache = config.cache.get config.key.(hash, config)
|
40
|
+
if cache
|
41
|
+
values = config.deserialize.(cache, config)
|
42
|
+
dataset.row_proc.call values
|
43
|
+
else
|
44
|
+
model = yield
|
45
|
+
Write.write model if model
|
46
|
+
model
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Unicache
|
3
|
+
class GlobalConfiguration
|
4
|
+
def initialize opts = {}
|
5
|
+
@opts = default_config.merge opts
|
6
|
+
end
|
7
|
+
|
8
|
+
def set opts
|
9
|
+
@opts.merge! opts
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_h
|
13
|
+
@opts
|
14
|
+
end
|
15
|
+
|
16
|
+
%i(cache ttl serialize deserialize key enabled logger).each do |attr|
|
17
|
+
define_method(attr) { @opts[attr] }
|
18
|
+
define_method("#{attr}=") { |val| @opts[attr] = val }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def default_config
|
24
|
+
{ serialize: ->(values, _) { Marshal.dump values },
|
25
|
+
deserialize: ->(cache, _) { Marshal.load cache },
|
26
|
+
key: ->(hash, opts) {
|
27
|
+
cls = opts.model_class.name
|
28
|
+
keys = hash.keys.sort.map {|key| [key, hash[key]] }.flatten.
|
29
|
+
map {|str| str.to_s.gsub(':', '\:') }.join(':')
|
30
|
+
"#{cls}:#{keys}"
|
31
|
+
},
|
32
|
+
enabled: true }
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
attr_reader :config
|
37
|
+
|
38
|
+
def self.extended base
|
39
|
+
base.instance_exec { @config = GlobalConfiguration.new }
|
40
|
+
end
|
41
|
+
|
42
|
+
def configure opts
|
43
|
+
@config.set opts
|
44
|
+
end
|
45
|
+
|
46
|
+
def enable
|
47
|
+
@disabled = false
|
48
|
+
end
|
49
|
+
|
50
|
+
def disable
|
51
|
+
@disabled = true
|
52
|
+
end
|
53
|
+
|
54
|
+
def enabled?
|
55
|
+
!@disabled
|
56
|
+
end
|
57
|
+
|
58
|
+
def unicache_suspended?
|
59
|
+
Thread.current[:unicache_suspended]
|
60
|
+
end
|
61
|
+
|
62
|
+
def suspend_unicache
|
63
|
+
if block_given?
|
64
|
+
origin, Thread.current[:unicache_suspended] = Thread.current[:unicache_suspended], true
|
65
|
+
yield
|
66
|
+
else
|
67
|
+
origin = true
|
68
|
+
end
|
69
|
+
ensure
|
70
|
+
Thread.current[:unicache_suspended] = origin
|
71
|
+
end
|
72
|
+
|
73
|
+
def unsuspend_unicache
|
74
|
+
Thread.current[:unicache_suspended] = false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|