| /* | |
| * | |
| * Copyright 2024 gRPC authors. | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| * | |
| */ | |
| package testutils | |
| import ( | |
| "context" | |
| "net" | |
| "sync" | |
| "google.golang.org/grpc/grpclog" | |
| ) | |
| var logger = grpclog.Component("testutils") | |
| // BlockingDialer is a dialer that waits for Resume() to be called before | |
| // dialing. | |
| type BlockingDialer struct { | |
| // mu protects holds. | |
| mu sync.Mutex | |
| // holds maps network addresses to a list of holds for that address. | |
| holds map[string][]*Hold | |
| } | |
| // NewBlockingDialer returns a dialer that waits for Resume() to be called | |
| // before dialing. | |
| func NewBlockingDialer() *BlockingDialer { | |
| return &BlockingDialer{ | |
| holds: make(map[string][]*Hold), | |
| } | |
| } | |
| // DialContext implements a context dialer for use with grpc.WithContextDialer | |
| // dial option for a BlockingDialer. | |
| func (d *BlockingDialer) DialContext(ctx context.Context, addr string) (net.Conn, error) { | |
| d.mu.Lock() | |
| holds := d.holds[addr] | |
| if len(holds) == 0 { | |
| // No hold for this addr. | |
| d.mu.Unlock() | |
| return (&net.Dialer{}).DialContext(ctx, "tcp", addr) | |
| } | |
| hold := holds[0] | |
| d.holds[addr] = holds[1:] | |
| d.mu.Unlock() | |
| logger.Infof("Hold %p: Intercepted connection attempt to addr %q", hold, addr) | |
| close(hold.waitCh) | |
| select { | |
| case err := <-hold.blockCh: | |
| if err != nil { | |
| return nil, err | |
| } | |
| return (&net.Dialer{}).DialContext(ctx, "tcp", addr) | |
| case <-ctx.Done(): | |
| logger.Infof("Hold %p: Connection attempt to addr %q timed out", hold, addr) | |
| return nil, ctx.Err() | |
| } | |
| } | |
| // Hold is a handle to a single connection attempt. It can be used to block, | |
| // fail and succeed connection attempts. | |
| type Hold struct { | |
| // dialer is the dialer that created this hold. | |
| dialer *BlockingDialer | |
| // waitCh is closed when a connection attempt is received. | |
| waitCh chan struct{} | |
| // blockCh receives the value to return from DialContext for this connection | |
| // attempt (nil on resume, an error on fail). It receives at most 1 value. | |
| blockCh chan error | |
| // addr is the address that this hold is for. | |
| addr string | |
| } | |
| // Hold blocks the dialer when a connection attempt is made to the given addr. | |
| // A hold is valid for exactly one connection attempt. Multiple holds for an | |
| // addr can be added, and they will apply in the order that the connections are | |
| // attempted. | |
| func (d *BlockingDialer) Hold(addr string) *Hold { | |
| d.mu.Lock() | |
| defer d.mu.Unlock() | |
| h := Hold{dialer: d, blockCh: make(chan error, 1), waitCh: make(chan struct{}), addr: addr} | |
| d.holds[addr] = append(d.holds[addr], &h) | |
| return &h | |
| } | |
| // Wait blocks until there is a connection attempt on this Hold, or the context | |
| // expires. Return false if the context has expired, true otherwise. | |
| func (h *Hold) Wait(ctx context.Context) bool { | |
| logger.Infof("Hold %p: Waiting for a connection attempt to addr %q", h, h.addr) | |
| select { | |
| case <-ctx.Done(): | |
| return false | |
| case <-h.waitCh: | |
| return true | |
| } | |
| } | |
| // Resume unblocks the dialer for the given addr. Either Resume or Fail must be | |
| // called at most once on a hold. Otherwise, Resume panics. | |
| func (h *Hold) Resume() { | |
| logger.Infof("Hold %p: Resuming connection attempt to addr %q", h, h.addr) | |
| h.blockCh <- nil | |
| close(h.blockCh) | |
| } | |
| // Fail fails the connection attempt. Either Resume or Fail must be | |
| // called at most once on a hold. Otherwise, Resume panics. | |
| func (h *Hold) Fail(err error) { | |
| logger.Infof("Hold %p: Failing connection attempt to addr %q", h, h.addr) | |
| h.blockCh <- err | |
| close(h.blockCh) | |
| } | |
| // IsStarted returns true if this hold has received a connection attempt. | |
| func (h *Hold) IsStarted() bool { | |
| select { | |
| case <-h.waitCh: | |
| return true | |
| default: | |
| return false | |
| } | |
| } |