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 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
@@ -0,0 +1,8 @@
1
+ require File.dirname(__FILE__) + '/../harness'
2
+
3
+ stuff = stash(:stuff)
4
+
5
+ stuff.eval(:answer) {42}
6
+ p stuff.get(:answer)
7
+
8
+ stuff.eval(:asnwer) {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]
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/../harness_cache'
2
+
3
+ # Simple get and set
4
+ Cache.set('answer', 42)
5
+ p Cache.get('answer')
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,4 @@
1
+ $LOAD_PATH << File.dirname(__FILE__) + '/lib'
2
+ require 'remix/stash'
3
+
4
+ stash.clear
data/harness_cache.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'memcache'
3
+
4
+ Cache = MemCache.new(%[localhost:11211])
5
+ Cache.flush_all
@@ -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,8 @@
1
+ module Stash::Extension
2
+ extend self
3
+
4
+ def stash(name = :root)
5
+ Stash.new(name)
6
+ end
7
+
8
+ 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
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH << File.dirname(__FILE__) + '/../lib'
6
+ require 'remix/stash'
7
+
8
+ Spec = Test::Unit::TestCase
@@ -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