blob: d6dbac1a93a07c22ca4b67fec88584c17484d86a [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.21
package quic
import (
"errors"
"fmt"
"time"
)
// A Conn is a QUIC connection.
//
// Multiple goroutines may invoke methods on a Conn simultaneously.
type Conn struct {
msgc chan any
donec chan struct{} // closed when conn loop exits
exited bool // set to make the conn loop exit immediately
testHooks connTestHooks
// idleTimeout is the time at which the connection will be closed due to inactivity.
// https://www.rfc-editor.org/rfc/rfc9000#section-10.1
maxIdleTimeout time.Duration
idleTimeout time.Time
}
// connTestHooks override conn behavior in tests.
type connTestHooks interface {
nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any)
}
func newConn(now time.Time, hooks connTestHooks) (*Conn, error) {
c := &Conn{
donec: make(chan struct{}),
testHooks: hooks,
maxIdleTimeout: defaultMaxIdleTimeout,
idleTimeout: now.Add(defaultMaxIdleTimeout),
}
// A one-element buffer allows us to wake a Conn's event loop as a
// non-blocking operation.
c.msgc = make(chan any, 1)
go c.loop(now)
return c, nil
}
type timerEvent struct{}
// loop is the connection main loop.
//
// Except where otherwise noted, all connection state is owned by the loop goroutine.
//
// The loop processes messages from c.msgc and timer events.
// Other goroutines may examine or modify conn state by sending the loop funcs to execute.
func (c *Conn) loop(now time.Time) {
defer close(c.donec)
// The connection timer sends a message to the connection loop on expiry.
// We need to give it an expiry when creating it, so set the initial timeout to
// an arbitrary large value. The timer will be reset before this expires (and it
// isn't a problem if it does anyway). Skip creating the timer in tests which
// take control of the connection message loop.
var timer *time.Timer
var lastTimeout time.Time
hooks := c.testHooks
if hooks == nil {
timer = time.AfterFunc(1*time.Hour, func() {
c.sendMsg(timerEvent{})
})
defer timer.Stop()
}
for !c.exited {
nextTimeout := c.idleTimeout
var m any
if hooks != nil {
// Tests only: Wait for the test to tell us to continue.
now, m = hooks.nextMessage(c.msgc, nextTimeout)
} else if !nextTimeout.IsZero() && nextTimeout.Before(now) {
// A connection timer has expired.
now = time.Now()
m = timerEvent{}
} else {
// Reschedule the connection timer if necessary
// and wait for the next event.
if !nextTimeout.Equal(lastTimeout) && !nextTimeout.IsZero() {
// Resetting a timer created with time.AfterFunc guarantees
// that the timer will run again. We might generate a spurious
// timer event under some circumstances, but that's okay.
timer.Reset(nextTimeout.Sub(now))
lastTimeout = nextTimeout
}
m = <-c.msgc
now = time.Now()
}
switch m := m.(type) {
case timerEvent:
// A connection timer has expired.
if !now.Before(c.idleTimeout) {
// "[...] the connection is silently closed and
// its state is discarded [...]"
// https://www.rfc-editor.org/rfc/rfc9000#section-10.1-1
c.exited = true
return
}
case func(time.Time, *Conn):
// Send a func to msgc to run it on the main Conn goroutine
m(now, c)
default:
panic(fmt.Sprintf("quic: unrecognized conn message %T", m))
}
}
}
// sendMsg sends a message to the conn's loop.
// It does not wait for the message to be processed.
func (c *Conn) sendMsg(m any) error {
select {
case c.msgc <- m:
case <-c.donec:
return errors.New("quic: connection closed")
}
return nil
}
// runOnLoop executes a function within the conn's loop goroutine.
func (c *Conn) runOnLoop(f func(now time.Time, c *Conn)) error {
donec := make(chan struct{})
if err := c.sendMsg(func(now time.Time, c *Conn) {
defer close(donec)
f(now, c)
}); err != nil {
return err
}
select {
case <-donec:
case <-c.donec:
return errors.New("quic: connection closed")
}
return nil
}
// exit fully terminates a connection immediately.
func (c *Conn) exit() {
c.runOnLoop(func(now time.Time, c *Conn) {
c.exited = true
})
<-c.donec
}