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 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