github.com/datastax/go-cassandra-native-protocol@v0.0.0-20220706104457-5e8aad05cf90/client/client.go (about)

     1  // Copyright 2020 DataStax
     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  
    15  package client
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"crypto/tls"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"math"
    25  	"net"
    26  	"sync"
    27  	"sync/atomic"
    28  	"time"
    29  
    30  	"github.com/rs/zerolog/log"
    31  
    32  	"github.com/datastax/go-cassandra-native-protocol/frame"
    33  	"github.com/datastax/go-cassandra-native-protocol/message"
    34  	"github.com/datastax/go-cassandra-native-protocol/primitive"
    35  	"github.com/datastax/go-cassandra-native-protocol/segment"
    36  )
    37  
    38  const (
    39  	DefaultConnectTimeout = time.Second * 5
    40  	DefaultReadTimeout    = time.Second * 12
    41  )
    42  
    43  const (
    44  	DefaultMaxInFlight = 1024
    45  	DefaultMaxPending  = 10
    46  )
    47  
    48  const ManagedStreamId int16 = 0
    49  
    50  // EventHandler An event handler is a callback function that gets invoked whenever a CqlClientConnection receives an incoming
    51  // event.
    52  type EventHandler func(event *frame.Frame, conn *CqlClientConnection)
    53  
    54  // CqlClient is a client for Cassandra-compatible backends. It is preferable to create CqlClient instances using the
    55  // constructor function NewCqlClient. Once the client is created and properly configured, use Connect or ConnectAndInit
    56  // to establish new connections to the server.
    57  type CqlClient struct {
    58  	// The remote contact point address to connect to.
    59  	RemoteAddress string
    60  	// The AuthCredentials for authenticated servers. If nil, no authentication will be used.
    61  	Credentials *AuthCredentials
    62  	// The compression to use; if unspecified, no compression will be used.
    63  	Compression primitive.Compression
    64  	// The maximum number of in-flight requests to apply for each connection created with Connect. Must be strictly
    65  	// positive.
    66  	MaxInFlight int
    67  	// The maximum number of pending responses awaiting delivery to store per request. Must be strictly positive.
    68  	// This is only useful when using continuous paging, a feature specific to DataStax Enterprise.
    69  	MaxPending int
    70  	// The timeout to apply when establishing new connections.
    71  	ConnectTimeout time.Duration
    72  	// The timeout to apply when waiting for incoming responses.
    73  	ReadTimeout time.Duration
    74  	// An optional list of handlers to handle incoming events.
    75  	EventHandlers []EventHandler
    76  	// TLSConfig is the TLS configuration to use.
    77  	TLSConfig *tls.Config
    78  }
    79  
    80  // NewCqlClient Creates a new CqlClient with default options. Leave credentials nil to opt out from authentication.
    81  func NewCqlClient(remoteAddress string, credentials *AuthCredentials) *CqlClient {
    82  	return &CqlClient{
    83  		RemoteAddress:  remoteAddress,
    84  		Credentials:    credentials,
    85  		MaxInFlight:    DefaultMaxInFlight,
    86  		MaxPending:     DefaultMaxPending,
    87  		ConnectTimeout: DefaultConnectTimeout,
    88  		ReadTimeout:    DefaultReadTimeout,
    89  	}
    90  }
    91  
    92  func (client *CqlClient) String() string {
    93  	return fmt.Sprintf("CQL client [%v]", client.RemoteAddress)
    94  }
    95  
    96  // Connect establishes a new TCP connection to the client's remote address.
    97  // Set ctx to context.Background if no parent context exists.
    98  // The returned CqlClientConnection is ready to use, but one must initialize it manually, for example by calling
    99  // CqlClientConnection.InitiateHandshake. Alternatively, use ConnectAndInit to get a fully-initialized connection.
   100  func (client *CqlClient) Connect(ctx context.Context) (*CqlClientConnection, error) {
   101  	log.Debug().Msgf("%v: connecting", client)
   102  	var conn net.Conn
   103  	var err error
   104  	connectCtx, connectCancel := context.WithTimeout(ctx, client.ConnectTimeout)
   105  	defer connectCancel()
   106  	if client.TLSConfig != nil {
   107  		dialer := tls.Dialer{Config: client.TLSConfig}
   108  		conn, err = dialer.DialContext(connectCtx, "tcp", client.RemoteAddress)
   109  	} else {
   110  		dialer := net.Dialer{}
   111  		conn, err = dialer.DialContext(connectCtx, "tcp", client.RemoteAddress)
   112  	}
   113  	if err != nil {
   114  		return nil, fmt.Errorf("%v: cannot establish TCP connection: %w", client, err)
   115  	} else {
   116  		log.Debug().Msgf("%v: new TCP connection established", client)
   117  		if connection, err := newCqlClientConnection(
   118  			conn,
   119  			ctx,
   120  			client.Credentials,
   121  			client.Compression,
   122  			client.MaxInFlight,
   123  			client.MaxPending,
   124  			client.ReadTimeout,
   125  			client.EventHandlers,
   126  		); err != nil {
   127  			log.Err(err).Msgf("%v: cannot establish CQL connection", client)
   128  			_ = conn.Close()
   129  			return nil, err
   130  		} else {
   131  			log.Info().Msgf("%v: new CQL connection established: %v", client, connection)
   132  			return connection, nil
   133  		}
   134  	}
   135  }
   136  
   137  // ConnectAndInit establishes a new TCP connection to the server, then initiates a handshake procedure using the
   138  // specified protocol version. The CqlClientConnection connection will be fully initialized when this method returns.
   139  // Use stream id zero to activate automatic stream id management.
   140  // Set ctx to context.Background if no parent context exists.
   141  func (client *CqlClient) ConnectAndInit(
   142  	ctx context.Context,
   143  	version primitive.ProtocolVersion,
   144  	streamId int16,
   145  ) (*CqlClientConnection, error) {
   146  	if connection, err := client.Connect(ctx); err != nil {
   147  		return nil, err
   148  	} else {
   149  		return connection, connection.InitiateHandshake(version, streamId)
   150  	}
   151  }
   152  
   153  // CqlClientConnection encapsulates a TCP client connection to a remote Cassandra-compatible backend.
   154  // CqlClientConnection instances should be created by calling CqlClient.Connect or CqlClient.ConnectAndInit.
   155  type CqlClientConnection struct {
   156  	conn               net.Conn
   157  	frameCodec         frame.Codec
   158  	segmentCodec       segment.Codec
   159  	compression        primitive.Compression
   160  	modernLayout       bool
   161  	readTimeout        time.Duration
   162  	credentials        *AuthCredentials
   163  	handlers           []EventHandler
   164  	inFlightHandler    *inFlightRequestsHandler
   165  	outgoing           chan *frame.Frame
   166  	events             chan *frame.Frame
   167  	waitGroup          *sync.WaitGroup
   168  	closed             int32
   169  	ctx                context.Context
   170  	cancel             context.CancelFunc
   171  	payloadAccumulator *payloadAccumulator
   172  }
   173  
   174  func newCqlClientConnection(
   175  	conn net.Conn,
   176  	ctx context.Context,
   177  	credentials *AuthCredentials,
   178  	compression primitive.Compression,
   179  	maxInFlight int,
   180  	maxPending int,
   181  	readTimeout time.Duration,
   182  	handlers []EventHandler,
   183  ) (*CqlClientConnection, error) {
   184  	if conn == nil {
   185  		return nil, fmt.Errorf("TCP connection cannot be nil")
   186  	}
   187  	if ctx == nil {
   188  		return nil, fmt.Errorf("context cannot be nil")
   189  	}
   190  	if maxInFlight < 1 {
   191  		return nil, fmt.Errorf("max in-flight: expecting positive, got: %v", maxInFlight)
   192  	} else if maxInFlight > math.MaxInt16 {
   193  		return nil, fmt.Errorf("max in-flight: expecting <= %v, got: %v", math.MaxInt16, maxInFlight)
   194  	}
   195  	if maxPending < 1 {
   196  		return nil, fmt.Errorf("max pending: expecting positive, got: %v", maxInFlight)
   197  	}
   198  	frameCodec := frame.NewCodecWithCompression(NewBodyCompressor(compression))
   199  	segmentCodec := segment.NewCodecWithCompression(NewPayloadCompressor(compression))
   200  	if compression == "" {
   201  		compression = primitive.CompressionNone
   202  	}
   203  	connection := &CqlClientConnection{
   204  		conn:         conn,
   205  		frameCodec:   frameCodec,
   206  		segmentCodec: segmentCodec,
   207  		compression:  compression,
   208  		readTimeout:  readTimeout,
   209  		credentials:  credentials,
   210  		handlers:     handlers,
   211  		outgoing:     make(chan *frame.Frame, maxInFlight),
   212  		events:       make(chan *frame.Frame, maxInFlight),
   213  		waitGroup:    &sync.WaitGroup{},
   214  		payloadAccumulator: &payloadAccumulator{
   215  			frameCodec: frame.NewRawCodec(), // without compression
   216  		},
   217  	}
   218  	connection.ctx, connection.cancel = context.WithCancel(ctx)
   219  	connection.inFlightHandler = newInFlightRequestsHandler(connection.String(), connection.ctx, maxInFlight, maxPending, readTimeout)
   220  	connection.incomingLoop()
   221  	connection.outgoingLoop()
   222  	connection.awaitDone()
   223  	return connection, nil
   224  }
   225  
   226  func (c *CqlClientConnection) String() string {
   227  	return fmt.Sprintf("CQL client conn [L:%v <-> R:%v]", c.conn.LocalAddr(), c.conn.RemoteAddr())
   228  }
   229  
   230  // LocalAddr returns the connection's local address (that is, the client address).
   231  func (c *CqlClientConnection) LocalAddr() net.Addr {
   232  	return c.conn.LocalAddr()
   233  }
   234  
   235  // RemoteAddr returns the connection's remote address (that is, the server address).
   236  func (c *CqlClientConnection) RemoteAddr() net.Addr {
   237  	return c.conn.RemoteAddr()
   238  }
   239  
   240  // Credentials returns a copy of the connection's AuthCredentials, if any, or nil if no authentication was configured.
   241  func (c *CqlClientConnection) Credentials() *AuthCredentials {
   242  	if c.credentials == nil {
   243  		return nil
   244  	}
   245  	return c.credentials.Copy()
   246  }
   247  
   248  type payloadAccumulator struct {
   249  	targetLength    int
   250  	accumulatedData []byte
   251  	frameCodec      frame.RawCodec
   252  }
   253  
   254  func (a *payloadAccumulator) reset() {
   255  	a.targetLength = 0
   256  	a.accumulatedData = nil
   257  }
   258  
   259  func (c *CqlClientConnection) incomingLoop() {
   260  	log.Debug().Msgf("%v: listening for incoming frames...", c)
   261  	c.waitGroup.Add(1)
   262  	go func() {
   263  		abort := false
   264  		for !abort && !c.IsClosed() {
   265  			if source, err := c.waitForIncomingData(); err != nil {
   266  				abort = c.reportConnectionFailure(err, true)
   267  			} else if c.modernLayout {
   268  				abort = c.readSegment(source)
   269  			} else {
   270  				abort = c.readFrame(source)
   271  			}
   272  		}
   273  		c.waitGroup.Done()
   274  		if abort {
   275  			c.abort()
   276  		}
   277  	}()
   278  }
   279  
   280  func (c *CqlClientConnection) outgoingLoop() {
   281  	log.Debug().Msgf("%v: listening for outgoing frames...", c)
   282  	c.waitGroup.Add(1)
   283  	go func() {
   284  		abort := false
   285  		for !abort && !c.IsClosed() {
   286  			if outgoing, ok := <-c.outgoing; !ok {
   287  				if !c.IsClosed() {
   288  					log.Error().Msgf("%v: outgoing frame channel was closed unexpectedly, closing connection", c)
   289  					abort = true
   290  				}
   291  				break
   292  			} else {
   293  				log.Debug().Msgf("%v: sending outgoing frame: %v", c, outgoing)
   294  				if c.modernLayout {
   295  					// TODO write coalescer
   296  					abort = c.writeSegment(outgoing, c.conn)
   297  				} else {
   298  					abort = c.writeFrame(outgoing, c.conn)
   299  				}
   300  			}
   301  		}
   302  		c.waitGroup.Done()
   303  		if abort {
   304  			c.abort()
   305  		}
   306  	}()
   307  }
   308  
   309  func (c *CqlClientConnection) waitForIncomingData() (io.Reader, error) {
   310  	buf := make([]byte, 1)
   311  	if _, err := io.ReadFull(c.conn, buf); err != nil {
   312  		return nil, err
   313  	} else {
   314  		return io.MultiReader(bytes.NewReader(buf), c.conn), nil
   315  	}
   316  }
   317  
   318  func (c *CqlClientConnection) readSegment(source io.Reader) (abort bool) {
   319  	if incoming, err := c.segmentCodec.DecodeSegment(source); err != nil {
   320  		abort = c.reportConnectionFailure(err, true)
   321  	} else if incoming.Header.IsSelfContained {
   322  		log.Debug().Msgf("%v: received incoming self-contained segment: %v", c, incoming)
   323  		abort = c.readSelfContainedSegment(incoming, abort)
   324  	} else {
   325  		log.Debug().Msgf("%v: received incoming multi-segment part: %v", c, incoming)
   326  		abort = c.addMultiSegmentPayload(incoming.Payload)
   327  	}
   328  	return abort
   329  }
   330  
   331  func (c *CqlClientConnection) readSelfContainedSegment(incoming *segment.Segment, abort bool) bool {
   332  	payloadReader := bytes.NewReader(incoming.Payload.UncompressedData)
   333  	for payloadReader.Len() > 0 {
   334  		if abort = c.readFrame(payloadReader); abort {
   335  			break
   336  		}
   337  	}
   338  	return abort
   339  }
   340  
   341  func (c *CqlClientConnection) addMultiSegmentPayload(payload *segment.Payload) (abort bool) {
   342  	accumulator := c.payloadAccumulator
   343  	if accumulator.targetLength == 0 {
   344  		// First reader, read ahead to find the target length
   345  		if header, err := accumulator.frameCodec.DecodeHeader(bytes.NewReader(payload.UncompressedData)); err != nil {
   346  			log.Error().Err(err).Msgf("%v: error decoding first frame header in multi-segment payload, closing connection", c)
   347  			return true
   348  		} else {
   349  			accumulator.targetLength = int(primitive.FrameHeaderLengthV3AndHigher + header.BodyLength)
   350  		}
   351  	}
   352  	accumulator.accumulatedData = append(accumulator.accumulatedData, payload.UncompressedData...)
   353  	if accumulator.targetLength == len(accumulator.accumulatedData) {
   354  		// We've received enough data to reassemble the whole frame
   355  		encodedFrame := bytes.NewReader(accumulator.accumulatedData)
   356  		accumulator.reset()
   357  		return c.readFrame(encodedFrame)
   358  	}
   359  	return false
   360  }
   361  
   362  func (c *CqlClientConnection) writeSegment(outgoing *frame.Frame, dest io.Writer) (abort bool) {
   363  	// never compress frames individually when included in a segment
   364  	outgoing.Header.Flags = outgoing.Header.Flags.Remove(primitive.HeaderFlagCompressed)
   365  	encodedFrame := &bytes.Buffer{}
   366  	if abort = c.writeFrame(outgoing, encodedFrame); abort {
   367  		abort = true
   368  	} else {
   369  		seg := &segment.Segment{
   370  			Header:  &segment.Header{IsSelfContained: true},
   371  			Payload: &segment.Payload{UncompressedData: encodedFrame.Bytes()},
   372  		}
   373  		if err := c.segmentCodec.EncodeSegment(seg, dest); err != nil {
   374  			abort = c.reportConnectionFailure(err, false)
   375  		} else {
   376  			log.Debug().Msgf("%v: outgoing segment successfully written: %v (frame: %v)", c, seg, outgoing)
   377  		}
   378  	}
   379  	return abort
   380  }
   381  
   382  func (c *CqlClientConnection) readFrame(source io.Reader) (abort bool) {
   383  	if incoming, err := c.frameCodec.DecodeFrame(source); err != nil {
   384  		abort = c.reportConnectionFailure(err, true)
   385  	} else {
   386  		c.maybeSwitchToModernLayout(incoming)
   387  		abort = c.processIncomingFrame(incoming)
   388  	}
   389  	return abort
   390  }
   391  
   392  func (c *CqlClientConnection) maybeSwitchToModernLayout(incoming *frame.Frame) {
   393  	if !c.modernLayout &&
   394  		incoming.Header.Version.SupportsModernFramingLayout() &&
   395  		(isReady(incoming) || isAuthenticate(incoming)) {
   396  		// Changing this value could be racy if some outgoing frame is being processed;
   397  		// but in theory, this should never happen during handshake.
   398  		log.Debug().Msgf("%v: switching to modern framing layout", c)
   399  		c.modernLayout = true
   400  	}
   401  }
   402  
   403  func (c *CqlClientConnection) writeFrame(outgoing *frame.Frame, dest io.Writer) (abort bool) {
   404  	if err := c.frameCodec.EncodeFrame(outgoing, dest); err != nil {
   405  		abort = c.reportConnectionFailure(err, false)
   406  	} else {
   407  		log.Debug().Msgf("%v: outgoing frame successfully written: %v", c, outgoing)
   408  	}
   409  	return abort
   410  }
   411  
   412  func (c *CqlClientConnection) reportConnectionFailure(err error, read bool) (abort bool) {
   413  	if !c.IsClosed() {
   414  		if errors.Is(err, io.EOF) {
   415  			log.Info().Msgf("%v: connection reset by peer, closing", c)
   416  		} else {
   417  			if read {
   418  				log.Error().Err(err).Msgf("%v: error reading, closing connection", c)
   419  			} else {
   420  				log.Error().Err(err).Msgf("%v: error writing, closing connection", c)
   421  			}
   422  		}
   423  		abort = true
   424  	}
   425  	return abort
   426  }
   427  
   428  func (c *CqlClientConnection) processIncomingFrame(incoming *frame.Frame) (abort bool) {
   429  	log.Debug().Msgf("%v: received incoming frame: %v", c, incoming)
   430  	if incoming.Header.OpCode == primitive.OpCodeEvent {
   431  		for _, handler := range c.handlers {
   432  			handler(incoming, c)
   433  		}
   434  		select {
   435  		case c.events <- incoming:
   436  			log.Debug().Msgf("%v: incoming event frame successfully delivered: %v", c, incoming)
   437  		default:
   438  			log.Error().Msgf("%v: events queue is full, discarding event frame: %v", c, incoming)
   439  		}
   440  	} else {
   441  		if err := c.inFlightHandler.onIncomingFrameReceived(incoming); err != nil {
   442  			log.Error().Err(err).Msgf("%v: incoming frame delivery failed: %v", c, incoming)
   443  		} else {
   444  			log.Debug().Msgf("%v: incoming frame successfully delivered: %v", c, incoming)
   445  		}
   446  		if incoming.Header.OpCode == primitive.OpCodeError {
   447  			e := incoming.Body.Message.(message.Error)
   448  			if e.GetErrorCode().IsFatalError() {
   449  				log.Error().Msgf("%v: server replied with fatal error code %v, closing connection", c, e.GetErrorCode())
   450  				abort = true
   451  			}
   452  		}
   453  	}
   454  	return
   455  }
   456  
   457  func (c *CqlClientConnection) awaitDone() {
   458  	c.waitGroup.Add(1)
   459  	go func() {
   460  		<-c.ctx.Done()
   461  		log.Debug().Err(c.ctx.Err()).Msgf("%v: context was closed", c)
   462  		c.waitGroup.Done()
   463  		c.abort()
   464  	}()
   465  }
   466  
   467  // NewStartupRequest is a convenience method to create a new STARTUP request frame. The compression option will be
   468  // automatically set to the appropriate compression algorithm, depending on whether the connection was configured to
   469  // use a compressor. Use stream id zero to activate automatic stream id management.
   470  func (c *CqlClientConnection) NewStartupRequest(version primitive.ProtocolVersion, streamId int16) (*frame.Frame, error) {
   471  	startup := message.NewStartup()
   472  	if c.compression != primitive.CompressionNone {
   473  		if version.SupportsCompression(c.compression) {
   474  			startup.SetCompression(c.compression)
   475  		} else {
   476  			return nil, fmt.Errorf("%v does not support compression %v", version, c.compression)
   477  		}
   478  	}
   479  	startup.SetDriverName("DataStax Go client")
   480  	return frame.NewFrame(version, streamId, startup), nil
   481  }
   482  
   483  // InFlightRequest is an in-flight request sent through CqlClientConnection.Send.
   484  type InFlightRequest interface {
   485  
   486  	// StreamId is the in-flight request stream id.
   487  	StreamId() int16
   488  
   489  	// Incoming returns a channel to receive incoming frames for this in-flight request. Typically the channel will
   490  	// only ever emit one single frame, except when using continuous paging (DataStax Enterprise only).
   491  	// The returned channel is never nil. It is closed after receiving the last frame, or if an error occurs
   492  	// (typically a timeout), whichever happens first; when the channel is closed, IsDone returns true.
   493  	// If the channel is closed because of an error, Err will return that error, otherwise it will return nil.
   494  	// Successive calls to Incoming return the same channel.
   495  	Incoming() <-chan *frame.Frame
   496  
   497  	// IsDone returns true if Incoming is closed, and false otherwise.
   498  	IsDone() bool
   499  
   500  	// Err returns nil if Incoming is not yet closed.
   501  	// If Incoming is closed, Err returns either nil if the channel was closed normally, or a non-nil error explaining
   502  	// why the channel was closed abnormally.
   503  	// After Err returns a non-nil error, successive calls to Err return the same error.
   504  	Err() error
   505  }
   506  
   507  // Send sends the given request frame and returns a receive channel that can be used to receive response frames and
   508  // errors matching the request's stream id. The channel will be closed after receiving the last frame, or if the
   509  // configured read timeout is triggered, or if the connection itself is closed, whichever happens first.
   510  // Stream id management: if the frame's stream id is ManagedStreamId (0), it is assumed that the frame's stream id is
   511  // to be automatically assigned by the connection upon write. Users are free to choose between managed stream ids or
   512  // manually assigned ones, but it is not recommended mixing managed stream ids with non-managed ones on the same
   513  // connection.
   514  func (c *CqlClientConnection) Send(f *frame.Frame) (InFlightRequest, error) {
   515  	if f == nil {
   516  		return nil, fmt.Errorf("%v: frame cannot be nil", c)
   517  	}
   518  	if c.IsClosed() {
   519  		return nil, fmt.Errorf("%v: connection closed", c)
   520  	}
   521  	log.Debug().Msgf("%v: enqueuing outgoing frame: %v", c, f)
   522  	if inFlight, err := c.inFlightHandler.onOutgoingFrameEnqueued(f); err != nil {
   523  		return nil, fmt.Errorf("%v: failed to register in-flight handler for frame: %v: %w", c, f, err)
   524  	} else {
   525  		select {
   526  		case c.outgoing <- f:
   527  			log.Debug().Msgf("%v: outgoing frame successfully enqueued: %v", c, f)
   528  			return inFlight, nil
   529  		default:
   530  			return nil, fmt.Errorf("%v: failed to enqueue outgoing frame: %v", c, f)
   531  		}
   532  	}
   533  }
   534  
   535  // Receive is a convenience method that takes an InFlightRequest obtained through Send and waits until the next response
   536  // frame is received, or an error occurs, whichever happens first.
   537  // If the in-flight request is completed already without returning more frames, this method return a nil frame and a
   538  // nil error.
   539  func (c *CqlClientConnection) Receive(ch InFlightRequest) (*frame.Frame, error) {
   540  	if ch == nil {
   541  		return nil, fmt.Errorf("%v: response channel cannot be nil", c)
   542  	}
   543  	log.Debug().Msgf("%v: waiting for incoming frame", c)
   544  	if incoming, ok := <-ch.Incoming(); !ok {
   545  		if ch.Err() == nil {
   546  			log.Debug().Msgf("%v: in-flight request closed for stream id: %d", c, ch.StreamId())
   547  			return nil, nil
   548  		} else {
   549  			return nil, fmt.Errorf("%v: failed to retrieve incoming frame: %w", c, ch.Err())
   550  		}
   551  	} else {
   552  		log.Debug().Msgf("%v: incoming frame successfully received: %v", c, incoming)
   553  		return incoming, nil
   554  	}
   555  }
   556  
   557  // SendAndReceive is a convenience method chaining a call to Send to a call to Receive.
   558  func (c *CqlClientConnection) SendAndReceive(f *frame.Frame) (*frame.Frame, error) {
   559  	if ch, err := c.Send(f); err != nil {
   560  		return nil, err
   561  	} else {
   562  		return c.Receive(ch)
   563  	}
   564  }
   565  
   566  // EventChannel is a receive-only channel for incoming events. A receive channel can be obtained through
   567  // CqlClientConnection.EventChannel.
   568  type EventChannel <-chan *frame.Frame
   569  
   570  // EventChannel returns a channel for listening to incoming events received on this connection. This channel will be
   571  // closed when the connection is closed. If this connection has already been closed, this method returns nil.
   572  func (c *CqlClientConnection) EventChannel() EventChannel {
   573  	return c.events
   574  }
   575  
   576  // ReceiveEvent waits until an event frame is received, or the configured read timeout is triggered, or the connection
   577  // is closed, whichever happens first. Returns the event frame, if any.
   578  func (c *CqlClientConnection) ReceiveEvent() (*frame.Frame, error) {
   579  	if c.IsClosed() {
   580  		return nil, fmt.Errorf("%v: connection closed", c)
   581  	}
   582  	select {
   583  	case incoming, ok := <-c.events:
   584  		if !ok {
   585  			return nil, fmt.Errorf("%v: incoming events channel closed", c)
   586  		}
   587  		return incoming, nil
   588  	case <-time.After(c.readTimeout):
   589  		return nil, fmt.Errorf("%v: timed out waiting for incoming events", c)
   590  	}
   591  }
   592  
   593  func (c *CqlClientConnection) IsClosed() bool {
   594  	return atomic.LoadInt32(&c.closed) == 1
   595  }
   596  
   597  func (c *CqlClientConnection) setClosed() bool {
   598  	return atomic.CompareAndSwapInt32(&c.closed, 0, 1)
   599  }
   600  
   601  func (c *CqlClientConnection) Close() (err error) {
   602  	if c.setClosed() {
   603  		log.Debug().Msgf("%v: closing", c)
   604  		c.cancel()
   605  		err = c.conn.Close()
   606  		outgoing := c.outgoing
   607  		events := c.events
   608  		c.outgoing = nil
   609  		c.events = nil
   610  		close(outgoing)
   611  		close(events)
   612  		c.inFlightHandler.close()
   613  		c.waitGroup.Wait()
   614  		if err != nil {
   615  			err = fmt.Errorf("%v: error closing: %w", c, err)
   616  		} else {
   617  			log.Info().Msgf("%v: successfully closed", c)
   618  		}
   619  	} else {
   620  		log.Debug().Err(err).Msgf("%v: already closed", c)
   621  	}
   622  	return err
   623  }
   624  
   625  func (c *CqlClientConnection) abort() {
   626  	log.Debug().Msgf("%v: forcefully closing", c)
   627  	if err := c.Close(); err != nil {
   628  		log.Error().Err(err).Msgf("%v: error closing", c)
   629  	}
   630  }