├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── logo.png └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License (MIT) 2 | 3 | Copyright (c) 2016 Josh Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | go build -o redraft main.go -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | REDRAFT 5 | 6 |

7 | 8 | RedRaft is an experimental project that allows for [Redis](http://redis.io) to run in a [Raft](https://raft.github.io/) environment. It uses [Finn](https://github.com/tidwall/finn) for the Raft implementation and Redis for the state-machine providing a fault-tolerant cluster. 9 | 10 | Getting Started 11 | --------------- 12 | 13 | ### Building 14 | 15 | 1. Install [Redis](http://redis.io/download). 16 | 17 | 2. Build RedRaft 18 | 19 | ```sh 20 | $ git clone https://github.com/tidwall/redraft 21 | $ cd redraft 22 | $ make 23 | ``` 24 | 25 | Optionally you can download a prebuilt [zip](https://github.com/tidwall/redraft/releases/download/demo/redraft-demo-darwin-amd64.zip) for Mac OS which includes Redis and RedRaft. 26 | 27 | ### Running 28 | 29 | First start a single-member cluster: 30 | ``` 31 | $ ./redraft 32 | ``` 33 | 34 | This will start the server listening on port 7481 for client and server-to-server communication. 35 | 36 | Next, let's set a single key, and then retrieve it: 37 | 38 | ``` 39 | $ redis-cli -p 7481 SET mykey "my value" 40 | OK 41 | $ redis-cli -p 7481 GET mykey 42 | "my value" 43 | ``` 44 | 45 | Adding members: 46 | ``` 47 | $ ./redraft -p 7482 -dir data2 -join 7481 48 | $ ./redraft -p 7483 -dir data3 -join 7481 49 | ``` 50 | 51 | That's it. Now if node1 goes down, node2 and node3 will continue to operate. 52 | 53 | 54 | Supported Commands 55 | ------------------ 56 | get, strlen, exists, getbit, getrange, substr, mget, 57 | llen, lindex, lrange, sismember, scard, srandmember, 58 | sinter, sunion, sdiff, smembers, sscan, zrange, 59 | zrangebyscore, zrevrangebyscore, zrangebylex, zrevrangebylex, 60 | zcount, zlexcount, zrevrange, zcard, zscore, zrank, 61 | zrevrank, zscan, hget, hmget, hlen, hstrlen, hkeys, 62 | hvals, hgetall, hexists, hscan, randomkey, keys, scan, 63 | dbsize, echo, type, info, ttl, pttl, dump, object, 64 | memory, time, bitpos, bitcount, geohash, geopos, geodist, 65 | pfcount, set, setnx, setex, psetex, append, del, setbit, 66 | bitfield, setrange, incr, decr, rpush, lpush, rpushx, 67 | lpushx, linsert, rpop, lpop, brpop, brpoplpush, blpop, 68 | lset, ltrim, lrem, rpoplpush, sadd, srem, smove, spop, 69 | sinterstore, sunionstore, sdiffstore, zadd, zincrby, zrem, 70 | zremrangebyscore, zremrangebyrank, zremrangebylex, zunionstore, 71 | zinterstore, hset, hsetnx, hmset, hincrby, hincrbyfloat, 72 | hdel, incrby, decrby, incrbyfloat, getset, mset, msetnx, 73 | select, move, rename, renamenx, expire, expireat, pexpire, 74 | pexpireat, auth, flushdb, flushall, sort, persist, config, 75 | restore, migrate, bitop, geoadd, georadius, 76 | georadiusbymember, pfadd, pfmerge: 77 | 78 | Not Supported 79 | ------------- 80 | Cluster, Replication, and PubSub commands. 81 | 82 | Built-in Raft Commands 83 | ---------------------- 84 | Here are a few commands for monitoring and managing the cluster: 85 | 86 | - **RAFTADDPEER addr** 87 | Adds a new member to the Raft cluster 88 | - **RAFTREMOVEPEER addr** 89 | Removes an existing member 90 | - **RAFTLEADER** 91 | Returns the Raft leader, if known 92 | - **RAFTSNAPSHOT** 93 | Triggers a snapshot operation 94 | - **RAFTSTATE** 95 | Returns the state of the node 96 | - **RAFTSTATS** 97 | Returns information and statistics for the node and cluster 98 | 99 | Read Consistency 100 | ---------------- 101 | 102 | The `--consistency` param has the following options: 103 | 104 | - `low` - all nodes accept reads, small risk of [stale](http://stackoverflow.com/questions/1563319/what-is-stale-state) data 105 | - `medium` - only the leader accepts reads, itty-bitty risk of stale data during a leadership change 106 | - `high` - only the leader accepts reads, the raft log index is incremented to guaranteeing no stale data 107 | 108 | For example, setting the following options: 109 | 110 | ``` 111 | $ ./redraft --consistency high 112 | ``` 113 | 114 | Provides the highest level of consistency. 115 | 116 | Leadership Changes 117 | ------------------ 118 | 119 | In a Raft cluster only the leader can apply commands. If a command is attempted on a follower you will be presented with the response: 120 | 121 | ``` 122 | SET x y 123 | -TRY 127.0.0.1:7481 124 | ``` 125 | 126 | This means you should try the same command at the specified address. 127 | 128 | Contact 129 | ------- 130 | Josh Baker [@tidwall](http://twitter.com/tidwall) 131 | 132 | License 133 | ------- 134 | RedRaft source code is available under the MIT [License](/LICENSE). 135 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tidwall/redraft 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/tidwall/finn v0.1.2 7 | github.com/tidwall/raft-redcon v0.1.0 8 | github.com/tidwall/redcon v1.0.0 9 | github.com/tidwall/redlog v0.0.0-20180507234857-bbed90f29893 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 2 | github.com/armon/go-metrics v0.3.0 h1:B7AQgHi8QSEi4uHu7Sbsga+IJDU+CENgjxoo81vDUqU= 3 | github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= 4 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 5 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= 6 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 7 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= 8 | github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 11 | github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= 12 | github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= 13 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= 15 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 16 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 17 | github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 18 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 19 | github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= 20 | github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 21 | github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 22 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 23 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 24 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 25 | github.com/hashicorp/raft v0.1.0 h1:OC+j7LWkv7x8s9c5wnXCEgtP1J0LDw2fKNxUiYCZFNo= 26 | github.com/hashicorp/raft v0.1.0/go.mod h1:DVSAWItjLjTOkVbSpWQ0j0kUADIvDaCtBxIcbNAQLkI= 27 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 28 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 29 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 30 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 31 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 32 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 33 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 35 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 36 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 37 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 38 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 41 | github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= 42 | github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= 43 | github.com/tidwall/finn v0.1.2 h1:gSRpZwQHmzwhYPRKoYswBqMcsnVvWwOsOnBO6Fu/ceA= 44 | github.com/tidwall/finn v0.1.2/go.mod h1:u5JsUQ5LMBO3rOd9OGp17BphJ89hnyOTvPN+qMBAdCs= 45 | github.com/tidwall/raft-boltdb v0.0.0-20160909211738-25b87f2c5677 h1:8FkXr+GCV4wb8WAct/V1vKB/Ivy11Y+fm919EHgdfWA= 46 | github.com/tidwall/raft-boltdb v0.0.0-20160909211738-25b87f2c5677/go.mod h1:O7b2tvwZmC+IFu8djLOZj0jc/tjssDPiJ8xIt+U2jTU= 47 | github.com/tidwall/raft-fastlog v0.0.0-20160922202426-2f0d0a0ce558 h1:hQYEIfMzrH6LRzjz7Jp5Rv8jrty1bAR5M0DjOYSxxks= 48 | github.com/tidwall/raft-fastlog v0.0.0-20160922202426-2f0d0a0ce558/go.mod h1:KNwBhka/a5Ucw5bfEzKHTEKuCO2Do1tKs+kDdu3Sbb4= 49 | github.com/tidwall/raft-leveldb v0.0.0-20170127185243-ada471496dc9 h1:Z5QMqF/MSuvnrTibHqs/xx+ZE5gypLV02YU8Ry4kJ7A= 50 | github.com/tidwall/raft-leveldb v0.0.0-20170127185243-ada471496dc9/go.mod h1:KNAMyK8s/oUOTbIL/T07fTL6/EgJfHhK8XeeEPq35eU= 51 | github.com/tidwall/raft-redcon v0.1.0 h1:qwYaFaAVNFleY2EFm0j7UK4vEpoNa19ohH7U4idbg+s= 52 | github.com/tidwall/raft-redcon v0.1.0/go.mod h1:YhoECfJs8MXbrwak9H7wKYDMZ3rMaB7el7zZ7MRw9Xw= 53 | github.com/tidwall/redcon v1.0.0 h1:D4AzzJ81Afeh144fgnj5H0aSVPBBJ5RI9Rzj0zThU+E= 54 | github.com/tidwall/redcon v1.0.0/go.mod h1:bdYBm4rlcWpst2XMwKVzWDF9CoUxEbUmM7CQrKeOZas= 55 | github.com/tidwall/redlog v0.0.0-20180507234857-bbed90f29893 h1:aGyVYs0o1pThR9i+SuYCG/VqWibHkUXl9kIMZGhAXDw= 56 | github.com/tidwall/redlog v0.0.0-20180507234857-bbed90f29893/go.mod h1:NssoNA+Uwqd5WHKkVwAzO7AT6VuG3wiC8r5nBqds3Ao= 57 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 58 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 59 | golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= 60 | golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 61 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 62 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 63 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 64 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 66 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 67 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 69 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 72 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 73 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 74 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 75 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidwall/redraft/47c684f77f9cc9b43cad52ad70af5ac14d357e78/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net" 12 | "os" 13 | "os/exec" 14 | "os/signal" 15 | "path" 16 | "strings" 17 | "sync" 18 | "syscall" 19 | "time" 20 | 21 | "github.com/tidwall/finn" 22 | "github.com/tidwall/raft-redcon" 23 | "github.com/tidwall/redcon" 24 | "github.com/tidwall/redlog" 25 | ) 26 | 27 | func main() { 28 | var port int 29 | var backend string 30 | var durability string 31 | var consistency string 32 | var loglevel string 33 | var join string 34 | var dir string 35 | 36 | flag.IntVar(&port, "p", 7481, "Bind port") 37 | flag.StringVar(&backend, "backend", "fastlog", "Raft log backend [fastlog,bolt,inmem]") 38 | flag.StringVar(&durability, "durability", "medium", "Log durability [low,medium,high]") 39 | flag.StringVar(&consistency, "consistency", "medium", "Raft consistency [low,medium,high]") 40 | flag.StringVar(&loglevel, "loglevel", "notice", "Log level [quiet,warning,notice,verbose,debug]") 41 | flag.StringVar(&dir, "dir", "data", "Data directory") 42 | flag.StringVar(&join, "join", "", "Join a cluster by providing an address") 43 | flag.Parse() 44 | 45 | var opts finn.Options 46 | 47 | switch strings.ToLower(backend) { 48 | default: 49 | log.Fatalf("invalid backend '%v'", backend) 50 | case "fastlog": 51 | opts.Backend = finn.FastLog 52 | case "bolt": 53 | opts.Backend = finn.Bolt 54 | case "inmem": 55 | opts.Backend = finn.InMem 56 | } 57 | switch strings.ToLower(durability) { 58 | default: 59 | log.Fatalf("invalid durability '%v'", durability) 60 | case "low": 61 | opts.Durability = finn.Low 62 | case "medium": 63 | opts.Durability = finn.Medium 64 | case "high": 65 | opts.Durability = finn.High 66 | } 67 | switch strings.ToLower(consistency) { 68 | default: 69 | log.Fatalf("invalid consistency '%v'", consistency) 70 | case "low": 71 | opts.Consistency = finn.Low 72 | case "medium": 73 | opts.Consistency = finn.Medium 74 | case "high": 75 | opts.Consistency = finn.High 76 | } 77 | opts.LogOutput = os.Stdout 78 | switch strings.ToLower(loglevel) { 79 | default: 80 | log.Fatalf("invalid loglevel '%v'", loglevel) 81 | case "quiet": 82 | opts.LogOutput = ioutil.Discard 83 | loglevel = "warning" // reassign for redis 84 | case "warning": 85 | opts.LogLevel = finn.Warning 86 | case "notice": 87 | opts.LogLevel = finn.Notice 88 | case "verbose": 89 | opts.LogLevel = finn.Verbose 90 | case "debug": 91 | opts.LogLevel = finn.Debug 92 | } 93 | 94 | // Store appendonly.aof and unix.sock in a temp directory 95 | tmpdir, err := ioutil.TempDir("", "redis") 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | defer os.RemoveAll(tmpdir) 100 | 101 | // make redis executable path 102 | wd, err := os.Getwd() 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | redisServerPath := "redis-server" 107 | fi, err := os.Stat(path.Join(wd, redisServerPath)) 108 | if err == nil && !fi.IsDir() { 109 | redisServerPath = path.Join(wd, redisServerPath) 110 | } 111 | 112 | // Create the Redis command line 113 | cmd := exec.Command(redisServerPath, 114 | "--port", "0", 115 | "--bind", "127.0.0.1", 116 | "--unixsocketperm", "755", 117 | "--unixsocket", path.Join(tmpdir, "unix.sock"), 118 | "--appendonly", "yes", 119 | "--syslog-enabled", "yes", 120 | "--save", "", 121 | "--loglevel", loglevel, 122 | ) 123 | cmd.Dir = tmpdir 124 | cmd.Stdout = redlog.RedisLogColorizer(opts.LogOutput) 125 | cmd.Stderr = os.Stderr 126 | 127 | // Start the Redis processes 128 | err = cmd.Start() 129 | if err != nil { 130 | log.Fatal(err) 131 | } 132 | defer cmd.Process.Kill() 133 | 134 | // Watch for forced termination such as Ctrl-C. 135 | ch := make(chan os.Signal) 136 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM) 137 | go func() { 138 | <-ch 139 | // Send a SIGTERM to Redis will force the process to end and 140 | // tear down Redis and Raft. 141 | cmd.Process.Signal(syscall.SIGTERM) 142 | }() 143 | 144 | // Start up Finn 145 | rr, err := NewRedRaft(cmd, tmpdir, &opts) 146 | if err != nil { 147 | log.Fatal(err) 148 | } 149 | n, err := finn.Open(dir, fmt.Sprintf(":%d", port), join, rr, &opts) 150 | if err != nil { 151 | log.Fatal(err) 152 | } 153 | defer n.Close() 154 | 155 | // Wait for the Redis process to complete. 156 | cmd.Wait() 157 | } 158 | 159 | type RedRaft struct { 160 | tmpdir string 161 | mu sync.RWMutex 162 | rconn net.Conn 163 | rd *bufio.Reader 164 | cmd *exec.Cmd 165 | } 166 | type connCtx struct { 167 | rconn net.Conn 168 | rd *bufio.Reader 169 | } 170 | 171 | func NewRedRaft( 172 | cmd *exec.Cmd, tmpdir string, opts *finn.Options, 173 | ) (*RedRaft, error) { 174 | rr := &RedRaft{ 175 | tmpdir: tmpdir, 176 | cmd: cmd, 177 | } 178 | unixSock := path.Join(tmpdir, "unix.sock") 179 | start := time.Now() 180 | for { 181 | // Interprocess connection to Redis. 182 | rconn, err := net.Dial("unix", unixSock) 183 | if err != nil { 184 | if time.Now().Sub(start) > time.Second*5 { 185 | // 5 second timeout waiting to connect. 186 | return nil, err 187 | } 188 | time.Sleep(time.Millisecond * 100) 189 | continue 190 | } 191 | rr.rconn = rconn 192 | break 193 | } 194 | rr.rd = bufio.NewReader(rr.rconn) 195 | opts.ConnAccept = func(conn redcon.Conn) bool { 196 | rconn, err := net.Dial("unix", unixSock) 197 | if err != nil { 198 | return false 199 | } 200 | conn.SetContext(&connCtx{rconn, bufio.NewReader(rconn)}) 201 | return true 202 | } 203 | opts.ConnClosed = func(conn redcon.Conn, err error) { 204 | conn.Context().(*connCtx).rconn.Close() 205 | } 206 | return rr, nil 207 | } 208 | 209 | const ( 210 | unknown = 0 211 | read = 1 212 | write = 2 213 | ) 214 | 215 | func commandKind(cmd redcon.Command) int { 216 | switch strings.ToLower(string(cmd.Args[0])) { 217 | default: 218 | return unknown 219 | case // Read commands 220 | "get", "strlen", "exists", "getbit", "getrange", "substr", "mget", 221 | "llen", "lindex", "lrange", "sismember", "scard", "srandmember", 222 | "sinter", "sunion", "sdiff", "smembers", "sscan", "zrange", 223 | "zrangebyscore", "zrevrangebyscore", "zrangebylex", "zrevrangebylex", 224 | "zcount", "zlexcount", "zrevrange", "zcard", "zscore", "zrank", 225 | "zrevrank", "zscan", "hget", "hmget", "hlen", "hstrlen", "hkeys", 226 | "hvals", "hgetall", "hexists", "hscan", "randomkey", "keys", "scan", 227 | "dbsize", "echo", "type", "info", "ttl", "pttl", "dump", "object", 228 | "memory", "time", "bitpos", "bitcount", "geohash", "geopos", "geodist", 229 | "pfcount": 230 | return read 231 | case // Write commands 232 | "set", "setnx", "setex", "psetex", "append", "del", "setbit", 233 | "bitfield", "setrange", "incr", "decr", "rpush", "lpush", "rpushx", 234 | "lpushx", "linsert", "rpop", "lpop", "brpop", "brpoplpush", "blpop", 235 | "lset", "ltrim", "lrem", "rpoplpush", "sadd", "srem", "smove", "spop", 236 | "sinterstore", "sunionstore", "sdiffstore", "zadd", "zincrby", "zrem", 237 | "zremrangebyscore", "zremrangebyrank", "zremrangebylex", "zunionstore", 238 | "zinterstore", "hset", "hsetnx", "hmset", "hincrby", "hincrbyfloat", 239 | "hdel", "incrby", "decrby", "incrbyfloat", "getset", "mset", "msetnx", 240 | "select", "move", "rename", "renamenx", "expire", "expireat", "pexpire", 241 | "pexpireat", "auth", "flushdb", "flushall", "sort", "persist", "config", 242 | "restore", "migrate", "bitop", "geoadd", "georadius", 243 | "georadiusbymember", "pfadd", "pfmerge": 244 | return write 245 | } 246 | } 247 | 248 | func (rr *RedRaft) Command(m finn.Applier, conn redcon.Conn, cmd redcon.Command) (interface{}, error) { 249 | switch commandKind(cmd) { 250 | default: 251 | return nil, finn.ErrUnknownCommand 252 | 253 | case write: 254 | return m.Apply(conn, cmd, 255 | func() (interface{}, error) { 256 | rr.mu.Lock() 257 | defer rr.mu.Unlock() 258 | resp, err := rr.do(conn, cmd.Raw) 259 | if err != nil { 260 | return nil, err 261 | } 262 | return resp, nil 263 | }, 264 | func(v interface{}) (interface{}, error) { 265 | conn.WriteRaw(v.([]byte)) 266 | return nil, nil 267 | }, 268 | ) 269 | 270 | case read: 271 | return m.Apply(conn, cmd, nil, 272 | func(v interface{}) (interface{}, error) { 273 | rr.mu.RLock() 274 | defer rr.mu.RUnlock() 275 | resp, err := rr.do(conn, cmd.Raw) 276 | if err != nil { 277 | return nil, err 278 | } 279 | conn.WriteRaw(resp) 280 | return nil, nil 281 | }, 282 | ) 283 | } 284 | return nil, nil 285 | } 286 | 287 | // do will proxy the input command to the Redis process. A failed 288 | // read or write indicates a broken pipe and will cause a panic. 289 | func (rr *RedRaft) do(conn redcon.Conn, cmd []byte) (resp []byte, err error) { 290 | var rconn net.Conn 291 | var rd *bufio.Reader 292 | if conn == nil { 293 | rconn = rr.rconn 294 | rd = rr.rd 295 | } else { 296 | ctx := conn.Context().(*connCtx) 297 | rconn = ctx.rconn 298 | rd = ctx.rd 299 | } 300 | if _, err = rconn.Write(cmd); err == nil { 301 | var kind byte 302 | resp, kind, err = raftredcon.ReadRawResponse(rd) 303 | if err == nil { 304 | if kind == '-' { 305 | err = errors.New(strings.TrimSpace(string(resp[1:]))) 306 | return 307 | } 308 | return 309 | } 310 | } 311 | fmt.Fprintf(os.Stderr, "panic: %v\n", err) 312 | rr.cmd.Process.Signal(syscall.SIGTERM) 313 | return 314 | } 315 | 316 | // Restore restores a snapshot 317 | func (rr *RedRaft) Restore(rd io.Reader) error { 318 | rr.mu.Lock() 319 | defer rr.mu.Unlock() 320 | conn, err := net.Dial("unix", path.Join(rr.tmpdir, "unix.sock")) 321 | if err != nil { 322 | return err 323 | } 324 | defer conn.Close() 325 | var crd = bufio.NewReader(conn) 326 | var buf []byte 327 | var count int 328 | // issue a FLUSHALL to delete all keys from Redis process. 329 | buf = append(buf, "*1\r\n$8\r\nFLUSHALL\r\n"...) 330 | count++ 331 | flush := func() error { 332 | if _, err := conn.Write(buf); err != nil { 333 | return err 334 | } 335 | for i := 0; i < count; i++ { 336 | _, _, err := raftredcon.ReadRawResponse(crd) 337 | if err != nil { 338 | return err 339 | } 340 | } 341 | buf = buf[:0] 342 | count = 0 343 | return nil 344 | } 345 | var brd = bufio.NewReader(rd) 346 | for { 347 | raw, kind, err := raftredcon.ReadRawResponse(brd) 348 | if err != nil { 349 | if err == io.EOF { 350 | if err := flush(); err != nil { 351 | return err 352 | } 353 | break 354 | } 355 | return err 356 | } 357 | if kind != '*' { 358 | return errors.New("invalid command") 359 | } 360 | buf = append(buf, raw...) 361 | count++ 362 | if len(buf) >= 16*1024*1024 { 363 | if err := flush(); err != nil { 364 | return err 365 | } 366 | } 367 | } 368 | return nil 369 | // return finn.ErrDisabled 370 | } 371 | 372 | // Snapshot creates a snapshot 373 | func (rr *RedRaft) Snapshot(wr io.Writer) error { 374 | // Open the file and read the file info behind a lock. 375 | // We'll use the file size this to limit the amount of writing. 376 | var end int64 377 | var f *os.File 378 | var err error 379 | func() { 380 | rr.mu.Lock() 381 | defer rr.mu.Unlock() 382 | f, err = os.Open(path.Join(rr.tmpdir, "appendonly.aof")) 383 | if err == nil { 384 | var fi os.FileInfo 385 | fi, err = f.Stat() 386 | if err != nil { 387 | f.Close() 388 | return 389 | } 390 | end = fi.Size() 391 | } 392 | }() 393 | if err != nil { 394 | return err 395 | } 396 | defer f.Close() 397 | // copy the file to the writer 398 | _, err = io.CopyN(wr, f, end) 399 | return err 400 | } 401 | --------------------------------------------------------------------------------