fast_cache 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format progress
2
+ --color
@@ -0,0 +1,4 @@
1
+ SimpleCov.start do
2
+ add_filter "/spec/"
3
+ add_filter "/tmp/"
4
+ end
@@ -0,0 +1,3 @@
1
+ lib/**/*.rb
2
+ -
3
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fast_cache.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Simeon Simeonov & Swoop, Inc.
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.
@@ -0,0 +1,185 @@
1
+ # FastCache
2
+
3
+
4
+ There are two reasons why you may want to immediately leave this page:
5
+
6
+ 1. This is yet another caching gem, which is grounds for extreme suspicion.
7
+
8
+ 2. Many Ruby developers don't care about performance.
9
+
10
+ If you're still reading, there are three reasons why you may want to check this out:
11
+
12
+ 1. Performance is a feature users love. Products from 37signals' to Google's have proven this time and time again. Performance almost never matters if you are not successful but almost always does if you are. At [Swoop](http://swoop.com) we have tens of millions of users. We care about correctness, simplicity and maintainability but also, quite a bit, about performance.
13
+
14
+ 2. This cache benchmarks 10-100x faster than ActiveSupport::Cache::MemoryStore without breaking a sweat. You can switch to FastCache in a couple minutes and, most likely, you won't have to refactor your tests. FastCache has 100% test coverage at 20+ hits/line. There are no third party runtime dependencies so you can use this anywhere with Ruby 1.9+.
15
+
16
+ 3. The implementation exploits some neat features of Ruby's native data structures that could be useful and fun to learn about.
17
+
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ gem 'fast_cache'
24
+
25
+ And then execute:
26
+
27
+ $ bundle
28
+
29
+ Or install it yourself as:
30
+
31
+ $ gem install fast_cache
32
+
33
+
34
+ ## Usage
35
+
36
+ FastCache::Cache is an in-process cache with least recently used (LRU) and time to live (TTL) expiration semantics, which makes it an easy replacement for ActiveSupport::Cache::MemoryStore as well as a great candidate for the in-process portion of a hierarchical caching system (FastCache sitting in front of, say, memcached or Redis).
37
+
38
+ The current implementation is not thread-safe because at [Swoop](http://swoop.com) we prefer to handle simple concurrency in MRI Ruby via the [reactor pattern](http://en.wikipedia.org/wiki/Reactor_pattern) with [eventmachine](https://github.com/eventmachine/eventmachine). An easy way to add thread safety would be via a [synchronizing subclass](https://github.com/SamSaffron/lru_redux/blob/master/lib/lru_redux/thread_safe_cache.rb) or decorator. Send a pull request, please!
39
+
40
+ The implementation does not use a separate thread for expiring stale cached values. Instead, before a value is returned from the cache, its expiration time is checked. In order to avoid the case where a value that is never accessed cannot be removed, every _N_ operations the cache removes all expired values.
41
+
42
+ ```ruby
43
+ require 'fast_cache'
44
+
45
+ # Creates a cache of one million items at most an hour old
46
+ cache = FastCache::Cache.new(1_000_000, 60*60)
47
+
48
+ # Sames as above but removes expired items after every 10,000 operations.
49
+ # The default is after 100 operations.
50
+ cache = FastCache::Cache.new(1_000_000, 60*60, 10_000)
51
+
52
+ # Cache the result of an expensive operation
53
+ cached_value = cache.fetch('my_key') do
54
+ HardProblem.new(inputs).solve.result
55
+ end
56
+
57
+ # Proactively release as much memory as you can
58
+ cache.expire!
59
+ ```
60
+
61
+
62
+ ## Performance
63
+
64
+ If you are looking for an in-process cache with LRU and time-to-live expiration semantics the go-to implementation is ActiveSupport::Cache::MemoryStore, which as of Rails 3.1 [started marshaling](http://apidock.com/rails/v3.2.13/ActiveSupport/Cache/Entry/value) the data even though the keys and values never leave the process boundary. The performance of the cache is dominated by marshaling and loading, i.e., by the size and complexity of keys and values. The better job you do of finding large, complex, cacheable data structures, the slower it will run. That doesn't feel right for an in-process cache.
65
+
66
+ We benchmark against [LruRedux::Cache](https://github.com/SamSaffron/lru_redux), which was the inspiration behind FastCache::Cache and, of course, ActiveSupport::Cache::MemoryStore.
67
+
68
+ ```bash
69
+ gem install lru_redux
70
+ gem install activesupport
71
+ bin/fast-cache-benchmark
72
+ ```
73
+
74
+ The [benchmark](bin/fast-cache-benchmark) includes a simple value test (caching just the Symbol `:value`) and a more complex value test (caching a [medium-size data structure](bench/caching_sample.json)). Both tests run for one million iterations with an expected cache hit rate of 50%.
75
+
76
+ ```
77
+ 12009[SPX/fast_cache(master *#)]$ bin/fast-cache-benchmark
78
+ Simple value benchmark
79
+ Rehearsal ------------------------------------------------
80
+ lru_redux 2.200000 0.020000 2.220000 ( 2.213863)
81
+ fast_cache 10.840000 0.040000 10.880000 ( 10.879686)
82
+ memory_store 53.300000 0.150000 53.450000 ( 53.459458)
83
+ -------------------------------------- total: 66.550000sec
84
+
85
+ user system total real
86
+ lru_redux 4.140000 0.010000 4.150000 ( 4.153546)
87
+ fast_cache 14.140000 0.040000 14.180000 ( 14.177038)
88
+ memory_store 71.510000 0.140000 71.650000 ( 71.659656)
89
+
90
+ Complex value benchmark
91
+ Rehearsal ------------------------------------------------
92
+ lru_redux 6.150000 0.030000 6.180000 ( 6.180459)
93
+ fast_cache 17.020000 0.040000 17.060000 ( 17.058475)
94
+ memory_store 1053.360000 1.620000 1054.980000 (1055.275237)
95
+ ------------------------------------ total: 1078.220000sec
96
+
97
+ user system total real
98
+ lru_redux 7.830000 0.020000 7.850000 ( 7.854760)
99
+ fast_cache 19.620000 0.030000 19.650000 ( 19.650379)
100
+ memory_store 1286.790000 1.850000 1288.640000 (1289.115472)
101
+ ```
102
+
103
+ In both tests FastCache::Cache is 2-3x slower than LruRedux::Cache, which only provides LRU semantics. For small values, FastCache::Cache is 5x faster than ActiveSupport::Cache::MemoryStore. For more complex values the difference grows to 50+x (67x in the particular benchmark).
104
+
105
+ In one case where we were generating CSVs where every row involved looking up model attributes the performance difference was 100x and operations that took many minutes now happen in seconds.
106
+
107
+
108
+ ## Implementation
109
+
110
+ [Sam Saffron](https://github.com/SamSaffron) noticed that Ruby 1.9 Hash's property to preserve insertion order can be used as a second index (in addition to indexing by a key). That led Sam to create the [lru_redux](https://github.com/SamSaffron/lru_redux) gem, whose cache behaves in a very non-intuitive way at first glance. For example, the simplified pseudocode for the cache get operation is:
111
+
112
+ ```
113
+ cache[key]:
114
+ value = @hash.delete(key)
115
+ @hash[key] = value
116
+ value
117
+ ```
118
+
119
+ In other words, the code performs two mutating operations (delete and insert) in order to satisfy a single non-mutating operation (get). Why? The reason is that this is how the cache maintains its least recently used removal property. The picture below shows the get operation step-by-step using a fictitious cache of names against some difficult-to-compute score.
120
+
121
+ ![lru](https://www.lucidchart.com/publicSegments/view/525be92f-6034-40f7-b3b6-377d0a005604/image.png)
122
+ If the cache gets full, it can create space by removing elements from the head of its items array. For those of you familiar with [Redis](http://redis.io), this approach to using a Ruby Hash may remind you of [sorted sets](http://redis.io/commands#sorted_set).
123
+
124
+ To add time-based expiration, we need to:
125
+
126
+ 1. Keep track of expiration times.
127
+
128
+ 2. Index by expiration time, to clean up in `expire!`.
129
+
130
+ 3. Efficiently remove items from the expiration index when a stale item is detected.
131
+
132
+ By exploiting the dual index property of Hash we can achieve this with just one extra hash. The diagram below shows the object relationships.
133
+
134
+ ![lru-and-ttl](https://www.lucidchart.com/publicSegments/view/525be9d7-fe08-40fb-9dd2-37850a005603/image.png)
135
+ ### A note about Time
136
+
137
+ Those who peek at the code may notice the expression `Time.now.to_f`. Rails has some very nice time extensions, e.g., dealing with time zones and the ability to add and subtract duration objects from date/time objects. All this sugar, which is of no use to us here, comes at a significant overhead so much so that I have benchmarked the cache running several times slower in a Rails environment without the conversion of the time object to a simple number.
138
+
139
+
140
+ ## Contributing
141
+
142
+ 1. Fork the repo
143
+ 2. Create a topic branch (`git checkout -b my-new-feature`)
144
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
145
+ 5. Push to the branch (`git push origin my-new-feature`)
146
+ 6. Create new Pull Request
147
+
148
+ Please don't change the version and add solid tests: [simplecov](https://github.com/colszowka/simplecov) is set to 100% minimum coverage.
149
+
150
+
151
+ ## Credits
152
+
153
+ I'd like to thank [Sam Saffron](https://github.com/SamSaffron) for his guiding insight as well as [Richard Schneeman](https://github.com/schneems) and [Piotr Sarnacki](https://github.com/drogus) for [helping me improve](https://github.com/rails/rails/issues/11512) ActiveSupport::Cache::MemoryStore.
154
+
155
+ Who says Ruby can't be fun **and** fast?
156
+
157
+ ![swoop](http://blog.swoop.com/Portals/160747/images/logo1.png)
158
+
159
+ fast_cache was written by [Simeon Simeonov](https://github.com/ssimeonov) and is maintained and funded by [Swoop, Inc,](http://swoop.com).
160
+
161
+ License
162
+ -------
163
+
164
+ fast_cache is Copyright © 2013 Simeon Simeonov and Swoop, Inc. It is free software, and may be redistributed under the terms specified below.
165
+
166
+ MIT License
167
+
168
+ Permission is hereby granted, free of charge, to any person obtaining
169
+ a copy of this software and associated documentation files (the
170
+ "Software"), to deal in the Software without restriction, including
171
+ without limitation the rights to use, copy, modify, merge, publish,
172
+ distribute, sublicense, and/or sell copies of the Software, and to
173
+ permit persons to whom the Software is furnished to do so, subject to
174
+ the following conditions:
175
+
176
+ The above copyright notice and this permission notice shall be
177
+ included in all copies or substantial portions of the Software.
178
+
179
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
180
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
181
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
182
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
183
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
184
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
185
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,133 @@
1
+ [
2
+ {
3
+ "example_taken_from" : "http://adobe.github.io/Spry/samples/data_region/JSONDataSetSample.html"
4
+ },
5
+ {
6
+ "id" : "0001",
7
+ "type" : "donut",
8
+ "name" : "Cake",
9
+ "ppu" : 0.55,
10
+ "batters" : {
11
+ "batter" : [
12
+ {
13
+ "id" : "1001",
14
+ "type" : "Regular"
15
+ },
16
+ {
17
+ "id" : "1002",
18
+ "type" : "Chocolate"
19
+ },
20
+ {
21
+ "id" : "1003",
22
+ "type" : "Blueberry"
23
+ },
24
+ {
25
+ "id" : "1004",
26
+ "type" : "Devil's Food"
27
+ }
28
+ ]
29
+ },
30
+ "topping" : [
31
+ {
32
+ "id" : "5001",
33
+ "type" : "None"
34
+ },
35
+ {
36
+ "id" : "5002",
37
+ "type" : "Glazed"
38
+ },
39
+ {
40
+ "id" : "5005",
41
+ "type" : "Sugar"
42
+ },
43
+ {
44
+ "id" : "5007",
45
+ "type" : "Powdered Sugar"
46
+ },
47
+ {
48
+ "id" : "5006",
49
+ "type" : "Chocolate with Sprinkles"
50
+ },
51
+ {
52
+ "id" : "5003",
53
+ "type" : "Chocolate"
54
+ },
55
+ {
56
+ "id" : "5004",
57
+ "type" : "Maple"
58
+ }
59
+ ]
60
+ },
61
+ {
62
+ "id" : "0002",
63
+ "type" : "donut",
64
+ "name" : "Raised",
65
+ "ppu" : 0.55,
66
+ "batters" : {
67
+ "batter" : [
68
+ {
69
+ "id" : "1001",
70
+ "type" : "Regular"
71
+ }
72
+ ]
73
+ },
74
+ "topping" : [
75
+ {
76
+ "id" : "5001",
77
+ "type" : "None"
78
+ },
79
+ {
80
+ "id" : "5002",
81
+ "type" : "Glazed"
82
+ },
83
+ {
84
+ "id" : "5005",
85
+ "type" : "Sugar"
86
+ },
87
+ {
88
+ "id" : "5003",
89
+ "type" : "Chocolate"
90
+ },
91
+ {
92
+ "id" : "5004",
93
+ "type" : "Maple"
94
+ }
95
+ ]
96
+ },
97
+ {
98
+ "id" : "0003",
99
+ "type" : "donut",
100
+ "name" : "Old Fashioned",
101
+ "ppu" : 0.55,
102
+ "batters" : {
103
+ "batter" : [
104
+ {
105
+ "id" : "1001",
106
+ "type" : "Regular"
107
+ },
108
+ {
109
+ "id" : "1002",
110
+ "type" : "Chocolate"
111
+ }
112
+ ]
113
+ },
114
+ "topping" : [
115
+ {
116
+ "id" : "5001",
117
+ "type" : "None"
118
+ },
119
+ {
120
+ "id" : "5002",
121
+ "type" : "Glazed"
122
+ },
123
+ {
124
+ "id" : "5003",
125
+ "type" : "Chocolate"
126
+ },
127
+ {
128
+ "id" : "5004",
129
+ "type" : "Maple"
130
+ }
131
+ ]
132
+ }
133
+ ]
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'fast_cache'
7
+ require 'lru_redux'
8
+ require 'active_support/cache'
9
+ require 'json'
10
+
11
+ class ModifiedLru < LruRedux::Cache
12
+ # Modify API to be consistent with other caches
13
+ def fetch(key, &block)
14
+ getset(key, &block)
15
+ end
16
+ end
17
+
18
+ def make_caches(size, exemplar_value, ttl)
19
+ marshaled = Marshal.dump(exemplar_value)
20
+ {
21
+ lru_redux: ModifiedLru.new(size),
22
+ fast_cache: FastCache::Cache.new(size, ttl),
23
+ memory_store: ActiveSupport::Cache.lookup_store(:memory_store, size: size * marshaled.length + 1)
24
+ }
25
+ end
26
+
27
+ def complex_value
28
+ sample_file = File.expand_path('../../bench/caching_sample.json', __FILE__)
29
+ JSON.parse(File.read(sample_file))
30
+ end
31
+
32
+ def run_test(size, value, ttl = 60*60)
33
+ caches = make_caches(size, value, ttl)
34
+
35
+ Benchmark.bmbm do |bm|
36
+ caches.each.map(&:last).map(&:clear)
37
+
38
+ caches.each_pair do |name, cache|
39
+ bm.report name do
40
+ 1000000.times do
41
+ cache.fetch(rand(2 * size)) { value }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ puts "Simple value benchmark\n"
49
+ run_test 1_000, :value
50
+
51
+ puts "\nComplex value benchmark\n"
52
+ run_test 1_000, complex_value
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fast_cache/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fast_cache"
8
+ spec.version = FastCache::VERSION
9
+ spec.authors = ["Simeon Simeonov"]
10
+ spec.email = ["sim@swoop.com"]
11
+ spec.description = %q{Very fast LRU + TTL cache}
12
+ spec.summary = %q{FastCache is an in-process cache with both least-recently used (LRU) and time to live (TTL) expiration semantics. It is typically 5-100x faster than ActiveSupport::Cache::MemoryStore, depending on the cached data.}
13
+ spec.homepage = "https://github.com/swoop-inc/fast_cache"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "simplecov"
25
+ spec.add_development_dependency "awesome_print"
26
+ spec.add_development_dependency "timecop"
27
+ end
@@ -0,0 +1,2 @@
1
+ require 'fast_cache/version'
2
+ require 'fast_cache/cache'
@@ -0,0 +1,221 @@
1
+ module FastCache
2
+
3
+ # @author {https://github.com/ssimeonov Simeon Simeonov}, {http://swoop.com Swoop, Inc.}
4
+ #
5
+ # In-process cache with least-recently used (LRU) and time-to-live (TTL)
6
+ # expiration semantics.
7
+ #
8
+ # This implementation is not thread-safe. It does not use a thread to clean
9
+ # up expired values. Instead, an expiration check is performed:
10
+ #
11
+ # 1. Every time you retrieve a value, against that value. If the value has
12
+ # expired, it will be removed and `nil` will be returned.
13
+ #
14
+ # 2. Every `expire_interval` operations as the cache is used to remove all
15
+ # expired values up to that point.
16
+ #
17
+ # For manual expiration call {#expire!}.
18
+ #
19
+ # @example
20
+ #
21
+ # # Create cache with one million elements no older than 1 hour
22
+ # cache = FastCache::Cache.new(1_000_000, 60 * 60)
23
+ # cached_value = cache.fetch('cached_value_key') do
24
+ # # Expensive computation that returns the value goes here
25
+ # end
26
+ class Cache
27
+
28
+ # Initializes the cache.
29
+ #
30
+ # @param [Integer] max_size Maximum number of elements in the cache.
31
+ # @param [Numeric] ttl Maximum time, in seconds, for a value to stay in
32
+ # the cache.
33
+ # @param [Integer] expire_interval Number of cache operations between
34
+ # calls to {#expire!}.
35
+ def initialize(max_size, ttl, expire_interval = 100)
36
+ @max_size = max_size
37
+ @ttl = ttl.to_f
38
+ @expire_interval = expire_interval
39
+ @op_count = 0
40
+ @data = {}
41
+ @expires_at = {}
42
+ end
43
+
44
+ # Retrieves a value from the cache, if available and not expired, or
45
+ # yields to a block that calculates the value to be stored in the cache.
46
+ #
47
+ # @param key [Object] the key to look up or store at
48
+ # @return [Object] the value at the key
49
+ # @yield yields when the value is not present
50
+ # @yieldreturn [Object] the value to store in the cache.
51
+ def fetch(key)
52
+ found, value = get(key)
53
+ if found
54
+ value
55
+ else
56
+ store(key, yield)
57
+ end
58
+ end
59
+
60
+ # Retrieves a value from the cache.
61
+ #
62
+ # @param key [Object] the key to look up
63
+ # @return [Object, nil] the value at the key, when present, or `nil`
64
+ def [](key)
65
+ _, value = get(key)
66
+ value
67
+ end
68
+
69
+ # Stores a value in the cache.
70
+ #
71
+ # @param key [Object] the key to store at
72
+ # @param val [Object] the value to store
73
+ # @return [Object] the value
74
+ def []=(key, val)
75
+ expire!
76
+ store(key, val)
77
+ end
78
+
79
+ # Removes a value from the cache.
80
+ #
81
+ # @param key [Object] the key to remove at
82
+ # @return [Object, nil] the value at the key, when present, or `nil`
83
+ def delete(key)
84
+ entry = @data.delete(key)
85
+ if entry
86
+ @expires_at.delete(entry)
87
+ entry.value
88
+ else
89
+ nil
90
+ end
91
+ end
92
+
93
+ # Checks whether the cache is empty.
94
+ #
95
+ # @note calls to {#empty?} do not count against `expire_interval`.
96
+ #
97
+ # @return [Boolean]
98
+ def empty?
99
+ count == 0
100
+ end
101
+
102
+ # Clears the cache.
103
+ #
104
+ # @return [self]
105
+ def clear
106
+ @data.clear
107
+ @expires_at.clear
108
+ self
109
+ end
110
+
111
+ # Returns the number of elements in the cache.
112
+ #
113
+ # @note calls to {#empty?} do not count against `expire_interval`.
114
+ # Therefore, the number of elements is that prior to any expiration.
115
+ #
116
+ # @return [Integer] number of elements in the cache.
117
+ def count
118
+ @data.count
119
+ end
120
+
121
+ alias_method :size, :count
122
+ alias_method :length, :count
123
+
124
+ # Allows iteration over the items in the cache.
125
+ #
126
+ # Enumeration is stable: it is not affected by changes to the cache,
127
+ # including value expiration. Expired values are removed first.
128
+ #
129
+ # @note The returned values could have expired by the time the client
130
+ # code gets to accessing them.
131
+ # @note Because of its stability, this operation is very expensive.
132
+ # Use with caution.
133
+ #
134
+ # @return [Enumerator, Array<key, value>] an Enumerator, when a block is
135
+ # not provided, or an array of key/value pairs.
136
+ # @yield [Array<key, value>] key/value pairs, when a block is provided.
137
+ def each(&block)
138
+ expire!
139
+ @data.map { |key, entry| [key, entry.value] }.each(&block)
140
+ end
141
+
142
+ # Removes expired values from the cache.
143
+ #
144
+ # @return [self]
145
+ def expire!
146
+ check_expired(Time.now.to_f)
147
+ self
148
+ end
149
+
150
+ # Returns information about the number of objects in the cache, its
151
+ # maximum size and TTL.
152
+ #
153
+ # @return [String]
154
+ def inspect
155
+ "<#{self.class.name} count=#{count} max_size=#{@max_size} ttl=#{@ttl}>"
156
+ end
157
+
158
+ private
159
+
160
+ # @private
161
+ class Entry
162
+ attr_reader :value
163
+ attr_reader :expires_at
164
+
165
+ def initialize(value, expires_at)
166
+ @value = value
167
+ @expires_at = expires_at
168
+ end
169
+ end
170
+
171
+ def get(key)
172
+ t = Time.now.to_f
173
+ check_expired(t)
174
+ found = true
175
+ entry = @data.delete(key) { found = false }
176
+ if found
177
+ if entry.expires_at <= t
178
+ @expires_at.delete(entry)
179
+ return false, nil
180
+ else
181
+ @data[key] = entry
182
+ return true, entry.value
183
+ end
184
+ else
185
+ return false, nil
186
+ end
187
+ end
188
+
189
+ def store(key, val)
190
+ expires_at = Time.now.to_f + @ttl
191
+ entry = Entry.new(val, expires_at)
192
+ store_entry(key, entry)
193
+ val
194
+ end
195
+
196
+ def store_entry(key, entry)
197
+ @data.delete(key)
198
+ @data[key] = entry
199
+ @expires_at[entry] = key
200
+ shrink_if_needed
201
+ end
202
+
203
+ def shrink_if_needed
204
+ if @data.length > @max_size
205
+ entry = delete(@data.shift)
206
+ @expires_at.delete(entry)
207
+ end
208
+ end
209
+
210
+ def check_expired(t)
211
+ if (@op_count += 1) % @expire_interval == 0
212
+ while (key_value_pair = @expires_at.first) &&
213
+ (entry = key_value_pair.first).expires_at <= t
214
+ key = @expires_at.delete(entry)
215
+ @data.delete(key)
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ end
@@ -0,0 +1,3 @@
1
+ module FastCache
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,227 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastCache::Cache do
4
+
5
+ context 'empty cache' do
6
+ subject { described_class.new(5, 60, 1) }
7
+
8
+ its(:empty?) { should be_true }
9
+ its(:length) { should eq 0 }
10
+ its(:size) { should eq 0 }
11
+ its(:count) { should eq 0 }
12
+
13
+ it 'returns nil' do
14
+ subject[:foo].should be_nil
15
+ end
16
+ end
17
+
18
+ context 'non-empty cache' do
19
+ before do
20
+ @cache = described_class.new(3, 60, 1)
21
+ @cache[:a] = 1
22
+ @cache[:b] = 2
23
+ @cache[:c] = 3
24
+ end
25
+ subject { @cache }
26
+
27
+ its(:empty?) { should be_false }
28
+ its(:length) { should eq 3 }
29
+ its(:size) { should eq 3 }
30
+ its(:count) { should eq 3 }
31
+
32
+ it 'returns stored values' do
33
+ subject[:a].should eq 1
34
+ subject[:b].should eq 2
35
+ subject[:c].should eq 3
36
+ end
37
+
38
+ it 'replaces stored values' do
39
+ subject[:a] = 10
40
+
41
+ subject[:a].should eq 10
42
+ end
43
+
44
+ describe '#fetch' do
45
+ it 'fetches from the cache when a key is present' do
46
+ subject.fetch(:a) do
47
+ 'failure'.should eq 'fetch body should not be called'
48
+ end.should eq 1
49
+ end
50
+
51
+ it 'evaluates and stores the value when it is absent' do
52
+ subject.fetch(:d) do
53
+ 5
54
+ end.should eq 5
55
+ subject[:d].should eq 5
56
+ end
57
+ end
58
+
59
+ describe '#delete' do
60
+ it 'deletes entries' do
61
+ subject.delete(:a).should eq 1
62
+ subject.count.should eq 2
63
+ subject.delete(:c).should eq 3
64
+ subject.count.should eq 1
65
+ end
66
+
67
+ it 'returns nil for missing keys' do
68
+ subject.delete(:d).should be_nil
69
+ subject.count.should eq 3
70
+ end
71
+ end
72
+
73
+ describe '#clear' do
74
+ it 'clears the cache' do
75
+ subject.clear
76
+
77
+ subject.should be_empty
78
+ end
79
+ end
80
+
81
+ describe '#each' do
82
+ it 'yields key value pairs' do
83
+ expect do |b|
84
+ subject.each(&b)
85
+ end.to yield_successive_args([:a, 1], [:b, 2], [:c, 3])
86
+ end
87
+
88
+ it 'returns an Enumerator when called without a block' do
89
+ subject.each.should be_kind_of Enumerator
90
+ end
91
+ end
92
+
93
+ describe '#inspect' do
94
+ it do
95
+ subject.inspect.should eq '<FastCache::Cache count=3 max_size=3 ttl=60.0>'
96
+ end
97
+ end
98
+
99
+ describe 'LRU behaviors' do
100
+ it 'removes least recently accessed entries when full' do
101
+ subject[:d] = 4
102
+
103
+ subject[:a].should be_nil
104
+ subject[:b].should eq 2
105
+
106
+ subject[:b] # access
107
+ subject[:e] = 6
108
+
109
+ subject[:b].should eq 2
110
+ subject[:c].should be_nil
111
+ subject[:d].should eq 4
112
+ subject[:e].should eq 6
113
+ end
114
+ end
115
+ end
116
+
117
+ describe 'TTL behaviors' do
118
+ context 'immediate expiration' do
119
+ before do
120
+ @cache = described_class.new(3, 0, 1)
121
+ @cache[:a] = 1
122
+ end
123
+ subject { @cache }
124
+
125
+ it 'reports the element count prior to expiration checking' do
126
+ subject.count.should eq 1
127
+ end
128
+
129
+ it 'expires all entries' do
130
+ subject.expire!
131
+
132
+ subject.count.should eq 0
133
+ end
134
+
135
+ it 'removes all entries upon access' do
136
+ subject[:a].should be_nil
137
+
138
+ subject.fetch(:a) do
139
+ true.should be_true
140
+ 5
141
+ end.should == 5
142
+
143
+ subject[:a].should be_nil
144
+ end
145
+ end
146
+
147
+ context '1 min expiration' do
148
+ before do
149
+ @t = Time.now
150
+ @cache = described_class.new(3, 60, 1)
151
+ @cache[:a] = 1
152
+ end
153
+ subject { @cache }
154
+
155
+ it 'removes expired values upon access' do
156
+ Timecop.freeze(@t + 61) do
157
+ subject[:a].should be_nil
158
+ end
159
+ end
160
+
161
+ it 'removes expired values when other values are written' do
162
+ Timecop.freeze(@t + 61) do
163
+ subject.count.should eq 1
164
+
165
+ subject[:b] = 2
166
+
167
+ subject.count.should eq 1
168
+ subject[:a].should be_nil
169
+ subject[:b].should eq 2
170
+ end
171
+ end
172
+
173
+ it 'removes expired values when other values are accessed' do
174
+ Timecop.freeze(@t + 30) do
175
+ subject[:b] = 2
176
+ end
177
+
178
+ Timecop.freeze(@t + 61) do
179
+ subject.count.should eq 2
180
+
181
+ subject[:b] # access
182
+
183
+ subject.count.should eq 1
184
+ subject[:a].should be_nil
185
+ subject[:b].should eq 2
186
+ end
187
+ end
188
+
189
+ it 'expires entries' do
190
+ Timecop.freeze(@t + 30) do
191
+ subject[:b] = 2
192
+ end
193
+
194
+ Timecop.freeze(@t + 61) do
195
+ subject.expire!
196
+
197
+ subject.count.should eq 1
198
+ subject[:b].should eq 2
199
+ end
200
+ end
201
+ end
202
+
203
+ context 'delayed expiration check' do
204
+ before do
205
+ @t = Time.now
206
+ @cache = described_class.new(3, 60, 10)
207
+ @cache[:a] = 1 # 1 op
208
+ @cache[:b] = 2 # 2 ops
209
+ end
210
+ subject { @cache }
211
+
212
+ it 'removes expired values after a specified number of operations' do
213
+ Timecop.freeze(@t + 61) do
214
+ subject.count.should eq 2
215
+ 7.times do # 3..9 ops
216
+ subject[:a].should eq nil
217
+ subject.count.should == 1
218
+ end
219
+ # 10 ops
220
+ subject[:a].should eq nil
221
+ subject.count.should eq 0
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ end
@@ -0,0 +1,16 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+ SimpleCov.minimum_coverage 100
4
+
5
+ require 'pp'
6
+ require 'timecop'
7
+
8
+ require 'fast_cache'
9
+
10
+ Dir['spec/support/**/*.rb'].each { |f| require File.expand_path(f) }
11
+
12
+ RSpec.configure do |config|
13
+ config.after do
14
+ Timecop.return
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fast_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Simeon Simeonov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: simplecov
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: awesome_print
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: timecop
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: Very fast LRU + TTL cache
111
+ email:
112
+ - sim@swoop.com
113
+ executables:
114
+ - fast-cache-benchmark
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - .gitignore
119
+ - .rspec
120
+ - .simplecov
121
+ - .yardopts
122
+ - Gemfile
123
+ - LICENSE.txt
124
+ - README.md
125
+ - Rakefile
126
+ - bench/caching_sample.json
127
+ - bin/fast-cache-benchmark
128
+ - fast_cache.gemspec
129
+ - lib/fast_cache.rb
130
+ - lib/fast_cache/cache.rb
131
+ - lib/fast_cache/version.rb
132
+ - spec/lib/fast_cache/cache_spec.rb
133
+ - spec/spec_helper.rb
134
+ homepage: https://github.com/swoop-inc/fast_cache
135
+ licenses:
136
+ - MIT
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ! '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ none: false
149
+ requirements:
150
+ - - ! '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubyforge_project:
155
+ rubygems_version: 1.8.25
156
+ signing_key:
157
+ specification_version: 3
158
+ summary: FastCache is an in-process cache with both least-recently used (LRU) and
159
+ time to live (TTL) expiration semantics. It is typically 5-100x faster than ActiveSupport::Cache::MemoryStore,
160
+ depending on the cached data.
161
+ test_files:
162
+ - spec/lib/fast_cache/cache_spec.rb
163
+ - spec/spec_helper.rb
164
+ has_rdoc: