openai_ws_client_read.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "time"
  6. coderws "github.com/coder/websocket"
  7. )
  8. type openAIWSClientReadResult struct {
  9. messageType coderws.MessageType
  10. payload []byte
  11. err error
  12. }
  13. // ReadOpenAIWSClientMessage keeps one reader alive while control events send
  14. // their close frame, then closes the transport and joins that reader.
  15. func ReadOpenAIWSClientMessage(
  16. controlCtx context.Context,
  17. conn *coderws.Conn,
  18. timeout time.Duration,
  19. timeoutStatus coderws.StatusCode,
  20. timeoutReason string,
  21. ) (coderws.MessageType, []byte, error) {
  22. return readOpenAIWSClientMessageWithTimeoutStart(
  23. controlCtx,
  24. conn,
  25. timeout,
  26. timeoutStatus,
  27. timeoutReason,
  28. nil,
  29. nil,
  30. )
  31. }
  32. // readOpenAIWSClientMessageWithTimeoutStart supports readers whose timeout
  33. // starts after a state transition, such as a completed passthrough turn. When
  34. // timeoutActive is nil, a positive timeout starts immediately.
  35. func readOpenAIWSClientMessageWithTimeoutStart(
  36. controlCtx context.Context,
  37. conn *coderws.Conn,
  38. timeout time.Duration,
  39. timeoutStatus coderws.StatusCode,
  40. timeoutReason string,
  41. timeoutStart <-chan struct{},
  42. timeoutActive func() bool,
  43. ) (coderws.MessageType, []byte, error) {
  44. if conn == nil {
  45. return 0, nil, errors.New("openai websocket client connection is nil")
  46. }
  47. if controlCtx == nil {
  48. controlCtx = context.Background()
  49. }
  50. readDone := make(chan openAIWSClientReadResult, 1)
  51. go func() {
  52. messageType, payload, err := conn.Read(context.Background())
  53. readDone <- openAIWSClientReadResult{messageType: messageType, payload: payload, err: err}
  54. }()
  55. var timer *time.Timer
  56. var timeoutCh <-chan time.Time
  57. startTimeout := func() {
  58. if timeout <= 0 || (timeoutActive != nil && !timeoutActive()) {
  59. return
  60. }
  61. if timer == nil {
  62. timer = time.NewTimer(timeout)
  63. } else {
  64. if !timer.Stop() {
  65. select {
  66. case <-timer.C:
  67. default:
  68. }
  69. }
  70. timer.Reset(timeout)
  71. }
  72. timeoutCh = timer.C
  73. }
  74. if timeoutActive == nil || timeoutActive() {
  75. startTimeout()
  76. }
  77. defer func() {
  78. if timer != nil {
  79. timer.Stop()
  80. }
  81. }()
  82. closeAndJoin := func(status coderws.StatusCode, reason string, cause error) (coderws.MessageType, []byte, error) {
  83. _ = conn.Close(status, reason)
  84. _ = conn.CloseNow()
  85. <-readDone
  86. return 0, nil, NewOpenAIWSClientCloseError(status, reason, cause)
  87. }
  88. for {
  89. select {
  90. case result := <-readDone:
  91. return result.messageType, result.payload, result.err
  92. case <-timeoutStart:
  93. startTimeout()
  94. case <-timeoutCh:
  95. return closeAndJoin(timeoutStatus, timeoutReason, context.DeadlineExceeded)
  96. case <-controlCtx.Done():
  97. cause := context.Cause(controlCtx)
  98. if errors.Is(cause, ErrOpenAIWSIngressLeaseLost) {
  99. return closeAndJoin(
  100. coderws.StatusTryAgainLater,
  101. "websocket ingress capacity lease lost; please reconnect",
  102. cause,
  103. )
  104. }
  105. return closeAndJoin(coderws.StatusGoingAway, "websocket request canceled", cause)
  106. }
  107. }
  108. }