go.uber.org/yarpc@v1.72.1/yarpctest/fake_transport.go (about) 1 // Copyright (c) 2022 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package yarpctest 22 23 import ( 24 "fmt" 25 "sync" 26 27 "go.uber.org/yarpc/api/peer" 28 "go.uber.org/yarpc/pkg/lifecycle" 29 ) 30 31 // FakeTransportOption is an option for NewFakeTransport. 32 type FakeTransportOption func(*FakeTransport) 33 34 // NopTransportOption returns a no-op option for NewFakeTransport. 35 // The option exists to verify that options work. 36 func NopTransportOption(nopOption string) FakeTransportOption { 37 return func(t *FakeTransport) { 38 t.nopOption = nopOption 39 } 40 } 41 42 // InitialConnectionStatus specifies the initial connection status for new 43 // peers of this transport. This is Available by default. With the status set 44 // to Unavailable, the test may manual simmulate connection and disconnection 45 // with the SimulateConnect and SimulateDisconnect methods. 46 func InitialConnectionStatus(s peer.ConnectionStatus) FakeTransportOption { 47 return func(t *FakeTransport) { 48 t.initialConnectionStatus = s 49 } 50 } 51 52 // RetainErrors specifies an error for RetainPeer to return for the given 53 // addresses. 54 func RetainErrors(err error, addrs []string) FakeTransportOption { 55 return func(t *FakeTransport) { 56 for _, addr := range addrs { 57 t.retainErrors[addr] = err 58 } 59 } 60 } 61 62 // ReleaseErrors specifies an error for ReleasePeer to return for the given 63 // addresses. 64 func ReleaseErrors(err error, addrs []string) FakeTransportOption { 65 return func(t *FakeTransport) { 66 for _, addr := range addrs { 67 t.releaseErrors[addr] = err 68 } 69 } 70 } 71 72 // NewFakeTransport returns a fake transport. 73 func NewFakeTransport(opts ...FakeTransportOption) *FakeTransport { 74 t := &FakeTransport{ 75 once: lifecycle.NewOnce(), 76 initialConnectionStatus: peer.Available, 77 initialPeerConnectionStatuses: make(map[string]peer.ConnectionStatus), 78 79 peers: make(map[string]*FakePeer), 80 retainErrors: make(map[string]error), 81 releaseErrors: make(map[string]error), 82 pendingStatusChanges: make(chan struct{}, 1), 83 done: make(chan struct{}), 84 } 85 for _, opt := range opts { 86 opt(t) 87 } 88 return t 89 } 90 91 // FakeTransport is a fake transport. 92 type FakeTransport struct { 93 nopOption string 94 initialConnectionStatus peer.ConnectionStatus 95 initialPeerConnectionStatuses map[string]peer.ConnectionStatus 96 retainErrors map[string]error 97 releaseErrors map[string]error 98 99 once *lifecycle.Once 100 mu sync.RWMutex 101 peers map[string]*FakePeer 102 changesQueue []statusChange 103 pendingStatusChanges chan struct{} 104 done chan struct{} 105 } 106 107 // NopOption returns the configured nopOption. It's fake. 108 func (t *FakeTransport) NopOption() string { 109 return t.nopOption 110 } 111 112 // SimulateRetainError leaves a note that any subsequent Retain for a 113 // particular address should return an error. 114 func (t *FakeTransport) SimulateRetainError(id peer.Identifier, err error) { 115 t.retainErrors[id.Identifier()] = err 116 } 117 118 // SimulateReleaseError leaves a note that any subsequent Release for a particular 119 // address should return an error. 120 func (t *FakeTransport) SimulateReleaseError(id peer.Identifier, err error) { 121 t.releaseErrors[id.Identifier()] = err 122 } 123 124 // SimulateStatusChange simulates a connection or disconnection to the peer, 125 // marking the peer connection status and notifying all subscribers. 126 func (t *FakeTransport) SimulateStatusChange(id peer.Identifier, status peer.ConnectionStatus) { 127 t.Peer(id).simulateStatusChange(status) 128 } 129 130 // SimulateConnect simulates a connection to the peer, marking the peer as 131 // available and notifying subscribers. 132 func (t *FakeTransport) SimulateConnect(id peer.Identifier) { 133 t.Peer(id).simulateConnect() 134 } 135 136 // SimulateDisconnect simulates a disconnection to the peer, marking the peer 137 // as unavailable and notifying subscribers. 138 func (t *FakeTransport) SimulateDisconnect(id peer.Identifier) { 139 t.Peer(id).simulateDisconnect() 140 } 141 142 // Peer returns the persistent peer object for that peer identifier for the 143 // lifetime of the fake transport. 144 func (t *FakeTransport) Peer(id peer.Identifier) *FakePeer { 145 t.mu.Lock() 146 defer t.mu.Unlock() 147 148 if p, ok := t.peers[id.Identifier()]; ok { 149 return p 150 } 151 p := &FakePeer{ 152 id: id, 153 status: peer.Status{ 154 ConnectionStatus: t.initialConnectionStatus, 155 }, 156 } 157 t.peers[id.Identifier()] = p 158 return p 159 } 160 161 // RetainPeer returns a fake peer. 162 func (t *FakeTransport) RetainPeer(id peer.Identifier, ps peer.Subscriber) (peer.Peer, error) { 163 if err := t.retainErrors[id.Identifier()]; err != nil { 164 return nil, err 165 } 166 peer := t.Peer(id) 167 peer.subscribe(ps) 168 t.enqueue(statusChange{ 169 Peer: peer, 170 Status: t.getInitialStatus(id.Identifier()), 171 }) 172 173 t.scheduleFlush() 174 return peer, nil 175 } 176 177 func (t *FakeTransport) getInitialStatus(addr string) peer.ConnectionStatus { 178 if status, ok := t.initialPeerConnectionStatuses[addr]; ok { 179 return status 180 } 181 return t.initialConnectionStatus 182 } 183 184 // ReleasePeer does nothing. 185 func (t *FakeTransport) ReleasePeer(id peer.Identifier, ps peer.Subscriber) error { 186 peer := t.Peer(id) 187 188 t.mu.Lock() 189 defer t.mu.Unlock() 190 191 if err := t.releaseErrors[id.Identifier()]; err != nil { 192 return err 193 } 194 195 if count := peer.unsubscribe(ps); count == 0 { 196 return fmt.Errorf("no such subscriber") 197 } else if count > 1 { 198 return fmt.Errorf("extra subscribers: %d", count-1) 199 } 200 201 t.scheduleFlush() 202 return nil 203 } 204 205 // Start spins up a goroutine to asynchronously flush status change notifications. 206 // 207 // If you do not start a fake dialer, you must call Flush explicitly. 208 func (t *FakeTransport) Start() error { 209 return t.once.Start(func() error { 210 go t.monitor() 211 return nil 212 }) 213 } 214 215 // Stop shuts down the fake dialer, allowing its status change notification 216 // loop to exit. 217 func (t *FakeTransport) Stop() error { 218 return t.once.Stop(func() error { 219 close(t.done) 220 return nil 221 }) 222 } 223 224 // IsRunning returns whether the fake transport is running. 225 func (t *FakeTransport) IsRunning() bool { 226 return t.once.IsRunning() 227 } 228 229 func (t *FakeTransport) scheduleFlush() { 230 select { 231 case t.pendingStatusChanges <- struct{}{}: 232 default: 233 } 234 } 235 236 func (t *FakeTransport) monitor() { 237 Loop: 238 for { 239 select { 240 case <-t.done: 241 break Loop 242 case <-t.pendingStatusChanges: 243 t.Flush() 244 } 245 } 246 } 247 248 type statusChange struct { 249 Peer *FakePeer 250 Status peer.ConnectionStatus 251 } 252 253 // Flush effects all queued status changes from retaining or releasing peers. 254 // 255 // Calling RetainPeer and ReleasePeer schedules a peer status change and its 256 // notifications. 257 // Concrete dialer implementations dispatch these notifications from a 258 // goroutine and subscribers may obtain a lock on the peer status. 259 // For testability, the fake transport queues these changes and calling Flush 260 // dispatches the notifications synchronously, but still off the RetainPeer and 261 // ReleasePeer stacks. 262 func (t *FakeTransport) Flush() { 263 for _, change := range t.dequeue() { 264 change.Peer.simulateStatusChange(change.Status) 265 } 266 } 267 268 func (t *FakeTransport) enqueue(change statusChange) { 269 t.mu.Lock() 270 defer t.mu.Unlock() 271 272 t.changesQueue = append(t.changesQueue, change) 273 } 274 275 func (t *FakeTransport) dequeue() []statusChange { 276 t.mu.Lock() 277 defer t.mu.Unlock() 278 279 queue := t.changesQueue 280 t.changesQueue = make([]statusChange, 0) 281 return queue 282 }