viscacha 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c52b3452c99fa766fe4d42c47dd47940587e1cd12578a20866211e96c490a861
4
+ data.tar.gz: 291ed1dbcfb138ce1efdda26bd4cc58940a8f13ccad0069d1712d3e626636c93
5
+ SHA512:
6
+ metadata.gz: 6aabf6a328716f393ce20daf38208d23a70b80c212316ebafbba2dc50be6f6826b1ba50b13f7e37a6fe159d0a1700a6cd08c1a52ff9172f3b0b45de6cb12cc53
7
+ data.tar.gz: 7e04dd4e9325ef4e8c34102e823140e3e53824b87e5fc919098180045a478266ca67f63a754e17281839870c5cdd281f8dc3eaa8a0c04fdcc2805157f473bec4
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ coverage
9
+ doc/
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
17
+
18
+ # Test databases
19
+ *.lmc
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format nested
3
+ --backtrace
@@ -0,0 +1,5 @@
1
+ rvm:
2
+ - "1.9.2"
3
+ - "1.9.3"
4
+ - "2.0.0"
5
+ # increment to force build: 002
data/Gemfile ADDED
@@ -0,0 +1,22 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ # building
6
+ gem "bundler", "~> 1.3"
7
+ gem "pry"
8
+ gem "pry-nav"
9
+
10
+ # testing
11
+ gem "rake"
12
+ gem "rspec"
13
+ gem 'terminal-notifier-guard'
14
+ gem "guard"
15
+ gem "guard-rspec"
16
+ gem 'coveralls'
17
+
18
+ # benchmarking
19
+ gem 'benchmark-ips'
20
+ gem 'memcache-client'
21
+ gem 'dalli'
22
+
@@ -0,0 +1,93 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ viscacha (0.0.1)
5
+ activesupport
6
+ localmemcache (~> 0.4.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (3.2.13)
12
+ i18n (= 0.6.1)
13
+ multi_json (~> 1.0)
14
+ benchmark-ips (1.2.0)
15
+ coderay (1.0.9)
16
+ colorize (0.5.8)
17
+ coveralls (0.6.7)
18
+ colorize
19
+ multi_json (~> 1.3)
20
+ rest-client
21
+ simplecov (>= 0.7)
22
+ thor
23
+ dalli (2.6.4)
24
+ diff-lcs (1.2.4)
25
+ ffi (1.9.0)
26
+ formatador (0.2.4)
27
+ guard (1.8.1)
28
+ formatador (>= 0.2.4)
29
+ listen (>= 1.0.0)
30
+ lumberjack (>= 1.0.2)
31
+ pry (>= 0.9.10)
32
+ thor (>= 0.14.6)
33
+ guard-rspec (3.0.2)
34
+ guard (>= 1.8)
35
+ rspec (~> 2.13)
36
+ i18n (0.6.1)
37
+ listen (1.2.2)
38
+ rb-fsevent (>= 0.9.3)
39
+ rb-inotify (>= 0.9)
40
+ rb-kqueue (>= 0.2)
41
+ localmemcache (0.4.4)
42
+ lumberjack (1.0.3)
43
+ memcache-client (1.8.5)
44
+ method_source (0.8.1)
45
+ mime-types (1.23)
46
+ multi_json (1.7.7)
47
+ pry (0.9.12.2)
48
+ coderay (~> 1.0.5)
49
+ method_source (~> 0.8)
50
+ slop (~> 3.4)
51
+ pry-nav (0.2.3)
52
+ pry (~> 0.9.10)
53
+ rake (10.0.4)
54
+ rb-fsevent (0.9.3)
55
+ rb-inotify (0.9.0)
56
+ ffi (>= 0.5.0)
57
+ rb-kqueue (0.2.0)
58
+ ffi (>= 0.5.0)
59
+ rest-client (1.6.7)
60
+ mime-types (>= 1.16)
61
+ rspec (2.13.0)
62
+ rspec-core (~> 2.13.0)
63
+ rspec-expectations (~> 2.13.0)
64
+ rspec-mocks (~> 2.13.0)
65
+ rspec-core (2.13.1)
66
+ rspec-expectations (2.13.0)
67
+ diff-lcs (>= 1.1.3, < 2.0)
68
+ rspec-mocks (2.13.1)
69
+ simplecov (0.7.1)
70
+ multi_json (~> 1.0)
71
+ simplecov-html (~> 0.7.1)
72
+ simplecov-html (0.7.1)
73
+ slop (3.4.5)
74
+ terminal-notifier-guard (1.5.3)
75
+ thor (0.18.1)
76
+
77
+ PLATFORMS
78
+ ruby
79
+
80
+ DEPENDENCIES
81
+ benchmark-ips
82
+ bundler (~> 1.3)
83
+ coveralls
84
+ dalli
85
+ guard
86
+ guard-rspec
87
+ memcache-client
88
+ pry
89
+ pry-nav
90
+ rake
91
+ rspec
92
+ terminal-notifier-guard
93
+ viscacha!
@@ -0,0 +1,8 @@
1
+ require 'terminal-notifier-guard'
2
+
3
+ guard :rspec do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 HouseTrip Ltd
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,127 @@
1
+ <h1>
2
+ Viscacha —<br/>
3
+ a fast shared memory cache for Rails apps.
4
+ </h1>
5
+
6
+ [![Gem Version](https://badge.fury.io/rb/viscacha.png)](http://badge.fury.io/rb/viscacha)
7
+ [![Build Status](https://travis-ci.org/mezis/viscacha.png?branch=master)](https://travis-ci.org/mezis/viscacha)
8
+ [![Dependency Status](https://gemnasium.com/mezis/viscacha.png)](https://gemnasium.com/mezis/viscacha)
9
+ [![Code Climate](https://codeclimate.com/github/mezis/viscacha.png)](https://codeclimate.com/github/mezis/viscacha)
10
+ [![Coverage Status](https://coveralls.io/repos/mezis/viscacha/badge.png)](https://coveralls.io/r/mezis/viscacha)
11
+
12
+ **TL;DR**: If you have more workers per machine than machines total, Viscacha may be much more efficient than Memcache. Of course YMMV.
13
+
14
+ Reads and writes to Viscacha will always be between 10 and **50 times faster than to a Memcache server**.
15
+
16
+ ### Use cases
17
+
18
+ If you run an app on few machines with multiple workers, typical for feldging apps hosted on Heroku, you're may already be using Memcache to store fragments and the odd flag.
19
+
20
+ The roundtrip to Memcache servers is expensive (3-5ms per `fetch` is typical), so it's not much of an advantage over in-memory caching… except you can't afford the memory for a large cache on each worker.
21
+
22
+ Viscacha lets you run an in-process cache that's almost as fast as `ActiveSupport::MemoryStore`, but
23
+
24
+ - shared between processes on the same machine (or dyno)
25
+ - persistent (to the extent that the machine keeps files—Heroku will of course not persist your cache across dyno restarts)
26
+ - memory mapped (so it doesn't hijack your low dyno resources)
27
+
28
+ It's not shared across *machines* like Memcache is (it's not a server) but for high worker-per-machine to machine ratio (e.g. 2:2, or 4 workers spread over 2 machines), it's really worth it.
29
+
30
+ For bigger apps running on few machines (e.g. 12:4 on Amazon's 8-core instances), it's even more efficient, as your cache will effectively be shared by more workers.
31
+
32
+ ### How it works
33
+
34
+ Viscacha is a fairly thin wrapper around [localmemcache](http://localmemcache.rubyforge.org).
35
+
36
+
37
+ ## Installation
38
+
39
+ Add this line to your application's Gemfile:
40
+
41
+ gem 'viscacha'
42
+
43
+ And then execute:
44
+
45
+ $ bundle
46
+
47
+ If using Rails, in `config/application.rb`:
48
+
49
+ config.cache_store = :viscacha
50
+
51
+ Done!
52
+
53
+
54
+ ## Usage
55
+
56
+ Use as you'd usually use any other ActiveSupport [cache backend](http://apidock.com/rails/ActiveSupport/Cache/Store), the
57
+ excellent [Dalli](https://github.com/mperham/dalli) for instance.
58
+
59
+ **CAVEAT**: by design, calling `#clear` on a Viscacha cache (e.g. through `Rails.cache.clear`), or any other write operation (`#delete`, `#write`), will not propagate to workers on other machines!
60
+
61
+ Be careful to only use `#fetch`, and design accordingly: no cache sweepers, rely on timestamps, IDs, and expiry instead.
62
+
63
+
64
+ ## Benchmarks
65
+
66
+ Bear in mind those are microbenchmarks, so your mileage may vary. The bottom
67
+ line is that on a single machine, Viscacha will be considerably faster than Memcache in pretty much all situations.
68
+
69
+ This compares how 5 cache stores react to repeated `#fetch` calls, using modern hardware and networking (2.2GHz Core i7, running Darwin).
70
+
71
+ - Viscacha
72
+ - `ActiveSupport::MemMacheStore` (using the `memcache-client`)
73
+ - `ActiveSupport::DalliStore` (using the `dalli` gem)
74
+ - `ActiveSupport::DalliStore` running off another machine (local, 1GBps copper connection)
75
+ - `ActiveSupport::MemoryStore` for reference
76
+
77
+ in 3 situations
78
+
79
+ - 100% miss: the key is statistically never present in the cache
80
+ - 100% hit: the key is always present in the cache
81
+ - 50% hit: the key is statistically present in the cache every other call
82
+
83
+ with two types of data:
84
+
85
+ 30 bytes data (could be a set of flags, numbers, a small serialized object):
86
+
87
+ viscacha 100% miss 24630.2 (±30.4%) i/s
88
+ viscacha 100% hit 28908.7 (±32.8%) i/s
89
+ viscacha 50% hit 23857.8 (±30.9%) i/s
90
+ memcache 100% miss 849.8 (±8.1%) i/s
91
+ memcache 100% hit 1667.7 (±11.4%) i/s
92
+ memcache 50% hit 884.8 (±9.5%) i/s
93
+ dalli 100% miss 4526.7 (±28.5%) i/s
94
+ dalli 100% hit 8239.3 (±28.2%) i/s
95
+ dalli 50% hit 4348.7 (±27.9%) i/s
96
+ dalli_r 100% miss 363.7 (±13.2%) i/s
97
+ dalli_r 100% hit 807.8 (±12.1%) i/s
98
+ dalli_r 50% hit 375.6 (±12.8%) i/s
99
+ memory 100% miss 23129.6 (±46.2%) i/s
100
+ memory 100% hit 39914.2 (±64.3%) i/s
101
+ memory 50% hit 22500.3 (±59.8%) i/s
102
+
103
+ 25kb data (a fairly large HTML fragment for instance):
104
+
105
+ viscacha 100% miss 10168.0 (±9.9%) i/s
106
+ viscacha 100% hit 13131.1 (±9.9%) i/s
107
+ viscacha 50% hit 10799.0 (±7.1%) i/s
108
+ memcache 100% miss 2163.7 (±7.9%) i/s
109
+ memcache 100% hit 5179.7 (±3.5%) i/s
110
+ memcache 50% hit 2315.1 (±7.0%) i/s
111
+ dalli 100% miss 3789.7 (±3.7%) i/s
112
+ dalli 100% hit 9539.2 (±8.8%) i/s
113
+ dalli 50% hit 4222.7 (±5.4%) i/s
114
+ dalli_r 100% miss 253.7 (±4.7%) i/s
115
+ dalli_r 100% hit 855.3 (±2.9%) i/s
116
+ dalli_r 50% hit 266.5 (±6.4%) i/s
117
+ memory 100% miss 15276.8 (±11.1%) i/s
118
+ memory 100% hit 29646.5 (±6.9%) i/s
119
+ memory 50% hit 16537.5 (±9.1%) i/s
120
+
121
+ ## Contributing
122
+
123
+ 1. Fork it
124
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
125
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
126
+ 4. Push to the branch (`git push origin my-new-feature`)
127
+ 5. Create a new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Benchmark for Viscacha
4
+ #
5
+ require 'rubygems'
6
+ require 'bundler/setup'
7
+ require 'pathname'
8
+ require 'benchmark/ips'
9
+
10
+ require 'viscacha/store'
11
+ require 'active_support/cache/dalli_store'
12
+ require 'active_support/cache/mem_cache_store'
13
+ require 'active_support/cache/memory_store'
14
+
15
+ # CACHE_PATH = Pathname('bench.lmc')
16
+ # CACHE_PATH.delete if CACHE_PATH.exist?
17
+
18
+ SIZE = 64.megabytes
19
+ DATA = SecureRandom.random_bytes(25.kilobytes)
20
+ # DATA = { foo:12.34, bar:56, qux:nil }
21
+ SLOTS = SIZE / Marshal.dump(DATA).bytesize
22
+
23
+
24
+ CACHES = {
25
+ viscacha: Viscacha::Store.new(name:'bench', directory:'.', size: 64.megabytes),
26
+ memcache: ActiveSupport::Cache::MemCacheStore.new(size: 64.megabytes),
27
+ dalli: ActiveSupport::Cache::DalliStore.new,
28
+ dalli_r: ActiveSupport::Cache::DalliStore.new('kitekat.local:11211'),
29
+ memory: ActiveSupport::Cache::MemoryStore.new(size: 64.megabytes)
30
+ }
31
+
32
+ Benchmark.ips do |x|
33
+ CACHES.each_pair do |name,cache|
34
+ x.report("#{name} 100% miss") { cache.fetch(rand(10_000_000).to_s, compress:false) { DATA } }
35
+ x.report("#{name} 100% hit") { cache.fetch('foo', compress:false) { DATA } }
36
+ x.report("#{name} 50% hit") { cache.fetch(rand(SLOTS * 2).to_s, compress:false) { DATA } }
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ #
2
+ # Tie Viscacha to the ActiveSupport namespace
3
+ #
4
+ require 'active_support/cache'
5
+ require 'viscacha/store'
6
+
7
+ class ActiveSupport::Cache::Viscacha < Viscacha::Store
8
+ end
@@ -0,0 +1 @@
1
+ require 'active_support/cache/viscacha'
@@ -0,0 +1,125 @@
1
+ require 'viscacha/version'
2
+ require 'active_support/cache'
3
+ require 'localmemcache'
4
+
5
+ module Viscacha
6
+ class Store < ActiveSupport::Cache::Store
7
+ DEFAULT_DIR = Pathname('tmp')
8
+ DEFAULT_NAME = 'viscacha'
9
+ DEFAULT_SIZE = 16.megabytes # also the minimum size, as localmemcache is
10
+ # unreliable below this value
11
+
12
+ def initialize(options = {})
13
+ super options
14
+
15
+ directory = options.fetch(:directory, DEFAULT_DIR)
16
+ name = options.fetch(:name, DEFAULT_NAME)
17
+ size = options.fetch(:size, DEFAULT_SIZE)
18
+
19
+ data_store_options = {
20
+ filename: Pathname.new(directory).join("#{name}-data.lmc").to_s,
21
+ size_mb: [DEFAULT_SIZE, size].max / 1.megabyte
22
+ }
23
+ meta_store_options = {
24
+ filename: Pathname.new(directory).join("#{name}-meta.lmc").to_s,
25
+ size_mb: data_store_options[:size_mb]
26
+ }
27
+
28
+ @data_store = LocalMemCache.new(data_store_options)
29
+ @meta_store = LocalMemCache.new(meta_store_options)
30
+ end
31
+
32
+ def clear(options = nil)
33
+ data_store.clear
34
+ meta_store.clear
35
+ self
36
+ end
37
+
38
+ def cleanup(options = nil)
39
+ true
40
+ end
41
+
42
+ def increment(name, amount = 1, options = nil)
43
+ raise NotImplementedError.new("#{self.class.name} does not support #{__method__}")
44
+ end
45
+
46
+ def decrement(name, amount = 1, options = nil)
47
+ raise NotImplementedError.new("#{self.class.name} does not support #{__method__}")
48
+ end
49
+
50
+ def delete_matched(matcher, options = nil)
51
+ raise NotImplementedError.new("#{self.class.name} does not support #{__method__}")
52
+ end
53
+
54
+
55
+ protected
56
+
57
+ attr_reader :data_store, :meta_store
58
+
59
+ def read_entry(key, options = {})
60
+ data = data_store[key]
61
+ meta = meta_store[key]
62
+ return nil if data.nil? || meta.nil? || data.empty? || meta.empty?
63
+ entry = metadata_unpack(meta, data)
64
+ touch_entry(entry, key)
65
+ entry
66
+ end
67
+
68
+ def write_entry(key, entry, options = {})
69
+ make_space_for(entry.raw_value.bytesize)
70
+ data_store[key] = entry.raw_value
71
+ meta_store[key] = metadata_pack(entry)
72
+ true
73
+ end
74
+
75
+ def delete_entry(key, options = {})
76
+ meta = meta_store[key]
77
+ data_store.delete(key)
78
+ meta_store.delete(key)
79
+ !(meta.nil? || meta.empty?)
80
+ end
81
+
82
+ def make_space_for(bytes)
83
+ return true if get_free_space > (bytes * 2)
84
+
85
+ keys = []
86
+ meta_store.each_pair do |key,meta|
87
+ keys << [key, meta.unpack('GGNC').first]
88
+ end
89
+
90
+ keys.sort_by(&:last).each do |key,_|
91
+ delete_entry(key)
92
+ return true if get_free_space > (bytes * 2) && get_free_ratio > 0.15
93
+ end
94
+
95
+ false
96
+ end
97
+
98
+ def touch_entry(entry, key)
99
+ meta_store[key] = metadata_pack(entry, Time.now.to_f)
100
+ end
101
+
102
+ def get_free_space
103
+ data_store.shm_status[:free_bytes]
104
+ end
105
+
106
+ def get_free_ratio
107
+ 1.0 * data_store.shm_status[:free_bytes] / data_store.shm_status[:total_bytes]
108
+ end
109
+
110
+ def metadata_pack(entry, used_at = nil)
111
+ used_at ||= entry.created_at
112
+ [used_at, entry.created_at, entry.expires_in || 0, entry.compressed? ? 1 : 0].pack('GGNC')
113
+ end
114
+
115
+ def metadata_unpack(meta, data)
116
+ used_at, created_at, expires_in, compressed = meta.unpack('GGNC')
117
+
118
+ compressed = (compressed == 1)
119
+ expires_in = nil if expires_in == 0
120
+
121
+ ActiveSupport::Cache::Entry.create(data, created_at, compressed: compressed, expires_in: expires_in)
122
+ end
123
+ end
124
+ end
125
+
@@ -0,0 +1,3 @@
1
+ module Viscacha
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ Pathname('tmp').mkpath
@@ -0,0 +1,176 @@
1
+ require 'spec_helper'
2
+ require 'viscacha/store'
3
+ require 'pathname'
4
+ require 'securerandom'
5
+
6
+ describe Viscacha::Store do
7
+ NAME = $$
8
+
9
+ describe 'cache behaviour' do
10
+ subject { described_class.new directory:'tmp', name:NAME }
11
+ before { subject.clear }
12
+
13
+ describe 'read/write/delete' do
14
+ context 'when cache is empty' do
15
+ it '#read returns nil' do
16
+ subject.read('foo').should be_nil
17
+ end
18
+
19
+ it '#write returns true' do
20
+ subject.write('foo', '1337').should be_true
21
+ end
22
+
23
+ it '#delete returns false' do
24
+ subject.delete('foo').should be_false
25
+ end
26
+ end
27
+
28
+ context 'when cache is not empty' do
29
+ before do
30
+ subject.write('foo', '1337')
31
+ end
32
+
33
+ it '#read returns cached value' do
34
+ subject.read('foo').should eq('1337')
35
+ end
36
+
37
+ it '#write returns true' do
38
+ subject.write('foo', '1338').should be_true
39
+ end
40
+
41
+ it '#delete returns true' do
42
+ subject.delete('foo').should be_true
43
+ end
44
+ end
45
+
46
+ it 'caches structured values' do
47
+ data = { foo: 12.34, bar: 56, qux: nil }
48
+ subject.write('foo', data)
49
+ subject.read('foo').should eq(data)
50
+ end
51
+ end
52
+
53
+ describe '#increment'
54
+ describe '#decrement'
55
+ describe '#cleanup'
56
+ describe '#clear'
57
+
58
+ describe '#fetch' do
59
+ it 'persists values' do
60
+ subject.fetch('foo') { '1337' }
61
+ result = subject.fetch('foo') { '1338' }
62
+ result.should == '1337'
63
+ end
64
+
65
+ it 'is lazy' do
66
+ generator = stub value:'1337'
67
+ generator.should_receive(:value).once
68
+
69
+ 2.times do
70
+ subject.fetch('foo') { generator.value }
71
+ end
72
+ end
73
+ end
74
+
75
+ describe '#write'
76
+ describe '#read'
77
+ describe '#exist?'
78
+ describe '#delete'
79
+ end
80
+
81
+
82
+ describe 'eviction' do
83
+ def blob(size_mb)
84
+ SecureRandom.random_bytes(size_mb.megabytes)
85
+ end
86
+
87
+ subject { described_class.new(directory: 'tmp', name: $$, size: 16.megabytes) }
88
+ before { subject.clear }
89
+
90
+ it 'evicts items' do
91
+ 16.times do |index|
92
+ subject.write(index.to_s, blob(1))
93
+ end
94
+
95
+ # backend = subject.send(:meta_store)
96
+ # backend = subject.send(:data_store)
97
+ # require 'pry' ; require 'pry-nav' ; binding.pry
98
+ end
99
+
100
+ it 'evicts the oldest item' do
101
+ subject.write('foo', 'bar')
102
+ 16.times do |index|
103
+ subject.write(index.to_s, blob(1))
104
+ end
105
+
106
+ subject.read('foo').should be_nil
107
+ end
108
+
109
+ it 'evicts the least recently used item' do
110
+ subject.write '1', blob(3)
111
+ # sleep 10e-3
112
+ subject.write '2', blob(3)
113
+ # sleep 10e-3
114
+ subject.write '3', blob(3)
115
+ # sleep 10e-3
116
+ subject.read '1'
117
+ # sleep 10e-3
118
+ subject.write '4', blob(3)
119
+
120
+ classes = (1..4).map { |index| subject.read(index.to_s).class }
121
+ classes.should == [String, NilClass, String, String]
122
+ end
123
+ end
124
+
125
+ describe 'persistence' do
126
+ subject { described_class.new directory:'tmp', name:NAME }
127
+
128
+ before do
129
+ fork do
130
+ subject.clear
131
+ subject.write 'foo', 'bar'
132
+ exit 0
133
+ end
134
+ Process.wait
135
+ end
136
+
137
+ it 'can read on-disk data' do
138
+ subject.read('foo').should == 'bar'
139
+ end
140
+ end
141
+
142
+ describe 'supports concurrency' do
143
+ def cache_factory
144
+ described_class.new directory:'tmp', name:NAME
145
+ end
146
+
147
+ it 'in the same thread' do
148
+ cache1 = cache_factory
149
+ cache2 = cache_factory
150
+
151
+ cache1.write('foo', 'bar1')
152
+ cache2.write('foo', 'bar2')
153
+ cache1.read('foo').should == 'bar2'
154
+ end
155
+
156
+ it 'across multiple processes' do
157
+ cache_factory.clear
158
+
159
+ (0..4).each do |process_index|
160
+ fork do
161
+ cache = cache_factory
162
+ (0..99).each do |index|
163
+ cache.write((index * 5 + process_index).to_s, "cache#{process_index}")
164
+ end
165
+ exit 0
166
+ end
167
+ end
168
+ Process.wait
169
+
170
+ cache = cache_factory
171
+ (0..499).each do |index|
172
+ cache.read(index.to_s).should =~ /cache\d/
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'viscacha/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "viscacha"
8
+ spec.version = Viscacha::VERSION
9
+ spec.authors = ["Julien Letessier"]
10
+ spec.email = ["julien.letessier@gmail.com"]
11
+ spec.description = %q{Shared memory cache for ActiveSupport, leveraging the localmemcache gem.}
12
+ spec.summary = %q{Shared memory cache for ActiveSupport}
13
+ spec.homepage = ""
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_dependency "localmemcache", "~> 0.4.0"
22
+ spec.add_dependency "activesupport"
23
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: viscacha
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Julien Letessier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: localmemcache
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Shared memory cache for ActiveSupport, leveraging the localmemcache gem.
42
+ email:
43
+ - julien.letessier@gmail.com
44
+ executables:
45
+ - bench
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - ".rspec"
51
+ - ".travis.yml"
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - Guardfile
55
+ - LICENSE.txt
56
+ - README.md
57
+ - Rakefile
58
+ - bin/bench
59
+ - lib/active_support/cache/viscacha.rb
60
+ - lib/viscacha.rb
61
+ - lib/viscacha/store.rb
62
+ - lib/viscacha/version.rb
63
+ - spec/spec_helper.rb
64
+ - spec/viscacha/store_spec.rb
65
+ - viscacha.gemspec
66
+ homepage: ''
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.7.6
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Shared memory cache for ActiveSupport
90
+ test_files:
91
+ - spec/spec_helper.rb
92
+ - spec/viscacha/store_spec.rb