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 +1 -0
- data/LICENSE +21 -0
- data/README.markdown +32 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/benchmarks/get_set.rb +35 -0
- data/benchmarks/payload.rb +125 -0
- data/examples/eval.rb +8 -0
- data/examples/gate.rb +6 -0
- data/examples/getset.rb +13 -0
- data/examples/scope.rb +17 -0
- data/examples/stash.rb +16 -0
- data/harness.rb +19 -0
- data/lib/remix/stash/cluster.rb +79 -0
- data/lib/remix/stash/extension.rb +7 -0
- data/lib/remix/stash/protocol.rb +139 -0
- data/lib/remix/stash.rb +256 -0
- data/remix-stash.gemspec +67 -0
- data/spec/extension_spec.rb +37 -0
- data/spec/spec.rb +10 -0
- data/spec/stash_spec.rb +411 -0
- metadata +36 -8
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
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
@@ -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,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
|