binary42-remix-stash 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/LICENSE +21 -0
- data/README.mdown +18 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/examples/bench.rb +20 -0
- data/examples/eval.rb +8 -0
- data/examples/gate.rb +6 -0
- data/examples/getset.rb +13 -0
- data/examples/getset_cache.rb +5 -0
- data/examples/scope.rb +17 -0
- data/examples/stash.rb +16 -0
- data/harness.rb +4 -0
- data/harness_cache.rb +5 -0
- data/lib/remix/stash.rb +210 -0
- data/lib/remix/stash/cluster.rb +62 -0
- data/lib/remix/stash/extension.rb +8 -0
- data/lib/remix/stash/protocol.rb +253 -0
- data/remix-stash.gemspec +70 -0
- data/spec/extension_spec.rb +25 -0
- data/spec/spec.rb +8 -0
- data/spec/stash_spec.rb +389 -0
- metadata +84 -0
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.mdown
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Coming Soon!
|
2
|
+
|
3
|
+
# Quick Specs
|
4
|
+
|
5
|
+
It's fast (over 2x faster than memcache-client). It's simple (pure ruby and only a few hundred lines). It's tested (shoulda).
|
6
|
+
|
7
|
+
It does require memcached 1.4+ but you should be running that anyway (if you aren't, upgrade already).
|
8
|
+
|
9
|
+
# TODO
|
10
|
+
|
11
|
+
* clear on root should do it for each host in a cluster
|
12
|
+
* optimize option merging with cache
|
13
|
+
* make clusters selectable per stash
|
14
|
+
* implement the rest of the memcached 1.4 binary API
|
15
|
+
* allow swappable cluster types for consistent hashing, ketama, etc...
|
16
|
+
* failsafe marshal load
|
17
|
+
* support non-marshal value dumps configured per stash
|
18
|
+
* support intersected stashes with joined vector sets
|
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.0
|
data/examples/bench.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require File.dirname(__FILE__) + '/../harness'
|
3
|
+
require File.dirname(__FILE__) + '/../harness_cache'
|
4
|
+
|
5
|
+
LARGE_NUMBER = 20_000
|
6
|
+
|
7
|
+
Benchmark.bm do |b|
|
8
|
+
b.report('get/set stash') do
|
9
|
+
LARGE_NUMBER.times {|n|
|
10
|
+
stash[:abcxyz123] = n
|
11
|
+
stash[:abcxyz123]
|
12
|
+
}
|
13
|
+
end
|
14
|
+
b.report('get/set cache') do
|
15
|
+
LARGE_NUMBER.times {|n|
|
16
|
+
Cache.set('abcxyz123', n)
|
17
|
+
Cache.get('abcxyz123')
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
data/examples/eval.rb
ADDED
data/examples/gate.rb
ADDED
data/examples/getset.rb
ADDED
@@ -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
data/examples/stash.rb
ADDED
data/harness.rb
ADDED
data/harness_cache.rb
ADDED
data/lib/remix/stash.rb
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
class Stash
|
2
|
+
require 'remix/stash/extension'
|
3
|
+
require 'remix/stash/cluster'
|
4
|
+
require 'remix/stash/protocol'
|
5
|
+
|
6
|
+
attr_accessor :name
|
7
|
+
|
8
|
+
@@instances = {}
|
9
|
+
@@clusters = {:default => Cluster.new(%w[localhost:11211])}
|
10
|
+
|
11
|
+
def self.cluster(name)
|
12
|
+
@@clusters[name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.cycle_action
|
16
|
+
@@instances.each {|name, stash|
|
17
|
+
stash.cycle if stash.default[:coherency] == :action}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.define_cluster(clusters)
|
21
|
+
clusters.each do |k,v|
|
22
|
+
@@clusters[k] = Cluster.new(v)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.new(name)
|
27
|
+
@@instances[name] ||= super
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(name)
|
31
|
+
@name = name
|
32
|
+
@scope = nil
|
33
|
+
@opts = name == :root ? {:coherency => :action, :ttl => 0} : {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear(*keys)
|
37
|
+
if keys.empty?
|
38
|
+
if @name == :root
|
39
|
+
cluster.each {|io| Protocol.flush(io)}
|
40
|
+
else
|
41
|
+
vk = vector_key
|
42
|
+
cluster.select(vk) {|io|
|
43
|
+
unless Protocol.incr(io, vk, 1)
|
44
|
+
Protocol.add(io, vk, '0')
|
45
|
+
Protocol.incr(io, vk, 1)
|
46
|
+
end
|
47
|
+
}
|
48
|
+
end
|
49
|
+
cycle
|
50
|
+
else
|
51
|
+
# remove a specific key
|
52
|
+
key = canonical_key(keys)
|
53
|
+
cluster.select(key) {|io| Protocol.delete(io, key)}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def clear_scope
|
58
|
+
@scope = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def cycle
|
62
|
+
@vector = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def decr(*keys)
|
66
|
+
step = keys.pop
|
67
|
+
key = canonical_key(keys)
|
68
|
+
cluster.select(key) {|io| Protocol.decr(io, key, step)}
|
69
|
+
end
|
70
|
+
|
71
|
+
def default(opts = {})
|
72
|
+
base = @opts.merge!(opts)
|
73
|
+
if opts.has_key? :coherency
|
74
|
+
[:dynamic, :action, :transaction].include?(opts[:coherency]) or raise ArgumentError,
|
75
|
+
"Invalid coherency setting used (#{opts[:coherency].inspect})"
|
76
|
+
end
|
77
|
+
root = @@instances[:roto] || Stash.new(:root)
|
78
|
+
self == root ?
|
79
|
+
base :
|
80
|
+
root.default.merge(base)
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete(*keys)
|
84
|
+
key = canonical_key(keys)
|
85
|
+
cluster.select(key) {|io| Protocol.delete(io, key)}
|
86
|
+
end
|
87
|
+
|
88
|
+
def eval(*keys)
|
89
|
+
opts = default_opts(keys)
|
90
|
+
key = canonical_key(keys)
|
91
|
+
cluster.select(key) {|io|
|
92
|
+
value = Protocol.get_value(io, key)
|
93
|
+
unless value
|
94
|
+
value = yield(*keys)
|
95
|
+
Protocol.set_value(io, key, value, opts[:ttl])
|
96
|
+
end
|
97
|
+
value
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def gate(*keys)
|
102
|
+
key = canonical_key(keys)
|
103
|
+
cluster.select(key) {|io|
|
104
|
+
if Protocol.get(io, key)
|
105
|
+
true
|
106
|
+
else
|
107
|
+
yield(*keys)
|
108
|
+
false
|
109
|
+
end
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def get(*keys)
|
114
|
+
key = canonical_key(keys)
|
115
|
+
cluster.select(key) {|io| Protocol.get_value(io, key)}
|
116
|
+
end
|
117
|
+
alias [] get
|
118
|
+
|
119
|
+
def incr(*keys)
|
120
|
+
step = keys.pop
|
121
|
+
key = canonical_key(keys)
|
122
|
+
cluster.select(key) {|io| Protocol.incr(io, key, step)}
|
123
|
+
end
|
124
|
+
|
125
|
+
def read(*keys)
|
126
|
+
key = canonical_key(keys)
|
127
|
+
cluster.select(key) {|io| Protocol.get(io, key)}
|
128
|
+
end
|
129
|
+
|
130
|
+
def release
|
131
|
+
@@instances.delete(@name)
|
132
|
+
end
|
133
|
+
|
134
|
+
def scope(&b)
|
135
|
+
@scope = b
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
def set(*keys)
|
140
|
+
opts = default_opts(keys)
|
141
|
+
value = keys.pop
|
142
|
+
key = canonical_key(keys)
|
143
|
+
cluster.select(key) {|io| Protocol.set_value(io, key, value, opts[:ttl])}
|
144
|
+
end
|
145
|
+
alias []= set
|
146
|
+
|
147
|
+
def transaction
|
148
|
+
yield self
|
149
|
+
ensure
|
150
|
+
cycle
|
151
|
+
end
|
152
|
+
|
153
|
+
def write(*keys)
|
154
|
+
opts = default_opts(keys)
|
155
|
+
value = keys.pop
|
156
|
+
key = canonical_key(keys)
|
157
|
+
cluster.select(key) {|io| Protocol.set(io, key, value, opts[:ttl])}
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def canonical_key(keys)
|
163
|
+
"#{implicit_scope}#{keys.join('/')}@#@name:#{vector}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def cluster
|
167
|
+
@@clusters[:default]
|
168
|
+
end
|
169
|
+
|
170
|
+
def coherency
|
171
|
+
default[:coherency]
|
172
|
+
end
|
173
|
+
|
174
|
+
def default_opts(params)
|
175
|
+
params.last.is_a?(Hash) ? default.merge(params.pop) : default
|
176
|
+
end
|
177
|
+
|
178
|
+
def implicit_scope
|
179
|
+
if @scope
|
180
|
+
scope = @scope.call(self)
|
181
|
+
scope ? "#{scope}/" : ''
|
182
|
+
else
|
183
|
+
''
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def vector
|
188
|
+
return 'static' if @name == :root
|
189
|
+
return @vector.to_s if @vector && coherency != :dynamic
|
190
|
+
vk = vector_key
|
191
|
+
cluster.select(vk) do |io|
|
192
|
+
@vector = Protocol.get(io, vk)
|
193
|
+
unless @vector
|
194
|
+
Protocol.add(io, vk, '0')
|
195
|
+
@vector = Protocol.get(io, vk)
|
196
|
+
end
|
197
|
+
@vector
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def vector_key
|
202
|
+
"#@name#{implicit_scope}_vector"
|
203
|
+
end
|
204
|
+
|
205
|
+
class ProtocolError < RuntimeError; end
|
206
|
+
class ClusterError < RuntimeError; end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
class Object; include Stash::Extension end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
class 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
|
+
yield(host_to_io(*h))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Note: Later, I'd like to support richer cluster definitions.
|
25
|
+
# This should do the trick for now... and it's fast.
|
26
|
+
def select(key)
|
27
|
+
count = @hosts.size
|
28
|
+
hash = nil
|
29
|
+
if count > 1
|
30
|
+
digest = Digest::MD5.digest(key)
|
31
|
+
hash = digest.unpack("L")[0]
|
32
|
+
else
|
33
|
+
hash = 0
|
34
|
+
end
|
35
|
+
count.times do |try|
|
36
|
+
begin
|
37
|
+
break yield(host_to_io(*@hosts[(hash + try) % count]))
|
38
|
+
rescue Stash::ProtocolError
|
39
|
+
next
|
40
|
+
end
|
41
|
+
raise Stash::ClusterError,
|
42
|
+
"Unable to find suitable host to communicate with for #{key.inspect} (MD5-32=#{hash})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def connect(host, port)
|
49
|
+
address = Socket.getaddrinfo(host, nil).first
|
50
|
+
socket = Socket.new(Socket.const_get(address[0]), SOCK_STREAM, 0)
|
51
|
+
timeout = [2,0].pack('l_2') # 2 seconds
|
52
|
+
socket.setsockopt(SOL_SOCKET, SO_SNDTIMEO, timeout)
|
53
|
+
socket.setsockopt(SOL_SOCKET, SO_RCVTIMEO, timeout)
|
54
|
+
socket.connect(Socket.pack_sockaddr_in(port, address[3]))
|
55
|
+
socket
|
56
|
+
end
|
57
|
+
|
58
|
+
def host_to_io(key, host, port)
|
59
|
+
@@connections[key] ||= connect(host, port)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
module Stash::Protocol
|
2
|
+
extend self
|
3
|
+
|
4
|
+
HEADER_FORMAT = "CCnCCnNa4a8"
|
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
|
+
def add(io, key, data)
|
54
|
+
# Field (offset) (value)
|
55
|
+
# Magic (0) : 0x80
|
56
|
+
# Opcode (1) : 0x02
|
57
|
+
# Key length (2,3) : 0x0005
|
58
|
+
# Extra length (4) : 0x08
|
59
|
+
# Data type (5) : 0x00
|
60
|
+
# Reserved (6,7) : 0x0000
|
61
|
+
# Total body (8-11) : 0x00000012
|
62
|
+
# Opaque (12-15): 0x00000000
|
63
|
+
# CAS (16-23): 0x0000000000000000
|
64
|
+
# Extras :
|
65
|
+
# Flags (24-27): 0xdeadbeef
|
66
|
+
# Expiry (28-31): 0x00000e10
|
67
|
+
# Key (32-36): The textual string "Hello"
|
68
|
+
# Value (37-41): The textual string "World"
|
69
|
+
header = [REQUEST, ADD, key.size, 8, 0, 0, data.size + key.size + 8, '', '', 0, 0, key, data].pack(HEADER_FORMAT + 'NNa*a*')
|
70
|
+
io.write(header)
|
71
|
+
resp = read_resp(io)
|
72
|
+
resp[:status] == NO_ERROR
|
73
|
+
end
|
74
|
+
|
75
|
+
def decr(io, key, step)
|
76
|
+
# Field (offset) (value)
|
77
|
+
# Magic (0) : 0x80
|
78
|
+
# Opcode (1) : 0x06
|
79
|
+
# Key length (2,3) : 0x0007
|
80
|
+
# Extra length (4) : 0x14
|
81
|
+
# Data type (5) : 0x00
|
82
|
+
# Reserved (6,7) : 0x0000
|
83
|
+
# Total body (8-11) : 0x0000001b
|
84
|
+
# Opaque (12-15): 0x00000000
|
85
|
+
# CAS (16-23): 0x0000000000000000
|
86
|
+
# Extras :
|
87
|
+
# delta (24-31): 0x0000000000000001
|
88
|
+
# initial (32-39): 0x0000000000000000
|
89
|
+
# exipration (40-43): 0x00000e10
|
90
|
+
# Key : Textual string "counter"
|
91
|
+
# Value : None
|
92
|
+
low, high = split64(step)
|
93
|
+
header = [REQUEST, DECREMENT, key.size, 20, 0, 0, key.size + 20, '', '', high, low, 0, COUNTER_FAULT_EXPIRATION, key].pack(HEADER_FORMAT + 'NNQNa*')
|
94
|
+
io.write(header)
|
95
|
+
resp = read_resp(io)
|
96
|
+
if resp[:status] == NO_ERROR
|
97
|
+
parse_counter(resp[:body])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def delete(io, key, ttl = 0)
|
102
|
+
# Field (offset) (value)
|
103
|
+
# Magic (0) : 0x80
|
104
|
+
# Opcode (1) : 0x04
|
105
|
+
# Key length (2,3) : 0x0005
|
106
|
+
# Extra length (4) : 0x00
|
107
|
+
# Data type (5) : 0x00
|
108
|
+
# Reserved (6,7) : 0x0000
|
109
|
+
# Total body (8-11) : 0x00000005
|
110
|
+
# Opaque (12-15): 0x00000000
|
111
|
+
# CAS (16-23): 0x0000000000000000
|
112
|
+
# Extras : None
|
113
|
+
# Key : The textual string "Hello"
|
114
|
+
# Value : None
|
115
|
+
header = [REQUEST, DELETE, key.size, 0, 0, 0, key.size, '', '', key].pack(HEADER_FORMAT + 'a*')
|
116
|
+
io.write(header)
|
117
|
+
resp = read_resp(io)
|
118
|
+
resp[:status] == NO_ERROR
|
119
|
+
end
|
120
|
+
|
121
|
+
def flush(io)
|
122
|
+
# Field (offset) (value)
|
123
|
+
# Magic (0) : 0x80
|
124
|
+
# Opcode (1) : 0x08
|
125
|
+
# Key length (2,3) : 0x0000
|
126
|
+
# Extra length (4) : 0x04
|
127
|
+
# Data type (5) : 0x00
|
128
|
+
# Reserved (6,7) : 0x0000
|
129
|
+
# Total body (8-11) : 0x00000004
|
130
|
+
# Opaque (12-15): 0x00000000
|
131
|
+
# CAS (16-23): 0x0000000000000000
|
132
|
+
# Extras :
|
133
|
+
# Expiry (24-27): 0x000e10
|
134
|
+
header = [REQUEST, FLUSH, 0, 4, 0, 0, 4, '', '', 0].pack(HEADER_FORMAT + 'N')
|
135
|
+
io.write(header)
|
136
|
+
resp = read_resp(io)
|
137
|
+
resp[:status] == NO_ERROR
|
138
|
+
end
|
139
|
+
|
140
|
+
def get(io, key)
|
141
|
+
# Field (offset) (value)
|
142
|
+
# Magic (0) : 0x80
|
143
|
+
# Opcode (1) : 0x00
|
144
|
+
# Key length (2,3) : 0x0005
|
145
|
+
# Extra length (4) : 0x00
|
146
|
+
# Data type (5) : 0x00
|
147
|
+
# Reserved (6,7) : 0x0000
|
148
|
+
# Total body (8-11) : 0x00000005
|
149
|
+
# Opaque (12-15): 0x00000000
|
150
|
+
# CAS (16-23): 0x0000000000000000
|
151
|
+
# Extras : None
|
152
|
+
# Key (24-29): The textual string: "Hello"
|
153
|
+
# Value : None
|
154
|
+
header = [REQUEST, GET, key.size, 0, 0, 0, key.size, '', '', key].pack(HEADER_FORMAT + 'a*')
|
155
|
+
io.write(header)
|
156
|
+
resp = read_resp(io)
|
157
|
+
resp[:status] == NO_ERROR ? parse_get(resp[:body])[:value] : nil
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_value(io, key)
|
161
|
+
load_ruby_value(get(io, key))
|
162
|
+
end
|
163
|
+
|
164
|
+
def incr(io, key, step)
|
165
|
+
# Field (offset) (value)
|
166
|
+
# Magic (0) : 0x80
|
167
|
+
# Opcode (1) : 0x05
|
168
|
+
# Key length (2,3) : 0x0007
|
169
|
+
# Extra length (4) : 0x14
|
170
|
+
# Data type (5) : 0x00
|
171
|
+
# Reserved (6,7) : 0x0000
|
172
|
+
# Total body (8-11) : 0x0000001b
|
173
|
+
# Opaque (12-15): 0x00000000
|
174
|
+
# CAS (16-23): 0x0000000000000000
|
175
|
+
# Extras :
|
176
|
+
# delta (24-31): 0x0000000000000001
|
177
|
+
# initial (32-39): 0x0000000000000000
|
178
|
+
# exipration (40-43): 0x00000e10
|
179
|
+
# Key : Textual string "counter"
|
180
|
+
# Value : None
|
181
|
+
low, high = split64(step)
|
182
|
+
header = [REQUEST, INCREMENT, key.size, 20, 0, 0, key.size + 20, '', '', high, low, 0, COUNTER_FAULT_EXPIRATION, key].pack(HEADER_FORMAT + 'NNQNa*')
|
183
|
+
io.write(header)
|
184
|
+
resp = read_resp(io)
|
185
|
+
if resp[:status] == NO_ERROR
|
186
|
+
parse_counter(resp[:body])
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def set(io, key, data, ttl = 0)
|
191
|
+
# Field (offset) (value)
|
192
|
+
# Magic (0) : 0x80
|
193
|
+
# Opcode (1) : 0x01
|
194
|
+
# Key length (2,3) : 0x0005
|
195
|
+
# Extra length (4) : 0x08
|
196
|
+
# Data type (5) : 0x00
|
197
|
+
# Reserved (6,7) : 0x0000
|
198
|
+
# Total body (8-11) : 0x00000012
|
199
|
+
# Opaque (12-15): 0x00000000
|
200
|
+
# CAS (16-23): 0x0000000000000000
|
201
|
+
# Extras :
|
202
|
+
# Flags (24-27): 0xdeadbeef
|
203
|
+
# Expiry (28-31): 0x00000e10
|
204
|
+
# Key (32-36): The textual string "Hello"
|
205
|
+
# Value (37-41): The textual string "World"
|
206
|
+
header = [REQUEST, SET, key.size, 8, 0, 0, data.size + key.size + 8, '', '', 0, 0, key, data].pack(HEADER_FORMAT + 'NNa*a*')
|
207
|
+
io.write(header)
|
208
|
+
resp = read_resp(io)
|
209
|
+
resp[:status] == NO_ERROR
|
210
|
+
end
|
211
|
+
|
212
|
+
def set_value(io, key, value, ttl = 0)
|
213
|
+
set(io, key, Marshal.dump(value), ttl)
|
214
|
+
end
|
215
|
+
|
216
|
+
def method_missing(message, *a)
|
217
|
+
fail [:NOT_IMPLEMENTED, self, message, *a].inspect
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def load_ruby_value(data)
|
223
|
+
return unless data
|
224
|
+
# TODO: Catch errors and try to fix them
|
225
|
+
Marshal.load(data)
|
226
|
+
end
|
227
|
+
|
228
|
+
def parse_get(body)
|
229
|
+
extra, value = body.unpack('Na*')
|
230
|
+
{:extra => extra, :value => value}
|
231
|
+
end
|
232
|
+
|
233
|
+
def parse_counter(body)
|
234
|
+
a, b = body.unpack('NN')
|
235
|
+
b | (a << 32)
|
236
|
+
end
|
237
|
+
|
238
|
+
def read_resp(io)
|
239
|
+
magic, opcode, key_length,
|
240
|
+
extra, type, status,
|
241
|
+
body_length, opaque, cas = *io.read(24).unpack(HEADER_FORMAT)
|
242
|
+
resp = { :magic => magic, :opcode => opcode, :key_length => key_length,
|
243
|
+
:extra => extra, :type => type, :status => status,
|
244
|
+
:body_length => body_length, :opaque => opaque, :cas => cas }
|
245
|
+
resp[:body] = io.read(resp[:body_length]) if resp[:body_length] > 0
|
246
|
+
resp
|
247
|
+
end
|
248
|
+
|
249
|
+
def split64(n)
|
250
|
+
[0xFFFFFFFF & n, n >> 32]
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
data/remix-stash.gemspec
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{remix-stash}
|
8
|
+
s.version = "0.9.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Brian Mitchell"]
|
12
|
+
s.date = %q{2009-08-29}
|
13
|
+
s.email = %q{binary42@gmail.com}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE",
|
16
|
+
"README.mdown"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"LICENSE",
|
21
|
+
"README.mdown",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"examples/bench.rb",
|
25
|
+
"examples/eval.rb",
|
26
|
+
"examples/gate.rb",
|
27
|
+
"examples/getset.rb",
|
28
|
+
"examples/getset_cache.rb",
|
29
|
+
"examples/scope.rb",
|
30
|
+
"examples/stash.rb",
|
31
|
+
"harness.rb",
|
32
|
+
"harness_cache.rb",
|
33
|
+
"lib/remix/stash.rb",
|
34
|
+
"lib/remix/stash/cluster.rb",
|
35
|
+
"lib/remix/stash/extension.rb",
|
36
|
+
"lib/remix/stash/protocol.rb",
|
37
|
+
"remix-stash.gemspec",
|
38
|
+
"spec/extension_spec.rb",
|
39
|
+
"spec/spec.rb",
|
40
|
+
"spec/stash_spec.rb"
|
41
|
+
]
|
42
|
+
s.has_rdoc = true
|
43
|
+
s.homepage = %q{http://github.com/binary42/remix-stash}
|
44
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
45
|
+
s.require_paths = ["lib"]
|
46
|
+
s.rubygems_version = %q{1.3.1}
|
47
|
+
s.summary = %q{Remix your memcache}
|
48
|
+
s.test_files = [
|
49
|
+
"spec/extension_spec.rb",
|
50
|
+
"spec/spec.rb",
|
51
|
+
"spec/stash_spec.rb",
|
52
|
+
"examples/bench.rb",
|
53
|
+
"examples/eval.rb",
|
54
|
+
"examples/gate.rb",
|
55
|
+
"examples/getset.rb",
|
56
|
+
"examples/getset_cache.rb",
|
57
|
+
"examples/scope.rb",
|
58
|
+
"examples/stash.rb"
|
59
|
+
]
|
60
|
+
|
61
|
+
if s.respond_to? :specification_version then
|
62
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
63
|
+
s.specification_version = 2
|
64
|
+
|
65
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
66
|
+
else
|
67
|
+
end
|
68
|
+
else
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec'
|
2
|
+
|
3
|
+
class ExtensionSpec < Spec
|
4
|
+
|
5
|
+
context '#stash' do
|
6
|
+
|
7
|
+
should 'return a stash object with the correct name' do
|
8
|
+
s = stash(:a)
|
9
|
+
assert_instance_of Stash, s
|
10
|
+
assert_equal :a, s.name
|
11
|
+
end
|
12
|
+
|
13
|
+
should 'return the same object when given the same name' do
|
14
|
+
assert_equal stash(:b), stash(:b)
|
15
|
+
assert_not_equal stash(:a), stash(:b)
|
16
|
+
end
|
17
|
+
|
18
|
+
should 'allow access to a default root stash' do
|
19
|
+
assert_equal stash, stash(:root)
|
20
|
+
assert_equal :root, stash.name
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/spec/spec.rb
ADDED
data/spec/stash_spec.rb
ADDED
@@ -0,0 +1,389 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec'
|
2
|
+
|
3
|
+
class StashSpec < Spec
|
4
|
+
|
5
|
+
def setup
|
6
|
+
stash.clear
|
7
|
+
Stash.class_eval("@@instances.clear")
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'coherency' do
|
11
|
+
|
12
|
+
should 'only allow valid coherency settings' do
|
13
|
+
stash.default(:coherency => :action)
|
14
|
+
stash.default(:coherency => :dynamic)
|
15
|
+
stash.default(:coherency => :transaction)
|
16
|
+
assert_raises ArgumentError do
|
17
|
+
stash.default(:coherency => :other)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
should 'support :action coherency' do
|
22
|
+
one = stash(:one).release
|
23
|
+
two = stash(:one)
|
24
|
+
two.default(:coherency => :action)
|
25
|
+
one[:value] = 42
|
26
|
+
assert_equal 42, two[:value]
|
27
|
+
one.clear
|
28
|
+
one[:value] = 43
|
29
|
+
assert_equal 42, two[:value]
|
30
|
+
Stash.cycle_action
|
31
|
+
assert_equal 43, two[:value]
|
32
|
+
end
|
33
|
+
|
34
|
+
should 'support :dynamic coherency' do
|
35
|
+
one = stash(:one).release
|
36
|
+
two = stash(:one)
|
37
|
+
two.default(:coherency => :dynamic)
|
38
|
+
one[:value] = 42
|
39
|
+
assert_equal 42, two[:value]
|
40
|
+
one.clear
|
41
|
+
one[:value] = 43
|
42
|
+
assert_equal 43, two[:value]
|
43
|
+
end
|
44
|
+
|
45
|
+
should 'support :explicit coherency' do
|
46
|
+
one = stash(:one).release
|
47
|
+
two = stash(:one)
|
48
|
+
two.default(:coherency => :transaction)
|
49
|
+
one[:value] = 42
|
50
|
+
assert_equal 42, two[:value]
|
51
|
+
one.clear
|
52
|
+
one[:value] = 43
|
53
|
+
assert_equal 42, two[:value]
|
54
|
+
Stash.cycle_action
|
55
|
+
assert_equal 42, two[:value]
|
56
|
+
two.cycle
|
57
|
+
assert_equal 43, two[:value]
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'defaults' do
|
63
|
+
|
64
|
+
should 'default to :action coherency' do
|
65
|
+
assert_equal :action, stash.default[:coherency]
|
66
|
+
end
|
67
|
+
|
68
|
+
should 'use a default cluster on localhost:11211' do
|
69
|
+
local = Stash.cluster(:default)
|
70
|
+
assert_equal [['localhost:11211', 'localhost', 11211]], local.hosts
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
context '.cycle_action' do
|
76
|
+
|
77
|
+
setup do
|
78
|
+
@cycle = stash(:action).release
|
79
|
+
@stash = stash(:action)
|
80
|
+
@stash.default(:coherency => :action)
|
81
|
+
end
|
82
|
+
|
83
|
+
should 'cycle all action conherent scopes' do
|
84
|
+
@stash.set('a', 42)
|
85
|
+
@cycle.clear
|
86
|
+
assert_equal 42, @stash.get('a')
|
87
|
+
Stash.cycle_action
|
88
|
+
assert_nil @stash.get('a')
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
context '.define_cluster' do
|
94
|
+
|
95
|
+
should 'setup a cluster using an array of host/port pairs' do
|
96
|
+
Stash.define_cluster(:simple => %w[one:1 two:2], :sample => %w[miro.local:11211])
|
97
|
+
assert Stash.cluster(:simple)
|
98
|
+
assert Stash.cluster(:sample)
|
99
|
+
end
|
100
|
+
|
101
|
+
should 'default to port 11211' do
|
102
|
+
Stash.define_cluster(:default_port => 'default')
|
103
|
+
assert_equal [['default', 'default', 11211]], Stash.cluster(:default_port).hosts
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
context '#clear' do
|
109
|
+
|
110
|
+
setup do
|
111
|
+
stash(:a).default(:coherency => :dynamic)
|
112
|
+
end
|
113
|
+
|
114
|
+
should 'flush all when called without keys on root' do
|
115
|
+
stash(:a).set(:b, :c)
|
116
|
+
stash.set(:d, :e)
|
117
|
+
stash.clear
|
118
|
+
assert_nil stash(:a).get(:b)
|
119
|
+
assert_nil stash.get(:d)
|
120
|
+
end
|
121
|
+
|
122
|
+
should 'clear just a scope when called without keys on a non-root node' do
|
123
|
+
stash(:a).set(:b, :c)
|
124
|
+
stash.set(:d, :e)
|
125
|
+
stash(:a).clear
|
126
|
+
assert_nil stash(:a).get(:b)
|
127
|
+
assert_equal :e, stash.get(:d)
|
128
|
+
end
|
129
|
+
|
130
|
+
should 'clear just a key when called with keys on any node' do
|
131
|
+
stash(:a).set(:b, :c)
|
132
|
+
stash.set(:d, :e)
|
133
|
+
stash.clear(:d)
|
134
|
+
assert_equal :c, stash(:a).get(:b)
|
135
|
+
assert_nil stash.get(:d)
|
136
|
+
stash(:a).clear
|
137
|
+
assert_nil stash(:a).get(:b)
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
context '#clear_scope' do
|
143
|
+
|
144
|
+
should 'remove the prior scope' do
|
145
|
+
stash[:foo] = :bar
|
146
|
+
stash.scope {42}
|
147
|
+
stash[:foo] = :qux
|
148
|
+
stash.clear_scope
|
149
|
+
assert_equal :bar, stash[:foo]
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
context '#cycle' do
|
155
|
+
|
156
|
+
should 'clear the cached vector' do
|
157
|
+
one = stash(:one).release
|
158
|
+
two = stash(:one)
|
159
|
+
one[:a] = :b
|
160
|
+
assert_equal :b, two[:a]
|
161
|
+
one.clear
|
162
|
+
one[:a] = :c
|
163
|
+
two.cycle
|
164
|
+
assert_equal :c, two[:a]
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
context '#decr' do
|
170
|
+
|
171
|
+
should 'decrement numeric values by a positive integer' do
|
172
|
+
stash.write(:a, '10')
|
173
|
+
stash.decr(:a, 1)
|
174
|
+
assert_equal 9, stash.read(:a).to_i
|
175
|
+
stash.decr(:a, 3)
|
176
|
+
assert_equal 6, stash.read(:a).to_i
|
177
|
+
end
|
178
|
+
|
179
|
+
should 'return the new numeric value' do
|
180
|
+
stash.write(:a, '45')
|
181
|
+
assert_equal 42, stash.decr(:a, 3)
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
context '#default' do
|
187
|
+
|
188
|
+
should 'return a Hash of default options' do
|
189
|
+
assert_kind_of Hash, stash(:one).default
|
190
|
+
end
|
191
|
+
|
192
|
+
should 'allow setting default options' do
|
193
|
+
s = stash(:two)
|
194
|
+
long_time = 5000
|
195
|
+
s.default(:ttl => long_time)
|
196
|
+
assert_equal long_time, s.default[:ttl]
|
197
|
+
end
|
198
|
+
|
199
|
+
should 'merge with top-level default options' do
|
200
|
+
s = stash(:three)
|
201
|
+
stash.default(:ttl => 3600)
|
202
|
+
assert_equal 3600, s.default[:ttl]
|
203
|
+
s.default(:ttl => 4800)
|
204
|
+
assert_equal 4800, s.default[:ttl]
|
205
|
+
assert_equal 3600, stash.default[:ttl]
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
context '#delete' do
|
211
|
+
|
212
|
+
should 'remove a key from the cache' do
|
213
|
+
stash[:foo] = 42
|
214
|
+
stash.delete(:foo)
|
215
|
+
assert_nil stash[:foo]
|
216
|
+
end
|
217
|
+
|
218
|
+
should 'return true when deleted' do
|
219
|
+
stash[:foo] = 42
|
220
|
+
assert stash.delete(:foo)
|
221
|
+
end
|
222
|
+
|
223
|
+
should 'return false when not found' do
|
224
|
+
assert !stash.delete(:foo)
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
context '#eval' do
|
230
|
+
|
231
|
+
should 'evaluate the block on a cache miss' do
|
232
|
+
ran = false
|
233
|
+
stash.eval(:a) {ran = true}
|
234
|
+
assert ran
|
235
|
+
end
|
236
|
+
|
237
|
+
should 'not evaluate on a cache hit' do
|
238
|
+
ran = false
|
239
|
+
stash[:a] = 42
|
240
|
+
stash.eval(:a) {ran = false}
|
241
|
+
assert !ran
|
242
|
+
end
|
243
|
+
|
244
|
+
should 'pass keys in as optional block arguments' do
|
245
|
+
assert 42, stash.eval(42) {|a| a}
|
246
|
+
end
|
247
|
+
|
248
|
+
end
|
249
|
+
|
250
|
+
context '#gate' do
|
251
|
+
|
252
|
+
should 'evaluate on a key miss' do
|
253
|
+
ran = false
|
254
|
+
stash.gate(:k) {ran = true}
|
255
|
+
assert ran
|
256
|
+
end
|
257
|
+
|
258
|
+
should 'not evaluate on a key hit' do
|
259
|
+
ran = false
|
260
|
+
stash[:k] = :hit
|
261
|
+
stash.gate(:k) {ran = true}
|
262
|
+
assert !ran
|
263
|
+
end
|
264
|
+
|
265
|
+
should 'return true on hit' do
|
266
|
+
stash[:k] = :hit
|
267
|
+
assert stash.gate(:k) {}
|
268
|
+
end
|
269
|
+
|
270
|
+
should 'return false on miss' do
|
271
|
+
assert !stash.gate(:k) {}
|
272
|
+
end
|
273
|
+
|
274
|
+
should 'pass keys in as optional block arguments' do
|
275
|
+
key = nil
|
276
|
+
stash.gate(42) {|k| key = k}
|
277
|
+
assert_equal 42, key
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
context '#get' do
|
283
|
+
|
284
|
+
should 'allow simple get on the same keyspace as eval' do
|
285
|
+
stash.eval(:foo) {42}
|
286
|
+
assert_equal 42, stash[:foo]
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|
290
|
+
|
291
|
+
context '#incr' do
|
292
|
+
|
293
|
+
should 'increment numeric values by the passed integer' do
|
294
|
+
stash.write(:a, '10')
|
295
|
+
assert_equal 12, stash.incr(:a, 2)
|
296
|
+
end
|
297
|
+
|
298
|
+
should 'return nil if it failed to increment' do
|
299
|
+
assert_nil stash.incr(:a, 3)
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
context '#read' do
|
305
|
+
|
306
|
+
should 'read raw strings from the cache' do
|
307
|
+
stash[:a] = 42
|
308
|
+
assert_equal Marshal.dump(42), stash.read(:a)
|
309
|
+
end
|
310
|
+
|
311
|
+
should 'return nil when the key is not found' do
|
312
|
+
assert_nil stash.read(:not_found)
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
|
317
|
+
context '#release' do
|
318
|
+
|
319
|
+
should 'return itself' do
|
320
|
+
assert_instance_of Stash, stash(:one).release
|
321
|
+
end
|
322
|
+
|
323
|
+
should 'remove it from the name registery' do
|
324
|
+
assert_not_equal stash(:one).release, stash(:one)
|
325
|
+
end
|
326
|
+
|
327
|
+
end
|
328
|
+
|
329
|
+
context '#scope' do
|
330
|
+
|
331
|
+
should 'set an implicit scope variable for keyspaces' do
|
332
|
+
a = 1
|
333
|
+
stash.scope {a}
|
334
|
+
stash[:k] = :v
|
335
|
+
a = 2
|
336
|
+
assert_nil stash[:k]
|
337
|
+
end
|
338
|
+
|
339
|
+
should 'be used by the vector key' do
|
340
|
+
one = stash(:one).release
|
341
|
+
two = stash(:one)
|
342
|
+
a = 0
|
343
|
+
one.scope {a}
|
344
|
+
one[:a] = 1
|
345
|
+
two.clear
|
346
|
+
one.cycle
|
347
|
+
assert_equal 1, one[:a]
|
348
|
+
end
|
349
|
+
|
350
|
+
should 'return self' do
|
351
|
+
assert_equal stash, stash.scope {}
|
352
|
+
end
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
context '#set' do
|
357
|
+
|
358
|
+
should 'allow simple set on the same keyspace as eval' do
|
359
|
+
stash.set(:a, 42)
|
360
|
+
assert_equal 42, stash.eval(:a) {fail 'expected cache hit'}
|
361
|
+
end
|
362
|
+
|
363
|
+
end
|
364
|
+
|
365
|
+
context '#transaction' do
|
366
|
+
|
367
|
+
should 'cycle the vector at the end of the transaction block' do
|
368
|
+
one = stash(:one).release
|
369
|
+
two = stash(:one)
|
370
|
+
one.transaction do
|
371
|
+
one[:a] = 42
|
372
|
+
two.clear
|
373
|
+
assert_equal 42, one[:a]
|
374
|
+
end
|
375
|
+
assert_nil one[:a]
|
376
|
+
end
|
377
|
+
|
378
|
+
end
|
379
|
+
|
380
|
+
context '#write' do
|
381
|
+
|
382
|
+
should 'write raw strings to the cache' do
|
383
|
+
stash.write(42, '42')
|
384
|
+
assert_equal '42', stash.read(42)
|
385
|
+
end
|
386
|
+
|
387
|
+
end
|
388
|
+
|
389
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: binary42-remix-stash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Mitchell
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-08-29 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: binary42@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.mdown
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- LICENSE
|
28
|
+
- README.mdown
|
29
|
+
- Rakefile
|
30
|
+
- VERSION
|
31
|
+
- examples/bench.rb
|
32
|
+
- examples/eval.rb
|
33
|
+
- examples/gate.rb
|
34
|
+
- examples/getset.rb
|
35
|
+
- examples/getset_cache.rb
|
36
|
+
- examples/scope.rb
|
37
|
+
- examples/stash.rb
|
38
|
+
- harness.rb
|
39
|
+
- harness_cache.rb
|
40
|
+
- lib/remix/stash.rb
|
41
|
+
- lib/remix/stash/cluster.rb
|
42
|
+
- lib/remix/stash/extension.rb
|
43
|
+
- lib/remix/stash/protocol.rb
|
44
|
+
- remix-stash.gemspec
|
45
|
+
- spec/extension_spec.rb
|
46
|
+
- spec/spec.rb
|
47
|
+
- spec/stash_spec.rb
|
48
|
+
has_rdoc: true
|
49
|
+
homepage: http://github.com/binary42/remix-stash
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options:
|
52
|
+
- --charset=UTF-8
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0"
|
66
|
+
version:
|
67
|
+
requirements: []
|
68
|
+
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 1.2.0
|
71
|
+
signing_key:
|
72
|
+
specification_version: 2
|
73
|
+
summary: Remix your memcache
|
74
|
+
test_files:
|
75
|
+
- spec/extension_spec.rb
|
76
|
+
- spec/spec.rb
|
77
|
+
- spec/stash_spec.rb
|
78
|
+
- examples/bench.rb
|
79
|
+
- examples/eval.rb
|
80
|
+
- examples/gate.rb
|
81
|
+
- examples/getset.rb
|
82
|
+
- examples/getset_cache.rb
|
83
|
+
- examples/scope.rb
|
84
|
+
- examples/stash.rb
|