Skip to content

Commit 11556e6

Browse files
committed
refactor: migrate SSE streaming from channel-based to iterator pattern
- Replace channel-based streaming in Client.SendStream with iter.Seq2 - Extract SSE parsing logic into dedicated sseHandler function - Update WorkflowService.RunStream to return iterator instead of channel - Use sync.Pool for buffer reuse to reduce allocations - Remove unused Event struct and defaultChannelBufferSize constant This simplifies the streaming API, eliminates goroutine management, and provides better resource cleanup through iterator yield control.
1 parent 0ef4015 commit 11556e6

3 files changed

Lines changed: 74 additions & 102 deletions

File tree

client/api/v1/workflow.go

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import (
77
"context"
88
"encoding/json"
99
"errors"
10+
"iter"
1011
"net/http"
1112

1213
"github.com/yeeaiclub/dify-go/internal/handler"
1314
"github.com/yeeaiclub/dify-go/schema"
1415
)
1516

1617
const (
17-
defaultChannelBufferSize = 10
1818
// StreamMode represents the streaming response mode for workflow execution
1919
StreamMode = "streaming"
2020
// BlockingMode represents the blocking response mode for workflow execution
@@ -41,10 +41,9 @@ func NewWorkflowService(baseURL, apiKey string) *WorkflowService {
4141
func (w *WorkflowService) RunStream(
4242
ctx context.Context,
4343
req schema.RunWorkflowRequest,
44-
respCh chan schema.StreamEvent[schema.RunWorkflowResponse],
45-
) error {
44+
) (iter.Seq2[[]byte, error], error) {
4645
if req.ResponseMode != StreamMode {
47-
return nil
46+
return nil, errors.New("invalid response mode")
4847
}
4948

5049
r, err := handler.NewRequestBuilder().
@@ -55,56 +54,10 @@ func (w *WorkflowService) RunStream(
5554
Body(req).
5655
Build()
5756
if err != nil {
58-
return err
57+
return nil, err
5958
}
6059

61-
go func() {
62-
evh := make(chan *handler.Event, defaultChannelBufferSize)
63-
defer close(respCh) // Ensure the response channel is closed when done
64-
65-
// Send the streaming request
66-
err := w.client.SendStream(ctx, r, evh)
67-
if err != nil {
68-
respCh <- schema.StreamEvent[schema.RunWorkflowResponse]{
69-
Err: err.Error(), // Send the actual error, not ctx.Err()
70-
}
71-
return
72-
}
73-
74-
// Process the stream events
75-
for {
76-
select {
77-
case <-ctx.Done():
78-
respCh <- schema.StreamEvent[schema.RunWorkflowResponse]{
79-
Err: ctx.Err().Error(),
80-
}
81-
return
82-
case ev, ok := <-evh:
83-
if !ok {
84-
return
85-
}
86-
if ev.Done {
87-
respCh <- schema.StreamEvent[schema.RunWorkflowResponse]{
88-
Done: true,
89-
}
90-
return
91-
}
92-
var data schema.RunWorkflowResponse
93-
err := json.NewDecoder(ev.Data).Decode(&data)
94-
if err != nil {
95-
respCh <- schema.StreamEvent[schema.RunWorkflowResponse]{
96-
Err: err.Error(),
97-
}
98-
return
99-
}
100-
respCh <- schema.StreamEvent[schema.RunWorkflowResponse]{
101-
Type: ev.Type,
102-
Data: data,
103-
}
104-
}
105-
}
106-
}()
107-
return nil
60+
return w.client.SendStream(ctx, r)
10861
}
10962

11063
// Run executes a workflow in blocking mode, Cannot execute if there is no published workflow.

internal/handler/client.go

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
package handler
55

66
import (
7-
"bufio"
87
"bytes"
98
"context"
109
"encoding/json"
11-
"errors"
1210
"fmt"
1311
"io"
12+
"iter"
1413
"net/http"
1514
"net/url"
1615
"time"
@@ -69,15 +68,15 @@ func (c *Client) Send(ctx context.Context, req Request) (*Response, error) {
6968
}
7069

7170
// SendStream sends an HTTP request and returns streaming responses via the provided channel.
72-
func (c *Client) SendStream(ctx context.Context, req Request, evh chan *Event) error {
71+
func (c *Client) SendStream(ctx context.Context, req Request) (iter.Seq2[[]byte, error], error) {
7372
httpReq, err := c.buildRequest(ctx, req)
7473
if err != nil {
75-
return err
74+
return nil, err
7675
}
7776
httpReq.Header.Set("Accept", "text/event-stream")
7877
httpReq.Header.Set("Cache-Control", "no-cache")
7978
httpReq.Header.Set("Connection", "keep-alive")
80-
return c.doStreamRequest(ctx, httpReq, evh)
79+
return c.doStreamRequest(ctx, httpReq)
8180
}
8281

8382
// marshalBody serializes the request body to JSON.
@@ -146,57 +145,18 @@ func (c *Client) doRequest(req *http.Request) (*Response, error) {
146145
}
147146

148147
// doStreamRequest executes the HTTP request and returns streaming responses via the provided channel.
149-
func (c *Client) doStreamRequest(ctx context.Context, req *http.Request, evCh chan *Event) error {
148+
func (c *Client) doStreamRequest(ctx context.Context, req *http.Request) (iter.Seq2[[]byte, error], error) {
150149
ctxReq := req.WithContext(ctx)
151-
resp, err := c.client.Do(ctxReq) //nolint:bodyclose // ignore the error, it will be handled in the next step
150+
resp, err := c.client.Do(ctxReq)
152151
if err != nil {
153-
return fmt.Errorf("failed to send HTTP request: %w", err)
152+
return nil, fmt.Errorf("failed to send HTTP request: %w", err)
154153
}
155154

156155
if resp.StatusCode != http.StatusOK {
157-
return fmt.Errorf("HTTP error resp code: %d", resp.StatusCode)
156+
resp.Body.Close()
157+
return nil, fmt.Errorf("HTTP error resp code: %d", resp.StatusCode)
158158
}
159-
160-
go func() {
161-
defer func() {
162-
err = resp.Body.Close()
163-
if err != nil {
164-
log.Errorf("failed to close the http body: %v", err)
165-
}
166-
}()
167-
reader := bufio.NewReader(resp.Body)
168-
for {
169-
line, err := reader.ReadBytes('\n')
170-
if err != nil {
171-
if errors.Is(err, io.EOF) {
172-
evCh <- &Event{Done: true}
173-
return
174-
}
175-
log.Errorf("failed to read: %v", err)
176-
return
177-
}
178-
line = bytes.TrimRight(line, "\r\n")
179-
if bytes.IndexByte(line, ':') == -1 {
180-
continue
181-
}
182-
field := line
183-
value := []byte{}
184-
if i := bytes.IndexByte(line, ':'); i >= 0 {
185-
field = line[:i]
186-
if i+1 < len(line) {
187-
value = line[i+1:]
188-
if len(value) > 0 && value[0] == ' ' {
189-
value = value[1:]
190-
}
191-
}
192-
}
193-
194-
if string(field) == "data" {
195-
evCh <- &Event{Data: bytes.NewBuffer(value), Type: string(field)}
196-
}
197-
}
198-
}()
199-
return nil
159+
return sseHandler(resp.Body), nil
200160
}
201161

202162
// buildURL constructs a complete URL from base URL, path, and query parameters.

internal/handler/sse.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package handler
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"io"
8+
"iter"
9+
"sync"
10+
)
11+
12+
var (
13+
sseBufferSize = 64 * 1024 // 64KB
14+
sseDataPrefix = "data:"
15+
16+
pool = sync.Pool{
17+
New: func() any {
18+
buf := make([]byte, 0, 4096)
19+
return &buf
20+
},
21+
}
22+
)
23+
24+
func getBuffer() *[]byte {
25+
return pool.Get().(*[]byte)
26+
}
27+
28+
func putBuffer(buf *[]byte) {
29+
*buf = (*buf)[:0]
30+
}
31+
32+
func sseHandler(body io.ReadCloser) iter.Seq2[[]byte, error] {
33+
return func(yield func([]byte, error) bool) {
34+
defer body.Close()
35+
36+
scanner := bufio.NewScanner(body)
37+
buf := getBuffer()
38+
defer putBuffer(buf)
39+
40+
scanner.Buffer(*buf, sseBufferSize)
41+
for scanner.Scan() {
42+
lineBytes := scanner.Bytes()
43+
prefix := []byte(sseDataPrefix)
44+
if !bytes.HasPrefix(lineBytes, prefix) {
45+
continue
46+
}
47+
data := lineBytes[len(prefix):]
48+
if len(data) > 0 && data[0] == ' ' {
49+
data = data[1:]
50+
}
51+
if !yield(data, nil) {
52+
return
53+
}
54+
}
55+
if err := scanner.Err(); err != nil {
56+
yield(nil, fmt.Errorf("sseHandler scanning error: %v", err))
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)