├── .gitignore ├── LICENSE ├── README.md ├── disposable_redis.go └── disposable_redis_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | *.a 3 | *.6 4 | *~ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Doat Media. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 10 | of conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY Do@ ``AS IS'' AND ANY EXPRESS OR IMPLIED 14 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR 16 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 18 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 20 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 21 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | The views and conclusions contained in the software and documentation are those of the 24 | authors and should not be interpreted as representing official policies, either expressed 25 | or implied, of Doat Media. 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Disposable-Redis 2 | ## Create disposable instances of redis server on random ports 3 | 4 | This can be used for testing redis dependent code without having to make 5 | assumptions on if and where redis server is running, or fear of corrupting data. 6 | 7 | You just create a redis server instance, run your code against it as if it were a mock, and then remove it without a trace. 8 | The only assumption here is that you have `redis-server` available in your path. 9 | 10 | For full documentation see [http://godoc.org/github.com/EverythingMe/disposable-redis](http://godoc.org/github.com/EverythingMe/disposable-redis) 11 | 12 | 13 | ## Example: 14 | 15 | ```go 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | disposable "github.com/EverythingMe/disposable-redis" 21 | redigo "github.com/garyburd/redigo/redis" 22 | ) 23 | 24 | func ExampleServer() { 25 | 26 | // create a new server on a random port 27 | r, err := disposable.NewServerRandomPort() 28 | if err != nil { 29 | panic("Could not create random server") 30 | } 31 | 32 | // we must remember to kill it at the end, or we'll have zombie redises 33 | defer r.Stop() 34 | 35 | // wait for our server to be ready for serving, for at least 50 ms. 36 | // This gives redis time to initialize itself and listen 37 | if err = r.WaitReady(50 * time.Millisecond); err != nil { 38 | panic("Couldn't connect to instance") 39 | } 40 | 41 | //now we can just connect and talk to it 42 | conn, err := redigo.Dial("tcp", r.Addr()) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | fmt.Println(redigo.String(conn.Do("SET", "foo", "bar"))) 48 | //Output: OK 49 | 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /disposable_redis.go: -------------------------------------------------------------------------------- 1 | // A utility to create disposable instances of redis server on random ports. 2 | // 3 | // This can be used for testing redis dependent code without having to make 4 | // assumptions on if and where redis server is running, or fear of corrupting data. 5 | // You create a redis server instance, run your code against it as if it were a mock, 6 | // and then remove it without a trace. 7 | package disposable_redis 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "math/rand" 13 | "os/exec" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | redigo "github.com/garyburd/redigo/redis" 19 | ) 20 | 21 | // The redis executable. This allows you to set it if you're using a custom one. 22 | // Can be an absolute path, or an executable in your $PATH 23 | var RedisCommand = "redis-server" 24 | 25 | const ( 26 | MaxRetries = 10 27 | 28 | //this is the amount of time we give the server to start itself up and start listening (or fail) 29 | LaunchWaitTimeout = 100 * time.Millisecond 30 | ) 31 | 32 | // A wrapper reperesenting a running disposable redis server 33 | type Server struct { 34 | cmd *exec.Cmd 35 | port uint16 36 | running bool 37 | wg sync.WaitGroup 38 | } 39 | 40 | // Start and run the process, return an error if it cannot be run 41 | func (r *Server) run() error { 42 | 43 | ret := r.cmd.Start() 44 | 45 | ch := make(chan error) 46 | 47 | // we wait for LaunchWaitTimeout and see if the server quit due to an error 48 | r.wg.Add(1) 49 | go func() { 50 | defer r.wg.Done() 51 | err := r.cmd.Wait() 52 | select { 53 | case ch <- err: 54 | default: 55 | } 56 | }() 57 | 58 | select { 59 | case e := <-ch: 60 | log.Println("Error waiting for process:", e) 61 | return e 62 | case <-time.After(LaunchWaitTimeout): 63 | break 64 | 65 | } 66 | 67 | return ret 68 | } 69 | 70 | // Create and run a new server on a given port. 71 | // Return an error if the server cannot be started 72 | func NewServer(port uint16) (*Server, error) { 73 | 74 | cmd := exec.Command(RedisCommand, 75 | "--port", fmt.Sprintf("%d", port), 76 | "--pidfile", fmt.Sprintf("/tmp/disposable_redis.%d.pid", port), 77 | "--dir", "/tmp", 78 | "--dbfilename", fmt.Sprintf("dump.%d.%d.rdb", port, time.Now().UnixNano()), 79 | ) 80 | 81 | log.Println("start args: ", cmd.Args) 82 | 83 | r := &Server{ 84 | cmd: cmd, 85 | port: port, 86 | running: false, 87 | } 88 | 89 | err := r.run() 90 | if err != nil { 91 | return nil, err 92 | } 93 | r.running = true 94 | 95 | return r, nil 96 | 97 | } 98 | 99 | // Create a new server on a random port. If the port is taken we retry (10 times). 100 | // If we still couldn't start the process, we return an error 101 | func NewServerRandomPort() (*Server, error) { 102 | 103 | var err error 104 | var r *Server 105 | for i := 0; i < MaxRetries; i++ { 106 | port := uint16(rand.Int31n(0xffff-1025) + 1025) 107 | log.Println("Trying port ", port) 108 | 109 | r, err = NewServer(port) 110 | if err == nil { 111 | return r, nil 112 | } 113 | } 114 | 115 | log.Println("Could not start throwaway redis") 116 | return nil, err 117 | 118 | } 119 | 120 | // Wait for the server to be ready, or until a timeout has elapsed. 121 | // This just blocks and waits using sleep intervals of 5ms if it can't connect 122 | func (r *Server) WaitReady(timeout time.Duration) error { 123 | 124 | deadline := time.Now().Add(timeout) 125 | var err error 126 | 127 | for time.Now().Before(deadline) { 128 | 129 | conn, e := redigo.Dial("tcp", fmt.Sprintf("localhost:%d", r.port)) 130 | if e != nil { 131 | log.Println("Could not connect, waiting 5ms") 132 | err = e 133 | time.Sleep(5 * time.Millisecond) 134 | } else { 135 | conn.Close() 136 | return nil 137 | } 138 | 139 | } 140 | return err 141 | 142 | } 143 | 144 | // Stop the running redis server 145 | func (r *Server) Stop() error { 146 | if !r.running { 147 | return nil 148 | } 149 | r.running = false 150 | if err := r.cmd.Process.Kill(); err != nil { 151 | return err 152 | } 153 | 154 | r.wg.Wait() 155 | r.cmd.Wait() 156 | 157 | return nil 158 | 159 | } 160 | 161 | // Info returns the value of the server's INFO command parsed into a map of strings 162 | func (r Server) Info() (map[string]string, error) { 163 | conn, e := redigo.Dial("tcp", r.Addr()) 164 | if e != nil { 165 | return nil, e 166 | } 167 | defer conn.Close() 168 | 169 | info, err := redigo.String(conn.Do("INFO")) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | ret := make(map[string]string) 175 | lines := strings.Split(info, "\r\n") 176 | for _, line := range lines { 177 | if len(line) == 0 || line[0] == '#' { 178 | continue 179 | } 180 | kv := strings.Split(line, ":") 181 | 182 | if len(kv) == 2 { 183 | ret[kv[0]] = kv[1] 184 | } 185 | } 186 | 187 | return ret, nil 188 | } 189 | 190 | // NewSlaveOf creates a new server with a random port and makes it a slave of the current server. 191 | func (r Server) NewSlaveOf() (*Server, error) { 192 | 193 | srv, err := NewServerRandomPort() 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | if err = srv.WaitReady(100 * time.Millisecond); err != nil { 199 | defer srv.Stop() 200 | return nil, err 201 | } 202 | 203 | conn, e := redigo.Dial("tcp", srv.Addr()) 204 | if e != nil { 205 | defer srv.Stop() 206 | return nil, e 207 | } 208 | defer conn.Close() 209 | 210 | _, err = conn.Do("SLAVEOF", "127.0.0.1", r.Port()) 211 | if err != nil { 212 | defer srv.Stop() 213 | return nil, err 214 | } 215 | 216 | // wait for the slave to be in sync 217 | for i := 0; i < 100; i++ { 218 | info, err := srv.Info() 219 | if err != nil { 220 | defer srv.Stop() 221 | return nil, err 222 | } 223 | 224 | linkStatus := info["master_link_status"] 225 | if linkStatus == "up" { 226 | break 227 | } 228 | time.Sleep(50 * time.Millisecond) 229 | 230 | } 231 | 232 | return srv, nil 233 | 234 | } 235 | 236 | // Get the port of this server 237 | func (r Server) Port() uint16 { 238 | return r.port 239 | } 240 | 241 | // Addr returns the address of the server as a host:port string 242 | func (r Server) Addr() string { 243 | return fmt.Sprintf("localhost:%d", r.port) 244 | } 245 | 246 | func init() { 247 | rand.Seed(time.Now().UnixNano()) 248 | } 249 | -------------------------------------------------------------------------------- /disposable_redis_test.go: -------------------------------------------------------------------------------- 1 | package disposable_redis 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | redigo "github.com/garyburd/redigo/redis" 9 | ) 10 | 11 | // make sure we can't start 2 servers on the same port 12 | func TestFailure(t *testing.T) { 13 | r, err := NewServerRandomPort() 14 | if err != nil { 15 | t.Fatal("Could not bind 1:", err) 16 | } 17 | defer r.Stop() 18 | 19 | if err != nil { 20 | t.Error("Could not connet", err) 21 | } 22 | 23 | r2, err2 := NewServer(r.Port()) 24 | if err2 == nil { 25 | t.Error("We sohuldn't be able not create second instance") 26 | r2.Stop() 27 | } 28 | 29 | } 30 | 31 | func TestDisposableRedis(t *testing.T) { 32 | 33 | r, err := NewServerRandomPort() 34 | if err != nil { 35 | t.Fatal("Could not create random server") 36 | } 37 | 38 | defer r.Stop() 39 | 40 | if r.Port() < 1024 { 41 | t.Fatalf("Invalid port") 42 | } 43 | 44 | if err = r.WaitReady(50 * time.Millisecond); err != nil { 45 | t.Fatalf("Could not connect to server in time") 46 | } 47 | 48 | conn, err := redigo.Dial("tcp", fmt.Sprintf("localhost:%d", r.Port())) 49 | if err != nil { 50 | t.Fatalf("Could not connect to disposable server", err) 51 | } 52 | 53 | if _, err := conn.Do("PING"); err != nil { 54 | t.Fatalf("Could not talk to redis") 55 | } 56 | conn.Close() 57 | 58 | err = r.Stop() 59 | if err != nil { 60 | t.Fatal("Could not stop server", err) 61 | } 62 | 63 | } 64 | 65 | func TestServerNewSlaveOf(t *testing.T) { 66 | 67 | master, err := NewServerRandomPort() 68 | if err != nil { 69 | t.Fatal("Could not create random server") 70 | } 71 | 72 | defer master.Stop() 73 | 74 | if err = master.WaitReady(50 * time.Millisecond); err != nil { 75 | t.Fatalf("Could not connect to server in time") 76 | } 77 | 78 | slave, err := master.NewSlaveOf() 79 | if err != nil { 80 | t.Fatal("Could not create slave:", err) 81 | } 82 | defer slave.Stop() 83 | 84 | conn, err := redigo.Dial("tcp", fmt.Sprintf(master.Addr())) 85 | if err != nil { 86 | t.Fatalf("Could not connect to disposable server", err) 87 | } 88 | 89 | if _, err := conn.Do("SET", "foo", "bar"); err != nil { 90 | t.Fatalf("Could not talk to master") 91 | } 92 | conn.Close() 93 | 94 | conn, err = redigo.Dial("tcp", fmt.Sprintf("localhost:%d", slave.Port())) 95 | if err != nil { 96 | t.Fatalf("Could not connect to slave server", err) 97 | } 98 | defer conn.Close() 99 | 100 | val, err := redigo.String(conn.Do("GET", "foo")) 101 | if err != nil { 102 | t.Fatal("Could not stop server", err) 103 | } 104 | 105 | if val != "bar" { 106 | t.Fatalf("Replication didn't work: ", val) 107 | } 108 | 109 | } 110 | 111 | func ExampleServer() { 112 | 113 | // create a new server on a random port 114 | r, err := NewServerRandomPort() 115 | if err != nil { 116 | panic("Could not create random server") 117 | } 118 | 119 | // we must remember to kill it at the end, or we'll have zombie redises 120 | defer r.Stop() 121 | 122 | // wait for our server to be ready for serving, for at least 50 ms. 123 | // This gives redis time to initialize itself and listen 124 | if err = r.WaitReady(50 * time.Millisecond); err != nil { 125 | panic("Couldn't connect to instance") 126 | } 127 | 128 | //now we can just connect and talk to it 129 | conn, err := redigo.Dial("tcp", fmt.Sprintf("localhost:%d", r.Port())) 130 | if err != nil { 131 | panic(err) 132 | } 133 | 134 | fmt.Println(redigo.String(conn.Do("SET", "foo", "bar"))) 135 | //Output: OK 136 | 137 | } 138 | --------------------------------------------------------------------------------