sequel-unicache 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/bachue/sequel-unicache.svg)](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
|