github.com/uber/kraken@v0.1.4/lib/torrent/scheduler/connstate/state.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 connstate 15 16 import ( 17 "errors" 18 "time" 19 20 "github.com/andres-erbsen/clock" 21 "github.com/uber/kraken/core" 22 "github.com/uber/kraken/lib/torrent/networkevent" 23 "github.com/uber/kraken/lib/torrent/scheduler/conn" 24 "go.uber.org/zap" 25 ) 26 27 // State errors. 28 var ( 29 ErrTorrentAtCapacity = errors.New("torrent is at capacity") 30 ErrConnAlreadyPending = errors.New("conn is already pending") 31 ErrConnAlreadyActive = errors.New("conn is already active") 32 ErrConnClosed = errors.New("conn is closed") 33 ErrInvalidActiveTransition = errors.New("conn must be pending to transition to active") 34 ErrTooManyMutualConns = errors.New("conn has too many mutual connections") 35 36 // This should NEVER happen. 37 errUnknownStatus = errors.New("invariant violation: unknown status") 38 ) 39 40 type status int 41 42 const ( 43 // _uninit indicates the connection is uninitialized. This is the default 44 // status for empty entries. 45 _uninit status = iota 46 _pending 47 _active 48 ) 49 50 type entry struct { 51 status status 52 conn *conn.Conn 53 } 54 55 type connKey struct { 56 hash core.InfoHash 57 peerID core.PeerID 58 } 59 60 type blacklistEntry struct { 61 expiration time.Time 62 } 63 64 func (e *blacklistEntry) Blacklisted(now time.Time) bool { 65 return e.Remaining(now) > 0 66 } 67 68 func (e *blacklistEntry) Remaining(now time.Time) time.Duration { 69 return e.expiration.Sub(now) 70 } 71 72 // State provides connection lifecycle management and enforces connection 73 // limits. A connection to a peer is identified by torrent info hash and peer id. 74 // Each connection may exist in the following states: pending, active, or 75 // blacklisted. Pending connections are unestablished connections which "reserve" 76 // connection capacity until they are done handshaking. Active connections are 77 // established connections. Blacklisted connections are failed connections which 78 // should be skipped in each peer handout. 79 // 80 // Note, State is NOT thread-safe. Synchronization must be provided by the client. 81 type State struct { 82 config Config 83 clk clock.Clock 84 netevents networkevent.Producer 85 localPeerID core.PeerID 86 logger *zap.SugaredLogger 87 88 // All pending or active conns. These count towards conn capacity. 89 conns map[core.InfoHash]map[core.PeerID]entry 90 91 // All blacklisted conns. These do not count towards conn capacity. 92 blacklist map[connKey]*blacklistEntry 93 } 94 95 // New creates a new State. 96 func New( 97 config Config, 98 clk clock.Clock, 99 localPeerID core.PeerID, 100 netevents networkevent.Producer, 101 logger *zap.SugaredLogger) *State { 102 103 config = config.applyDefaults() 104 105 return &State{ 106 config: config, 107 clk: clk, 108 netevents: netevents, 109 localPeerID: localPeerID, 110 logger: logger, 111 conns: make(map[core.InfoHash]map[core.PeerID]entry), 112 blacklist: make(map[connKey]*blacklistEntry), 113 } 114 } 115 116 // ActiveConns returns a list of all active connections. 117 func (s *State) ActiveConns() []*conn.Conn { 118 var active []*conn.Conn 119 for _, peers := range s.conns { 120 for _, e := range peers { 121 if e.status == _active { 122 active = append(active, e.conn) 123 } 124 } 125 } 126 return active 127 } 128 129 // Saturated returns true if h is at capacity and all the conns are active. 130 func (s *State) Saturated(h core.InfoHash) bool { 131 peers, ok := s.conns[h] 132 if !ok { 133 return false 134 } 135 var active int 136 for _, e := range peers { 137 if e.status == _active { 138 active++ 139 } 140 } 141 return active == s.config.MaxOpenConnectionsPerTorrent 142 } 143 144 // Blacklist blacklists peerID/h for the configured BlacklistDuration. 145 // Returns error if the connection is already blacklisted. 146 func (s *State) Blacklist(peerID core.PeerID, h core.InfoHash) error { 147 if s.config.DisableBlacklist { 148 return nil 149 } 150 151 k := connKey{h, peerID} 152 if e, ok := s.blacklist[k]; ok && e.Blacklisted(s.clk.Now()) { 153 return errors.New("conn is already blacklisted") 154 } 155 s.blacklist[k] = &blacklistEntry{s.clk.Now().Add(s.config.BlacklistDuration)} 156 157 s.log("peer", peerID, "hash", h).Infof( 158 "Connection blacklisted for %s", s.config.BlacklistDuration) 159 s.netevents.Produce( 160 networkevent.BlacklistConnEvent(h, s.localPeerID, peerID, s.config.BlacklistDuration)) 161 162 return nil 163 } 164 165 // Blacklisted returns true if peerID/h is blacklisted. 166 func (s *State) Blacklisted(peerID core.PeerID, h core.InfoHash) bool { 167 e, ok := s.blacklist[connKey{h, peerID}] 168 return ok && e.Blacklisted(s.clk.Now()) 169 } 170 171 // ClearBlacklist un-blacklists all connections for h. 172 func (s *State) ClearBlacklist(h core.InfoHash) { 173 for k := range s.blacklist { 174 if k.hash == h { 175 delete(s.blacklist, k) 176 } 177 } 178 } 179 180 // AddPending sets the connection for peerID/h as pending and reserves capacity 181 // for it. 182 func (s *State) AddPending(peerID core.PeerID, h core.InfoHash, neighbors []core.PeerID) error { 183 if len(s.conns[h]) == s.config.MaxOpenConnectionsPerTorrent { 184 return ErrTorrentAtCapacity 185 } 186 switch s.get(h, peerID).status { 187 case _uninit: 188 if s.numMutualConns(h, neighbors) > s.config.MaxMutualConnections { 189 return ErrTooManyMutualConns 190 } 191 s.put(h, peerID, entry{status: _pending}) 192 s.log("hash", h, "peer", peerID).Infof( 193 "Added pending conn, capacity now at %d", s.capacity(h)) 194 return nil 195 case _pending: 196 return ErrConnAlreadyPending 197 case _active: 198 return ErrConnAlreadyActive 199 default: 200 return errUnknownStatus 201 } 202 } 203 204 // DeletePending deletes the pending connection for peerID/h and frees capacity. 205 func (s *State) DeletePending(peerID core.PeerID, h core.InfoHash) { 206 if s.get(h, peerID).status != _pending { 207 return 208 } 209 s.delete(h, peerID) 210 s.log("hash", h, "peer", peerID).Infof( 211 "Deleted pending conn, capacity now at %d", s.capacity(h)) 212 } 213 214 // MovePendingToActive sets a previously pending connection as active. 215 func (s *State) MovePendingToActive(c *conn.Conn) error { 216 if c.IsClosed() { 217 return ErrConnClosed 218 } 219 if s.get(c.InfoHash(), c.PeerID()).status != _pending { 220 return ErrInvalidActiveTransition 221 } 222 s.put(c.InfoHash(), c.PeerID(), entry{status: _active, conn: c}) 223 224 s.log("hash", c.InfoHash(), "peer", c.PeerID()).Info("Moved conn from pending to active") 225 s.netevents.Produce(networkevent.AddActiveConnEvent(c.InfoHash(), s.localPeerID, c.PeerID())) 226 227 return nil 228 } 229 230 // DeleteActive deletes c. No-ops if c is not an active conn. 231 func (s *State) DeleteActive(c *conn.Conn) { 232 e := s.get(c.InfoHash(), c.PeerID()) 233 if e.status != _active { 234 return 235 } 236 if e.conn != c { 237 // It is possible that some new conn shares the same hash/peer as the old conn, 238 // so we need to make sure we're deleting the right one. 239 return 240 } 241 s.delete(c.InfoHash(), c.PeerID()) 242 243 s.log("hash", c.InfoHash(), "peer", c.PeerID()).Infof( 244 "Deleted active conn, capacity now at %d", s.capacity(c.InfoHash())) 245 s.netevents.Produce(networkevent.DropActiveConnEvent( 246 c.InfoHash(), s.localPeerID, c.PeerID())) 247 } 248 249 func (s *State) numMutualConns(h core.InfoHash, neighbors []core.PeerID) int { 250 var n int 251 for _, id := range neighbors { 252 e := s.get(h, id) 253 if e.status == _pending || e.status == _active { 254 n++ 255 } 256 } 257 return n 258 } 259 260 // BlacklistedConn represents a connection which has been blacklisted. 261 type BlacklistedConn struct { 262 PeerID core.PeerID `json:"peer_id"` 263 InfoHash core.InfoHash `json:"info_hash"` 264 Remaining time.Duration `json:"remaining"` 265 } 266 267 // BlacklistSnapshot returns a snapshot of all valid blacklist entries. 268 func (s *State) BlacklistSnapshot() []BlacklistedConn { 269 var conns []BlacklistedConn 270 for k, e := range s.blacklist { 271 c := BlacklistedConn{ 272 PeerID: k.peerID, 273 InfoHash: k.hash, 274 Remaining: e.Remaining(s.clk.Now()), 275 } 276 conns = append(conns, c) 277 } 278 return conns 279 } 280 281 func (s *State) get(h core.InfoHash, peerID core.PeerID) entry { 282 peers, ok := s.conns[h] 283 if !ok { 284 return entry{} 285 } 286 return peers[peerID] 287 } 288 289 func (s *State) put(h core.InfoHash, peerID core.PeerID, e entry) { 290 peers, ok := s.conns[h] 291 if !ok { 292 peers = make(map[core.PeerID]entry) 293 s.conns[h] = peers 294 } 295 peers[peerID] = e 296 } 297 298 func (s *State) delete(h core.InfoHash, peerID core.PeerID) { 299 peers, ok := s.conns[h] 300 if !ok { 301 return 302 } 303 delete(peers, peerID) 304 if len(peers) == 0 { 305 delete(s.conns, h) 306 } 307 } 308 309 func (s *State) capacity(h core.InfoHash) int { 310 return s.config.MaxOpenConnectionsPerTorrent - len(s.conns[h]) 311 } 312 313 func (s *State) log(args ...interface{}) *zap.SugaredLogger { 314 return s.logger.With(args...) 315 }