118 lines
2.9 KiB
Go
118 lines
2.9 KiB
Go
package service
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
coderws "github.com/coder/websocket"
|
||
|
|
)
|
||
|
|
|
||
|
|
type openAIWSClientReadResult struct {
|
||
|
|
messageType coderws.MessageType
|
||
|
|
payload []byte
|
||
|
|
err error
|
||
|
|
}
|
||
|
|
|
||
|
|
// ReadOpenAIWSClientMessage keeps one reader alive while control events send
|
||
|
|
// their close frame, then closes the transport and joins that reader.
|
||
|
|
func ReadOpenAIWSClientMessage(
|
||
|
|
controlCtx context.Context,
|
||
|
|
conn *coderws.Conn,
|
||
|
|
timeout time.Duration,
|
||
|
|
timeoutStatus coderws.StatusCode,
|
||
|
|
timeoutReason string,
|
||
|
|
) (coderws.MessageType, []byte, error) {
|
||
|
|
return readOpenAIWSClientMessageWithTimeoutStart(
|
||
|
|
controlCtx,
|
||
|
|
conn,
|
||
|
|
timeout,
|
||
|
|
timeoutStatus,
|
||
|
|
timeoutReason,
|
||
|
|
nil,
|
||
|
|
nil,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// readOpenAIWSClientMessageWithTimeoutStart supports readers whose timeout
|
||
|
|
// starts after a state transition, such as a completed passthrough turn. When
|
||
|
|
// timeoutActive is nil, a positive timeout starts immediately.
|
||
|
|
func readOpenAIWSClientMessageWithTimeoutStart(
|
||
|
|
controlCtx context.Context,
|
||
|
|
conn *coderws.Conn,
|
||
|
|
timeout time.Duration,
|
||
|
|
timeoutStatus coderws.StatusCode,
|
||
|
|
timeoutReason string,
|
||
|
|
timeoutStart <-chan struct{},
|
||
|
|
timeoutActive func() bool,
|
||
|
|
) (coderws.MessageType, []byte, error) {
|
||
|
|
if conn == nil {
|
||
|
|
return 0, nil, errors.New("openai websocket client connection is nil")
|
||
|
|
}
|
||
|
|
if controlCtx == nil {
|
||
|
|
controlCtx = context.Background()
|
||
|
|
}
|
||
|
|
|
||
|
|
readDone := make(chan openAIWSClientReadResult, 1)
|
||
|
|
go func() {
|
||
|
|
messageType, payload, err := conn.Read(context.Background())
|
||
|
|
readDone <- openAIWSClientReadResult{messageType: messageType, payload: payload, err: err}
|
||
|
|
}()
|
||
|
|
|
||
|
|
var timer *time.Timer
|
||
|
|
var timeoutCh <-chan time.Time
|
||
|
|
startTimeout := func() {
|
||
|
|
if timeout <= 0 || (timeoutActive != nil && !timeoutActive()) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if timer == nil {
|
||
|
|
timer = time.NewTimer(timeout)
|
||
|
|
} else {
|
||
|
|
if !timer.Stop() {
|
||
|
|
select {
|
||
|
|
case <-timer.C:
|
||
|
|
default:
|
||
|
|
}
|
||
|
|
}
|
||
|
|
timer.Reset(timeout)
|
||
|
|
}
|
||
|
|
timeoutCh = timer.C
|
||
|
|
}
|
||
|
|
if timeoutActive == nil || timeoutActive() {
|
||
|
|
startTimeout()
|
||
|
|
}
|
||
|
|
defer func() {
|
||
|
|
if timer != nil {
|
||
|
|
timer.Stop()
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
closeAndJoin := func(status coderws.StatusCode, reason string, cause error) (coderws.MessageType, []byte, error) {
|
||
|
|
_ = conn.Close(status, reason)
|
||
|
|
_ = conn.CloseNow()
|
||
|
|
<-readDone
|
||
|
|
return 0, nil, NewOpenAIWSClientCloseError(status, reason, cause)
|
||
|
|
}
|
||
|
|
|
||
|
|
for {
|
||
|
|
select {
|
||
|
|
case result := <-readDone:
|
||
|
|
return result.messageType, result.payload, result.err
|
||
|
|
case <-timeoutStart:
|
||
|
|
startTimeout()
|
||
|
|
case <-timeoutCh:
|
||
|
|
return closeAndJoin(timeoutStatus, timeoutReason, context.DeadlineExceeded)
|
||
|
|
case <-controlCtx.Done():
|
||
|
|
cause := context.Cause(controlCtx)
|
||
|
|
if errors.Is(cause, ErrOpenAIWSIngressLeaseLost) {
|
||
|
|
return closeAndJoin(
|
||
|
|
coderws.StatusTryAgainLater,
|
||
|
|
"websocket ingress capacity lease lost; please reconnect",
|
||
|
|
cause,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
return closeAndJoin(coderws.StatusGoingAway, "websocket request canceled", cause)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|