├── .gitignore ├── LICENSE ├── README.md ├── sentinel_tunnel_configuration_example.json ├── sentinel_tunnelling_client.go ├── st_logger └── st_logger.go └── st_sentinel_connection └── st_sentinel_connection.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | sentinel_tunnel 27 | log.txt 28 | commit.txt 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Redis Labs 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sentinel Tunnel 2 | Sentinel Tunnel is a tool that allows you using the Redis Sentinel capabilities, without any code modifications to your application. 3 | 4 | Redis Sentinel provides high availability (HA) for Redis. In practical terms this means that using Sentinel you can create a Redis deployment that tolerates certain kinds of failures without human intervention. For more information about Redis Sentinel refer to: https://redis.io/topics/sentinel. 5 | 6 | ## Overview 7 | 8 | Connecting an application to a Sentinel-managed Redis deployment is usually done with a Sentinel-aware Redis client. While most Redis clients do support Sentinel, the application needs to call a specialized connection management interface of the client to use it. When one wishes to migrate to a Sentinel-enabled Redis deployment, she/he must modify the application to use Sentinel-based connection management. Moreover, when the application uses a Redis client that does not provide support for Sentinel, the migration becomes that much more complex because it also requires replacing the entire client library. 9 | 10 | Sentinel Tunnel (ST) discovers the current Redis master via Sentinel, and creates a TCP tunnel between a local port on the client computer to the master. When the master fails, ST disconnects your client's connection. When the client reconnects, ST rediscovers the current master via Sentinel and provides the new address. 11 | The following diagram illustrates that: 12 | 13 | ``` _ 14 | +----------------------------------------------------------+ _,-'*'-,_ 15 | | +---------------------------------------+ | _,-._ (_ o v # _) 16 | | | +--------+ | +----------+ | +----------+ _,-' * `-._ (_'-,_,-'_) 17 | | |Application code | Redis | | | Sentinel | | | Redis | + (_ O # _) (_'|,_,|'_) 18 | | |(uses regular connections) | client +<--->+ Tunnel +<----->+ Sentinel +<--+---->(_`-._ ^ _,-'_) '-,_,-' 19 | | | +--------+ | +----------+ | +----------+ | | (_`|._`|'_,|'_) 20 | | +---------------------------------------+ | +----------+ | (_`|._`|'_,|'_) 21 | | Application node | +----------+ `-._`|'_,-' 22 | +----------------------------------------------------------+ `-' 23 | ``` 24 | ## Install 25 | 26 | Make sure you have a working Go environment - [see the installation instructions here](http://golang.org/doc/install.html). 27 | 28 | To install `sentinel_tunnel`, run: 29 | ```bash 30 | $ go get github.com/RedisLabs/sentinel_tunnel 31 | ``` 32 | Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can be easily used: 33 | 34 | ```bash 35 | $ export PATH=$PATH:$GOPATH/bin 36 | ``` 37 | 38 | ## Configure 39 | The code contains an example configuration file named `sentinel_tunnel_configuration_example.json`. The configuration file is a json file that contains the following information: 40 | 41 | * The Sentinels addresses list 42 | * The list of databases and their corresponding local port 43 | 44 | For example, the following config file contains two Sentinel addresses and two databases. When the client connects to the local port `12345` it actually connect to `db1`. 45 | 46 | ```json 47 | { 48 | "Sentinels_addresses_list":[ 49 | "node1.local:8001", 50 | "node2.local:8001" 51 | ], 52 | "Databases":[ 53 | { 54 | "Name":"db1", 55 | "Local_port":"12345" 56 | }, 57 | { 58 | "Name":"db2", 59 | "Local_port":"12346" 60 | } 61 | ] 62 | } 63 | ``` 64 | 65 | ## Run 66 | In order to run `sentinel_tunnel`: 67 | 68 | ```bash 69 | $ ./sentinel_tunnel 70 | ``` 71 | Set `log_file_path` to `/dev/null` for no logs. 72 | 73 | ## License 74 | 75 | [2-Clause BSD](LICENSE) 76 | -------------------------------------------------------------------------------- /sentinel_tunnel_configuration_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sentinels_addresses_list":[ 3 | "node1.local:8001", 4 | "node2.local:8001" 5 | ], 6 | "Databases":[ 7 | { 8 | "Name":"db1", 9 | "Local_port":"12345" 10 | }, 11 | { 12 | "Name":"db2", 13 | "Local_port":"12346" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /sentinel_tunnelling_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | // "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/RedisLabs/sentinel_tunnel/st_logger" 8 | "github.com/RedisLabs/sentinel_tunnel/st_sentinel_connection" 9 | "io" 10 | "io/ioutil" 11 | "net" 12 | "os" 13 | "time" 14 | ) 15 | 16 | type SentinelTunnellingDbConfig struct { 17 | Name string 18 | Local_port string 19 | } 20 | 21 | type SentinelTunnellingConfiguration struct { 22 | Sentinels_addresses_list []string 23 | Databases []SentinelTunnellingDbConfig 24 | } 25 | 26 | type SentinelTunnellingClient struct { 27 | configuration SentinelTunnellingConfiguration 28 | sentinel_connection *st_sentinel_connection.Sentinel_connection 29 | } 30 | 31 | type get_db_address_by_name_function func(db_name string) (string, error) 32 | 33 | func NewSentinelTunnellingClient(config_file_location string) *SentinelTunnellingClient { 34 | data, err := ioutil.ReadFile(config_file_location) 35 | if err != nil { 36 | st_logger.WriteLogMessage(st_logger.FATAL, "an error has occur during configuration read", 37 | err.Error()) 38 | } 39 | 40 | Tunnelling_client := SentinelTunnellingClient{} 41 | err = json.Unmarshal(data, &(Tunnelling_client.configuration)) 42 | if err != nil { 43 | st_logger.WriteLogMessage(st_logger.FATAL, "an error has occur during configuration read,", 44 | err.Error()) 45 | } 46 | 47 | Tunnelling_client.sentinel_connection, err = 48 | st_sentinel_connection.NewSentinelConnection(Tunnelling_client.configuration.Sentinels_addresses_list) 49 | if err != nil { 50 | st_logger.WriteLogMessage(st_logger.FATAL, "an error has occur, ", 51 | err.Error()) 52 | } 53 | 54 | st_logger.WriteLogMessage(st_logger.INFO, "done initializing Tunnelling") 55 | 56 | return &Tunnelling_client 57 | } 58 | 59 | func createTunnelling(conn1 net.Conn, conn2 net.Conn) { 60 | io.Copy(conn1, conn2) 61 | conn1.Close() 62 | conn2.Close() 63 | } 64 | 65 | func handleConnection(c net.Conn, db_name string, 66 | get_db_address_by_name get_db_address_by_name_function) { 67 | db_address, err := get_db_address_by_name(db_name) 68 | if err != nil { 69 | st_logger.WriteLogMessage(st_logger.ERROR, "cannot get db address for ", db_name, 70 | ",", err.Error()) 71 | c.Close() 72 | return 73 | } 74 | db_conn, err := net.Dial("tcp", db_address) 75 | if err != nil { 76 | st_logger.WriteLogMessage(st_logger.ERROR, "cannot connect to db ", db_name, 77 | ",", err.Error()) 78 | c.Close() 79 | return 80 | } 81 | go createTunnelling(c, db_conn) 82 | go createTunnelling(db_conn, c) 83 | } 84 | 85 | func handleSigleDbConnections(listening_port string, db_name string, 86 | get_db_address_by_name get_db_address_by_name_function) { 87 | 88 | listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", listening_port)) 89 | if err != nil { 90 | st_logger.WriteLogMessage(st_logger.FATAL, "cannot listen to port ", 91 | listening_port, err.Error()) 92 | } 93 | 94 | st_logger.WriteLogMessage(st_logger.INFO, "listening on port ", listening_port, 95 | " for connections to database: ", db_name) 96 | for { 97 | conn, err := listener.Accept() 98 | if err != nil { 99 | st_logger.WriteLogMessage(st_logger.FATAL, "cannot accept connections on port ", 100 | listening_port, err.Error()) 101 | } 102 | go handleConnection(conn, db_name, get_db_address_by_name) 103 | } 104 | 105 | } 106 | 107 | func (st_client *SentinelTunnellingClient) Start() { 108 | for _, db_conf := range st_client.configuration.Databases { 109 | go handleSigleDbConnections(db_conf.Local_port, db_conf.Name, 110 | st_client.sentinel_connection.GetAddressByDbName) 111 | } 112 | } 113 | 114 | func main() { 115 | if len(os.Args) < 3 { 116 | fmt.Println("usage : sentinel_tunnel ") 117 | return 118 | } 119 | st_logger.InitializeLogger(os.Args[2]) 120 | st_client := NewSentinelTunnellingClient(os.Args[1]) 121 | st_client.Start() 122 | for { 123 | time.Sleep(1000 * time.Millisecond) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /st_logger/st_logger.go: -------------------------------------------------------------------------------- 1 | package st_logger 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "os" 7 | ) 8 | 9 | var logger *log.Logger 10 | 11 | const ( 12 | INFO = iota 13 | ERROR = iota 14 | FATAL = iota 15 | DEBUG = iota 16 | ) 17 | 18 | func InitializeLogger(log_file_path string) { 19 | file, err := os.OpenFile(log_file_path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 20 | if err != nil { 21 | log.Fatalln("Failed to open log file", log_file_path, ":", err) 22 | } 23 | logger = log.New(file, 24 | "", 25 | log.Ldate|log.Ltime) 26 | } 27 | 28 | func WriteLogMessage(level int, message ...string) { 29 | var buffer bytes.Buffer 30 | if level == INFO { 31 | buffer.WriteString("info : ") 32 | } else if level == ERROR { 33 | buffer.WriteString("error : ") 34 | } else if level == FATAL { 35 | buffer.WriteString("fatal : ") 36 | } else if level == FATAL { 37 | buffer.WriteString("debug : ") 38 | } 39 | 40 | for _, m := range message { 41 | buffer.WriteString(m) 42 | buffer.WriteString(" ") 43 | } 44 | 45 | logger.Println(buffer.String()) 46 | 47 | if level == FATAL { 48 | logger.Println("fatal error occure commiting suicide") 49 | os.Exit(1) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /st_sentinel_connection/st_sentinel_connection.go: -------------------------------------------------------------------------------- 1 | package st_sentinel_connection 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | type Get_master_addr_reply struct { 13 | reply string 14 | err error 15 | } 16 | 17 | type Sentinel_connection struct { 18 | sentinels_addresses []string 19 | current_sentinel_connection net.Conn 20 | reader *bufio.Reader 21 | writer *bufio.Writer 22 | get_master_address_by_name_reply chan *Get_master_addr_reply 23 | get_master_address_by_name chan string 24 | } 25 | 26 | const ( 27 | client_closed = true 28 | client_not_closed = false 29 | ) 30 | 31 | func (c *Sentinel_connection) parseResponse() (request []string, err error, is_client_closed bool) { 32 | var ret []string 33 | buf, _, e := c.reader.ReadLine() 34 | if e != nil { 35 | return nil, errors.New("failed read line from client"), client_closed 36 | } 37 | if len(buf) == 0 { 38 | return nil, errors.New("failed read line from client"), client_closed 39 | } 40 | if buf[0] != '*' { 41 | return nil, errors.New("first char in mbulk is not *"), client_not_closed 42 | } 43 | mbulk_size, _ := strconv.Atoi(string(buf[1:])) 44 | if mbulk_size == -1 { 45 | return nil, errors.New("null request"), client_not_closed 46 | } 47 | ret = make([]string, mbulk_size) 48 | for i := 0; i < mbulk_size; i++ { 49 | buf1, _, e1 := c.reader.ReadLine() 50 | if e1 != nil { 51 | return nil, errors.New("failed read line from client"), client_closed 52 | } 53 | if len(buf1) == 0 { 54 | return nil, errors.New("failed read line from client"), client_closed 55 | } 56 | if buf1[0] != '$' { 57 | return nil, errors.New("first char in bulk is not $"), client_not_closed 58 | } 59 | bulk_size, _ := strconv.Atoi(string(buf1[1:])) 60 | buf2, _, e2 := c.reader.ReadLine() 61 | if e2 != nil { 62 | return nil, errors.New("failed read line from client"), client_closed 63 | } 64 | bulk := string(buf2) 65 | if len(bulk) != bulk_size { 66 | return nil, errors.New("wrong bulk size"), client_not_closed 67 | } 68 | ret[i] = bulk 69 | } 70 | return ret, nil, client_not_closed 71 | } 72 | 73 | func (c *Sentinel_connection) getMasterAddrByNameFromSentinel(db_name string) (addr []string, returned_err error, is_client_closed bool) { 74 | c.writer.WriteString("*3\r\n") 75 | c.writer.WriteString("$8\r\n") 76 | c.writer.WriteString("sentinel\r\n") 77 | c.writer.WriteString("$23\r\n") 78 | c.writer.WriteString("get-master-addr-by-name\r\n") 79 | c.writer.WriteString(fmt.Sprintf("$%d\r\n", len(db_name))) 80 | c.writer.WriteString(db_name) 81 | c.writer.WriteString("\r\n") 82 | c.writer.Flush() 83 | 84 | return c.parseResponse() 85 | } 86 | 87 | func (c *Sentinel_connection) retrieveAddressByDbName() { 88 | for db_name := range c.get_master_address_by_name { 89 | addr, err, is_client_closed := c.getMasterAddrByNameFromSentinel(db_name) 90 | if err != nil { 91 | fmt.Println("err: ", err.Error()) 92 | if !is_client_closed { 93 | c.get_master_address_by_name_reply <- &Get_master_addr_reply{ 94 | reply: "", 95 | err: errors.New("failed to retrieve db name from the sentinel, db_name:" + db_name), 96 | } 97 | } 98 | if !c.reconnectToSentinel() { 99 | c.get_master_address_by_name_reply <- &Get_master_addr_reply{ 100 | reply: "", 101 | err: errors.New("failed to connect to any of the sentinel services"), 102 | } 103 | } 104 | continue 105 | } 106 | c.get_master_address_by_name_reply <- &Get_master_addr_reply{ 107 | reply: net.JoinHostPort(addr[0], addr[1]), 108 | err: nil, 109 | } 110 | } 111 | } 112 | 113 | func (c *Sentinel_connection) reconnectToSentinel() bool { 114 | for _, sentinelAddr := range c.sentinels_addresses { 115 | 116 | if c.current_sentinel_connection != nil { 117 | c.current_sentinel_connection.Close() 118 | c.reader = nil 119 | c.writer = nil 120 | c.current_sentinel_connection = nil 121 | } 122 | 123 | var err error 124 | c.current_sentinel_connection, err = net.DialTimeout("tcp", sentinelAddr, 300*time.Millisecond) 125 | if err == nil { 126 | c.reader = bufio.NewReader(c.current_sentinel_connection) 127 | c.writer = bufio.NewWriter(c.current_sentinel_connection) 128 | return true 129 | } 130 | fmt.Println(err.Error()) 131 | } 132 | return false 133 | } 134 | 135 | func (c *Sentinel_connection) GetAddressByDbName(name string) (string, error) { 136 | c.get_master_address_by_name <- name 137 | reply := <-c.get_master_address_by_name_reply 138 | return reply.reply, reply.err 139 | } 140 | 141 | func NewSentinelConnection(addresses []string) (*Sentinel_connection, error) { 142 | connection := Sentinel_connection{ 143 | sentinels_addresses: addresses, 144 | get_master_address_by_name: make(chan string), 145 | get_master_address_by_name_reply: make(chan *Get_master_addr_reply), 146 | current_sentinel_connection: nil, 147 | reader: nil, 148 | writer: nil, 149 | } 150 | 151 | if !connection.reconnectToSentinel() { 152 | return nil, errors.New("could not connect to any sentinels") 153 | } 154 | 155 | go connection.retrieveAddressByDbName() 156 | 157 | return &connection, nil 158 | } 159 | --------------------------------------------------------------------------------