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  }