github.com/uber/kraken@v0.1.4/tracker/peerstore/redis.go (about) 1 // Copyright (c) 2016-2019 Uber Technologies, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package peerstore 15 16 import ( 17 "errors" 18 "fmt" 19 "strconv" 20 "strings" 21 22 "github.com/uber/kraken/core" 23 "github.com/uber/kraken/utils/log" 24 "github.com/uber/kraken/utils/randutil" 25 26 "github.com/andres-erbsen/clock" 27 "github.com/garyburd/redigo/redis" 28 ) 29 30 func peerSetKey(h core.InfoHash, window int64) string { 31 return fmt.Sprintf("peerset:%s:%d", h.String(), window) 32 } 33 34 func serializePeer(p *core.PeerInfo) string { 35 var completeBit int 36 if p.Complete { 37 completeBit = 1 38 } 39 return fmt.Sprintf("%s:%s:%d:%d", p.PeerID.String(), p.IP, p.Port, completeBit) 40 } 41 42 type peerIdentity struct { 43 peerID core.PeerID 44 ip string 45 port int 46 } 47 48 func deserializePeer(s string) (id peerIdentity, complete bool, err error) { 49 parts := strings.Split(s, ":") 50 if len(parts) != 4 { 51 return id, false, fmt.Errorf("invalid peer encoding: expected 'pid:ip:port:complete'") 52 } 53 peerID, err := core.NewPeerID(parts[0]) 54 if err != nil { 55 return id, false, fmt.Errorf("parse peer id: %s", err) 56 } 57 ip := parts[1] 58 port, err := strconv.Atoi(parts[2]) 59 if err != nil { 60 return id, false, fmt.Errorf("parse port: %s", err) 61 } 62 id = peerIdentity{peerID, ip, port} 63 complete = parts[3] == "1" 64 return id, complete, nil 65 } 66 67 // RedisStore is a Store backed by Redis. 68 type RedisStore struct { 69 config RedisConfig 70 pool *redis.Pool 71 clk clock.Clock 72 } 73 74 // NewRedisStore creates a new RedisStore. 75 func NewRedisStore(config RedisConfig, clk clock.Clock) (*RedisStore, error) { 76 config.applyDefaults() 77 78 if config.Addr == "" { 79 return nil, errors.New("invalid config: missing addr") 80 } 81 82 s := &RedisStore{ 83 config: config, 84 pool: &redis.Pool{ 85 Dial: func() (redis.Conn, error) { 86 // TODO Add options 87 return redis.Dial( 88 "tcp", 89 config.Addr, 90 redis.DialConnectTimeout(config.DialTimeout), 91 redis.DialReadTimeout(config.ReadTimeout), 92 redis.DialWriteTimeout(config.WriteTimeout)) 93 }, 94 MaxIdle: config.MaxIdleConns, 95 MaxActive: config.MaxActiveConns, 96 IdleTimeout: config.IdleConnTimeout, 97 Wait: true, 98 }, 99 clk: clk, 100 } 101 102 // Ensure we can connect to Redis. 103 c, err := s.pool.Dial() 104 if err != nil { 105 return nil, fmt.Errorf("dial redis: %s", err) 106 } 107 c.Close() 108 109 return s, nil 110 } 111 112 func (s *RedisStore) curPeerSetWindow() int64 { 113 t := s.clk.Now().Unix() 114 return t - (t % int64(s.config.PeerSetWindowSize.Seconds())) 115 } 116 117 func (s *RedisStore) peerSetWindows() []int64 { 118 cur := s.curPeerSetWindow() 119 ws := make([]int64, s.config.MaxPeerSetWindows) 120 for i := range ws { 121 ws[i] = cur - int64(i)*int64(s.config.PeerSetWindowSize.Seconds()) 122 } 123 return ws 124 } 125 126 // UpdatePeer writes p to Redis with a TTL. 127 func (s *RedisStore) UpdatePeer(h core.InfoHash, p *core.PeerInfo) error { 128 c := s.pool.Get() 129 defer c.Close() 130 131 w := s.curPeerSetWindow() 132 expireAt := w + int64(s.config.PeerSetWindowSize.Seconds())*int64(s.config.MaxPeerSetWindows) 133 134 // Add p to the current window. 135 k := peerSetKey(h, w) 136 137 if err := c.Send("SADD", k, serializePeer(p)); err != nil { 138 return fmt.Errorf("send SADD: %s", err) 139 } 140 if err := c.Send("EXPIREAT", k, expireAt); err != nil { 141 return fmt.Errorf("send EXPIREAT: %s", err) 142 } 143 if err := c.Flush(); err != nil { 144 return fmt.Errorf("flush: %s", err) 145 } 146 if _, err := c.Receive(); err != nil { 147 return fmt.Errorf("SADD: %s", err) 148 } 149 if _, err := c.Receive(); err != nil { 150 return fmt.Errorf("EXPIREAT: %s", err) 151 } 152 return nil 153 } 154 155 // GetPeers returns at most n PeerInfos associated with h. 156 func (s *RedisStore) GetPeers(h core.InfoHash, n int) ([]*core.PeerInfo, error) { 157 c := s.pool.Get() 158 defer c.Close() 159 160 // Try to sample n peers from each window in randomized order until we have 161 // collected n distinct peers. This achieves random sampling across multiple 162 // windows. 163 // TODO(codyg): One limitation of random window sampling is we're no longer 164 // guaranteed to include the latest completion bits. A simple way to mitigate 165 // this is to decrease the number of windows. 166 windows := s.peerSetWindows() 167 randutil.ShuffleInt64s(windows) 168 169 // Eliminate duplicates from other windows and collapses complete bits. 170 selected := make(map[peerIdentity]bool) 171 172 for i := 0; len(selected) < n && i < len(windows); i++ { 173 k := peerSetKey(h, windows[i]) 174 result, err := redis.Strings(c.Do("SRANDMEMBER", k, n-len(selected))) 175 if err == redis.ErrNil { 176 continue 177 } else if err != nil { 178 return nil, err 179 } 180 for _, s := range result { 181 id, complete, err := deserializePeer(s) 182 if err != nil { 183 log.Errorf("Error deserializing peer %q: %s", s, err) 184 continue 185 } 186 selected[id] = selected[id] || complete 187 } 188 } 189 190 var peers []*core.PeerInfo 191 for id, complete := range selected { 192 p := core.NewPeerInfo(id.peerID, id.ip, id.port, false, complete) 193 peers = append(peers, p) 194 } 195 return peers, nil 196 }