remix-stash 0.9.0 → 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2009 Paperless Post Inc.
2
+ Copyright (c) 2009 Brian Mitchell
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,32 @@
1
+ Real docs coming soon! Check out the examples directory for more.
2
+
3
+ # Quick Specs
4
+
5
+ New API! I've rethought a lot of the API and this comes with a lot of new capabilities. More work is being done on making it as expressive as possible without terrible overhead. This includes vectorized keys which allow emulation of partial cache clearing as well as nice shortcuts like eval and gate for expressions. Options, clusters, and implicit scope are easy to manage on a stash-by-stash basis. Keys are also easy to pass in as it will create composite keys from whatever you pass in (as long as it has to_s) so no more ugly string interpolation all over the place.
6
+
7
+ It's fast (faster than memcache-client). It's simple (pure ruby and only a few hundred lines). It's tested (shoulda). Of course, because it's pure ruby it will run almost anywhere as well unlike many other clients.
8
+
9
+ It does require memcached 1.4+ but you should be running that anyway (if you aren't, upgrade already).
10
+
11
+ Take a look and let me know what you think!
12
+
13
+ # TODO
14
+
15
+ * namespacing
16
+ * implement the rest of the memcached 1.4 binary API (replace, append, prepend)
17
+ * allow swappable cluster types for consistent hashing, ketama, etc...
18
+ * failsafe marshal load
19
+ * support non-marshal value dumps configured per stash
20
+ * support multi vector sets
21
+ * thread safe cluster
22
+ * add block form
23
+ * quiet/multi command forms (will require a protocol refactoring most likely)
24
+ * server pings
25
+ * complete stats API
26
+ * incr/decr should take default value flags
27
+ * get/set add/replace read/write should allow a CAS flag to be passed
28
+ * accelerated binary API implementation with Ruby fallback
29
+ * redis support for vectors and/or value
30
+ * large key handling support
31
+ * UDP support (more experimentation on the tradeoffs)
32
+ * EventMachine integration (non-blocking?)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = 'remix-stash'
5
+ gemspec.summary = 'Remix your memcache'
6
+ gemspec.email = 'binary42@gmail.com'
7
+ gemspec.homepage = 'http://github.com/binary42/remix-stash'
8
+ gemspec.authors = ['Brian Mitchell']
9
+ end
10
+ rescue LoadError
11
+ puts 'Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com'
12
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.6
@@ -0,0 +1,35 @@
1
+ require 'benchmark'
2
+ require File.dirname(__FILE__) + '/../harness'
3
+
4
+ LARGE_NUMBER = 50_000
5
+
6
+ Benchmark.bmbm do |b|
7
+ b.report('get/set remix-stash') do
8
+ LARGE_NUMBER.times {|n|
9
+ stash[:abcxyz123] = n
10
+ stash[:abcxyz123]
11
+ }
12
+ end
13
+ b.report('get/set remix-stash named') do
14
+ LARGE_NUMBER.times {|n|
15
+ stash(:stuff)[:abcxyz123] = n
16
+ stash(:stuff)[:abcxyz123]
17
+ }
18
+ end
19
+ if defined?(CCache)
20
+ b.report('get/set memcached') do
21
+ LARGE_NUMBER.times {|n|
22
+ CCache.set('abcxyz123', n, 0 , true)
23
+ CCache.get('abcxyz123', true)
24
+ }
25
+ end
26
+ end
27
+ if defined?(RCache)
28
+ b.report('get/set memcache-client') do
29
+ LARGE_NUMBER.times {|n|
30
+ RCache.set('abcxyz123', n)
31
+ RCache.get('abcxyz123')
32
+ }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,125 @@
1
+ require 'benchmark'
2
+ require File.dirname(__FILE__) + '/../harness'
3
+
4
+ LARGE_NUMBER = 20_000
5
+
6
+ huge_value = 'a' * 100_000
7
+ large_value = 'b' * 20_000
8
+ med_value = 'c' * 2_000
9
+ small_value = 'd' * 100
10
+ tiny_value = 'e'
11
+
12
+ KEY = 'abc123xyz'
13
+
14
+ Benchmark.bmbm do |b|
15
+ b.report('100k remix-stash') do
16
+ LARGE_NUMBER.times {
17
+ stash.write(KEY, huge_value)
18
+ stash.read(KEY)
19
+ }
20
+ end
21
+ if defined?(CCache)
22
+ b.report('100k memcached') do
23
+ LARGE_NUMBER.times {
24
+ CCache.set(KEY, huge_value, 0, false)
25
+ CCache.get(KEY, false)
26
+ }
27
+ end
28
+ end
29
+ if defined?(RCache)
30
+ b.report('100k memcache-client') do
31
+ LARGE_NUMBER.times {
32
+ RCache.set(KEY, huge_value, 0, true)
33
+ RCache.get(KEY, true)
34
+ }
35
+ end
36
+ end
37
+ b.report('20k remix-stash') do
38
+ LARGE_NUMBER.times {
39
+ stash.write(KEY, large_value)
40
+ stash.read(KEY)
41
+ }
42
+ end
43
+ if defined?(CCache)
44
+ b.report('20k memcached') do
45
+ LARGE_NUMBER.times {
46
+ CCache.set(KEY, large_value, 0, false)
47
+ CCache.get(KEY, false)
48
+ }
49
+ end
50
+ end
51
+ if defined?(RCache)
52
+ b.report('20k memcache-client') do
53
+ LARGE_NUMBER.times {
54
+ RCache.set(KEY, large_value, 0, true)
55
+ RCache.get(KEY, true)
56
+ }
57
+ end
58
+ end
59
+ b.report('2k remix-stash') do
60
+ LARGE_NUMBER.times {
61
+ stash.write(KEY, med_value)
62
+ stash.read(KEY)
63
+ }
64
+ end
65
+ if defined?(CCache)
66
+ b.report('2k memcached') do
67
+ LARGE_NUMBER.times {
68
+ CCache.set(KEY, med_value, 0, false)
69
+ CCache.get(KEY, false)
70
+ }
71
+ end
72
+ end
73
+ if defined?(RCache)
74
+ b.report('2k memcache-client') do
75
+ LARGE_NUMBER.times {
76
+ RCache.set(KEY, med_value, 0, true)
77
+ RCache.get(KEY, true)
78
+ }
79
+ end
80
+ end
81
+ b.report('100b remix-stash') do
82
+ LARGE_NUMBER.times {
83
+ stash.write(KEY, small_value)
84
+ stash.read(KEY)
85
+ }
86
+ end
87
+ if defined?(CCache)
88
+ b.report('100b memcached') do
89
+ LARGE_NUMBER.times {
90
+ CCache.set(KEY, small_value, 0, false)
91
+ CCache.get(KEY, false)
92
+ }
93
+ end
94
+ end
95
+ if defined?(RCache)
96
+ b.report('100b memcache-client') do
97
+ LARGE_NUMBER.times {
98
+ RCache.set(KEY, small_value, 0, true)
99
+ RCache.get(KEY, true)
100
+ }
101
+ end
102
+ end
103
+ b.report('1b remix-stash') do
104
+ LARGE_NUMBER.times {
105
+ stash.write(KEY, tiny_value)
106
+ stash.read(KEY)
107
+ }
108
+ end
109
+ if defined?(CCache)
110
+ b.report('1b memcached') do
111
+ LARGE_NUMBER.times {
112
+ CCache.set(KEY, tiny_value, 0, false)
113
+ CCache.get(KEY, false)
114
+ }
115
+ end
116
+ end
117
+ if defined?(RCache)
118
+ b.report('1b memcache-client') do
119
+ LARGE_NUMBER.times {
120
+ RCache.set(KEY, tiny_value, 0, true)
121
+ RCache.get(KEY, true)
122
+ }
123
+ end
124
+ end
125
+ end
data/examples/eval.rb ADDED
@@ -0,0 +1,8 @@
1
+ require File.dirname(__FILE__) + '/../harness'
2
+
3
+ stuff = stash(:stuff)
4
+
5
+ stuff.eval(:ans) {42}
6
+ p stuff.get(:ans)
7
+
8
+ stuff.eval(:ans) {fail 'Cache miss'}
data/examples/gate.rb ADDED
@@ -0,0 +1,6 @@
1
+ require File.dirname(__FILE__) + '/../harness'
2
+
3
+ p stash.gate(:x) {p :miss}
4
+ p :set_x
5
+ stash[:x] = true
6
+ p stash.gate(:x) {p :miss}
@@ -0,0 +1,13 @@
1
+ require File.dirname(__FILE__) + '/../harness'
2
+
3
+ # Get and set a simple key
4
+ stash.set('answer', 42)
5
+ p stash.get('answer')
6
+
7
+ # Alternate methods
8
+ stash[:answer] = :fortytwo
9
+ p stash[:answer]
10
+
11
+ # Composite keys
12
+ stash[1,2,3,4] = 5
13
+ p stash[1,2,3,4]
data/examples/scope.rb ADDED
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/../harness'
2
+
3
+ a = 0
4
+
5
+ stash(:x).scope {a}
6
+
7
+ stash(:x)[1] = 2
8
+
9
+ p stash(:x)[1]
10
+
11
+ a += 1
12
+
13
+ p stash(:x)[1]
14
+
15
+ a = 0
16
+
17
+ p stash(:x)[1]
data/examples/stash.rb ADDED
@@ -0,0 +1,16 @@
1
+ require File.dirname(__FILE__) + '/../harness'
2
+
3
+ one = stash(:one)
4
+ two = stash(:two)
5
+
6
+ one[:x] = 10
7
+ two[:x] = 12
8
+
9
+ p one[:x]
10
+ p two[:x]
11
+
12
+ p :clearing
13
+ one.clear
14
+
15
+ p one[:x]
16
+ p two[:x]
data/harness.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/lib'
3
+ require 'remix/stash'
4
+
5
+ begin
6
+ require 'memcached'
7
+ CCache = Memcached.new('localhost:11211')
8
+ rescue
9
+ puts "memcached not found (skipping)"
10
+ end
11
+
12
+ begin
13
+ require 'memcache'
14
+ RCache = MemCache.new('localhost:11211')
15
+ rescue
16
+ puts "memcached-client not found (skipping)"
17
+ end
18
+
19
+ stash.clear
@@ -0,0 +1,79 @@
1
+ require 'socket'
2
+ require 'digest/md5'
3
+
4
+ class Remix::Stash::Cluster
5
+ include Socket::Constants
6
+
7
+ @@connections = {}
8
+
9
+ attr_reader :hosts
10
+
11
+ def initialize(hosts)
12
+ @hosts = hosts.map {|x|
13
+ host, port = x.split(':')
14
+ [x, host, (port || 11211).to_i]
15
+ }.sort_by {|(_,h,p)| [h,p]}
16
+ end
17
+
18
+ def each
19
+ @hosts.each do |h|
20
+ begin
21
+ io = host_to_io(*h)
22
+ break yield(io)
23
+ rescue Errno::EPIPE, Errno::ECONNRESET, Remix::Stash::ProtocolError
24
+ io.close
25
+ retry
26
+ rescue Errno::EAGAIN, Errno::ECONNREFUSED
27
+ next
28
+ end
29
+ end
30
+ end
31
+
32
+ # Note: Later, I'd like to support richer cluster definitions.
33
+ # This should do the trick for now... and it's fast.
34
+ def select(key)
35
+ count = @hosts.size
36
+ hash = nil
37
+ if count > 1
38
+ digest = Digest::MD5.digest(key)
39
+ hash = digest.unpack("L")[0]
40
+ else
41
+ hash = 0
42
+ end
43
+ count.times do |try|
44
+ begin
45
+ io = host_to_io(*@hosts[(hash + try) % count])
46
+ return yield(io)
47
+ rescue Errno::EPIPE, Errno::ECONNRESET, Remix::Stash::ProtocolError
48
+ io.close
49
+ retry
50
+ rescue Errno::EAGAIN, Errno::ECONNREFUSED
51
+ next
52
+ end
53
+ end
54
+ raise Remix::Stash::ClusterError,
55
+ "Unable to find suitable host to communicate with for #{key.inspect} (MD5-32=#{hash})"
56
+ end
57
+
58
+ private
59
+
60
+ def connect(host, port)
61
+ socket = TCPSocket.new(host, port)
62
+ set_timeout(socket, 2)
63
+ socket
64
+ end
65
+
66
+ def host_to_io(key, host, port)
67
+ socket = @@connections[key] ||= connect(host, port)
68
+ return socket unless socket.closed?
69
+ @@connections.delete(key)
70
+ host_to_io(key, host, port)
71
+ end
72
+
73
+ def set_timeout(socket, seconds)
74
+ timeout = [seconds, 0].pack('l_2') # 2 seconds
75
+ socket.setsockopt(SOL_SOCKET, SO_SNDTIMEO, timeout)
76
+ socket.setsockopt(SOL_SOCKET, SO_RCVTIMEO, timeout)
77
+ end
78
+
79
+ end
@@ -0,0 +1,7 @@
1
+ module Remix::Stash::Extension
2
+
3
+ def stash(name = :root)
4
+ Remix::Stash.new(name)
5
+ end
6
+
7
+ end
@@ -0,0 +1,139 @@
1
+ module Remix::Stash::Protocol
2
+ extend self
3
+
4
+ HEADER_FORMAT = "CCnCCnNNQ"
5
+
6
+ # Magic codes
7
+ REQUEST = 0x80
8
+ RESPONSE = 0x81
9
+
10
+ # Command codes
11
+ GET = 0x00
12
+ SET = 0x01
13
+ ADD = 0x02
14
+ REPLACE = 0x03
15
+ DELETE = 0x04
16
+ INCREMENT = 0x05
17
+ DECREMENT = 0x06
18
+ QUIT = 0x07
19
+ FLUSH = 0x08
20
+ GET_Q = 0x09
21
+ NO_OP = 0x0A
22
+ VERSION = 0x0B
23
+ GET_K = 0x0C
24
+ GET_K_Q = 0x0D
25
+ APPEND = 0x0E
26
+ PREPEND = 0x0F
27
+ STAT = 0x10
28
+ SET_Q = 0x11
29
+ ADD_Q = 0x12
30
+ REPLACE_Q = 0x13
31
+ DELETE_Q = 0x14
32
+ INCREMENT_Q = 0x15
33
+ DECREMENT_Q = 0x16
34
+ QUIT_Q = 0x17
35
+ FLUSH_Q = 0x18
36
+ APPEND_Q = 0x19
37
+ PREPEND_Q = 0x20
38
+
39
+ # Response codes
40
+ NO_ERROR = 0x0000
41
+ KEY_NOT_FOUND = 0x0001
42
+ KEY_EXISTS = 0x0002
43
+ VALUE_TOO_LARGE = 0x0003
44
+ INVALID_ARGUMENTS = 0x0004
45
+ ITEM_NOT_STORED = 0x0005
46
+ INCR_ON_NON_NUMERIC_VALUE = 0x0006
47
+ UNKNOWN_COMMAND = 0x0081
48
+ OUT_OF_MEMORY = 0x0082
49
+
50
+ # Extras
51
+ COUNTER_FAULT_EXPIRATION = 0xFFFFFFFF
52
+
53
+ ADD_PACKET = HEADER_FORMAT + 'NNa*a*'
54
+ def add(io, key, data, ttl = 0)
55
+ header = [REQUEST, ADD, key.size, 8, 0, 0, data.size + key.size + 8, 0, 0, 0, ttl, key, data].pack(ADD_PACKET)
56
+ io.write(header)
57
+ resp = read_resp(io)
58
+ resp[:status] == NO_ERROR
59
+ end
60
+
61
+ DECR_PACKET = HEADER_FORMAT + 'NNQNa*'
62
+ def decr(io, key, step)
63
+ low, high = split64(step)
64
+ header = [REQUEST, DECREMENT, key.size, 20, 0, 0, key.size + 20, 0, 0, high, low, 0, COUNTER_FAULT_EXPIRATION, key].pack(DECR_PACKET)
65
+ io.write(header)
66
+ resp = read_resp(io)
67
+ if resp[:status] == NO_ERROR
68
+ parse_counter(resp[:body])
69
+ end
70
+ end
71
+
72
+ DELETE_PACKET = HEADER_FORMAT + 'a*'
73
+ def delete(io, key, ttl = 0)
74
+ header = [REQUEST, DELETE, key.size, 0, 0, 0, key.size, 0, 0, key].pack(DELETE_PACKET)
75
+ io.write(header)
76
+ resp = read_resp(io)
77
+ resp[:status] == NO_ERROR
78
+ end
79
+
80
+ FLUSH_PACKET = HEADER_FORMAT + 'N'
81
+ def flush(io)
82
+ header = [REQUEST, FLUSH, 0, 4, 0, 0, 4, 0, 0, 0].pack(FLUSH_PACKET)
83
+ io.write(header)
84
+ resp = read_resp(io)
85
+ resp[:status] == NO_ERROR
86
+ end
87
+
88
+ GET_PACKET = HEADER_FORMAT + 'a*'
89
+ GET_BODY = 4..-1
90
+ def get(io, key)
91
+ header = [REQUEST, GET, key.size, 0, 0, 0, key.size, 0, 0, key].pack(GET_PACKET)
92
+ io.write(header)
93
+ resp = read_resp(io)
94
+ resp[:status] == NO_ERROR ? resp[:body][GET_BODY] : nil
95
+ end
96
+
97
+ INCR_PACKET = HEADER_FORMAT + 'NNQNa*'
98
+ def incr(io, key, step)
99
+ low, high = split64(step)
100
+ header = [REQUEST, INCREMENT, key.size, 20, 0, 0, key.size + 20, 0, 0, high, low, 0, COUNTER_FAULT_EXPIRATION, key].pack(INCR_PACKET)
101
+ io.write(header)
102
+ resp = read_resp(io)
103
+ if resp[:status] == NO_ERROR
104
+ parse_counter(resp[:body])
105
+ end
106
+ end
107
+
108
+ SET_PACKET = HEADER_FORMAT + 'NNa*a*'
109
+ def set(io, key, data, ttl = 0)
110
+ header = [REQUEST, SET, key.size, 8, 0, 0, data.size + key.size + 8, 0, 0, 0, ttl, key, data].pack(SET_PACKET)
111
+ io << header
112
+ resp = read_resp(io)
113
+ resp[:status] == NO_ERROR
114
+ end
115
+
116
+ private
117
+
118
+ COUNTER_SPLIT = 'NN'
119
+ def parse_counter(body)
120
+ a, b = body.unpack(COUNTER_SPLIT)
121
+ b | (a << 32)
122
+ end
123
+
124
+ RESP_HEADER = '@6nN'
125
+ def read_resp(io)
126
+ header = io.read(24)
127
+ header or raise Remix::Stash::ProtocolError,
128
+ "No data in response header"
129
+ status, body_length = *header.unpack(RESP_HEADER)
130
+ body_length.zero? ?
131
+ {:status => status} :
132
+ {:status => status, :body => io.read(body_length)}
133
+ end
134
+
135
+ def split64(n)
136
+ [0xFFFFFFFF & n, n >> 32]
137
+ end
138
+
139
+ end