fast_cache 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.simplecov +4 -0
- data/.yardopts +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +185 -0
- data/Rakefile +1 -0
- data/bench/caching_sample.json +133 -0
- data/bin/fast-cache-benchmark +52 -0
- data/fast_cache.gemspec +27 -0
- data/lib/fast_cache.rb +2 -0
- data/lib/fast_cache/cache.rb +221 -0
- data/lib/fast_cache/version.rb +3 -0
- data/spec/lib/fast_cache/cache_spec.rb +227 -0
- data/spec/spec_helper.rb +16 -0
- metadata +164 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.simplecov
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/fast_cache.gemspec
ADDED
@@ -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
|
data/lib/fast_cache.rb
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|