sequel-unicache 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.yml
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sequel-unicache.gemspec
4
+ gemspec
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