├── benchmark ├── .gitignore ├── ben │ ├── ben.png │ ├── ben.plot │ ├── experiment.json │ ├── README.md │ └── cucache.dat ├── tom │ ├── tom.png │ ├── tom.plot │ ├── experiment.json │ ├── README.md │ └── cucache.dat ├── internal │ ├── bench.png │ ├── bench.plot │ └── README.md ├── bench-results.sh ├── README.md ├── bench.sh └── bench-results.rb ├── src ├── .gitignore └── cuckood │ ├── vals.go │ ├── spinlock.go │ ├── cucache │ ├── text │ │ ├── out.go │ │ ├── help.go │ │ ├── in.go │ │ ├── in_test.go │ │ └── out_test.go │ ├── helper_test.go │ ├── execute.go │ ├── main.go │ └── execute_test.go │ ├── bench │ └── main.go │ ├── search.go │ ├── bins.go │ ├── map_test.go │ ├── memcache.go │ ├── external.go │ └── map.go ├── .travis.yml ├── .gitignore ├── LICENSE ├── wip.md └── README.md /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | results/ 2 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | github.com/ 2 | -------------------------------------------------------------------------------- /benchmark/ben/ben.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonhoo/cucache/HEAD/benchmark/ben/ben.png -------------------------------------------------------------------------------- /benchmark/tom/tom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonhoo/cucache/HEAD/benchmark/tom/tom.png -------------------------------------------------------------------------------- /benchmark/internal/bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonhoo/cucache/HEAD/benchmark/internal/bench.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | install: env GOPATH=$PWD go get -t cuckood/... 3 | script: env GOPATH=$PWD go test -v cuckood/... 4 | go: 5 | - 1.4 6 | - tip 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | pkg/ 3 | 4 | *.out 5 | *.svg 6 | err 7 | test 8 | 9 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 10 | *.o 11 | *.a 12 | *.so 13 | 14 | # Folders 15 | _obj 16 | _test 17 | 18 | # Architecture specific extensions/prefixes 19 | *.[568vq] 20 | [568vq].out 21 | 22 | *.cgo1.go 23 | *.cgo2.c 24 | _cgo_defun.c 25 | _cgo_gotypes.go 26 | _cgo_export.* 27 | 28 | _testmain.go 29 | 30 | *.exe 31 | *.test 32 | *.prof 33 | -------------------------------------------------------------------------------- /src/cuckood/vals.go: -------------------------------------------------------------------------------- 1 | package cuckoo 2 | 3 | import ( 4 | "bytes" 5 | "time" 6 | ) 7 | 8 | // cval is a container for Cuckoo key data 9 | type cval struct { 10 | bno int 11 | key keyt 12 | val Memval 13 | } 14 | 15 | // keyt is the internal representation of a map key 16 | type keyt []byte 17 | 18 | // present returns true if this key data has not yet expired 19 | func (v *cval) present(now time.Time) bool { 20 | return v.val.Expires.IsZero() || v.val.Expires.After(now) 21 | } 22 | 23 | func (v *cval) holds(key keyt, now time.Time) bool { 24 | return v.present(now) && bytes.Equal(v.key, key) 25 | } 26 | -------------------------------------------------------------------------------- /benchmark/ben/ben.plot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/gnuplot -p 2 | set key top left 3 | set term pngcairo size 1200,600 4 | set output "ben.png" 5 | set xlabel "CPU Cores" 6 | set ylabel "median ops/s" 7 | plot \ 8 | "< grep memcached cucache.dat | sed s/memcached-// | grep Sets" u 1:3 dt 2 lt 1 t "Memcached set" w linespoints,\ 9 | "< grep cucache cucache.dat | sed s/cucache-// | grep Sets" u 1:3 lt 1 t "Cucache set" w linespoints,\ 10 | "< grep memcached cucache.dat | sed s/memcached-// | grep Gets" u 1:5 dt 2 lt 2 t "Memcached get" w linespoints,\ 11 | "< grep cucache cucache.dat | sed s/cucache-// | grep Gets" u 1:5 lt 2 t "Cucache get" w linespoints,\ 12 | # 13 | -------------------------------------------------------------------------------- /benchmark/tom/tom.plot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/gnuplot -p 2 | set key top left 3 | set term pngcairo size 1200,600 4 | set output "tom.png" 5 | set xlabel "CPU Cores" 6 | set ylabel "median ops/s" 7 | plot \ 8 | "< grep memcached cucache.dat | sed s/memcached-// | grep Sets" u 1:3 dt 2 lt 1 t "Memcached set" w linespoints,\ 9 | "< grep cucache cucache.dat | sed s/cucache-// | grep Sets" u 1:3 lt 1 t "Cucache set" w linespoints,\ 10 | "< grep memcached cucache.dat | sed s/memcached-// | grep Gets" u 1:5 dt 2 lt 2 t "Memcached get" w linespoints,\ 11 | "< grep cucache cucache.dat | sed s/cucache-// | grep Gets" u 1:5 lt 2 t "Cucache get" w linespoints,\ 12 | # 13 | -------------------------------------------------------------------------------- /benchmark/ben/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": "env GOPATH=$(pwd) $GO get cuckood/...", 3 | "checkout": "master", 4 | "experiment": "Vary cores", 5 | "keep-stdout": true, 6 | "iterations": 20, 7 | "parallelism": 1, 8 | "versions": { 9 | "cucache-$cores": { 10 | "vary": { 11 | "cores": "range(2,21,2)" 12 | }, 13 | "arguments": [ 14 | "$SRC/benchmark/bench.sh", 15 | "$cores", 16 | "60", 17 | "$SRC/bin/cucache" 18 | ] 19 | }, 20 | "memcached-$cores": { 21 | "vary": { 22 | "cores": "range(2,21,2)" 23 | }, 24 | "arguments": [ 25 | "$SRC/benchmark/bench.sh", 26 | "$cores", 27 | "60", 28 | "memcached", 29 | "-t", 30 | "SCORES" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /benchmark/tom/experiment.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": "env GOPATH=$(pwd) $GO get cuckood/...", 3 | "checkout": "master", 4 | "experiment": "Vary cores", 5 | "keep-stdout": true, 6 | "iterations": 20, 7 | "parallelism": 1, 8 | "versions": { 9 | "cucache-$cores": { 10 | "vary": { 11 | "cores": "set(1,2,4,6,8,10,12,14,16,18,20)" 12 | }, 13 | "arguments": [ 14 | "$SRC/benchmark/bench.sh", 15 | "$cores", 16 | "28", 17 | "$SRC/bin/cucache" 18 | ] 19 | }, 20 | "memcached-$cores": { 21 | "vary": { 22 | "cores": "set(1,2,4,6,8,10,12,14,16,18,20)" 23 | }, 24 | "arguments": [ 25 | "$SRC/benchmark/bench.sh", 26 | "$cores", 27 | "28", 28 | "memcached", 29 | "-t", 30 | "SCORES" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /benchmark/internal/bench.plot: -------------------------------------------------------------------------------- 1 | set term pngcairo size 1200,600 2 | set output "bench.png" 3 | set logscale y 4 | #set logscale y2 5 | #set y2tics 6 | set ylabel "us/op" 7 | #set y2label "MB allocated" 8 | set rmargin 5 9 | 10 | set arrow from (2**12)*8,1e-1 to (2**12)*8,1e7 nohead lc rgb 'red' 11 | set arrow from (2**13)*8,1e-1 to (2**13)*8,1e7 nohead lc rgb 'red' 12 | set arrow from (2**14)*8,1e-1 to (2**14)*8,1e7 nohead lc rgb 'red' 13 | set arrow from (2**15)*8,1e-1 to (2**15)*8,1e7 nohead lc rgb 'red' 14 | set arrow from (2**16)*8,1e-1 to (2**16)*8,1e7 nohead lc rgb 'red' 15 | set arrow from (2**17)*8,1e-1 to (2**17)*8,1e7 nohead lc rgb 'red' 16 | 17 | plot \ 18 | 'bench.dat' u 1:($2*1000000) t 'set', \ 19 | '' u 1:($3*1000000) t 'get' 20 | #'' u 1:($4/1000000.0) t "alloc'd" axis x1y2 21 | -------------------------------------------------------------------------------- /src/cuckood/spinlock.go: -------------------------------------------------------------------------------- 1 | package cuckoo 2 | 3 | // from https://github.com/OneOfOne/go-utils/blob/master/sync/spinlock.go 4 | 5 | import ( 6 | "runtime" 7 | "sync/atomic" 8 | ) 9 | 10 | // SpinLock implements a simple atomic spin lock, the zero value for a SpinLock is an unlocked spinlock. 11 | type SpinLock struct { 12 | f uint32 13 | } 14 | 15 | // Lock locks sl. If the lock is already in use, the caller blocks until Unlock is called 16 | func (sl *SpinLock) Lock() { 17 | for !sl.TryLock() { 18 | runtime.Gosched() //allow other goroutines to do stuff. 19 | } 20 | } 21 | 22 | // Unlock unlocks sl, unlike [Mutex.Unlock](http://golang.org/pkg/sync/#Mutex.Unlock), 23 | // there's no harm calling it on an unlocked SpinLock 24 | func (sl *SpinLock) Unlock() { 25 | atomic.StoreUint32(&sl.f, 0) 26 | } 27 | 28 | // TryLock will try to lock sl and return whether it succeed or not without blocking. 29 | func (sl *SpinLock) TryLock() bool { 30 | return atomic.CompareAndSwapUint32(&sl.f, 0, 1) 31 | } 32 | 33 | func (sl *SpinLock) String() string { 34 | if atomic.LoadUint32(&sl.f) == 1 { 35 | return "Locked" 36 | } 37 | return "Unlocked" 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jon Gjengset 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /benchmark/tom/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | tom$ uname -a 3 | Linux tom 3.13.0+ #1 SMP Fri Sep 12 19:16:24 EDT 2014 x86_64 GNU/Linux 4 | tom$ lscpu 5 | Architecture: x86_64 6 | CPU op-mode(s): 32-bit, 64-bit 7 | Byte Order: Little Endian 8 | CPU(s): 48 9 | On-line CPU(s) list: 0-47 10 | Thread(s) per core: 1 11 | Core(s) per socket: 6 12 | Socket(s): 8 13 | NUMA node(s): 8 14 | Vendor ID: AuthenticAMD 15 | CPU family: 16 16 | Model: 8 17 | Stepping: 0 18 | CPU MHz: 2411.075 19 | BogoMIPS: 4822.71 20 | Virtualization: AMD-V 21 | L1d cache: 64K 22 | L1i cache: 64K 23 | L2 cache: 512K 24 | L3 cache: 5118K 25 | NUMA node0 CPU(s): 0-5 26 | NUMA node1 CPU(s): 6-11 27 | NUMA node2 CPU(s): 12-17 28 | NUMA node3 CPU(s): 18-23 29 | NUMA node4 CPU(s): 24-29 30 | NUMA node5 CPU(s): 30-35 31 | NUMA node6 CPU(s): 36-41 32 | NUMA node7 CPU(s): 42-47 33 | ``` 34 | 35 | Server is run on the first 20 cores, clients on remaining cores. 36 | 37 | ![Benchmark results for tom](tom.png) 38 | -------------------------------------------------------------------------------- /benchmark/ben/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ben$ uname -a 3 | Linux ben 3.16-2-amd64 #1 SMP Debian 3.16.3-2 (2014-09-20) x86_64 GNU/Linux 4 | ben$ lscpu 5 | Architecture: x86_64 6 | CPU op-mode(s): 32-bit, 64-bit 7 | Byte Order: Little Endian 8 | CPU(s): 40 9 | On-line CPU(s) list: 0-39 10 | Thread(s) per core: 1 11 | Core(s) per socket: 10 12 | Socket(s): 4 13 | NUMA node(s): 4 14 | Vendor ID: GenuineIntel 15 | CPU family: 6 16 | Model: 47 17 | Stepping: 2 18 | CPU MHz: 1064.000 19 | BogoMIPS: 4800.31 20 | Virtualization: VT-x 21 | L1d cache: 32K 22 | L1i cache: 32K 23 | L2 cache: 256K 24 | L3 cache: 30720K 25 | NUMA node0 CPU(s): 0-9 26 | NUMA node1 CPU(s): 10-19 27 | NUMA node2 CPU(s): 20-29 28 | NUMA node3 CPU(s): 30-39 29 | ``` 30 | 31 | Server is run on the first 16 cores, clients on remaining cores. 32 | The graph likely flattens out as the clients on ben are not able to 33 | produce enough work to saturate all the server cores. 34 | 35 | ![Benchmark results for ben](ben.png) 36 | -------------------------------------------------------------------------------- /benchmark/ben/cucache.dat: -------------------------------------------------------------------------------- 1 | memcached-1 Sets 9551.000000 2 | memcached-1 Gets 6.275000 95456.500000 95462.725000 3 | cucache-1 Sets 6035.910000 4 | cucache-1 Gets 3.320000 60325.880000 60329.200000 5 | cucache-2 Sets 10581.320000 6 | cucache-2 Gets 5.815000 105755.035000 105760.850000 7 | memcached-2 Sets 17170.940000 8 | memcached-2 Gets 13.905000 171611.700000 171624.440000 9 | cucache-4 Sets 18711.210000 10 | cucache-4 Gets 10.290000 187009.190000 187019.480000 11 | memcached-4 Sets 26226.845000 12 | memcached-4 Gets 23.775000 262115.370000 262138.690000 13 | memcached-6 Sets 28936.230000 14 | memcached-6 Gets 22.880000 289197.410000 289219.125000 15 | cucache-6 Sets 21511.080000 16 | cucache-6 Gets 11.825000 214992.535000 215004.360000 17 | cucache-8 Sets 23559.805000 18 | cucache-8 Gets 12.950000 235468.530000 235481.480000 19 | memcached-8 Sets 29499.520000 20 | memcached-8 Gets 22.870000 294826.725000 294849.265000 21 | cucache-10 Sets 24749.350000 22 | cucache-10 Gets 13.610000 247357.430000 247371.040000 23 | memcached-10 Sets 29594.795000 24 | memcached-10 Gets 23.545000 295777.210000 295801.495000 25 | memcached-12 Sets 29549.900000 26 | memcached-12 Gets 36.680000 295314.235000 295352.820000 27 | cucache-12 Sets 23739.525000 28 | cucache-12 Gets 13.050000 237264.745000 237277.795000 29 | -------------------------------------------------------------------------------- /benchmark/internal/README.md: -------------------------------------------------------------------------------- 1 | These benchmarks do not use the loopback network interface, nor any of 2 | the Memcache protocols. Instead, functions are called directly on the 3 | cuckoo cache by the [benchmarking 4 | tool](../../src/cuckood/bench/main.go). A tight loop performs a set and 5 | a get on a random key for each iteration, and the time spent on the set 6 | and get are plotted. 7 | 8 | ![Performance of sets and gets on random keys](bench.png) 9 | 10 | Red lines indicate the size steps for the Cuckoo hash table. The 11 | duration of sets/gets increases over time as the number of hashing 12 | functions increases, and then drops back down when the table is resized 13 | and the number of hashes is lowered. Note that the occupancy of the 14 | hash table only ever grows to ~85%; increasing the number of hash 15 | functions or the max search depth is likely to improve this. 16 | 17 | Three "steps" of set durations are visible in the graph. The bulk of the 18 | sets take on the order of tens of microseconds, and correspond to the 19 | common case where the table can accommodate the new item. The sets 20 | taking on the order of tens of thousands of microseconds are those that 21 | increase the number of hash functions used by the table. This is 22 | expensive partially because of the CompareAndSwap on the number of 23 | hashes, and partially due to the following second insert that needs to 24 | be performed. The sets on the order of a second are those that perform a 25 | table resize, which is a fairly expensive operation. 26 | 27 | Two steps of get durations are visible. The bulk of them take on the 28 | order of a microsecond, but some edge up towards 10us. I'm not sure why 29 | this second step exists.. Gets are dominated by calls to cuckood.has(), 30 | GC, and malloc. 31 | -------------------------------------------------------------------------------- /benchmark/bench-results.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # rev because otherwise exp-pre will be sorted before tom, but after ben 3 | versions=$(awk '{print $1}' cucache.dat | sed 's/-[0-9]*$//' | sort -u | rev | sort | rev) 4 | versions=($versions) 5 | 6 | read -r -d '' out <<'EOF' 7 | set key top left 8 | set xlabel "CPU Cores" 9 | set ylabel "ops/s" 10 | plot 11 | EOF 12 | 13 | i=2 14 | mi=9 15 | seen_mc=0 16 | for f in "${versions[@]}"; do 17 | # Tidy up title 18 | t="$f" 19 | t=$(echo "$t" | perl -pe 's/-(tom|ben)(-.*|$)/\2 on \1/') 20 | t=$(echo "$t" | sed 's/^exp-//') 21 | t=$(echo "$t" | sed 's/^-memcached-//') 22 | t=$(echo "$t" | perl -pe 's/^(.*)-memcached-/memcached /') 23 | 24 | # Is this a memcached entry? 25 | mc=0 26 | echo "$f" | grep memcached >/dev/null 27 | [ $? -eq 0 ] && mc=1 28 | 29 | # line type 30 | lt=$i 31 | [ $mc -eq 1 ] && lt=$mi 32 | 33 | # Add plot lines; tt is needed to not print ' get hit' for hidden 34 | # memcached lines 35 | tt="" 36 | tt="get hit ($t)" 37 | #out+=" \"< grep '$f' cucache.dat | perl -pe 's/^.*?-(\\\\d+)/\\\\1/' | grep Gets\" u 1:3 dt 1 pt 1 lt $lt t '$tt' w linespoints," 38 | tt="set ($t)" 39 | out+=" \"< grep '$f' cucache.dat | perl -pe 's/^.*?-(\\\\d+)/\\\\1/' | grep Sets\" u 1:3 dt 2 pt 2 lt $lt t '$tt' w linespoints," 40 | tt="get miss ($t)" 41 | #out+=" \"< grep '$f' cucache.dat | perl -pe 's/^.*?-(\\\\d+)/\\\\1/' | grep Gets\" u 1:4 dt 3 pt 3 lt $lt t '$tt' w linespoints," 42 | tt="get ($t)" 43 | out+=" \"< grep '$f' cucache.dat | perl -pe 's/^.*?-(\\\\d+)/\\\\1/' | grep Gets\" u 1:5 dt 4 pt 4 lt $lt t '$tt' w linespoints," 44 | 45 | # Only "real" lines increment dash type 46 | [ $mc -eq 0 ] && ((i=i+1)) 47 | [ $mc -eq 1 ] && ((mi=mi-1)) 48 | done 49 | 50 | # Strip trailing comma 51 | out=${out%,} 52 | 53 | echo "$out" 54 | echo "$out" | gnuplot -p 55 | -------------------------------------------------------------------------------- /wip.md: -------------------------------------------------------------------------------- 1 | Max item limit 2 | - Use this + available memory to determine #records 3 | 4 | Table resize (grow in particular) 5 | - How can we be cleverer about this to allow resizing without locking 6 | all concurrent inserts? 7 | - What's the best way to resize? 8 | Currently we just copy over the elements, but we could also allocate 9 | a new table, copy over all items in same location (using `copy()`, 10 | which should be faster), but keep original hash functions 11 | **including mod**. We then add (at least one) new hash function with 12 | new (larger) mod. This will slow down the new table (more hash 13 | functions to check), but should significantly speed up the resize 14 | itself. 15 | - How should shrink be supported (if at all)? 16 | 17 | Avoid iterating over empty bins for touchall? 18 | 19 | How should resizing table be traded off against evicting items? 20 | - Currently a goroutine periodically checks how many items were 21 | evicted during the last pass, and will resize if this exceeds a 22 | threshold. This threshold needs to be tweaked. 23 | 24 | Benchmarks: 25 | - What is the performance as occupancy increases? 26 | - Is 8 the right number of values for each bin? Will slow down lookup 27 | (even with MemC3 tags) 28 | - Facebook-inspired numbers: 29 | - http://www.ece.eng.wayne.edu/~sjiang/pubs/papers/atikoglu12-memcached.pdf 30 | - Use proper read:write ratio (Facebook reports >30:1, sometimes 500:1) 31 | - Use proper sizes (Facebook reports ~32b keys, ~100s of bytes values) 32 | - Hit rate 95% 33 | - 90% of keys occur in 10% of requests: Figure 5 34 | - 10% of keys in 90% of requests? 35 | - Key size distribution: GEV dist u = 30.7984, sig = 8.20449, k = 0.078688 36 | Value size distribution: RP dist θ = 0, sig = 214.476, k = 0.348238 37 | 38 | UDP protocol needs to be implemented 39 | 40 | Should key slice be copied (not aliased) on append/prepend to avoid keeping 41 | body+extra around? 42 | -------------------------------------------------------------------------------- /benchmark/tom/cucache.dat: -------------------------------------------------------------------------------- 1 | memcached-1 Sets 4939.555000 2 | memcached-1 Gets 2.715000 49368.370000 49371.085000 3 | cucache-1 Sets 3610.880000 4 | cucache-1 Gets 1.985000 36088.930000 36090.915000 5 | cucache-2 Sets 5754.050000 6 | cucache-2 Gets 3.160000 57508.840000 57512.000000 7 | memcached-2 Sets 9295.430000 8 | memcached-2 Gets 5.110000 92903.210000 92908.320000 9 | cucache-4 Sets 9734.640000 10 | cucache-4 Gets 5.350000 97292.865000 97298.215000 11 | memcached-4 Sets 19338.500000 12 | memcached-4 Gets 11.160000 193278.165000 193289.310000 13 | memcached-6 Sets 24989.885000 14 | memcached-6 Gets 14.730000 249760.935000 249775.195000 15 | cucache-6 Sets 11948.380000 16 | cucache-6 Gets 6.565000 119418.090000 119424.655000 17 | cucache-8 Sets 13738.705000 18 | cucache-8 Gets 7.555000 137311.550000 137319.105000 19 | memcached-8 Sets 28168.130000 20 | memcached-8 Gets 17.620000 281523.030000 281541.910000 21 | cucache-10 Sets 16689.110000 22 | cucache-10 Gets 9.175000 166799.310000 166808.485000 23 | memcached-10 Sets 29590.170000 24 | memcached-10 Gets 17.165000 295737.330000 295755.305000 25 | memcached-12 Sets 29321.035000 26 | memcached-12 Gets 17.990000 293047.450000 293065.260000 27 | cucache-12 Sets 18983.660000 28 | cucache-12 Gets 10.435000 189732.195000 189742.630000 29 | cucache-14 Sets 20496.765000 30 | cucache-14 Gets 11.270000 204854.940000 204866.210000 31 | memcached-14 Sets 28762.300000 32 | memcached-14 Gets 20.840000 287461.550000 287480.705000 33 | cucache-16 Sets 21457.915000 34 | cucache-16 Gets 11.795000 214461.200000 214472.995000 35 | memcached-16 Sets 27033.765000 36 | memcached-16 Gets 15.605000 270182.815000 270203.910000 37 | memcached-18 Sets 23669.735000 38 | memcached-18 Gets 14.735000 236565.070000 236580.260000 39 | cucache-18 Sets 21512.240000 40 | cucache-18 Gets 11.825000 215004.120000 215015.945000 41 | memcached-20 Sets 19857.735000 42 | memcached-20 Gets 13.655000 198461.255000 198479.100000 43 | cucache-20 Sets 22317.115000 44 | cucache-20 Gets 12.270000 223048.495000 223060.765000 45 | -------------------------------------------------------------------------------- /src/cuckood/cucache/text/out.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | 9 | gomem "github.com/dustin/gomemcached" 10 | ) 11 | 12 | func WriteMCResponse(res *gomem.MCResponse, out io.Writer) (err error) { 13 | if res.Opcode.IsQuiet() && res.Opcode != gomem.GETKQ && res.Status == gomem.SUCCESS { 14 | // there is absolutely no reason to reply here 15 | return nil 16 | } 17 | 18 | switch res.Status { 19 | case gomem.SUCCESS: 20 | switch res.Opcode { 21 | case gomem.GETK, gomem.GETKQ: 22 | flags := binary.BigEndian.Uint32(res.Extras[0:4]) 23 | _, err = out.Write([]byte(fmt.Sprintf("VALUE %s %d %d %d\r\n", res.Key, flags, len(res.Body), res.Cas))) 24 | if err != nil { 25 | return 26 | } 27 | _, err = out.Write(res.Body) 28 | if err != nil { 29 | return 30 | } 31 | _, err = out.Write([]byte{'\r', '\n'}) 32 | if err != nil { 33 | return 34 | } 35 | if res.Opcode == gomem.GETK { 36 | _, err = out.Write([]byte("END\r\n")) 37 | } 38 | case gomem.SET, gomem.ADD, gomem.REPLACE: 39 | _, err = out.Write([]byte("STORED\r\n")) 40 | case gomem.DELETE: 41 | _, err = out.Write([]byte("DELETED\r\n")) 42 | case gomem.INCREMENT, gomem.DECREMENT: 43 | v := binary.BigEndian.Uint64(res.Body) 44 | _, err = out.Write([]byte(strconv.FormatUint(v, 10) + "\r\n")) 45 | } 46 | case gomem.KEY_ENOENT: 47 | if res.Opcode == gomem.GETK { 48 | _, err = out.Write([]byte("END\r\n")) 49 | } else if res.Opcode == gomem.GETKQ { 50 | } else { 51 | _, err = out.Write([]byte("NOT_FOUND\r\n")) 52 | } 53 | case gomem.KEY_EEXISTS: 54 | _, err = out.Write([]byte("EXISTS\r\n")) 55 | case gomem.NOT_STORED: 56 | _, err = out.Write([]byte("NOT_STORED\r\n")) 57 | case gomem.ENOMEM: 58 | _, err = out.Write([]byte("SERVER_ERROR no space for new entry\r\n")) 59 | case gomem.DELTA_BADVAL: 60 | _, err = out.Write([]byte("CLIENT_ERROR incr/decr on non-numeric field\r\n")) 61 | case gomem.UNKNOWN_COMMAND: 62 | _, err = out.Write([]byte("ERROR\r\n")) 63 | } 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | cucache has has been benchmarked on two multi-core machines thus far, 2 | [ben](ben/) and [tom](tom/). Results can be found by going to each of 3 | those directory. The former is a 40-core machine with four NUMA nodes, 4 | and the latter is a slower 48-core machine with eight NUMA nodes. 5 | Performance without the overhead of the memcache protocol and the 6 | loopback interface can be found in [internal](internal/). 7 | 8 | Benchmarks were performed with 9 | [memtier_benchmark](https://github.com/RedisLabs/memtier_benchmark) and 10 | repeated using [experiment](https://github.com/jonhoo/experiment) on 11 | the same machine as the servers with all connections going over 12 | loopback, and using Go tip. The exact parameters used can be seen in [bench.sh](bench.sh). 13 | Experimental results suggest that when a real network link is used, 14 | memcached and cucache perform roughly the same. When used across 15 | loopback, cucache scales better than memcached, though its absolute 16 | performance is lower when the number of cores is small. 17 | 18 | It is worth noting that this benchmark is still somewhat artificial. It 19 | does not model key contention, which is likely to be an issue for 20 | memcached, but not so much for cucache. Furthermore, as the benchmark 21 | has to be run on a single machine to not have the network interface be 22 | the bottleneck, clients will eventually struggle to generate enough load 23 | to saturate the server's capacity. We can see this happening on ben. 24 | 25 | The numbers reported by memtier_benchmark can also be somewhat 26 | misleading. For example, it reports hits/s, sets/s, and misses/s, but 27 | these numbers are *not* necessarily the maximum throughput the server 28 | *could* achieve. Instead, they are the highest throughput 29 | memtier_benchmark ever *saw* for that operation. With a read/write ratio 30 | of 10:1 (the default), memtier_benchmark will execute ten times fewer 31 | sets than gets, and thus the reported throughput can never exceed 1/10th 32 | of the number of gets. Similarly, if all the keys miss, the number of 33 | hits/s will be reported as being very low, simply because 34 | memtier_benchmark didn't see very many hits. 35 | 36 | ## Profiling results 37 | 38 | Single-core CPU profile (ben): 39 | ![Single-core CPU profile](https://cdn.rawgit.com/jonhoo/cucache/65fe27c4bbd6a16c87a141478121b3e62e526ab9/benchmark/single-core-profile.svg) 40 | 41 | Multi-core CPU profile (10 server cores, ben): 42 | ![20-core CPU profile](https://cdn.rawgit.com/jonhoo/cucache/65fe27c4bbd6a16c87a141478121b3e62e526ab9/benchmark/multi-core-profile.svg) 43 | -------------------------------------------------------------------------------- /benchmark/bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | command -v numactl >/dev/null 2>&1 3 | no_numa=$? 4 | 5 | if [ $no_numa -eq 1 ]; then 6 | echo "no numactl, so cannot force core locality; exiting..." > /dev/stderr 7 | exit 1 8 | fi 9 | 10 | scores="$1"; shift 11 | ccores="$1"; shift 12 | ((needed=scores+ccores)); 13 | 14 | # where are we? 15 | DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 16 | 17 | cores=$(numactl -H | grep cpus | sed 's/.*: //' | paste -sd' ') 18 | cores=($cores) 19 | ncores=${#cores[@]} 20 | 21 | if ((needed>ncores)); then 22 | echo "Cannot use more server+client cores ($needed) than there are CPU cores ($ncores)" >/dev/stderr 23 | exit 1 24 | fi 25 | 26 | srange=$(echo ${cores[@]} | cut -d' ' -f1-${scores} | tr ' ' ',') 27 | crange=$(echo ${cores[@]} | rev | cut -d' ' -f1-${ccores} | rev | tr ' ' ',') 28 | 29 | args=() 30 | for i in "$@"; do 31 | if [ "$i" == "CCORES" ]; then 32 | args+=("$ccores") 33 | elif [ "$i" == "SCORES" ]; then 34 | args+=("$scores") 35 | else 36 | args+=("$i") 37 | fi 38 | done 39 | 40 | # run the server 41 | echo numactl -C $srange env GOMAXPROCS=$scores "${args[@]}" -p 2222 -U 2222 > /dev/stderr 42 | numactl -C $srange env GOMAXPROCS=$scores "${args[@]}" -p 2222 -U 2222 & 43 | pid=$! 44 | 45 | # let it initialize 46 | sleep 1 47 | 48 | memargs="" 49 | memargs="$memargs -n 20000" # lots o' requests 50 | 51 | # this number is taken out of thin air 52 | # if you have an good estimate, please let me know 53 | concurrent_clients=200 54 | ((nc=concurrent_clients/ccores)) 55 | memargs="$memargs -t $ccores -c $nc" 56 | 57 | # numbers below from 58 | # http://www.ece.eng.wayne.edu/~sjiang/pubs/papers/atikoglu12-memcached.pdf 59 | 60 | # each request has keys of ~32b, and values of ~200b 61 | memargs="$memargs --key-prefix trytomakekey32byteslong" 62 | memargs="$memargs --data-size-range=150-350" 63 | 64 | # set:get varies between 1:30 to 1:500 65 | memargs="$memargs --ratio 1:100" 66 | 67 | # number of keys is hard to extract from the paper, but given ~100000 req/s, 68 | # and ~30% unique keys, number of keys can be 0.3*100000 69 | memargs="$memargs --key-minimum=1" 70 | memargs="$memargs --key-maximum=30000" 71 | 72 | # let's say keys are roughly normal distributed, 73 | # but a small number of keys (1%) are hot. 74 | memargs="$memargs --key-pattern=G:G" 75 | memargs="$memargs --key-stddev=300" 76 | 77 | # run the client 78 | echo numactl -C $crange memtier_benchmark -p 2222 -P memcache_binary $memargs > /dev/stderr 79 | numactl -C $crange memtier_benchmark -p 2222 -P memcache_binary $memargs 2>/dev/null 80 | 81 | # terminate the server 82 | kill $pid 2>/dev/null 83 | wait $pid 2>/dev/null 84 | -------------------------------------------------------------------------------- /src/cuckood/bench/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cuckood" 5 | "flag" 6 | "fmt" 7 | "math/rand" 8 | "os" 9 | "runtime" 10 | "runtime/pprof" 11 | "time" 12 | ) 13 | 14 | func main() { 15 | cpuprofile := flag.Bool("cpuprofile", false, "CPU profile") 16 | n := flag.Int("n", 10000, "Number of requests to make") 17 | s := flag.Uint64("s", 0, "Initial db size") 18 | flag.Parse() 19 | 20 | c := cuckoo.New(*s) 21 | 22 | var pf *os.File 23 | var err error 24 | if *cpuprofile { 25 | fmt.Fprintln(os.Stderr, "starting CPU profiling of set/get") 26 | pf, err = os.Create("set.out") 27 | if err != nil { 28 | fmt.Fprintf(os.Stderr, "could not create CPU profile file set.out: %v\n", err) 29 | return 30 | } 31 | err = pprof.StartCPUProfile(pf) 32 | if err != nil { 33 | fmt.Fprintf(os.Stderr, "could not start CPU profiling: %v\n", err) 34 | return 35 | } 36 | } 37 | 38 | at := *n / 10 39 | v := []byte{0x01} 40 | var mem runtime.MemStats 41 | rand.Seed(1) 42 | for i := 0; i < *n; i++ { 43 | k := []byte(fmt.Sprintf("%d-%d", i, rand.Int63())) 44 | 45 | sstart := time.Now() 46 | c.Set(k, v, 0, time.Time{}) 47 | 48 | gstart := time.Now() 49 | c.Get(k) 50 | 51 | end := time.Now() 52 | 53 | if i%at == 0 { 54 | runtime.ReadMemStats(&mem) 55 | fmt.Println(i, gstart.Sub(sstart).Seconds(), end.Sub(gstart).Seconds(), mem.Alloc, mem.Mallocs) 56 | fmt.Fprintln(os.Stderr, i) 57 | } else { 58 | fmt.Println(i, gstart.Sub(sstart).Seconds(), end.Sub(gstart).Seconds()) 59 | } 60 | } 61 | 62 | if pf != nil { 63 | pprof.StopCPUProfile() 64 | err := pf.Close() 65 | if err != nil { 66 | fmt.Fprintln(os.Stderr, "could not end cpu profile:", err) 67 | } 68 | 69 | fmt.Fprintln(os.Stderr, "starting CPU profiling of get") 70 | pf, err = os.Create("get.out") 71 | if err != nil { 72 | fmt.Fprintf(os.Stderr, "could not create CPU profile file get.out: %v\n", err) 73 | return 74 | } 75 | err = pprof.StartCPUProfile(pf) 76 | if err != nil { 77 | fmt.Fprintf(os.Stderr, "could not start CPU profiling: %v\n", err) 78 | return 79 | } 80 | } 81 | 82 | var num int 83 | var avg float64 84 | rand.Seed(1) 85 | for i := 0; i < *n; i++ { 86 | k := []byte(fmt.Sprintf("%d-%d", i, rand.Int63())) 87 | 88 | start := time.Now() 89 | c.Get(k) 90 | end := time.Now() 91 | 92 | avg = (end.Sub(start).Seconds() + avg*float64(num)) / float64(num+1) 93 | num++ 94 | } 95 | 96 | fmt.Fprintf(os.Stderr, "average get speed: %.2fus\n", avg*1000000) 97 | 98 | if pf != nil { 99 | pprof.StopCPUProfile() 100 | err := pf.Close() 101 | if err != nil { 102 | fmt.Fprintln(os.Stderr, "could not end cpu profile:", err) 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/cuckood/search.go: -------------------------------------------------------------------------------- 1 | package cuckoo 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const MAX_SEARCH_DEPTH int = 1000 10 | 11 | type mv struct { 12 | key keyt 13 | from int 14 | to int 15 | tobn int 16 | } 17 | 18 | func (m *cmap) search(now time.Time, bins ...int) []mv { 19 | for depth := 1; depth < MAX_SEARCH_DEPTH; depth++ { 20 | for _, b := range bins { 21 | path := m.find(nil, b, depth, now) 22 | if path != nil { 23 | return path 24 | } 25 | } 26 | } 27 | return nil 28 | } 29 | 30 | func (m *cmap) find(path []mv, bin int, depth int, now time.Time) []mv { 31 | if depth >= 0 { 32 | for i := 0; i < ASSOCIATIVITY; i++ { 33 | v := m.bins[bin].v(i) 34 | if v == nil || !v.present(now) { 35 | return path 36 | } 37 | 38 | path_ := make([]mv, len(path)+1) 39 | for i := range path { 40 | path_[i] = path[i] 41 | } 42 | 43 | from := bin 44 | to := from 45 | bno := v.bno + 1 46 | key := v.key 47 | for i := 0; i < int(m.hashes); i++ { 48 | bno = (bno + 1) % int(m.hashes) 49 | to = m.bin(bno, key) 50 | // XXX: could potentially try all bins here and 51 | // check each for available()? extra-broad 52 | // search... 53 | if to != from { 54 | break 55 | } 56 | } 57 | if to == from { 58 | continue 59 | } 60 | 61 | skip := false 62 | for _, p := range path { 63 | if p.from == to { 64 | skip = true 65 | break 66 | } 67 | } 68 | 69 | if skip { 70 | // XXX: could instead try next bin here 71 | continue 72 | } 73 | 74 | path_[len(path)] = mv{key, from, to, bno} 75 | if m.bins[to].available(now) { 76 | return path_ 77 | } else { 78 | return m.find(path_, to, depth-1, now) 79 | } 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | func (m *cmap) validate_execute(path []mv, now time.Time) bool { 86 | for i := len(path) - 1; i >= 0; i-- { 87 | k := path[i] 88 | 89 | m.lock_in_order(k.from, k.to) 90 | if !m.bins[k.to].available(now) { 91 | m.unlock(k.from, k.to) 92 | fmt.Println("path to occupancy no longer valid, target bucket now full") 93 | return false 94 | } 95 | 96 | ki := -1 97 | for j := 0; j < ASSOCIATIVITY; j++ { 98 | jk := m.bins[k.from].v(j) 99 | if jk != nil && jk.present(now) && bytes.Equal(jk.key, k.key) { 100 | ki = j 101 | break 102 | } 103 | } 104 | if ki == -1 { 105 | m.unlock(k.from, k.to) 106 | fmt.Println("path to occupancy no longer valid, key already swapped") 107 | return false 108 | } 109 | 110 | v := m.bins[k.from].v(ki) 111 | v.bno = k.tobn 112 | 113 | m.bins[k.to].subin(v, now) 114 | m.bins[k.from].kill(ki) 115 | 116 | m.unlock(k.from, k.to) 117 | } 118 | 119 | return true 120 | } 121 | -------------------------------------------------------------------------------- /src/cuckood/cucache/helper_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "cuckood" 6 | "encoding/binary" 7 | "testing" 8 | 9 | gomem "github.com/dustin/gomemcached" 10 | ) 11 | 12 | var noflag = []byte{0, 0, 0, 0} 13 | var noexp = []byte{0, 0, 0, 0} 14 | var nullset = []byte{0, 0, 0, 0, 0, 0, 0, 0} 15 | 16 | func do(c *cuckoo.Cuckoo, t *testing.T, req *gomem.MCRequest) *gomem.MCResponse { 17 | res := req2res(c, req) 18 | if res.Status != gomem.SUCCESS { 19 | t.Errorf("expected operation %v to succeed; got %v", req, res.Status) 20 | } 21 | return res 22 | } 23 | 24 | func get(c *cuckoo.Cuckoo, key string) *gomem.MCResponse { 25 | return req2res(c, &gomem.MCRequest{ 26 | Opcode: gomem.GET, 27 | Key: []byte(key), 28 | }) 29 | } 30 | 31 | func set(c *cuckoo.Cuckoo, key string, val []byte, as gomem.CommandCode) *gomem.MCResponse { 32 | return req2res(c, &gomem.MCRequest{ 33 | Opcode: as, 34 | Key: []byte(key), 35 | Body: val, 36 | Extras: nullset, 37 | }) 38 | } 39 | 40 | func pm(c *cuckoo.Cuckoo, key string, by uint64, def uint64, nocreate bool, as gomem.CommandCode) *gomem.MCResponse { 41 | extras := make([]byte, 20) 42 | binary.BigEndian.PutUint64(extras[0:8], by) 43 | binary.BigEndian.PutUint64(extras[8:16], def) 44 | binary.BigEndian.PutUint32(extras[16:20], 0) 45 | if nocreate { 46 | binary.BigEndian.PutUint32(extras[16:20], 0xffffffff) 47 | } 48 | return req2res(c, &gomem.MCRequest{ 49 | Opcode: as, 50 | Key: []byte(key), 51 | Extras: extras, 52 | }) 53 | } 54 | 55 | func assertGet(c *cuckoo.Cuckoo, t *testing.T, key string, val []byte) *gomem.MCResponse { 56 | res := get(c, key) 57 | if res.Status != gomem.SUCCESS { 58 | t.Errorf("expected get success on key %s, got %v", key, res.Status) 59 | } else if !bytes.Equal(res.Body, val) { 60 | t.Errorf("expected get to return '%v' for key %s, got '%v'", string(val), key, string(res.Body)) 61 | } 62 | return res 63 | } 64 | 65 | func assertNotExists(c *cuckoo.Cuckoo, t *testing.T, key string) { 66 | res := get(c, key) 67 | if res.Status != gomem.KEY_ENOENT { 68 | t.Errorf("expected get KEY_ENOENT on key %s, got %v", key, res.Status) 69 | } 70 | return 71 | } 72 | 73 | func assertSet(c *cuckoo.Cuckoo, t *testing.T, key string, val []byte, as gomem.CommandCode) *gomem.MCResponse { 74 | res := set(c, key, val, as) 75 | if res.Status != gomem.SUCCESS { 76 | t.Errorf("expected %v success for %s => %s, got %v", as, key, string(val), res.Status) 77 | } 78 | return res 79 | } 80 | 81 | func assertPM(c *cuckoo.Cuckoo, t *testing.T, key string, by uint64, def uint64, nocreate bool, as gomem.CommandCode) *gomem.MCResponse { 82 | res := pm(c, key, by, def, nocreate, as) 83 | if res.Status != gomem.SUCCESS { 84 | t.Errorf("expected success for %v(%d, %d, %v) on key %s, got %v", as, by, def, nocreate, key, res.Status) 85 | } 86 | return res 87 | } 88 | -------------------------------------------------------------------------------- /benchmark/bench-results.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | aggregators = { 4 | "median" => lambda do |array| 5 | len = array.length 6 | if len == 0 7 | return 0 8 | end 9 | if len == 1 10 | return array[0] 11 | end 12 | sorted = array.sort 13 | return (sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0 14 | end, 15 | "max" => lambda { |array| array.max }, 16 | "min" => lambda { |array| array.min }, 17 | "mean" => lambda { |array| array.inject{ |sum, el| sum + el }.to_f / array.size } 18 | } 19 | 20 | $aggregator = aggregators["median"] 21 | if ARGV.length > 0 and aggregators.include? ARGV[0] 22 | $aggregator = aggregators[ARGV.shift] 23 | end 24 | 25 | versions = {} 26 | ARGV.each do |dir| 27 | base = "" 28 | if File.basename(dir) =~ /^(.*)-(\d+)$/ 29 | base = "#{$1}-" 30 | end 31 | Dir.glob(File.join(dir, "*")) do |v| 32 | if not File.directory? v 33 | next 34 | end 35 | 36 | version = base + File.basename(v) 37 | if not versions.include? version 38 | versions[version] = [] 39 | end 40 | 41 | Dir.glob(File.join(v, "run-*")) do |r| 42 | File.open File.join(r, "stdout.log"), "r" do |f| 43 | result = {} 44 | f.each_line do |l| 45 | if l =~ /(Sets|Gets)/ 46 | t = $1 47 | fields = l.gsub(/\s+/m, ' ').strip.split(" ").map { |v| v =~ /^(---|[\d\.]+)$/ ? v.gsub(/---/, '').to_f : v } 48 | if t == "Sets" 49 | result[t.downcase] = fields[1] 50 | elsif t == "Gets" 51 | result[t.downcase] = { 52 | "hit" => fields[2], 53 | "miss" => fields[3], 54 | } 55 | end 56 | end 57 | end 58 | if result.include? "gets" 59 | versions[version].push result 60 | end 61 | end 62 | end 63 | end 64 | end 65 | versions.each_pair do |k, v| 66 | accum = { 67 | "set" => [], 68 | "get" => { 69 | "hit" => [], 70 | "miss" => [], 71 | "total" => [], 72 | }, 73 | } 74 | v.each do |r| 75 | accum["set"].push r["sets"] 76 | accum["get"]["hit"].push r["gets"]["hit"] 77 | accum["get"]["miss"].push r["gets"]["miss"] 78 | accum["get"]["total"].push r["gets"]["hit"] + r["gets"]["miss"] 79 | end 80 | 81 | if accum["set"].length == 0 82 | versions.delete k 83 | else 84 | versions[k] = accum 85 | end 86 | end 87 | 88 | versions.each_pair do |k, v| 89 | agg = { 90 | "set" => $aggregator.call(v["set"]), 91 | "get" => { 92 | "hit" => $aggregator.call(v["get"]["hit"]), 93 | "miss" => $aggregator.call(v["get"]["miss"]), 94 | "total" => $aggregator.call(v["get"]["total"]), 95 | }, 96 | } 97 | versions[k] = agg 98 | end 99 | 100 | versions.keys.sort { |a, b| a.gsub(/^.*-/, '').to_i <=> b.gsub(/^.*-/, '').to_i }.each do |k| 101 | v = versions[k] 102 | printf("%s\tSets\t%f\n", k, v["set"]) 103 | printf("%s\tGets\t%f\t%f\t%f\n", k, v["get"]["hit"], v["get"]["miss"], v["get"]["total"]) 104 | end 105 | -------------------------------------------------------------------------------- /src/cuckood/cucache/text/help.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | 9 | gomem "github.com/dustin/gomemcached" 10 | ) 11 | 12 | func str2op(cmd string, quiet bool) (cc gomem.CommandCode) { 13 | cc = 0xff 14 | if !quiet { 15 | switch cmd { 16 | case "set", "cas": 17 | cc = gomem.SET 18 | case "get", "gets": 19 | cc = gomem.GETK 20 | case "add": 21 | cc = gomem.ADD 22 | case "replace": 23 | cc = gomem.REPLACE 24 | case "delete": 25 | cc = gomem.DELETE 26 | case "incr": 27 | cc = gomem.INCREMENT 28 | case "decr": 29 | cc = gomem.DECREMENT 30 | case "quit": 31 | cc = gomem.QUIT 32 | case "flush_all": 33 | cc = gomem.FLUSH 34 | case "noop": 35 | cc = gomem.NOOP 36 | case "append": 37 | cc = gomem.APPEND 38 | case "prepend": 39 | cc = gomem.PREPEND 40 | case "version": 41 | cc = gomem.VERSION 42 | } 43 | } else { 44 | switch cmd { 45 | case "set", "cas": 46 | cc = gomem.SETQ 47 | case "add": 48 | cc = gomem.ADDQ 49 | case "replace": 50 | cc = gomem.REPLACEQ 51 | case "delete": 52 | cc = gomem.DELETEQ 53 | case "incr": 54 | cc = gomem.INCREMENTQ 55 | case "decr": 56 | cc = gomem.DECREMENTQ 57 | case "quit": 58 | cc = gomem.QUITQ 59 | case "flush_all": 60 | cc = gomem.FLUSHQ 61 | case "append": 62 | cc = gomem.APPENDQ 63 | case "prepend": 64 | cc = gomem.PREPENDQ 65 | } 66 | } 67 | return 68 | } 69 | 70 | func check_args(args []string, argv int) error { 71 | if len(args) != argv { 72 | return fmt.Errorf("CLIENT_ERROR wrong number of arguments (got %d, expected %d)\r\n", len(args), argv) 73 | } 74 | return nil 75 | } 76 | 77 | func strtm(in string) (uint32, error) { 78 | v, e := strconv.ParseUint(in, 10, 32) 79 | return uint32(v), e 80 | } 81 | 82 | func setargs(args []string, in io.Reader) (flags uint32, exp uint32, value []byte, err error) { 83 | nbytes := args[len(args)-1] 84 | args = args[:len(args)-1] 85 | if len(args) >= 1 { 86 | var flags_ uint64 87 | flags_, err = strconv.ParseUint(args[0], 10, 16) 88 | if err != nil { 89 | return 90 | } 91 | flags = uint32(flags_) 92 | } 93 | 94 | if len(args) >= 2 { 95 | exp, err = strtm(args[1]) 96 | if err != nil { 97 | return 98 | } 99 | } 100 | 101 | value, err = data(nbytes, in) 102 | return 103 | } 104 | 105 | func data(lenarg string, in io.Reader) ([]byte, error) { 106 | ln, err := strconv.Atoi(lenarg) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | b := make([]byte, ln) 112 | _, err = io.ReadFull(in, b) 113 | if err == nil { 114 | // remove \r\n 115 | rn := make([]byte, 2) 116 | _, err = io.ReadFull(in, rn) 117 | if err != nil { 118 | return nil, err 119 | } 120 | if rn[0] != '\r' || rn[1] != '\n' { 121 | return nil, errors.New("data not terminated by \\r\\n") 122 | } 123 | } 124 | return b, err 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cucache 2 | Fast PUT/GET/DELETE in-memory key-value store for lookaside caching. 3 | 4 | [![Build Status](https://travis-ci.org/jonhoo/cucache.svg?branch=master)](https://travis-ci.org/jonhoo/cucache) 5 | 6 | A mostly complete implementation the memcache 7 | [text](https://github.com/memcached/memcached/blob/master/doc/protocol.txt) 8 | and 9 | [binary](https://code.google.com/p/memcached/wiki/MemcacheBinaryProtocol) 10 | protocols can be found inside [cucache](cucache/). The binary protocol 11 | has been tested using 12 | [memcached-test](https://github.com/dustin/memcached-test), and the text 13 | protocol with simple test cases extracted from the protocol 14 | specification. 15 | 16 | The implementation uses Cuckoo hashing along with several 17 | [concurrency](https://www.cs.cmu.edu/~dga/papers/memc3-nsdi2013.pdf) 18 | [optimizations](https://www.cs.princeton.edu/~mfreed/docs/cuckoo-eurosys14.pdf). 19 | The implementation uses much of the code from [Dustin Sallings' 20 | gomemcached](https://github.com/dustin/gomemcached) for the binary 21 | protocol. 22 | 23 | ## Known limitations and outstanding things 24 | 25 | - Needs configurable debugging information output 26 | - The touch command is not implemented; see [dustin/gomemcached#12](https://github.com/dustin/gomemcached/pull/12) 27 | - Protocol should be tested against [mctest](https://github.com/victorkirkebo/mctest) 28 | 29 | Current implementation notes can be found in [wip.md](wip.md). 30 | 31 | ## Want to use it? 32 | 33 | Great! Please submit pull-requests and issues if you come across 34 | anything you think is wrong. Note that this is very much a WIP, and I 35 | give no guarantees about support. 36 | 37 | ## Why another memcached? 38 | 39 | Cuckoo hashing is cool, and fast. Go is cool, and fast. Maybe the two 40 | can outcompete the aging (though still very much relevant) memcached 41 | while keeping the code nice and readable. Furthermore, as the Go runtime 42 | improves over time, cucache might itself get faster *automatically*! 43 | 44 | The hope is that fine-grained write locking and lock-free reading might 45 | speed up concurrent access significantly, and allow better scalability 46 | to many cores. While the traditional memcached wisdom of "just shard 47 | your data more" works well most of the time, there comes a point where 48 | you have some single key that is extremely hot, and then sharing simply 49 | won't help you. You need to be able to distribute that keys load across 50 | multiple cores. Although memcached does support multi-threaded 51 | execution, the fact that it locks during reads is a potential scaling 52 | bottleneck. 53 | 54 | ## Experimental results 55 | 56 | cucache is currently slightly slower than memcached in terms of 57 | over-the-network performance simply due to Go being slower than C for 58 | many operations: network socket operations have more overhead, system 59 | calls are slower, request and response marshalling is slower, and 60 | goroutine scheduling and GC incur additional runtime cost. In terms of 61 | pure performance (i.e. direct hash table operations), cuache is probably 62 | significantly faster than memcached already. 63 | 64 | See [benchmark/](benchmark/) for more in-depth performance evaluation. 65 | -------------------------------------------------------------------------------- /src/cuckood/bins.go: -------------------------------------------------------------------------------- 1 | package cuckoo 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | "unsafe" 7 | ) 8 | 9 | // offset64 is the fnv1a 64-bit offset 10 | var offset64 uint64 = 14695981039346656037 11 | 12 | // prime64 is the fnv1a 64-bit prime 13 | var prime64 uint64 = 1099511628211 14 | 15 | var intoffs []uint = []uint{0, 8, 16, 24} 16 | 17 | // bin returns the nth hash of the given key 18 | func (m *cmap) bin(n int, key keyt) int { 19 | s := offset64 20 | for _, c := range key { 21 | s ^= uint64(c) 22 | s *= prime64 23 | } 24 | for _, i := range intoffs { 25 | s ^= uint64(n >> i) 26 | s *= prime64 27 | } 28 | return int(s & (uint64(len(m.bins)) - 1)) 29 | } 30 | 31 | // kbins returns all hashes of the given key. 32 | // as m.hashes increases, this function will return more hashes. 33 | func (m *cmap) kbins(key keyt, into []int) { 34 | nb := uint64(len(m.bins)) - 1 35 | 36 | // only hash the key once 37 | s := offset64 38 | for _, c := range key { 39 | s ^= uint64(c) 40 | s *= prime64 41 | } 42 | 43 | for i := 0; i < len(into); i++ { 44 | // compute key for this i 45 | s_ := s 46 | for _, o := range intoffs { 47 | s_ ^= uint64(i >> o) 48 | s_ *= prime64 49 | } 50 | into[i] = int(s_ & nb) 51 | } 52 | } 53 | 54 | type aval struct { 55 | val unsafe.Pointer 56 | tag byte 57 | read bool 58 | } 59 | 60 | // cbin is a single Cuckoo map bin holding up to ASSOCIATIVITY values. 61 | // each bin has a lock that must be used for *writes*. 62 | // values should never be accessed directly, but rather through v() 63 | type cbin struct { 64 | vals [ASSOCIATIVITY]aval 65 | mx SpinLock 66 | } 67 | 68 | // v returns a pointer to the current key data for a given slot (if any). 69 | // this function may return nil if no key data is set for the given slot. 70 | // this function is safe in the face of concurrent updates, assuming writers 71 | // use setv(). 72 | func (b *cbin) v(i int) *cval { 73 | return (*cval)(atomic.LoadPointer(&b.vals[i].val)) 74 | } 75 | 76 | // vpresent returns true if the given slot contains unexpired key data 77 | func (b *cbin) vpresent(i int, now time.Time) bool { 78 | v := b.v(i) 79 | return v != nil && v.present(now) 80 | } 81 | 82 | // setv will atomically update the key data for the given slot 83 | func (b *cbin) setv(i int, v *cval) { 84 | tov := &b.vals[i] 85 | if v != nil { 86 | tov.tag = v.key[0] 87 | } 88 | atomic.StorePointer(&tov.val, unsafe.Pointer(v)) 89 | } 90 | 91 | // subin atomically replaces the first free slot in this bin with the given key 92 | // data 93 | func (b *cbin) subin(v *cval, now time.Time) { 94 | for i := 0; i < ASSOCIATIVITY; i++ { 95 | if !b.vpresent(i, now) { 96 | b.setv(i, v) 97 | return 98 | } 99 | } 100 | } 101 | 102 | // kill will immediately and atomically invalidate the given slot's key data 103 | func (b *cbin) kill(i int) { 104 | b.setv(i, nil) 105 | } 106 | 107 | // available returns true if this bin has a slot that is currently unoccupied 108 | // or expired 109 | func (b *cbin) available(now time.Time) bool { 110 | for i := 0; i < ASSOCIATIVITY; i++ { 111 | if !b.vpresent(i, now) { 112 | return true 113 | } 114 | } 115 | return false 116 | } 117 | 118 | // add will atomically replace the first available slot in this bin with the 119 | // given key data. this function may return an error if there are no free 120 | // slots. 121 | func (b *cbin) add(val *cval, upd Memop, now time.Time) (ret MemopRes) { 122 | b.mx.Lock() 123 | defer b.mx.Unlock() 124 | 125 | ret.T = SERVER_ERROR 126 | if b.available(now) { 127 | val.val, ret = upd(val.val, false) 128 | if ret.T == STORED { 129 | b.subin(val, now) 130 | } 131 | return 132 | } 133 | return 134 | } 135 | 136 | // has returns the slot holding the key data for the given key in this bin. 137 | // if no slot has the relevant key data, -1 is returned. 138 | func (b *cbin) has(key keyt, now time.Time) (i int, v *cval) { 139 | for i = 0; i < ASSOCIATIVITY; i++ { 140 | if b.vals[i].tag == key[0] { 141 | v = b.v(i) 142 | if v != nil && v.holds(key, now) { 143 | return 144 | } 145 | } 146 | } 147 | return -1, nil 148 | } 149 | -------------------------------------------------------------------------------- /src/cuckood/cucache/text/in.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "strconv" 7 | "strings" 8 | 9 | gomem "github.com/dustin/gomemcached" 10 | ) 11 | 12 | func ToMCRequest(cmd string, in io.Reader) (reqs []gomem.MCRequest, err error) { 13 | args := strings.Fields(strings.TrimSpace(cmd)) 14 | cmd = args[0] 15 | args = args[1:] 16 | 17 | quiet := false 18 | isget := strings.HasPrefix(cmd, "get") 19 | if !isget && len(args) != 0 && args[len(args)-1] == "noreply" { 20 | quiet = true 21 | args = args[:len(args)-1] 22 | } 23 | 24 | if cmd == "get" || cmd == "gets" { 25 | reqs = make([]gomem.MCRequest, 0, len(args)) 26 | for _, k := range args[:len(args)-1] { 27 | // MUST have key. 28 | // MUST NOT have extras. 29 | // MUST NOT have value. 30 | reqs = append(reqs, gomem.MCRequest{ 31 | Opcode: gomem.GETKQ, 32 | Key: []byte(k), 33 | }) 34 | } 35 | reqs = append(reqs, gomem.MCRequest{ 36 | Opcode: gomem.GETK, 37 | Key: []byte(args[len(args)-1]), 38 | }) 39 | return 40 | } 41 | 42 | reqs = make([]gomem.MCRequest, 1) 43 | req := &reqs[0] 44 | req.Opcode = str2op(cmd, quiet) 45 | 46 | switch cmd { 47 | case "set", "cas", "add", "replace": 48 | // MUST have key. 49 | req.Key = []byte(args[0]) 50 | args = args[1:] 51 | // MUST have extras. 52 | // - 4 byte flags 53 | // - 4 byte expiration time 54 | // MUST have value. 55 | 56 | nargs := 3 /* flags expiration bytes */ 57 | if cmd == "cas" { 58 | nargs++ /* + cas id */ 59 | } 60 | 61 | err = check_args(args, nargs) 62 | if err != nil { 63 | return 64 | } 65 | 66 | if cmd == "cas" { 67 | req.Cas, err = strconv.ParseUint(args[len(args)-1], 10, 64) 68 | if err != nil { 69 | return 70 | } 71 | args = args[:len(args)-1] 72 | } 73 | 74 | var flags uint32 75 | var exp uint32 76 | var val []byte 77 | flags, exp, val, err = setargs(args, in) 78 | if err != nil { 79 | return 80 | } 81 | 82 | req.Body = val 83 | req.Extras = make([]byte, 8) 84 | binary.BigEndian.PutUint32(req.Extras[0:4], flags) 85 | binary.BigEndian.PutUint32(req.Extras[4:8], exp) 86 | case "delete": 87 | // MUST have key. 88 | req.Key = []byte(args[0]) 89 | args = args[1:] 90 | // MUST NOT have extras. 91 | // MUST NOT have value. 92 | case "incr", "decr": 93 | // MUST have key. 94 | req.Key = []byte(args[0]) 95 | args = args[1:] 96 | // MUST have extras. 97 | // - 8 byte value to add / subtract 98 | // - 8 byte initial value (unsigned) 99 | // - 4 byte expiration time 100 | // MUST NOT have value. 101 | // 102 | // NOTE: binary protocol allows setting default and expiry for 103 | // incr/decr, but text protocol does not. We therefore set them 104 | // to 0 here to be correct. 105 | 106 | err = check_args(args, 1) /* amount */ 107 | if err != nil { 108 | return 109 | } 110 | 111 | var by uint64 112 | by, err = strconv.ParseUint(args[0], 10, 64) 113 | if err != nil { 114 | return 115 | } 116 | 117 | req.Extras = make([]byte, 8+8+4) 118 | binary.BigEndian.PutUint64(req.Extras[0:8], by) 119 | binary.BigEndian.PutUint64(req.Extras[8:16], 0) 120 | 121 | /* 122 | * the item must already exist for incr/decr to work; these 123 | * commands won't pretend that a non-existent key exists with 124 | * value 0; instead, they will fail. 125 | */ 126 | binary.BigEndian.PutUint32(req.Extras[16:20], 0xffffffff) 127 | case "quit": 128 | // MUST NOT have extras. 129 | // MUST NOT have key. 130 | case "flush_all": 131 | // MAY have extras. 132 | // - 4 byte expiration time 133 | // MUST NOT have key. 134 | // MUST NOT have value. 135 | // 136 | // TODO: handle optional "now" argument 137 | case "noop": 138 | // MUST NOT have extras. 139 | // MUST NOT have key. 140 | // MUST NOT have value. 141 | case "version": 142 | // MUST NOT have extras. 143 | // MUST NOT have key. 144 | // MUST NOT have value. 145 | case "append", "prepend": 146 | // MUST have key. 147 | req.Key = []byte(args[0]) 148 | args = args[1:] 149 | // MUST NOT have extras. 150 | // MUST have value. 151 | 152 | err = check_args(args, 1) 153 | if err != nil { 154 | return 155 | } 156 | 157 | var val []byte 158 | _, _, val, err = setargs(args, in) 159 | if err != nil { 160 | return 161 | } 162 | 163 | req.Body = val 164 | // TODO: case "stat": 165 | } 166 | return 167 | } 168 | -------------------------------------------------------------------------------- /src/cuckood/map_test.go: -------------------------------------------------------------------------------- 1 | package cuckoo_test 2 | 3 | import ( 4 | "bytes" 5 | "cuckood" 6 | "encoding/binary" 7 | "fmt" 8 | "math/rand" 9 | "runtime" 10 | "strconv" 11 | "sync" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | var never = time.Time{} 17 | 18 | func TestSimple(t *testing.T) { 19 | c := cuckoo.New(0) 20 | c.Set([]byte("hello"), []byte("world"), 0, never) 21 | v, ok := c.Get([]byte("hello")) 22 | 23 | if !ok { 24 | t.Error("Get did not return successfully") 25 | } 26 | 27 | if string(v.Bytes) != "world" { 28 | t.Error("Get returned wrong string") 29 | } 30 | } 31 | 32 | func TestMany(t *testing.T) { 33 | c := cuckoo.New(1 << 10) 34 | 35 | for i := 0; i < 1<<9; i++ { 36 | j := uint64(rand.Int63()) 37 | b := make([]byte, 8) 38 | binary.BigEndian.PutUint64(b, j) 39 | c.Set([]byte(strconv.FormatUint(j, 10)), b, 0, never) 40 | v, ok := c.Get([]byte(strconv.FormatUint(j, 10))) 41 | if !ok { 42 | t.Error("Concurrent get failed for key", []byte(strconv.FormatUint(j, 10))) 43 | return 44 | } 45 | if !bytes.Equal(b, v.Bytes) { 46 | t.Error("Concurrent get did not return correct value") 47 | } 48 | } 49 | } 50 | 51 | func TestResize(t *testing.T) { 52 | c := cuckoo.New(1 << 9) 53 | 54 | for i := 0; i < 1<<10; i++ { 55 | j := uint64(rand.Int63()) 56 | b := make([]byte, 8) 57 | binary.BigEndian.PutUint64(b, j) 58 | c.Set([]byte(strconv.FormatUint(j, 10)), b, 0, never) 59 | v, ok := c.Get([]byte(strconv.FormatUint(j, 10))) 60 | if !ok { 61 | t.Error("Concurrent get failed for key", []byte(strconv.FormatUint(j, 10))) 62 | return 63 | } 64 | if !bytes.Equal(b, v.Bytes) { 65 | t.Error("Concurrent get did not return correct value") 66 | } 67 | } 68 | } 69 | 70 | func TestConcurrent(t *testing.T) { 71 | runtime.GOMAXPROCS(4) 72 | c := cuckoo.New(1 << 16) 73 | 74 | ech := make(chan bool) 75 | errs := 0 76 | go func() { 77 | for range ech { 78 | errs++ 79 | } 80 | }() 81 | 82 | var wg sync.WaitGroup 83 | ch := make(chan int) 84 | for i := 0; i < 1e3; i++ { 85 | wg.Add(1) 86 | go func(wid int) { 87 | defer wg.Done() 88 | for i := range ch { 89 | j := i 90 | b := make([]byte, 8) 91 | binary.BigEndian.PutUint64(b, uint64(j)) 92 | 93 | e := c.Set([]byte(strconv.Itoa(i)), b, 0, never) 94 | 95 | if e.T != cuckoo.STORED { 96 | ech <- true 97 | continue 98 | } 99 | 100 | v, ok := c.Get([]byte(strconv.Itoa(i))) 101 | 102 | if !ok { 103 | t.Error("Concurrent get failed") 104 | } 105 | if !bytes.Equal(b, v.Bytes) { 106 | t.Error("Concurrent get did not return correct value") 107 | } 108 | } 109 | }(i) 110 | } 111 | 112 | for i := 0; i < 1<<15; i++ { 113 | ch <- i 114 | 115 | if i%(1<<12) == 0 { 116 | fmt.Println(i) 117 | } 118 | } 119 | close(ch) 120 | wg.Wait() 121 | 122 | if errs != 0 { 123 | t.Error("observed", errs, "insert errors") 124 | } 125 | } 126 | 127 | func TestSameKey(t *testing.T) { 128 | runtime.GOMAXPROCS(4) 129 | c := cuckoo.New(1 << 10) 130 | 131 | get := func() { 132 | v, ok := c.Get([]byte("a")) 133 | if !ok { 134 | t.Error("key lost") 135 | } 136 | if len(v.Bytes) != 1 || (v.Bytes[0] != 0x1 && v.Bytes[0] != 0x2) { 137 | t.Error("value is not one of the inserted values") 138 | } 139 | } 140 | 141 | var wg sync.WaitGroup 142 | wg.Add(1) 143 | go func() { 144 | defer wg.Done() 145 | b := []byte{0x1} 146 | for i := 0; i < 1e5; i++ { 147 | c.Set([]byte("a"), b, 0, never) 148 | get() 149 | } 150 | }() 151 | wg.Add(1) 152 | go func() { 153 | defer wg.Done() 154 | b := []byte{0x2} 155 | for i := 0; i < 1e5; i++ { 156 | c.Set([]byte("a"), b, 0, never) 157 | get() 158 | } 159 | }() 160 | wg.Wait() 161 | } 162 | 163 | func TestNoEvict(t *testing.T) { 164 | c := cuckoo.New(uint64(cuckoo.ASSOCIATIVITY)) 165 | 166 | for i := 0; i < cuckoo.ASSOCIATIVITY; i++ { 167 | res := c.Add(append([]byte("hello"), byte(i)), []byte("world"), 0, never) 168 | if res.T != cuckoo.STORED { 169 | t.Error("could not insert element", res) 170 | return 171 | } 172 | } 173 | 174 | // table should now be full 175 | 176 | res := c.Add(append([]byte("hello"), byte(cuckoo.ASSOCIATIVITY+1)), []byte("world"), 0, never) 177 | if res.T != cuckoo.STORED { 178 | t.Error("table did not make room for new item") 179 | return 180 | } 181 | 182 | out := 0 183 | for i := 0; i < cuckoo.ASSOCIATIVITY; i++ { 184 | _, ok := c.Get(append([]byte("hello"), byte(i))) 185 | if !ok { 186 | out++ 187 | } 188 | } 189 | 190 | if out != 0 { 191 | t.Error(out, "items were evicted, when eviction is disabled") 192 | } 193 | } 194 | 195 | func TestEvict(t *testing.T) { 196 | c := cuckoo.New(uint64(cuckoo.ASSOCIATIVITY)) 197 | 198 | for i := 0; i < cuckoo.ASSOCIATIVITY; i++ { 199 | res := c.Add(append([]byte("hello"), byte(i)), []byte("world"), 0, never) 200 | if res.T != cuckoo.STORED { 201 | t.Error("could not insert element", res) 202 | return 203 | } 204 | } 205 | 206 | // table should now be full 207 | c.EnableEviction() 208 | 209 | res := c.Add(append([]byte("hello"), byte(cuckoo.ASSOCIATIVITY+1)), []byte("world"), 0, never) 210 | if res.T != cuckoo.STORED { 211 | t.Error("table did not evict to make room for new item") 212 | return 213 | } 214 | 215 | if c.Capacity() != uint64(cuckoo.ASSOCIATIVITY) { 216 | t.Error("table was resized when eviction was possible") 217 | return 218 | } 219 | 220 | out := 0 221 | for i := 0; i < cuckoo.ASSOCIATIVITY; i++ { 222 | _, ok := c.Get(append([]byte("hello"), byte(i))) 223 | if !ok { 224 | out++ 225 | } 226 | } 227 | 228 | if out != 1 { 229 | t.Error(out, "items were evicted, when only one should have been") 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/cuckood/cucache/execute.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cuckood" 5 | "encoding/binary" 6 | "fmt" 7 | "math" 8 | "strconv" 9 | "time" 10 | 11 | gomem "github.com/dustin/gomemcached" 12 | ) 13 | 14 | var errorValues = map[gomem.Status][]byte{ 15 | gomem.KEY_ENOENT: []byte("Not found"), 16 | gomem.KEY_EEXISTS: []byte("Data exists for key."), 17 | gomem.NOT_STORED: []byte("Not stored."), 18 | gomem.ENOMEM: []byte("Out of memory"), 19 | gomem.UNKNOWN_COMMAND: []byte("Unknown command"), 20 | gomem.EINVAL: []byte("Invalid arguments"), 21 | gomem.E2BIG: []byte("Too large."), 22 | gomem.DELTA_BADVAL: []byte("Non-numeric server-side value for incr or decr"), 23 | } 24 | 25 | func tm(i uint32) (t time.Time) { 26 | if i == 0 { 27 | return 28 | } 29 | 30 | if i < 60*60*24*30 { 31 | t = time.Now().Add(time.Duration(i) * time.Second) 32 | } else { 33 | t = time.Unix(int64(i), 0) 34 | } 35 | return 36 | } 37 | 38 | func req2res(c *cuckoo.Cuckoo, req *gomem.MCRequest) (res *gomem.MCResponse) { 39 | res = resP.Get().(*gomem.MCResponse) 40 | res.Cas = 0 41 | res.Key = nil 42 | res.Body = nil 43 | res.Extras = nil 44 | res.Fatal = false 45 | res.Status = 0 46 | 47 | res.Opaque = req.Opaque 48 | res.Opcode = req.Opcode 49 | 50 | switch req.Opcode { 51 | case gomem.GET, gomem.GETQ, gomem.GETK, gomem.GETKQ: 52 | res.Status = gomem.KEY_ENOENT 53 | v, ok := c.Get(req.Key) 54 | if ok { 55 | res.Status = gomem.SUCCESS 56 | res.Extras = make([]byte, 4) 57 | binary.BigEndian.PutUint32(res.Extras, v.Flags) 58 | res.Cas = v.Casid 59 | res.Body = v.Bytes 60 | 61 | if req.Opcode == gomem.GETK || req.Opcode == gomem.GETKQ { 62 | res.Key = req.Key 63 | } 64 | } 65 | case gomem.SET, gomem.SETQ, 66 | gomem.ADD, gomem.ADDQ, 67 | gomem.REPLACE, gomem.REPLACEQ: 68 | 69 | if len(req.Extras) != 8 { 70 | res.Status = gomem.EINVAL 71 | break 72 | } 73 | 74 | flags := binary.BigEndian.Uint32(req.Extras[0:4]) 75 | expiry := tm(binary.BigEndian.Uint32(req.Extras[4:8])) 76 | var v cuckoo.MemopRes 77 | switch req.Opcode { 78 | case gomem.SET, gomem.SETQ: 79 | if req.Cas == 0 { 80 | v = c.Set(req.Key, req.Body, flags, expiry) 81 | } else { 82 | v = c.CAS(req.Key, req.Body, flags, expiry, req.Cas) 83 | } 84 | case gomem.ADD, gomem.ADDQ: 85 | v = c.Add(req.Key, req.Body, flags, expiry) 86 | case gomem.REPLACE, gomem.REPLACEQ: 87 | if req.Cas == 0 { 88 | v = c.Replace(req.Key, req.Body, flags, expiry) 89 | } else { 90 | v = c.CAS(req.Key, req.Body, flags, expiry, req.Cas) 91 | } 92 | } 93 | 94 | switch v.T { 95 | case cuckoo.STORED: 96 | res.Status = gomem.SUCCESS 97 | res.Cas = v.M.Casid 98 | case cuckoo.NOT_STORED: 99 | res.Status = gomem.NOT_STORED 100 | case cuckoo.NOT_FOUND: 101 | res.Status = gomem.KEY_ENOENT 102 | case cuckoo.EXISTS: 103 | res.Status = gomem.KEY_EEXISTS 104 | case cuckoo.SERVER_ERROR: 105 | res.Status = gomem.ENOMEM 106 | fmt.Println(v.E) 107 | default: 108 | wtf(req, v) 109 | } 110 | case gomem.DELETE, gomem.DELETEQ: 111 | v := c.Delete(req.Key, req.Cas) 112 | 113 | switch v.T { 114 | case cuckoo.STORED: 115 | res.Status = gomem.SUCCESS 116 | case cuckoo.NOT_FOUND: 117 | res.Status = gomem.KEY_ENOENT 118 | case cuckoo.EXISTS: 119 | res.Status = gomem.KEY_EEXISTS 120 | default: 121 | wtf(req, v) 122 | } 123 | case gomem.INCREMENT, gomem.INCREMENTQ, 124 | gomem.DECREMENT, gomem.DECREMENTQ: 125 | 126 | if len(req.Extras) != 20 { 127 | res.Status = gomem.EINVAL 128 | break 129 | } 130 | 131 | by := binary.BigEndian.Uint64(req.Extras[0:8]) 132 | def := binary.BigEndian.Uint64(req.Extras[8:16]) 133 | exp := tm(binary.BigEndian.Uint32(req.Extras[16:20])) 134 | 135 | if binary.BigEndian.Uint32(req.Extras[16:20]) == 0xffffffff { 136 | exp = time.Unix(math.MaxInt64, 0) 137 | } 138 | 139 | var v cuckoo.MemopRes 140 | if req.Opcode == gomem.INCREMENT || req.Opcode == gomem.INCREMENTQ { 141 | v = c.Incr(req.Key, by, def, exp) 142 | } else { 143 | v = c.Decr(req.Key, by, def, exp) 144 | } 145 | 146 | switch v.T { 147 | case cuckoo.STORED: 148 | res.Status = gomem.SUCCESS 149 | res.Cas = v.M.Casid 150 | newVal, _ := strconv.ParseUint(string(v.M.Bytes), 10, 64) 151 | res.Body = make([]byte, 8) 152 | binary.BigEndian.PutUint64(res.Body, newVal) 153 | case cuckoo.CLIENT_ERROR: 154 | res.Status = gomem.DELTA_BADVAL 155 | case cuckoo.NOT_FOUND: 156 | res.Status = gomem.KEY_ENOENT 157 | default: 158 | wtf(req, v) 159 | } 160 | case gomem.QUIT, gomem.QUITQ: 161 | return 162 | case gomem.FLUSH, gomem.FLUSHQ: 163 | if len(req.Extras) != 4 { 164 | res.Status = gomem.EINVAL 165 | break 166 | } 167 | 168 | at := tm(binary.BigEndian.Uint32(req.Extras[0:4])) 169 | if at.IsZero() { 170 | at = time.Now() 171 | } 172 | c.TouchAll(at) 173 | res.Status = gomem.SUCCESS 174 | case gomem.NOOP: 175 | res.Status = gomem.SUCCESS 176 | case gomem.VERSION: 177 | res.Status = gomem.SUCCESS 178 | // TODO 179 | res.Body = []byte("0.0.1") 180 | case gomem.APPEND, gomem.APPENDQ, 181 | gomem.PREPEND, gomem.PREPENDQ: 182 | 183 | var v cuckoo.MemopRes 184 | switch req.Opcode { 185 | case gomem.APPEND, gomem.APPENDQ: 186 | v = c.Append(req.Key, req.Body, req.Cas) 187 | case gomem.PREPEND, gomem.PREPENDQ: 188 | v = c.Prepend(req.Key, req.Body, req.Cas) 189 | } 190 | 191 | switch v.T { 192 | case cuckoo.STORED: 193 | res.Status = gomem.SUCCESS 194 | case cuckoo.EXISTS: 195 | res.Status = gomem.KEY_EEXISTS 196 | case cuckoo.NOT_FOUND: 197 | res.Status = gomem.KEY_ENOENT 198 | default: 199 | wtf(req, v) 200 | } 201 | default: 202 | res.Status = gomem.UNKNOWN_COMMAND 203 | } 204 | 205 | if b, ok := errorValues[res.Status]; ok { 206 | res.Body = b 207 | } 208 | 209 | return 210 | } 211 | -------------------------------------------------------------------------------- /src/cuckood/cucache/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "cuckood" 7 | "cuckood/cucache/text" 8 | "flag" 9 | "fmt" 10 | "io" 11 | "net" 12 | "os" 13 | "os/signal" 14 | "runtime/pprof" 15 | "strconv" 16 | "sync" 17 | "syscall" 18 | 19 | gomem "github.com/dustin/gomemcached" 20 | ) 21 | 22 | var reqP sync.Pool 23 | var resP sync.Pool 24 | 25 | func init() { 26 | reqP.New = func() interface{} { 27 | return new(gomem.MCRequest) 28 | } 29 | resP.New = func() interface{} { 30 | return new(gomem.MCResponse) 31 | } 32 | } 33 | 34 | var not_found []byte 35 | 36 | func main() { 37 | cpuprofile := flag.String("cpuprofile", "", "CPU profile output file") 38 | port := flag.Int("p", 11211, "TCP port to listen on") 39 | udpport := flag.Int("U", 11211, "UDP port to listen on") 40 | flag.Parse() 41 | 42 | c := cuckoo.New(1e5) 43 | 44 | var pf *os.File 45 | sigs := make(chan os.Signal, 1) 46 | signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGABRT) 47 | go func() { 48 | for range sigs { 49 | if pf != nil { 50 | pprof.StopCPUProfile() 51 | err := pf.Close() 52 | if err != nil { 53 | fmt.Println("could not end cpu profile:", err) 54 | } 55 | } 56 | os.Exit(0) 57 | } 58 | }() 59 | 60 | var err error 61 | if cpuprofile != nil && *cpuprofile != "" { 62 | fmt.Println("starting CPU profiling") 63 | pf, err = os.Create(*cpuprofile) 64 | if err != nil { 65 | fmt.Printf("could not create CPU profile file %v: %v\n", *cpuprofile, err) 66 | return 67 | } 68 | err = pprof.StartCPUProfile(pf) 69 | if err != nil { 70 | fmt.Printf("could not start CPU profiling: %v\n", err) 71 | return 72 | } 73 | } 74 | 75 | not_found = req2res(c, &gomem.MCRequest{ 76 | Opcode: gomem.GET, 77 | Key: []byte("there is no key"), 78 | }).Bytes() 79 | 80 | var wg sync.WaitGroup 81 | wg.Add(1) 82 | go func() { 83 | defer wg.Done() 84 | ln, err := net.Listen("tcp", ":"+strconv.Itoa(*port)) 85 | if err != nil { 86 | panic(err) 87 | } 88 | for { 89 | conn, err := ln.Accept() 90 | if err != nil { 91 | fmt.Println(err) 92 | continue 93 | } 94 | go handleConnection(c, conn) 95 | } 96 | }() 97 | wg.Add(1) 98 | go func() { 99 | defer wg.Done() 100 | ln, err := net.ListenPacket("udp", ":"+strconv.Itoa(*udpport)) 101 | if err != nil { 102 | panic(err) 103 | } 104 | for { 105 | b := make([]byte, 0, 10240) 106 | _, addr, err := ln.ReadFrom(b) 107 | if err != nil { 108 | fmt.Println(err) 109 | continue 110 | } 111 | go replyTo(c, b, addr.(*net.UDPAddr)) 112 | } 113 | }() 114 | wg.Wait() 115 | } 116 | 117 | func wtf(req *gomem.MCRequest, v cuckoo.MemopRes) { 118 | panic(fmt.Sprintf("unexpected result when handling %v: %v\n", req.Opcode, v)) 119 | } 120 | 121 | func execute(c *cuckoo.Cuckoo, in <-chan *gomem.MCRequest, out chan<- *gomem.MCResponse) { 122 | mx := new(sync.Mutex) 123 | 124 | for req := range in { 125 | res := req2res(c, req) 126 | if req.Opcode.IsQuiet() && res.Status == gomem.SUCCESS { 127 | if req.Opcode == gomem.GETQ || req.Opcode == gomem.GETKQ { 128 | // simply don't flush 129 | } else { 130 | continue 131 | } 132 | } 133 | 134 | if (req.Opcode == gomem.GETQ || req.Opcode == gomem.GETKQ) && res.Status == gomem.KEY_ENOENT { 135 | // no warning on cache miss 136 | continue 137 | } 138 | 139 | if res.Status != gomem.SUCCESS { 140 | if !(res.Status == gomem.KEY_ENOENT && (req.Opcode == gomem.GET || req.Opcode == gomem.GETK)) { 141 | fmt.Println(req.Opcode, res.Status) 142 | } 143 | } 144 | 145 | reqP.Put(req) 146 | mx.Lock() 147 | go func() { 148 | out <- res 149 | mx.Unlock() 150 | }() 151 | } 152 | close(out) 153 | } 154 | 155 | func writeback(in <-chan *gomem.MCResponse, out_ io.Writer) { 156 | out := bufio.NewWriter(out_) 157 | mx := new(sync.Mutex) 158 | 159 | for res := range in { 160 | if res.Opaque != 0xffffffff { 161 | // binary protocol 162 | var b []byte 163 | quiet := res.Opcode.IsQuiet() 164 | if res.Opcode == gomem.GET && res.Opaque == 0 && res.Status == gomem.KEY_ENOENT { 165 | b = not_found 166 | } else { 167 | b = res.Bytes() 168 | } 169 | resP.Put(res) 170 | 171 | mx.Lock() 172 | out.Write(b) 173 | 174 | // "The getq command is both mum on cache miss and quiet, 175 | // holding its response until a non-quiet command is issued." 176 | if !quiet { 177 | // This allows us to do Bytes() and Flush() in 178 | // parallel 179 | go func() { 180 | out.Flush() 181 | mx.Unlock() 182 | }() 183 | } else { 184 | mx.Unlock() 185 | } 186 | continue 187 | } 188 | 189 | // we've got a text protocol client 190 | err := text.WriteMCResponse(res, out) 191 | _ = err // TODO 192 | resP.Put(res) 193 | } 194 | } 195 | 196 | func parse(in_ io.Reader, out chan<- *gomem.MCRequest) { 197 | in := bufio.NewReader(in_) 198 | buf := make([]byte, gomem.HDR_LEN) 199 | 200 | for { 201 | b, err := in.Peek(1) 202 | if err != nil { 203 | if err == io.EOF { 204 | return 205 | } 206 | // TODO print error 207 | return 208 | } 209 | 210 | if b[0] == gomem.REQ_MAGIC { 211 | req := reqP.Get().(*gomem.MCRequest) 212 | req.Cas = 0 213 | req.Key = nil 214 | req.Body = nil 215 | req.Extras = nil 216 | req.Opcode = 0 217 | req.Opaque = 0 218 | _, err := req.Receive(in, buf) 219 | if err != nil { 220 | if err == io.EOF { 221 | reqP.Put(req) 222 | return 223 | } 224 | // TODO: print error 225 | continue 226 | } 227 | out <- req 228 | } else { 229 | // text protocol fallback 230 | cmd, err := in.ReadString('\n') 231 | if err != nil { 232 | if err == io.EOF { 233 | return 234 | } 235 | // TODO: print error 236 | return 237 | } 238 | 239 | reqs, err := text.ToMCRequest(cmd, in) 240 | if err != nil { 241 | // TODO: print error 242 | return 243 | } 244 | for _, req := range reqs { 245 | req.Opaque = 0xffffffff 246 | out <- &req 247 | } 248 | } 249 | } 250 | close(out) 251 | } 252 | 253 | func setup(c *cuckoo.Cuckoo, in io.Reader, out io.Writer) { 254 | dispatch := make(chan *gomem.MCRequest, 50) 255 | bridge := make(chan *gomem.MCResponse, 50) 256 | go execute(c, dispatch, bridge) 257 | go writeback(bridge, out) 258 | parse(in, dispatch) 259 | } 260 | 261 | func replyTo(c *cuckoo.Cuckoo, in []byte, to *net.UDPAddr) { 262 | u, err := net.ListenPacket("udp", "127.0.0.1:0") 263 | if err != nil { 264 | fmt.Println(err) 265 | return 266 | } 267 | defer u.Close() 268 | 269 | var o bytes.Buffer 270 | setup(c, bytes.NewBuffer(in), &o) 271 | _, err = u.WriteTo(o.Bytes(), to) 272 | if err != nil { 273 | fmt.Println(err) 274 | } 275 | } 276 | 277 | func handleConnection(c *cuckoo.Cuckoo, conn net.Conn) { 278 | conn.(*net.TCPConn).SetNoDelay(true) 279 | setup(c, conn, conn) 280 | conn.Close() 281 | } 282 | -------------------------------------------------------------------------------- /src/cuckood/cucache/text/in_test.go: -------------------------------------------------------------------------------- 1 | package text_test 2 | 3 | import ( 4 | "bytes" 5 | "cuckood/cucache/text" 6 | "encoding/binary" 7 | "fmt" 8 | "testing" 9 | 10 | gomem "github.com/dustin/gomemcached" 11 | ) 12 | 13 | func hlp(t *testing.T, cmd string, in []byte, exp gomem.MCRequest) { 14 | hlps(t, cmd, in, []gomem.MCRequest{exp}) 15 | } 16 | 17 | func hlps(t *testing.T, cmd string, in []byte, exp []gomem.MCRequest) { 18 | var in_ bytes.Buffer 19 | in_.Write(in) 20 | 21 | reqs, err := text.ToMCRequest(cmd, &in_) 22 | if err != nil { 23 | t.Error(err) 24 | return 25 | } 26 | 27 | if len(exp) != len(reqs) { 28 | t.Errorf("expected %d request objects, got %d\n", len(exp), len(reqs)) 29 | } 30 | 31 | for i := range exp { 32 | if !bytes.Equal(reqs[i].Bytes(), exp[i].Bytes()) { 33 | t.Errorf("\n[%d] expected:\n%+v\ngot:\n%+v", i, &exp[i], reqs[i]) 34 | } 35 | } 36 | } 37 | 38 | func TestStorage(t *testing.T) { 39 | bits := []byte("value") 40 | extras := make([]byte, 8) 41 | binary.BigEndian.PutUint32(extras[0:4], 1) 42 | binary.BigEndian.PutUint32(extras[4:8], 2) 43 | 44 | hlp(t, fmt.Sprintf("set a 1 2 %d", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 45 | Opcode: gomem.SET, 46 | Cas: 0, 47 | Opaque: 0, 48 | Extras: extras, 49 | Key: []byte("a"), 50 | Body: bits, 51 | }) 52 | hlp(t, fmt.Sprintf("cas a 1 2 %d 3", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 53 | Opcode: gomem.SET, 54 | Cas: 3, 55 | Opaque: 0, 56 | Extras: extras, 57 | Key: []byte("a"), 58 | Body: bits, 59 | }) 60 | hlp(t, fmt.Sprintf("add a 1 2 %d", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 61 | Opcode: gomem.ADD, 62 | Cas: 0, 63 | Opaque: 0, 64 | Extras: extras, 65 | Key: []byte("a"), 66 | Body: bits, 67 | }) 68 | hlp(t, fmt.Sprintf("replace a 1 2 %d", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 69 | Opcode: gomem.REPLACE, 70 | Cas: 0, 71 | Opaque: 0, 72 | Extras: extras, 73 | Key: []byte("a"), 74 | Body: bits, 75 | }) 76 | hlp(t, fmt.Sprintf("append a %d", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 77 | Opcode: gomem.APPEND, 78 | Cas: 0, 79 | Opaque: 0, 80 | Extras: nil, 81 | Key: []byte("a"), 82 | Body: bits, 83 | }) 84 | hlp(t, fmt.Sprintf("prepend a %d", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 85 | Opcode: gomem.PREPEND, 86 | Cas: 0, 87 | Opaque: 0, 88 | Extras: nil, 89 | Key: []byte("a"), 90 | Body: bits, 91 | }) 92 | } 93 | 94 | func TestStorageQuiet(t *testing.T) { 95 | bits := []byte("value") 96 | extras := make([]byte, 8) 97 | binary.BigEndian.PutUint32(extras[0:4], 1) 98 | binary.BigEndian.PutUint32(extras[4:8], 2) 99 | 100 | hlp(t, fmt.Sprintf("set a 1 2 %d noreply", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 101 | Opcode: gomem.SETQ, 102 | Cas: 0, 103 | Opaque: 0, 104 | Extras: extras, 105 | Key: []byte("a"), 106 | Body: bits, 107 | }) 108 | hlp(t, fmt.Sprintf("cas a 1 2 %d 3 noreply", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 109 | Opcode: gomem.SETQ, 110 | Cas: 3, 111 | Opaque: 0, 112 | Extras: extras, 113 | Key: []byte("a"), 114 | Body: bits, 115 | }) 116 | hlp(t, fmt.Sprintf("add a 1 2 %d noreply", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 117 | Opcode: gomem.ADDQ, 118 | Cas: 0, 119 | Opaque: 0, 120 | Extras: extras, 121 | Key: []byte("a"), 122 | Body: bits, 123 | }) 124 | hlp(t, fmt.Sprintf("replace a 1 2 %d noreply", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 125 | Opcode: gomem.REPLACEQ, 126 | Cas: 0, 127 | Opaque: 0, 128 | Extras: extras, 129 | Key: []byte("a"), 130 | Body: bits, 131 | }) 132 | hlp(t, fmt.Sprintf("append a %d noreply", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 133 | Opcode: gomem.APPENDQ, 134 | Cas: 0, 135 | Opaque: 0, 136 | Extras: nil, 137 | Key: []byte("a"), 138 | Body: bits, 139 | }) 140 | hlp(t, fmt.Sprintf("prepend a %d noreply", len(bits)), []byte(string(bits)+"\r\n"), gomem.MCRequest{ 141 | Opcode: gomem.PREPENDQ, 142 | Cas: 0, 143 | Opaque: 0, 144 | Extras: nil, 145 | Key: []byte("a"), 146 | Body: bits, 147 | }) 148 | } 149 | 150 | func TestRetrieval(t *testing.T) { 151 | hlp(t, "get a", nil, gomem.MCRequest{ 152 | Opcode: gomem.GETK, 153 | Cas: 0, 154 | Opaque: 0, 155 | Extras: nil, 156 | Key: []byte("a"), 157 | Body: nil, 158 | }) 159 | hlp(t, "gets a", nil, gomem.MCRequest{ 160 | Opcode: gomem.GETK, 161 | Cas: 0, 162 | Opaque: 0, 163 | Extras: nil, 164 | Key: []byte("a"), 165 | Body: nil, 166 | }) 167 | } 168 | 169 | func TestMultiRetrieval(t *testing.T) { 170 | key_a := gomem.MCRequest{ 171 | Opcode: gomem.GETKQ, 172 | Cas: 0, 173 | Opaque: 0, 174 | Extras: nil, 175 | Key: []byte("a"), 176 | Body: nil, 177 | } 178 | 179 | key_b := key_a 180 | key_b.Opcode = gomem.GETK 181 | key_b.Key = []byte("b") 182 | 183 | hlps(t, "get a b", nil, []gomem.MCRequest{key_a, key_b}) 184 | hlps(t, "gets a b", nil, []gomem.MCRequest{key_a, key_b}) 185 | } 186 | 187 | func TestDeletion(t *testing.T) { 188 | hlp(t, "delete a", nil, gomem.MCRequest{ 189 | Opcode: gomem.DELETE, 190 | Cas: 0, 191 | Opaque: 0, 192 | Extras: nil, 193 | Key: []byte("a"), 194 | Body: nil, 195 | }) 196 | hlp(t, "delete a noreply", nil, gomem.MCRequest{ 197 | Opcode: gomem.DELETEQ, 198 | Cas: 0, 199 | Opaque: 0, 200 | Extras: nil, 201 | Key: []byte("a"), 202 | Body: nil, 203 | }) 204 | } 205 | 206 | func TestIncrementDecrement(t *testing.T) { 207 | extras := make([]byte, 20) 208 | binary.BigEndian.PutUint64(extras[0:8], 1) 209 | binary.BigEndian.PutUint64(extras[8:16], 0) 210 | binary.BigEndian.PutUint32(extras[16:20], 0xffffffff) 211 | 212 | hlp(t, "incr a 1", nil, gomem.MCRequest{ 213 | Opcode: gomem.INCREMENT, 214 | Cas: 0, 215 | Opaque: 0, 216 | Extras: extras, 217 | Key: []byte("a"), 218 | Body: nil, 219 | }) 220 | hlp(t, "decr a 1", nil, gomem.MCRequest{ 221 | Opcode: gomem.DECREMENT, 222 | Cas: 0, 223 | Opaque: 0, 224 | Extras: extras, 225 | Key: []byte("a"), 226 | Body: nil, 227 | }) 228 | } 229 | 230 | func TestIncrementDecrementQuiet(t *testing.T) { 231 | extras := make([]byte, 20) 232 | binary.BigEndian.PutUint64(extras[0:8], 1) 233 | binary.BigEndian.PutUint64(extras[8:16], 0) 234 | binary.BigEndian.PutUint32(extras[16:20], 0xffffffff) 235 | 236 | hlp(t, "incr a 1 noreply", nil, gomem.MCRequest{ 237 | Opcode: gomem.INCREMENTQ, 238 | Cas: 0, 239 | Opaque: 0, 240 | Extras: extras, 241 | Key: []byte("a"), 242 | Body: nil, 243 | }) 244 | hlp(t, "decr a 1 noreply", nil, gomem.MCRequest{ 245 | Opcode: gomem.DECREMENTQ, 246 | Cas: 0, 247 | Opaque: 0, 248 | Extras: extras, 249 | Key: []byte("a"), 250 | Body: nil, 251 | }) 252 | } 253 | 254 | // TODO: TestTouch 255 | -------------------------------------------------------------------------------- /src/cuckood/memcache.go: -------------------------------------------------------------------------------- 1 | package cuckoo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // Memval is a container for all data a Memcache client may wish to store for a 12 | // particular key. 13 | type Memval struct { 14 | Bytes []byte 15 | Flags uint32 16 | Casid uint64 17 | Expires time.Time 18 | } 19 | 20 | // MemopResType is a status code for the result of a map operation. 21 | type MemopResType int 22 | 23 | // MemopRes is a container for the result of a map operation. 24 | type MemopRes struct { 25 | T MemopResType 26 | M *Memval 27 | E error 28 | } 29 | 30 | const ( 31 | STORED MemopResType = iota 32 | NOT_STORED = iota 33 | EXISTS = iota 34 | NOT_FOUND = iota 35 | CLIENT_ERROR = iota 36 | SERVER_ERROR = -1 37 | ) 38 | 39 | func (t MemopResType) String() string { 40 | switch t { 41 | case STORED: 42 | return "STORED" 43 | case NOT_STORED: 44 | return "NOT_STORED" 45 | case EXISTS: 46 | return "EXISTS" 47 | case NOT_FOUND: 48 | return "NOT_FOUND" 49 | case CLIENT_ERROR: 50 | return "CLIENT_ERROR" 51 | case SERVER_ERROR: 52 | return "SERVER_ERROR" 53 | default: 54 | panic(fmt.Sprintf("unknown type %d\n", t)) 55 | } 56 | } 57 | 58 | // Memop is a map operation to be performed for some key. 59 | // The operation will be passed the current value (Memop{} if no value exists), 60 | // and a boolean flag indicating if the key already existed in the database. 61 | // The operation is executed within a lock for the given item. 62 | type Memop func(Memval, bool) (Memval, MemopRes) 63 | 64 | // fset returns a Memop that overwrites the current value for a key. 65 | func fset(bytes []byte, flags uint32, expires time.Time) Memop { 66 | return func(old Memval, _ bool) (m Memval, r MemopRes) { 67 | m = Memval{bytes, flags, old.Casid + 1, expires} 68 | r.T = STORED 69 | r.M = &m 70 | return 71 | } 72 | } 73 | 74 | // fadd returns a Memop that adds the current value for a non-existing key. 75 | func fadd(bytes []byte, flags uint32, expires time.Time) Memop { 76 | return func(old Memval, exists bool) (m Memval, r MemopRes) { 77 | r.T = EXISTS 78 | if !exists { 79 | m = Memval{bytes, flags, old.Casid + 1, expires} 80 | r.T = STORED 81 | r.M = &m 82 | } 83 | return 84 | } 85 | } 86 | 87 | // freplace returns a Memop that replaces the current value for an existing key. 88 | func freplace(bytes []byte, flags uint32, expires time.Time) Memop { 89 | return func(old Memval, exists bool) (m Memval, r MemopRes) { 90 | r.T = NOT_FOUND 91 | if exists { 92 | m = Memval{bytes, flags, old.Casid + 1, expires} 93 | r.T = STORED 94 | r.M = &m 95 | } 96 | return 97 | } 98 | } 99 | 100 | // fjoin returns a Memop that prepends or appends the given bytes to the value 101 | // of an existing key. if casid is non-zero, a cas check will be performed. 102 | func fjoin(bytes []byte, prepend bool, casid uint64) Memop { 103 | return func(old Memval, exists bool) (m Memval, r MemopRes) { 104 | r.T = NOT_FOUND 105 | if exists { 106 | r.T = EXISTS 107 | if casid == 0 || old.Casid == casid { 108 | nb := make([]byte, 0, len(old.Bytes)+len(bytes)) 109 | if prepend { 110 | nb = append(nb, bytes...) 111 | nb = append(nb, old.Bytes...) 112 | } else { 113 | nb = append(nb, old.Bytes...) 114 | nb = append(nb, bytes...) 115 | } 116 | m = Memval{nb, old.Flags, old.Casid + 1, old.Expires} 117 | r.T = STORED 118 | r.M = &m 119 | } 120 | } 121 | return 122 | } 123 | } 124 | 125 | // fappend returns a Memop that appends the given bytes to the value of an 126 | // existing key. if casid is non-zero, a cas check will be performed. 127 | func fappend(bytes []byte, casid uint64) Memop { 128 | return fjoin(bytes, false, casid) 129 | } 130 | 131 | // fprepend returns a Memop that prepends the given bytes to the value of an 132 | // existing key. if casid is non-zero, a cas check will be performed. 133 | func fprepend(bytes []byte, casid uint64) Memop { 134 | return fjoin(bytes, true, casid) 135 | } 136 | 137 | // fcas returns a Memop that overwrites the value of an existing key, assuming 138 | // no write has happened since a get returned the data tagged with casid. 139 | func fcas(bytes []byte, flags uint32, expires time.Time, casid uint64) Memop { 140 | return func(old Memval, exists bool) (m Memval, r MemopRes) { 141 | r.T = NOT_FOUND 142 | if exists { 143 | r.T = EXISTS 144 | if old.Casid == casid { 145 | m = Memval{bytes, flags, casid + 1, expires} 146 | r.T = STORED 147 | r.M = &m 148 | } 149 | } 150 | return 151 | } 152 | } 153 | 154 | // CasPMVal is used to hold both CAS and value for incr/decr operations 155 | type CasVal struct { 156 | Casid uint64 157 | NewVal uint64 158 | } 159 | 160 | // fpm returns a Memop that increments or decrements the value of an existing 161 | // key. it assumes the key's value is a 64-bit unsigned integer, and will fail 162 | // if the value is larger than 64 bits. overflow will wrap around. underflow is 163 | // set to 0. 164 | func fpm(by uint64, def uint64, expires time.Time, plus bool) Memop { 165 | return func(old Memval, exists bool) (m Memval, r MemopRes) { 166 | r.T = NOT_FOUND 167 | if exists { 168 | v, err := strconv.ParseUint(string(old.Bytes), 10, 64) 169 | if err != nil { 170 | r.T = CLIENT_ERROR 171 | r.E = errors.New("non-numeric value found for incr/decr key") 172 | return 173 | } 174 | 175 | if plus { 176 | v += by 177 | } else { 178 | if by > v { 179 | v = 0 180 | } else { 181 | v -= by 182 | } 183 | } 184 | m = Memval{[]byte(strconv.FormatUint(v, 10)), old.Flags, old.Casid + 1, old.Expires} 185 | r.T = STORED 186 | r.M = &m 187 | } else { 188 | // If the counter does not exist, one of two things may 189 | // happen: 190 | // 191 | // 1. If the expiration value is all one-bits 192 | // (0xffffffff), the operation will fail with 193 | // NOT_FOUND. 194 | // 2. For all other expiration values, the operation 195 | // will succeed by seeding the value for this key 196 | // with the provided initial value to expire with 197 | // the provided expiration time. The flags will be 198 | // set to zero. 199 | // 200 | if expires.Unix() != math.MaxInt64 { 201 | m.Bytes = []byte(strconv.FormatUint(def, 10)) 202 | m.Expires = expires 203 | m.Casid = 1 204 | m.Flags = 0 205 | 206 | r.T = STORED 207 | r.M = &m 208 | } 209 | } 210 | return 211 | } 212 | } 213 | 214 | // fincr returns a Memop that increments the value of an existing key. 215 | // the value is assumed to be a 64-bit unsigned integer. overflow wraps. 216 | func fincr(by uint64, def uint64, expires time.Time) Memop { 217 | return fpm(by, def, expires, true) 218 | } 219 | 220 | // fdecr returns a Memop that decrements the value of an existing key. 221 | // the value is assumed to be a 64-bit unsigned integer. underflow is set to 0. 222 | func fdecr(by uint64, def uint64, expires time.Time) Memop { 223 | return fpm(by, def, expires, false) 224 | } 225 | 226 | // ftouch returns a Memop that updates the expiration time of the given key. 227 | func ftouch(expires time.Time) Memop { 228 | return func(old Memval, exists bool) (m Memval, r MemopRes) { 229 | r.T = NOT_FOUND 230 | if exists { 231 | m = Memval{old.Bytes, old.Flags, old.Casid, expires} 232 | r.T = STORED 233 | } 234 | return 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/cuckood/cucache/text/out_test.go: -------------------------------------------------------------------------------- 1 | package text_test 2 | 3 | import ( 4 | "bytes" 5 | "cuckood/cucache/text" 6 | "encoding/binary" 7 | "fmt" 8 | "testing" 9 | 10 | gomem "github.com/dustin/gomemcached" 11 | ) 12 | 13 | func hlp_out(t *testing.T, res gomem.MCResponse, exp string) { 14 | var out bytes.Buffer 15 | err := text.WriteMCResponse(&res, &out) 16 | if err != nil { 17 | t.Error(err) 18 | return 19 | } 20 | 21 | if !bytes.Equal(out.Bytes(), []byte(exp)) { 22 | t.Errorf("\nexpected:\n%+v\ngot:\n%+v", exp, out.String()) 23 | } 24 | } 25 | 26 | func TestStorageOut(t *testing.T) { 27 | hlp_out(t, gomem.MCResponse{ 28 | Opcode: gomem.SET, 29 | Status: gomem.SUCCESS, 30 | Opaque: 0, 31 | Cas: 0, 32 | Extras: nil, 33 | Key: nil, 34 | Body: nil, 35 | Fatal: false, 36 | }, "STORED\r\n") 37 | hlp_out(t, gomem.MCResponse{ 38 | Opcode: gomem.SET, 39 | Status: gomem.NOT_STORED, 40 | Opaque: 0, 41 | Cas: 0, 42 | Extras: nil, 43 | Key: nil, 44 | Body: nil, 45 | Fatal: false, 46 | }, "NOT_STORED\r\n") 47 | hlp_out(t, gomem.MCResponse{ 48 | Opcode: gomem.SET, 49 | Status: gomem.KEY_EEXISTS, 50 | Opaque: 0, 51 | Cas: 0, 52 | Extras: nil, 53 | Key: nil, 54 | Body: nil, 55 | Fatal: false, 56 | }, "EXISTS\r\n") 57 | hlp_out(t, gomem.MCResponse{ 58 | Opcode: gomem.SET, 59 | Status: gomem.KEY_ENOENT, 60 | Opaque: 0, 61 | Cas: 0, 62 | Extras: nil, 63 | Key: nil, 64 | Body: nil, 65 | Fatal: false, 66 | }, "NOT_FOUND\r\n") 67 | } 68 | 69 | func TestStorageOutQuiet(t *testing.T) { 70 | hlp_out(t, gomem.MCResponse{ 71 | Opcode: gomem.SETQ, 72 | Status: gomem.SUCCESS, 73 | Opaque: 0, 74 | Cas: 0, 75 | Extras: nil, 76 | Key: nil, 77 | Body: nil, 78 | Fatal: false, 79 | }, "") 80 | hlp_out(t, gomem.MCResponse{ 81 | Opcode: gomem.ADDQ, 82 | Status: gomem.SUCCESS, 83 | Opaque: 0, 84 | Cas: 0, 85 | Extras: nil, 86 | Key: nil, 87 | Body: nil, 88 | Fatal: false, 89 | }, "") 90 | hlp_out(t, gomem.MCResponse{ 91 | Opcode: gomem.REPLACEQ, 92 | Status: gomem.SUCCESS, 93 | Opaque: 0, 94 | Cas: 0, 95 | Extras: nil, 96 | Key: nil, 97 | Body: nil, 98 | Fatal: false, 99 | }, "") 100 | hlp_out(t, gomem.MCResponse{ 101 | Opcode: gomem.APPENDQ, 102 | Status: gomem.SUCCESS, 103 | Opaque: 0, 104 | Cas: 0, 105 | Extras: nil, 106 | Key: nil, 107 | Body: nil, 108 | Fatal: false, 109 | }, "") 110 | hlp_out(t, gomem.MCResponse{ 111 | Opcode: gomem.PREPENDQ, 112 | Status: gomem.SUCCESS, 113 | Opaque: 0, 114 | Cas: 0, 115 | Extras: nil, 116 | Key: nil, 117 | Body: nil, 118 | Fatal: false, 119 | }, "") 120 | } 121 | 122 | func TestStorageOutQuietFail(t *testing.T) { 123 | hlp_out(t, gomem.MCResponse{ 124 | Opcode: gomem.SETQ, 125 | Status: gomem.NOT_STORED, 126 | Opaque: 0, 127 | Cas: 0, 128 | Extras: nil, 129 | Key: nil, 130 | Body: nil, 131 | Fatal: false, 132 | }, "NOT_STORED\r\n") 133 | hlp_out(t, gomem.MCResponse{ 134 | Opcode: gomem.SETQ, 135 | Status: gomem.KEY_EEXISTS, 136 | Opaque: 0, 137 | Cas: 0, 138 | Extras: nil, 139 | Key: nil, 140 | Body: nil, 141 | Fatal: false, 142 | }, "EXISTS\r\n") 143 | hlp_out(t, gomem.MCResponse{ 144 | Opcode: gomem.SETQ, 145 | Status: gomem.KEY_ENOENT, 146 | Opaque: 0, 147 | Cas: 0, 148 | Extras: nil, 149 | Key: nil, 150 | Body: nil, 151 | Fatal: false, 152 | }, "NOT_FOUND\r\n") 153 | } 154 | 155 | func TestRetrievalOut(t *testing.T) { 156 | data := []byte("hello") 157 | flag := make([]byte, 4) 158 | binary.BigEndian.PutUint32(flag[0:4], 2) 159 | 160 | hlp_out(t, gomem.MCResponse{ 161 | Opcode: gomem.GETK, 162 | Status: gomem.SUCCESS, 163 | Opaque: 0, 164 | Cas: 1, 165 | Extras: flag, 166 | Key: []byte("a"), 167 | Body: data, 168 | Fatal: false, 169 | }, fmt.Sprintf("VALUE a 2 %d 1\r\n%s\r\nEND\r\n", len(data), data)) 170 | hlp_out(t, gomem.MCResponse{ 171 | Opcode: gomem.GETK, 172 | Status: gomem.KEY_ENOENT, 173 | Opaque: 0, 174 | Cas: 0, 175 | Extras: flag, 176 | Key: nil, 177 | Body: nil, 178 | Fatal: false, 179 | }, "END\r\n") 180 | } 181 | 182 | func TestMultiRetrievalOut(t *testing.T) { 183 | data := []byte("hello") 184 | flag := make([]byte, 4) 185 | binary.BigEndian.PutUint32(flag[0:4], 2) 186 | 187 | hlp_out(t, gomem.MCResponse{ 188 | Opcode: gomem.GETK, 189 | Status: gomem.SUCCESS, 190 | Opaque: 0, 191 | Cas: 1, 192 | Extras: flag, 193 | Key: []byte("a"), 194 | Body: data, 195 | Fatal: false, 196 | }, fmt.Sprintf("VALUE a 2 %d 1\r\n%s\r\nEND\r\n", len(data), data)) 197 | hlp_out(t, gomem.MCResponse{ 198 | Opcode: gomem.GETK, 199 | Status: gomem.KEY_ENOENT, 200 | Opaque: 0, 201 | Cas: 0, 202 | Extras: flag, 203 | Key: nil, 204 | Body: nil, 205 | Fatal: false, 206 | }, "END\r\n") 207 | 208 | hlp_out(t, gomem.MCResponse{ 209 | Opcode: gomem.GETKQ, 210 | Status: gomem.SUCCESS, 211 | Opaque: 0, 212 | Cas: 1, 213 | Extras: flag, 214 | Key: []byte("a"), 215 | Body: data, 216 | Fatal: false, 217 | }, fmt.Sprintf("VALUE a 2 %d 1\r\n%s\r\n", len(data), data)) 218 | hlp_out(t, gomem.MCResponse{ 219 | Opcode: gomem.GETKQ, 220 | Status: gomem.KEY_ENOENT, 221 | Opaque: 0, 222 | Cas: 0, 223 | Extras: flag, 224 | Key: nil, 225 | Body: nil, 226 | Fatal: false, 227 | }, "") 228 | } 229 | 230 | func TestDeletionOut(t *testing.T) { 231 | hlp_out(t, gomem.MCResponse{ 232 | Opcode: gomem.DELETE, 233 | Status: gomem.SUCCESS, 234 | Opaque: 0, 235 | Cas: 0, 236 | Extras: nil, 237 | Key: nil, 238 | Body: nil, 239 | Fatal: false, 240 | }, "DELETED\r\n") 241 | hlp_out(t, gomem.MCResponse{ 242 | Opcode: gomem.DELETEQ, 243 | Status: gomem.SUCCESS, 244 | Opaque: 0, 245 | Cas: 0, 246 | Extras: nil, 247 | Key: nil, 248 | Body: nil, 249 | Fatal: false, 250 | }, "") 251 | hlp_out(t, gomem.MCResponse{ 252 | Opcode: gomem.DELETE, 253 | Status: gomem.KEY_ENOENT, 254 | Opaque: 0, 255 | Cas: 0, 256 | Extras: nil, 257 | Key: nil, 258 | Body: nil, 259 | Fatal: false, 260 | }, "NOT_FOUND\r\n") 261 | hlp_out(t, gomem.MCResponse{ 262 | Opcode: gomem.DELETEQ, 263 | Status: gomem.KEY_ENOENT, 264 | Opaque: 0, 265 | Cas: 0, 266 | Extras: nil, 267 | Key: nil, 268 | Body: nil, 269 | Fatal: false, 270 | }, "NOT_FOUND\r\n") 271 | } 272 | 273 | func TestIncrementDecrementOut(t *testing.T) { 274 | newv := make([]byte, 8) 275 | binary.BigEndian.PutUint64(newv, 1) 276 | hlp_out(t, gomem.MCResponse{ 277 | Opcode: gomem.INCREMENT, 278 | Status: gomem.SUCCESS, 279 | Opaque: 0, 280 | Cas: 0, 281 | Extras: nil, 282 | Key: nil, 283 | Body: newv, 284 | Fatal: false, 285 | }, "1\r\n") 286 | hlp_out(t, gomem.MCResponse{ 287 | Opcode: gomem.INCREMENTQ, 288 | Status: gomem.SUCCESS, 289 | Opaque: 0, 290 | Cas: 0, 291 | Extras: nil, 292 | Key: nil, 293 | Body: newv, 294 | Fatal: false, 295 | }, "") 296 | hlp_out(t, gomem.MCResponse{ 297 | Opcode: gomem.DECREMENT, 298 | Status: gomem.SUCCESS, 299 | Opaque: 0, 300 | Cas: 0, 301 | Extras: nil, 302 | Key: nil, 303 | Body: newv, 304 | Fatal: false, 305 | }, "1\r\n") 306 | hlp_out(t, gomem.MCResponse{ 307 | Opcode: gomem.DECREMENTQ, 308 | Status: gomem.SUCCESS, 309 | Opaque: 0, 310 | Cas: 0, 311 | Extras: nil, 312 | Key: nil, 313 | Body: newv, 314 | Fatal: false, 315 | }, "") 316 | hlp_out(t, gomem.MCResponse{ 317 | Opcode: gomem.INCREMENT, 318 | Status: gomem.KEY_ENOENT, 319 | Opaque: 0, 320 | Cas: 0, 321 | Extras: nil, 322 | Key: nil, 323 | Body: newv, 324 | Fatal: false, 325 | }, "NOT_FOUND\r\n") 326 | hlp_out(t, gomem.MCResponse{ 327 | Opcode: gomem.INCREMENTQ, 328 | Status: gomem.KEY_ENOENT, 329 | Opaque: 0, 330 | Cas: 0, 331 | Extras: nil, 332 | Key: nil, 333 | Body: newv, 334 | Fatal: false, 335 | }, "NOT_FOUND\r\n") 336 | hlp_out(t, gomem.MCResponse{ 337 | Opcode: gomem.DECREMENTQ, 338 | Status: gomem.KEY_ENOENT, 339 | Opaque: 0, 340 | Cas: 0, 341 | Extras: nil, 342 | Key: nil, 343 | Body: newv, 344 | Fatal: false, 345 | }, "NOT_FOUND\r\n") 346 | hlp_out(t, gomem.MCResponse{ 347 | Opcode: gomem.DECREMENT, 348 | Status: gomem.KEY_ENOENT, 349 | Opaque: 0, 350 | Cas: 0, 351 | Extras: nil, 352 | Key: nil, 353 | Body: newv, 354 | Fatal: false, 355 | }, "NOT_FOUND\r\n") 356 | } 357 | -------------------------------------------------------------------------------- /src/cuckood/external.go: -------------------------------------------------------------------------------- 1 | // package cuckoo provides a Memcache-like interface to a concurrent, in-memory 2 | // Cuckoo hash map. 3 | package cuckoo 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | "unsafe" 12 | ) 13 | 14 | // MAX_HASHES indicates the maximum number of Cuckoo hashes permitted. 15 | // A higher number will increase the capacity of the map, but will slow down 16 | // operations. 17 | const MAX_HASHES = 10 18 | 19 | // EVICTION_THRESHOLD sets a threshold for how many items must be evicted in 20 | // the space of one second for a table resize to be performed. 21 | const EVICTION_THRESHOLD = 1 22 | 23 | // Cuckoo is an externally visible wrapper to a Cuckoo map implementation 24 | type Cuckoo struct { 25 | resize sync.RWMutex 26 | cmap unsafe.Pointer 27 | size uint64 28 | tick *time.Ticker 29 | done chan struct{} 30 | } 31 | 32 | // New produces a new Cuckoo map. esize will be rounded to the next power of 33 | // two. By default, 2 hashes are used. If esize is 0, the map will be 34 | // initialized to hold 8192 elements. The map will automatically increase the 35 | // number of hashes as the map fills to avoid spilling items. 36 | func New(esize uint64) (c *Cuckoo) { 37 | if esize == 0 { 38 | esize = 1 << 16 39 | } 40 | 41 | c = &Cuckoo{ 42 | sync.RWMutex{}, 43 | unsafe.Pointer(create(esize)), 44 | esize, 45 | time.NewTicker(1 * time.Second), 46 | make(chan struct{}), 47 | } 48 | go c.fixer() 49 | return 50 | } 51 | 52 | func (c *Cuckoo) Capacity() uint64 { 53 | return atomic.LoadUint64(&c.size) 54 | } 55 | 56 | func (c *Cuckoo) Close() { 57 | close(c.done) 58 | 59 | c.resize.RLock() 60 | 61 | m := c.get() 62 | if m.requestEvict != nil { 63 | close(m.requestEvict) 64 | } 65 | atomic.StorePointer(&c.cmap, nil) 66 | atomic.StoreUint64(&c.size, 0) 67 | 68 | c.resize.RUnlock() 69 | } 70 | 71 | func (c *Cuckoo) EnableEviction() { 72 | c.resize.RLock() 73 | defer c.resize.RUnlock() 74 | c.get().enableEviction() 75 | } 76 | 77 | func (c Cuckoo) get() (m *cmap) { 78 | m = (*cmap)(atomic.LoadPointer(&c.cmap)) 79 | if m == nil { 80 | panic("tried to use closed map") 81 | } 82 | return 83 | } 84 | 85 | // Set overwites the given key 86 | func (c *Cuckoo) Set(key []byte, bytes []byte, flags uint32, expires time.Time) MemopRes { 87 | return c.op(key, fset(bytes, flags, expires)) 88 | } 89 | 90 | // Add adds a non-existing key 91 | func (c *Cuckoo) Add(key []byte, bytes []byte, flags uint32, expires time.Time) MemopRes { 92 | return c.op(key, fadd(bytes, flags, expires)) 93 | } 94 | 95 | // Replace replaces an existing key 96 | func (c *Cuckoo) Replace(key []byte, bytes []byte, flags uint32, expires time.Time) MemopRes { 97 | return c.op(key, freplace(bytes, flags, expires)) 98 | } 99 | 100 | // Append adds to an existing key 101 | func (c *Cuckoo) Append(key []byte, bytes []byte, casid uint64) MemopRes { 102 | return c.op(key, fappend(bytes, casid)) 103 | } 104 | 105 | // Prepend adds to the beginning of an existing key 106 | func (c *Cuckoo) Prepend(key []byte, bytes []byte, casid uint64) MemopRes { 107 | return c.op(key, fprepend(bytes, casid)) 108 | } 109 | 110 | // CAS overwrites the value for a key if it has not changed 111 | func (c *Cuckoo) CAS(key []byte, bytes []byte, flags uint32, expires time.Time, casid uint64) MemopRes { 112 | return c.op(key, fcas(bytes, flags, expires, casid)) 113 | } 114 | 115 | // Incr increments the value for an existing key 116 | func (c *Cuckoo) Incr(key []byte, by uint64, def uint64, expires time.Time) MemopRes { 117 | return c.op(key, fincr(by, def, expires)) 118 | } 119 | 120 | // Decr decrements the value for an existing key 121 | func (c *Cuckoo) Decr(key []byte, by uint64, def uint64, expires time.Time) MemopRes { 122 | return c.op(key, fdecr(by, def, expires)) 123 | } 124 | 125 | // Touch updates the expiration time for an existing key 126 | func (c *Cuckoo) Touch(key []byte, expires time.Time) MemopRes { 127 | return c.op(key, ftouch(expires)) 128 | } 129 | 130 | // TouchAll updates the expiration time for all entries 131 | func (c *Cuckoo) TouchAll(expires time.Time) { 132 | c.resize.RLock() 133 | defer c.resize.RUnlock() 134 | c.get().touchall(expires) 135 | } 136 | 137 | // fix is called whenever an operation detects that the table is becoming 138 | // crowded. this could be due to a large number of evictions, or because 139 | // inserts start failing. fix will first attempt to increase the number of hash 140 | // functions, and if that fails, it will resize the table. 141 | func (c *Cuckoo) fix(nhashes uint32, oldm *cmap) { 142 | c.resize.RLock() 143 | 144 | m := c.get() 145 | if oldm != m { 146 | // someone else has resized the table 147 | c.resize.RUnlock() 148 | return 149 | } 150 | 151 | if nhashes+1 < MAX_HASHES { 152 | sw := atomic.CompareAndSwapUint32(&m.hashes, nhashes, nhashes+1) 153 | if sw { 154 | fmt.Fprintln(os.Stderr, "increased the number of hashes to", nhashes+1) 155 | } 156 | 157 | // regardless whether we succeeded or failed, the number of 158 | // hashes is now greater 159 | c.resize.RUnlock() 160 | return 161 | } 162 | 163 | c.resize.RUnlock() 164 | 165 | c.resize.Lock() 166 | defer c.resize.Unlock() 167 | if c.get() != m { 168 | // someone else already resized the map 169 | return 170 | } 171 | 172 | // note that there is no need to check #hashes, as it cannot have 173 | // increased beyong MAX_HASHES 174 | 175 | // if we get here, we're forced to resize the table 176 | // first, find new table size, and create it 177 | nsize := c.size << 1 178 | newm := create(nsize) 179 | fmt.Fprintln(os.Stderr, "growing hashtable to", nsize) 180 | 181 | // next, copy over all items 182 | var res MemopRes 183 | // TODO: do in parallel 184 | for v := range m.iterate() { 185 | // TODO: keep CAS values 186 | res = newm.insert(v.key, fadd(v.val.Bytes, v.val.Flags, v.val.Expires)) 187 | if res.T != STORED { 188 | atomic.AddUint32(&newm.hashes, 1) 189 | res = newm.insert(v.key, fadd(v.val.Bytes, v.val.Flags, v.val.Expires)) 190 | if res.T != STORED { 191 | panic(fmt.Sprintln("Failed to move element to new map", v, res)) 192 | } 193 | } 194 | } 195 | 196 | // stop eviction in old map 197 | // start eviction in new map 198 | if m.requestEvict != nil { 199 | close(m.requestEvict) 200 | newm.enableEviction() 201 | } 202 | 203 | // and finally, make the new map active 204 | atomic.StorePointer(&c.cmap, unsafe.Pointer(newm)) 205 | atomic.StoreUint64(&c.size, nsize) 206 | } 207 | 208 | // fixer periodically checks how many evictions the table has seen, and will 209 | // resize it if the number of evictions exceeds a threshold 210 | func (c *Cuckoo) fixer() { 211 | var m *cmap 212 | var nh uint32 213 | 214 | var now uint 215 | var then uint 216 | var evicted uint 217 | for { 218 | select { 219 | case <-c.done: 220 | return 221 | case <-c.tick.C: 222 | } 223 | 224 | m = c.get() 225 | now = m.evicted 226 | nh = atomic.LoadUint32(&m.hashes) 227 | 228 | evicted = now - then 229 | if evicted > EVICTION_THRESHOLD { 230 | fmt.Fprintln(os.Stderr, "saw", evicted, "recent evictions; trying to fix") 231 | c.fix(nh, m) 232 | } 233 | 234 | then = now 235 | } 236 | } 237 | 238 | // op executes a particular Memop on the given key. 239 | func (c *Cuckoo) op(key []byte, upd Memop) MemopRes { 240 | c.resize.RLock() 241 | m := c.get() 242 | nh := atomic.LoadUint32(&m.hashes) 243 | res := m.insert(keyt(key), upd) 244 | c.resize.RUnlock() 245 | 246 | for res.T == SERVER_ERROR { 247 | fmt.Fprintln(os.Stderr, "insert failed on key", string(key), "so trying to fix") 248 | c.fix(nh, m) 249 | 250 | c.resize.RLock() 251 | m = c.get() 252 | nh = atomic.LoadUint32(&m.hashes) 253 | res = m.insert(keyt(key), upd) 254 | c.resize.RUnlock() 255 | } 256 | 257 | return res 258 | } 259 | 260 | // Delete removes the value for the given key 261 | func (c *Cuckoo) Delete(key []byte, casid uint64) MemopRes { 262 | c.resize.RLock() 263 | defer c.resize.RUnlock() 264 | return c.get().del(keyt(key), casid) 265 | } 266 | 267 | // Get returns the current value for the given key 268 | func (c Cuckoo) Get(key []byte) (*Memval, bool) { 269 | v := c.get().get(keyt(key)) 270 | if v.T == NOT_FOUND { 271 | return nil, false 272 | } 273 | return v.M, true 274 | } 275 | 276 | // Iterate returns a list of Memvals present in the map 277 | func (c Cuckoo) Iterate() <-chan *Memval { 278 | ch := make(chan *Memval) 279 | go func() { 280 | for v := range c.get().iterate() { 281 | ch <- &v.val 282 | } 283 | close(ch) 284 | }() 285 | return ch 286 | } 287 | 288 | // Iterate returns a list of keys present in the map 289 | func (c Cuckoo) IterateKeys() <-chan string { 290 | ch := make(chan string) 291 | go func() { 292 | for v := range c.get().iterate() { 293 | ch <- string(v.key) 294 | } 295 | close(ch) 296 | }() 297 | return ch 298 | } 299 | -------------------------------------------------------------------------------- /src/cuckood/map.go: -------------------------------------------------------------------------------- 1 | package cuckoo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "sort" 8 | "time" 9 | ) 10 | 11 | const ASSOCIATIVITY_E uint = 3 12 | 13 | // ASSOCIATIVITY is the set-associativity of each Cuckoo bin 14 | const ASSOCIATIVITY int = 1 << ASSOCIATIVITY_E 15 | 16 | // cmap holds a number of Cuckoo bins (each with room for ASSOCIATIVITY values), 17 | // and keeps track of the number of hashes being used. 18 | type cmap struct { 19 | bins []cbin 20 | hashes uint32 21 | requestEvict chan chan struct{} 22 | evicted uint 23 | } 24 | 25 | // create allocates a new Cuckoo map of the given size. 26 | // Two hash functions are used. 27 | func create(bins uint64) *cmap { 28 | if bins == 0 { 29 | panic("tried to create empty cuckoo map") 30 | } 31 | 32 | if bins&(bins-1) != 0 { 33 | // unless explicitly told otherwise, we'll create a decently 34 | // sized table by default 35 | var shift uint64 = 1 << 10 36 | for bins > shift { 37 | shift <<= 1 38 | } 39 | bins = shift 40 | } 41 | 42 | // since each bin can hold ASSOCIATIVITY elements 43 | // we don't need as many bins 44 | bins >>= ASSOCIATIVITY_E 45 | 46 | if bins == 0 { 47 | bins = 1 48 | } 49 | fmt.Fprintln(os.Stderr, "will initialize with", bins, "bins") 50 | 51 | m := new(cmap) 52 | m.bins = make([]cbin, bins) 53 | m.hashes = 2 54 | return m 55 | } 56 | 57 | func (m *cmap) enableEviction() { 58 | if m.requestEvict == nil { 59 | m.requestEvict = make(chan chan struct{}) 60 | go m.processEvictions() 61 | } 62 | } 63 | 64 | func (m *cmap) processEvictions() { 65 | var echan chan struct{} 66 | now := time.Now() 67 | for { 68 | for i, bin := range m.bins { 69 | for vi := 0; vi < ASSOCIATIVITY; vi++ { 70 | v := bin.v(vi) 71 | // evict should be required to actually 72 | // evict an item. just because this 73 | // slot is free doesn't mean there is 74 | // room for a new element of a 75 | // particular key! 76 | if v == nil || !v.present(now) { 77 | continue 78 | } 79 | 80 | // we don't evict recently read items. 81 | // if they're recently read, we label 82 | // them as not recently read. 83 | if bin.vals[vi].read { 84 | bin.vals[vi].read = false 85 | continue 86 | } 87 | 88 | // we've moved to the first evictable 89 | // record. now we just wait for someone 90 | // to tell us to evict (unless we're 91 | // already trying to evict something). 92 | if echan == nil { 93 | var ok bool 94 | if echan, ok = <-m.requestEvict; !ok { 95 | // make sure we haven't 96 | // been told to 97 | // terminate 98 | return 99 | } 100 | } 101 | 102 | // new eviction request came in! make 103 | // sure we have an up-to-date time 104 | // estimate -- we might have slept for 105 | // a long time 106 | now = time.Now() 107 | 108 | // we now need to redo the checks under 109 | // a lock to ensure the element hasn't 110 | // changed. note that we need m.bins[i] 111 | // here because bin is a *copy*. 112 | m.bins[i].mx.Lock() 113 | v = bin.v(vi) 114 | if v != nil && v.present(now) && !bin.vals[vi].read { 115 | //fmt.Fprintln(os.Stderr, "evicting", v.key) 116 | m.bins[i].kill(vi) 117 | m.evicted++ 118 | 119 | m.bins[i].mx.Unlock() 120 | 121 | echan <- struct{}{} 122 | echan = nil 123 | } else { 124 | m.bins[i].mx.Unlock() 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | // iterate returns a channel that contains every currently set value. 132 | // in the face of concurrent updates, some elements may be repeated or lost. 133 | func (m *cmap) iterate() <-chan cval { 134 | now := time.Now() 135 | ch := make(chan cval) 136 | go func() { 137 | for i, bin := range m.bins { 138 | vals := make([]cval, 0, ASSOCIATIVITY) 139 | m.bins[i].mx.Lock() 140 | for vi := 0; vi < ASSOCIATIVITY; vi++ { 141 | v := bin.v(vi) 142 | if v != nil && v.present(now) { 143 | vals = append(vals, *v) 144 | } 145 | } 146 | m.bins[i].mx.Unlock() 147 | for _, cv := range vals { 148 | ch <- cv 149 | } 150 | } 151 | close(ch) 152 | }() 153 | return ch 154 | } 155 | 156 | // touchall changes the expiry time of all entries to be at the latest the 157 | // given value. all concurrent modifications are blocked. 158 | func (m *cmap) touchall(exp time.Time) { 159 | for i := range m.bins { 160 | m.bins[i].mx.Lock() 161 | } 162 | 163 | for i := range m.bins { 164 | for vi := 0; vi < ASSOCIATIVITY; vi++ { 165 | v := m.bins[i].v(vi) 166 | if v != nil && v.present(exp) { 167 | v.val.Expires = exp 168 | } 169 | } 170 | } 171 | 172 | for i := range m.bins { 173 | m.bins[i].mx.Unlock() 174 | } 175 | } 176 | 177 | // del removes the entry with the given key (if any), and returns its value. if 178 | // casid is non-zero, the element will only be deleted if its id matches. 179 | func (m *cmap) del(key keyt, casid uint64) (ret MemopRes) { 180 | now := time.Now() 181 | bins := make([]int, int(m.hashes)) 182 | m.kbins(key, bins) 183 | 184 | m.lock_in_order(bins...) 185 | defer m.unlock(bins...) 186 | 187 | ret.T = NOT_FOUND 188 | for _, bin := range bins { 189 | ki, v := m.bins[bin].has(key, now) 190 | if ki != -1 { 191 | v := &v.val 192 | 193 | if casid != 0 && v.Casid != casid { 194 | ret.T = EXISTS 195 | return 196 | } 197 | 198 | ret.T = STORED 199 | ret.M = v 200 | m.bins[bin].kill(ki) 201 | return 202 | } 203 | } 204 | return 205 | } 206 | 207 | // insert sets or updates the entry with the given key. 208 | // the update function is used to determine the new value, and is passed the 209 | // old value under a lock. 210 | func (m *cmap) insert(key keyt, upd Memop) (ret MemopRes) { 211 | now := time.Now() 212 | ival := cval{key: key} 213 | 214 | // we do some additional trickery here so that when we recompute bins 215 | // in the loop below, we don't need to do further allocations 216 | nh := int(m.hashes) 217 | var bins_ [MAX_HASHES]int 218 | bins := bins_[0:nh] 219 | m.kbins(key, bins) 220 | 221 | // Check if this element is already present 222 | m.lock_in_order(bins...) 223 | for bi, bin := range bins { 224 | b := &m.bins[bin] 225 | ki, v := b.has(key, now) 226 | if ki != -1 { 227 | ival.bno = bi 228 | ival.val, ret = upd(v.val, true) 229 | if ret.T == STORED { 230 | b.setv(ki, &ival) 231 | b.vals[ki].read = true 232 | b.vals[ki].tag = key[0] 233 | } 234 | m.unlock(bins...) 235 | return 236 | } 237 | } 238 | m.unlock(bins...) 239 | 240 | // if the operation fails if a current element does not exist, 241 | // there is no point doing the expensive insert search 242 | _, ret = upd(Memval{}, false) 243 | if ret.T != STORED { 244 | return ret 245 | } 246 | 247 | // Item not currently present, is there room without a search? 248 | for i, b := range bins { 249 | if m.bins[b].available(now) { 250 | ival.bno = i 251 | ret = m.bins[b].add(&ival, upd, now) 252 | if ret.T != SERVER_ERROR { 253 | return 254 | } 255 | } 256 | } 257 | 258 | // Keep trying to find a cuckoo path of replacements 259 | for { 260 | path := m.search(now, bins...) 261 | if path == nil { 262 | if m.evict() { 263 | return m.insert(key, upd) 264 | } 265 | return MemopRes{ 266 | T: SERVER_ERROR, 267 | E: errors.New("no storage space found for element"), 268 | } 269 | } 270 | 271 | freeing := path[0].from 272 | 273 | // recompute bins because #hashes might have changed 274 | if nh != int(m.hashes) { 275 | nh = int(m.hashes) 276 | bins = bins_[0:nh] 277 | m.kbins(key, bins) 278 | } 279 | 280 | // sanity check that this path will make room in the right bin 281 | tobin := -1 282 | for i, bin := range bins { 283 | if freeing == bin { 284 | tobin = i 285 | } 286 | } 287 | if tobin == -1 { 288 | panic(fmt.Sprintf("path %v leads to occupancy in bin %v, but is unhelpful for key %s with bins: %v", path, freeing, key, bins)) 289 | } 290 | 291 | // only after the search do we acquire locks 292 | if m.validate_execute(path, now) { 293 | ival.bno = tobin 294 | 295 | // after replacements, someone else might have beaten 296 | // us to the free slot, so we need to do add under a 297 | // lock too 298 | ret = m.bins[freeing].add(&ival, upd, now) 299 | if ret.T != SERVER_ERROR { 300 | return 301 | } 302 | } 303 | } 304 | } 305 | 306 | // get returns the current value (if any) for the given key 307 | func (m *cmap) get(key keyt) (ret MemopRes) { 308 | now := time.Now() 309 | var bins_ [MAX_HASHES]int 310 | bins := bins_[0:int(m.hashes)] 311 | m.kbins(key, bins) 312 | 313 | ret.T = NOT_FOUND 314 | for _, bin := range bins { 315 | if i, v := m.bins[bin].has(key, now); v != nil { 316 | m.bins[bin].vals[i].read = true 317 | ret.T = EXISTS 318 | ret.M = &v.val 319 | return 320 | } 321 | } 322 | return 323 | } 324 | 325 | func (m *cmap) evict() bool { 326 | if m.requestEvict != nil { 327 | ret := make(chan struct{}) 328 | m.requestEvict <- ret 329 | <-ret 330 | return true 331 | } 332 | return false 333 | } 334 | 335 | // lock_in_order will acquire the given locks in a fixed order that ensures 336 | // competing lockers will not deadlock. 337 | func (m *cmap) lock_in_order(bins ...int) { 338 | locks := make([]int, len(bins)) 339 | for i := range bins { 340 | locks[i] = bins[i] 341 | } 342 | 343 | sort.Ints(locks) 344 | last := -1 345 | for _, bin := range locks { 346 | if bin != last { 347 | m.bins[bin].mx.Lock() 348 | last = bin 349 | } 350 | } 351 | } 352 | 353 | // unlock will release the given locks while ensuring no lock is released 354 | // multiple times. 355 | func (m *cmap) unlock(bins ...int) { 356 | locks := make([]int, len(bins)) 357 | for i := range bins { 358 | locks[i] = bins[i] 359 | } 360 | 361 | sort.Ints(locks) 362 | last := -1 363 | for _, bin := range locks { 364 | if bin != last { 365 | m.bins[bin].mx.Unlock() 366 | last = bin 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/cuckood/cucache/execute_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "cuckood" 5 | "encoding/binary" 6 | "testing" 7 | "time" 8 | 9 | gomem "github.com/dustin/gomemcached" 10 | ) 11 | 12 | // These tests are all taken from the brilliant memcached-test Python 13 | // application here: https://github.com/dustin/memcached-test 14 | // and have been translated into Go tests 15 | 16 | /* 17 | def testVersion(self): 18 | """Test the version command returns something.""" 19 | v=self.mc.version() 20 | self.assertTrue(len(v) > 0, "Bad version: ``" + str(v) + "''") 21 | */ 22 | func TestVersion(t *testing.T) { 23 | c := cuckoo.New(0) 24 | res := do(c, t, &gomem.MCRequest{ 25 | Opcode: gomem.VERSION, 26 | }) 27 | version := string(res.Body) 28 | if len(version) == 0 { 29 | t.Error("Empty version string given") 30 | } 31 | } 32 | 33 | /* 34 | def testSimpleSetGet(self): 35 | """Test a simple set and get.""" 36 | self.mc.set("x", 5, 19, "somevalue") 37 | self.assertGet((19, "somevalue"), self.mc.get("x")) 38 | */ 39 | func TestSimpleSetGet(t *testing.T) { 40 | c := cuckoo.New(0) 41 | assertSet(c, t, "x", []byte("somevalue"), gomem.SET) 42 | assertGet(c, t, "x", []byte("somevalue")) 43 | } 44 | 45 | /* 46 | def testZeroExpiration(self): 47 | """Ensure zero-expiration sets work properly.""" 48 | self.mc.set("x", 0, 19, "somevalue") 49 | time.sleep(1.1) 50 | self.assertGet((19, "somevalue"), self.mc.get("x")) 51 | */ 52 | func TestZeroExpiration(t *testing.T) { 53 | c := cuckoo.New(0) 54 | assertSet(c, t, "x", []byte("somevalue"), gomem.SET) 55 | time.Sleep(1*time.Second + 100*time.Millisecond) 56 | assertGet(c, t, "x", []byte("somevalue")) 57 | } 58 | 59 | /* 60 | def testDelete(self): 61 | """Test a set, get, delete, get sequence.""" 62 | self.mc.set("x", 5, 19, "somevalue") 63 | self.assertGet((19, "somevalue"), self.mc.get("x")) 64 | self.mc.delete("x") 65 | self.assertNotExists("x") 66 | */ 67 | func TestDelete(t *testing.T) { 68 | c := cuckoo.New(0) 69 | assertSet(c, t, "x", []byte("somevalue"), gomem.SET) 70 | assertGet(c, t, "x", []byte("somevalue")) 71 | do(c, t, &gomem.MCRequest{ 72 | Opcode: gomem.DELETE, 73 | Key: []byte("x"), 74 | }) 75 | assertNotExists(c, t, "x") 76 | } 77 | 78 | /* 79 | def testFlush(self): 80 | """Test flushing.""" 81 | self.mc.set("x", 5, 19, "somevaluex") 82 | self.mc.set("y", 5, 17, "somevaluey") 83 | self.assertGet((19, "somevaluex"), self.mc.get("x")) 84 | self.assertGet((17, "somevaluey"), self.mc.get("y")) 85 | self.mc.flush() 86 | self.assertNotExists("x") 87 | self.assertNotExists("y") 88 | */ 89 | func TestFlush(t *testing.T) { 90 | c := cuckoo.New(0) 91 | assertSet(c, t, "x", []byte("somevaluex"), gomem.SET) 92 | assertSet(c, t, "y", []byte("somevaluey"), gomem.SET) 93 | assertGet(c, t, "x", []byte("somevaluex")) 94 | assertGet(c, t, "y", []byte("somevaluey")) 95 | do(c, t, &gomem.MCRequest{ 96 | Opcode: gomem.FLUSH, 97 | Extras: noexp, 98 | }) 99 | assertNotExists(c, t, "x") 100 | assertNotExists(c, t, "y") 101 | } 102 | 103 | /* 104 | def testNoop(self): 105 | """Making sure noop is understood.""" 106 | self.mc.noop() 107 | */ 108 | func TestNoop(t *testing.T) { 109 | c := cuckoo.New(0) 110 | do(c, t, &gomem.MCRequest{ 111 | Opcode: gomem.NOOP, 112 | }) 113 | } 114 | 115 | /* 116 | def testAdd(self): 117 | """Test add functionality.""" 118 | self.assertNotExists("x") 119 | self.mc.add("x", 5, 19, "ex") 120 | self.assertGet((19, "ex"), self.mc.get("x")) 121 | try: 122 | self.mc.add("x", 5, 19, "ex2") 123 | self.fail("Expected failure to add existing key") 124 | except MemcachedError, e: 125 | self.assertEquals(memcacheConstants.ERR_EXISTS, e.status) 126 | self.assertGet((19, "ex"), self.mc.get("x")) 127 | */ 128 | func TestAdd(t *testing.T) { 129 | c := cuckoo.New(0) 130 | assertNotExists(c, t, "x") 131 | 132 | assertSet(c, t, "x", []byte("ex"), gomem.ADD) 133 | assertGet(c, t, "x", []byte("ex")) 134 | 135 | res := set(c, "x", []byte("ex2"), gomem.ADD) 136 | if res.Status != gomem.KEY_EEXISTS { 137 | t.Errorf("expected add on existing key to fail, got %v", res.Status) 138 | } 139 | 140 | assertGet(c, t, "x", []byte("ex")) 141 | } 142 | 143 | /* 144 | def testReplace(self): 145 | """Test replace functionality.""" 146 | self.assertNotExists("x") 147 | try: 148 | self.mc.replace("x", 5, 19, "ex") 149 | self.fail("Expected failure to replace missing key") 150 | except MemcachedError, e: 151 | self.assertEquals(memcacheConstants.ERR_NOT_FOUND, e.status) 152 | self.mc.add("x", 5, 19, "ex") 153 | self.assertGet((19, "ex"), self.mc.get("x")) 154 | self.mc.replace("x", 5, 19, "ex2") 155 | self.assertGet((19, "ex2"), self.mc.get("x")) 156 | */ 157 | func TestReplace(t *testing.T) { 158 | c := cuckoo.New(0) 159 | assertNotExists(c, t, "x") 160 | 161 | res := set(c, "x", []byte("ex"), gomem.REPLACE) 162 | if res.Status != gomem.KEY_ENOENT { 163 | t.Errorf("expected replace on non-existing key to fail, got %v", res.Status) 164 | } 165 | 166 | assertSet(c, t, "x", []byte("ex"), gomem.ADD) 167 | assertGet(c, t, "x", []byte("ex")) 168 | assertSet(c, t, "x", []byte("ex2"), gomem.REPLACE) 169 | assertGet(c, t, "x", []byte("ex2")) 170 | } 171 | 172 | /* 173 | def testMultiGet(self): 174 | """Testing multiget functionality""" 175 | self.mc.add("x", 5, 1, "ex") 176 | self.mc.add("y", 5, 2, "why") 177 | vals=self.mc.getMulti('xyz') 178 | self.assertGet((1, 'ex'), vals['x']) 179 | self.assertGet((2, 'why'), vals['y']) 180 | self.assertEquals(2, len(vals)) 181 | 182 | XXX: multi-get is the same as single get as far as req2res is concerned 183 | */ 184 | 185 | /* 186 | def testIncrDoesntExistNoCreate(self): 187 | """Testing incr when a value doesn't exist (and not creating).""" 188 | try: 189 | self.mc.incr("x", exp=memcacheConstants.INCRDECR_SPECIAL) 190 | self.fail("Expected failure to increment non-existent key") 191 | except MemcachedError, e: 192 | self.assertEquals(memcacheConstants.ERR_NOT_FOUND, e.status) 193 | self.assertNotExists("x") 194 | */ 195 | func TestIncrDoesntExistNoCreate(t *testing.T) { 196 | c := cuckoo.New(0) 197 | res := pm(c, "x", 1, 1, true, gomem.INCREMENT) 198 | if res.Status != gomem.KEY_ENOENT { 199 | t.Errorf("expected incr on non-existing key to fail, got %v", res.Status) 200 | } 201 | } 202 | 203 | /* 204 | def testIncrDoesntExistCreate(self): 205 | """Testing incr when a value doesn't exist (and we make a new one)""" 206 | self.assertNotExists("x") 207 | self.assertEquals(19, self.mc.incr("x", init=19)[0]) 208 | */ 209 | func TestIncrDoesntExistCreate(t *testing.T) { 210 | c := cuckoo.New(0) 211 | assertPM(c, t, "x", 0, 19, false, gomem.INCREMENT) 212 | assertGet(c, t, "x", []byte("19")) 213 | } 214 | 215 | /* 216 | def testDecrDoesntExistNoCreate(self): 217 | """Testing decr when a value doesn't exist (and not creating).""" 218 | try: 219 | self.mc.decr("x", exp=memcacheConstants.INCRDECR_SPECIAL) 220 | self.fail("Expected failiure to decrement non-existent key.") 221 | except MemcachedError, e: 222 | self.assertEquals(memcacheConstants.ERR_NOT_FOUND, e.status) 223 | self.assertNotExists("x") 224 | */ 225 | func TestDecrDoesntExistNoCreate(t *testing.T) { 226 | c := cuckoo.New(0) 227 | res := pm(c, "x", 1, 1, true, gomem.DECREMENT) 228 | if res.Status != gomem.KEY_ENOENT { 229 | t.Errorf("expected decr on non-existing key to fail, got %v", res.Status) 230 | } 231 | } 232 | 233 | /* 234 | def testDecrDoesntExistCreate(self): 235 | """Testing decr when a value doesn't exist (and we make a new one)""" 236 | self.assertNotExists("x") 237 | self.assertEquals(19, self.mc.decr("x", init=19)[0]) 238 | */ 239 | func TestDecrDoesntExistCreate(t *testing.T) { 240 | c := cuckoo.New(0) 241 | assertPM(c, t, "x", 0, 19, false, gomem.DECREMENT) 242 | assertGet(c, t, "x", []byte("19")) 243 | } 244 | 245 | /* 246 | 247 | def testIncr(self): 248 | """Simple incr test.""" 249 | val, cas=self.mc.incr("x") 250 | self.assertEquals(0, val) 251 | val, cas=self.mc.incr("x") 252 | self.assertEquals(1, val) 253 | val, cas=self.mc.incr("x", 211) 254 | self.assertEquals(212, val) 255 | val, cas=self.mc.incr("x", 2**33) 256 | self.assertEquals(8589934804L, val) 257 | */ 258 | func TestIncr(t *testing.T) { 259 | c := cuckoo.New(0) 260 | assertPM(c, t, "x", 1, 0, false, gomem.INCREMENT) 261 | assertGet(c, t, "x", []byte("0")) 262 | assertPM(c, t, "x", 1, 0, false, gomem.INCREMENT) 263 | assertGet(c, t, "x", []byte("1")) 264 | assertPM(c, t, "x", 211, 0, false, gomem.INCREMENT) 265 | assertGet(c, t, "x", []byte("212")) 266 | assertPM(c, t, "x", 1<<33, 0, false, gomem.INCREMENT) 267 | assertGet(c, t, "x", []byte("8589934804")) 268 | } 269 | 270 | /* 271 | 272 | def testDecr(self): 273 | """Simple decr test.""" 274 | val, cas=self.mc.incr("x", init=5) 275 | self.assertEquals(5, val) 276 | val, cas=self.mc.decr("x") 277 | self.assertEquals(4, val) 278 | val, cas=self.mc.decr("x", 211) 279 | self.assertEquals(0, val) 280 | */ 281 | func TestDecr(t *testing.T) { 282 | c := cuckoo.New(0) 283 | assertPM(c, t, "x", 1, 5, false, gomem.DECREMENT) 284 | assertGet(c, t, "x", []byte("5")) 285 | assertPM(c, t, "x", 1, 0, false, gomem.DECREMENT) 286 | assertGet(c, t, "x", []byte("4")) 287 | assertPM(c, t, "x", 211, 0, false, gomem.DECREMENT) 288 | assertGet(c, t, "x", []byte("0")) 289 | } 290 | 291 | /* 292 | 293 | def testCas(self): 294 | """Test CAS operation.""" 295 | try: 296 | self.mc.cas("x", 5, 19, 0x7fffffffff, "bad value") 297 | self.fail("Expected error CASing with no existing value") 298 | except MemcachedError, e: 299 | self.assertEquals(memcacheConstants.ERR_NOT_FOUND, e.status) 300 | self.mc.add("x", 5, 19, "original value") 301 | flags, i, val=self.mc.get("x") 302 | self.assertEquals("original value", val) 303 | try: 304 | self.mc.cas("x", 5, 19, i+1, "broken value") 305 | self.fail("Expected error CASing with invalid id") 306 | except MemcachedError, e: 307 | self.assertEquals(memcacheConstants.ERR_EXISTS, e.status) 308 | self.mc.cas("x", 5, 19, i, "new value") 309 | newflags, newi, newval=self.mc.get("x") 310 | self.assertEquals("new value", newval) 311 | 312 | # Test a CAS replay 313 | try: 314 | self.mc.cas("x", 5, 19, i, "crap value") 315 | self.fail("Expected error CASing with invalid id") 316 | except MemcachedError, e: 317 | self.assertEquals(memcacheConstants.ERR_EXISTS, e.status) 318 | newflags, newi, newval=self.mc.get("x") 319 | self.assertEquals("new value", newval) 320 | */ 321 | func TestCas(t *testing.T) { 322 | c := cuckoo.New(0) 323 | req := &gomem.MCRequest{ 324 | Opcode: gomem.SET, 325 | Key: []byte("x"), 326 | Body: []byte("bad value"), 327 | Extras: nullset, 328 | Cas: 0x7fffffffff, 329 | } 330 | 331 | res := req2res(c, req) 332 | if res.Status != gomem.KEY_ENOENT { 333 | t.Errorf("expected cas on non-existent key to fail with ERR_NOT_FOUND, got %v", res.Status) 334 | } 335 | 336 | assertSet(c, t, "x", []byte("original value"), gomem.ADD) 337 | res = assertGet(c, t, "x", []byte("original value")) 338 | 339 | req.Cas = res.Cas + 1 340 | req.Body = []byte("broken value") 341 | res = req2res(c, req) 342 | if res.Status != gomem.KEY_EEXISTS { 343 | t.Errorf("expected set with invalid cas to fail with EEXISTS, got %v", res.Status) 344 | } 345 | 346 | req.Cas = req.Cas - 1 347 | req.Body = []byte("new value") 348 | res = req2res(c, req) 349 | if res.Status != gomem.SUCCESS { 350 | t.Errorf("expected set with valid cas to succeed, got %v", res.Status) 351 | } 352 | 353 | res = assertGet(c, t, "x", []byte("new value")) 354 | req.Body = []byte("crap value") 355 | res = req2res(c, req) 356 | if res.Status != gomem.KEY_EEXISTS { 357 | t.Errorf("expected replayed cas to fail with EEXISTS, got %v", res.Status) 358 | } 359 | 360 | assertGet(c, t, "x", []byte("new value")) 361 | } 362 | 363 | /* 364 | # Assert we know the correct CAS for a given key. 365 | def assertValidCas(self, key, cas): 366 | flags, currentcas, val=self.mc.get(key) 367 | self.assertEquals(currentcas, cas) 368 | 369 | def testSetReturnsCas(self): 370 | """Ensure a set command returns the current CAS.""" 371 | vals=self.mc.set('x', 5, 19, 'some val') 372 | self.assertValidCas('x', vals[1]) 373 | */ 374 | func TestSetReturnsCas(t *testing.T) { 375 | c := cuckoo.New(0) 376 | res_set := assertSet(c, t, "x", []byte("some val"), gomem.SET) 377 | res_get := assertGet(c, t, "x", []byte("some val")) 378 | if res_set.Cas != res_get.Cas { 379 | t.Errorf("expected CAS from SET to match CAS from GET (s: %d, g: %d)", res_set.Cas, res_get.Cas) 380 | } 381 | } 382 | 383 | /* 384 | def testAddReturnsCas(self): 385 | """Ensure an add command returns the current CAS.""" 386 | vals=self.mc.add('x', 5, 19, 'some val') 387 | self.assertValidCas('x', vals[1]) 388 | */ 389 | func TestAddReturnsCas(t *testing.T) { 390 | c := cuckoo.New(0) 391 | res_set := assertSet(c, t, "x", []byte("some val"), gomem.ADD) 392 | res_get := assertGet(c, t, "x", []byte("some val")) 393 | if res_set.Cas != res_get.Cas { 394 | t.Errorf("expected CAS from ADD to match CAS from GET (a: %d, g: %d)", res_set.Cas, res_get.Cas) 395 | } 396 | } 397 | 398 | /* 399 | def testReplaceReturnsCas(self): 400 | """Ensure a replace command returns the current CAS.""" 401 | vals=self.mc.add('x', 5, 19, 'some val') 402 | vals=self.mc.replace('x', 5, 19, 'other val') 403 | self.assertValidCas('x', vals[1]) 404 | */ 405 | func TestReplaceReturnsCas(t *testing.T) { 406 | c := cuckoo.New(0) 407 | assertSet(c, t, "x", []byte("some val"), gomem.ADD) 408 | res_set := assertSet(c, t, "x", []byte("other val"), gomem.REPLACE) 409 | res_get := assertGet(c, t, "x", []byte("other val")) 410 | if res_set.Cas != res_get.Cas { 411 | t.Errorf("expected CAS from REPLACE to match CAS from GET (r: %d, g: %d)", res_set.Cas, res_get.Cas) 412 | } 413 | } 414 | 415 | /* 416 | def testIncrReturnsCAS(self): 417 | """Ensure an incr command returns the current CAS.""" 418 | val, cas, something=self.mc.set("x", 5, 19, '4') 419 | val, cas=self.mc.incr("x", init=5) 420 | self.assertEquals(5, val) 421 | self.assertValidCas('x', cas) 422 | */ 423 | func TestIncrReturnsCas(t *testing.T) { 424 | c := cuckoo.New(0) 425 | assertSet(c, t, "x", []byte("4"), gomem.ADD) 426 | res_set := assertPM(c, t, "x", 1, 5, true, gomem.INCREMENT) 427 | res_get := assertGet(c, t, "x", []byte("5")) 428 | if res_set.Cas != res_get.Cas { 429 | t.Errorf("expected CAS from INCR to match CAS from GET (i: %d, g: %d)", res_set.Cas, res_get.Cas) 430 | } 431 | } 432 | 433 | /* 434 | 435 | def testDecrReturnsCAS(self): 436 | """Ensure an decr command returns the current CAS.""" 437 | val, cas, something=self.mc.set("x", 5, 19, '4') 438 | val, cas=self.mc.decr("x", init=5) 439 | self.assertEquals(3, val) 440 | self.assertValidCas('x', cas) 441 | */ 442 | func TestDecrReturnsCas(t *testing.T) { 443 | c := cuckoo.New(0) 444 | assertSet(c, t, "x", []byte("4"), gomem.ADD) 445 | res_set := assertPM(c, t, "x", 1, 5, true, gomem.DECREMENT) 446 | res_get := assertGet(c, t, "x", []byte("3")) 447 | if res_set.Cas != res_get.Cas { 448 | t.Errorf("expected CAS from DECR to match CAS from GET (d: %d, g: %d)", res_set.Cas, res_get.Cas) 449 | } 450 | } 451 | 452 | /* 453 | def testDeletionCAS(self): 454 | """Validation deletion honors cas.""" 455 | try: 456 | self.mc.delete("x") 457 | except MemcachedError, e: 458 | self.assertEquals(memcacheConstants.ERR_NOT_FOUND, e.status) 459 | val, cas, something=self.mc.set("x", 5, 19, '4') 460 | try: 461 | self.mc.delete('x', cas=cas+1) 462 | self.fail("Deletion should've failed.") 463 | except MemcachedError, e: 464 | self.assertEquals(memcacheConstants.ERR_EXISTS, e.status) 465 | self.assertGet((19, '4'), self.mc.get('x')) 466 | self.mc.delete('x', cas=cas) 467 | self.assertNotExists('x') 468 | */ 469 | func TestDeletionCAS(t *testing.T) { 470 | c := cuckoo.New(0) 471 | req := &gomem.MCRequest{ 472 | Opcode: gomem.DELETE, 473 | Key: []byte("x"), 474 | } 475 | 476 | res := req2res(c, req) 477 | if res.Status != gomem.KEY_ENOENT { 478 | t.Errorf("expected delete of non-existing key to fail with ENOENT, got %v", res.Status) 479 | } 480 | 481 | res = assertSet(c, t, "x", []byte("4"), gomem.ADD) 482 | req.Cas = res.Cas + 1 483 | res = req2res(c, req) 484 | if res.Status != gomem.KEY_EEXISTS { 485 | t.Errorf("expected delete with invalid cas to fail with EEXISTS, got %v", res.Status) 486 | } 487 | 488 | assertGet(c, t, "x", []byte("4")) 489 | req.Cas = req.Cas - 1 490 | res = req2res(c, req) 491 | if res.Status != gomem.SUCCESS { 492 | t.Errorf("expected delete with valid cas to succeed, got %v", res.Status) 493 | } 494 | 495 | assertNotExists(c, t, "x") 496 | } 497 | 498 | /* 499 | def testAppend(self): 500 | """Test append functionality.""" 501 | val, cas, something=self.mc.set("x", 5, 19, "some") 502 | val, cas, something=self.mc.append("x", "thing") 503 | self.assertGet((19, 'something'), self.mc.get("x")) 504 | */ 505 | func TestAppend(t *testing.T) { 506 | c := cuckoo.New(0) 507 | 508 | assertSet(c, t, "x", []byte("some"), gomem.SET) 509 | do(c, t, &gomem.MCRequest{ 510 | Opcode: gomem.APPEND, 511 | Key: []byte("x"), 512 | Body: []byte("thing"), 513 | }) 514 | assertGet(c, t, "x", []byte("something")) 515 | } 516 | 517 | /* 518 | def testAppendCAS(self): 519 | """Test append functionality honors CAS.""" 520 | val, cas, something=self.mc.set("x", 5, 19, "some") 521 | try: 522 | val, cas, something=self.mc.append("x", "thing", cas+1) 523 | self.fail("expected CAS failure.") 524 | except MemcachedError, e: 525 | self.assertEquals(memcacheConstants.ERR_EXISTS, e.status) 526 | self.assertGet((19, 'some'), self.mc.get("x")) 527 | */ 528 | func TestAppendCAS(t *testing.T) { 529 | c := cuckoo.New(0) 530 | req := &gomem.MCRequest{ 531 | Opcode: gomem.APPEND, 532 | Key: []byte("x"), 533 | Body: []byte("thing"), 534 | } 535 | 536 | res := assertSet(c, t, "x", []byte("some"), gomem.ADD) 537 | req.Cas = res.Cas + 1 538 | res = req2res(c, req) 539 | if res.Status != gomem.KEY_EEXISTS { 540 | t.Errorf("expected append with invalid cas to fail with EEXISTS, got %v", res.Status) 541 | } 542 | assertGet(c, t, "x", []byte("some")) 543 | } 544 | 545 | /* 546 | def testPrepend(self): 547 | """Test prepend functionality.""" 548 | val, cas, something=self.mc.set("x", 5, 19, "some") 549 | val, cas, something=self.mc.prepend("x", "thing") 550 | self.assertGet((19, 'thingsome'), self.mc.get("x")) 551 | */ 552 | func TestPrepend(t *testing.T) { 553 | c := cuckoo.New(0) 554 | 555 | assertSet(c, t, "x", []byte("some"), gomem.SET) 556 | do(c, t, &gomem.MCRequest{ 557 | Opcode: gomem.PREPEND, 558 | Key: []byte("x"), 559 | Body: []byte("thing"), 560 | }) 561 | assertGet(c, t, "x", []byte("thingsome")) 562 | } 563 | 564 | /* 565 | def testPrependCAS(self): 566 | """Test prepend functionality honors CAS.""" 567 | val, cas, something=self.mc.set("x", 5, 19, "some") 568 | try: 569 | val, cas, something=self.mc.prepend("x", "thing", cas+1) 570 | self.fail("expected CAS failure.") 571 | except MemcachedError, e: 572 | self.assertEquals(memcacheConstants.ERR_EXISTS, e.status) 573 | self.assertGet((19, 'some'), self.mc.get("x")) 574 | */ 575 | func TestPrependCAS(t *testing.T) { 576 | c := cuckoo.New(0) 577 | req := &gomem.MCRequest{ 578 | Opcode: gomem.PREPEND, 579 | Key: []byte("x"), 580 | Body: []byte("thing"), 581 | } 582 | 583 | res := assertSet(c, t, "x", []byte("some"), gomem.ADD) 584 | req.Cas = res.Cas + 1 585 | res = req2res(c, req) 586 | if res.Status != gomem.KEY_EEXISTS { 587 | t.Errorf("expected prepend with invalid cas to fail with EEXISTS, got %v", res.Status) 588 | } 589 | assertGet(c, t, "x", []byte("some")) 590 | } 591 | 592 | /* 593 | def testTimeBombedFlush(self): 594 | """Test a flush with a time bomb.""" 595 | val, cas, something=self.mc.set("x", 5, 19, "some") 596 | self.mc.flush(2) 597 | self.assertGet((19, 'some'), self.mc.get("x")) 598 | time.sleep(2.1) 599 | self.assertNotExists('x') 600 | */ 601 | func TestTimeBombedFlush(t *testing.T) { 602 | c := cuckoo.New(0) 603 | assertSet(c, t, "x", []byte("some"), gomem.ADD) 604 | 605 | exp := make([]byte, 4) 606 | binary.BigEndian.PutUint32(exp, 2) 607 | do(c, t, &gomem.MCRequest{ 608 | Opcode: gomem.FLUSH, 609 | Extras: exp, 610 | }) 611 | 612 | assertGet(c, t, "x", []byte("some")) 613 | time.Sleep(2*time.Second + 100*time.Millisecond) 614 | assertNotExists(c, t, "x") 615 | } 616 | --------------------------------------------------------------------------------