fix query race. fix stream id collision behavior

This commit is contained in:
Jay
2026-05-18 10:40:35 -04:00
parent 34a487568b
commit ef2308518f
+12 -30
View File
@@ -140,6 +140,9 @@ func (m *RequestManager) newStream(
isQuery bool, isQuery bool,
opts ...RequestOption, opts ...RequestOption,
) (string, <-chan ReqEvent, <-chan ReqClosed, error) { ) (string, <-chan ReqEvent, <-chan ReqClosed, error) {
m.mu.Lock()
defer m.mu.Unlock()
var o requestOptions var o requestOptions
for _, opt := range opts { for _, opt := range opts {
opt(&o) opt(&o)
@@ -147,6 +150,9 @@ func (m *RequestManager) newStream(
var id string var id string
if o.id != "" { if o.id != "" {
if _, exists := m.reqs[o.id]; exists {
return "", nil, nil, fmt.Errorf("Stream: id %q already in use", o.id)
}
id = o.id id = o.id
} else { } else {
label := o.label label := o.label
@@ -158,20 +164,11 @@ func (m *RequestManager) newStream(
buffer := make(chan ReqEvent, 64) buffer := make(chan ReqEvent, 64)
closed := make(chan ReqClosed, 1) closed := make(chan ReqClosed, 1)
var events chan ReqEvent events := make(chan ReqEvent)
if isQuery {
// Query reads directly from buffer. bufferedPipe drains asynchronously,
// so EOSE can close the pipe's output before all buffered EVENTs have
// passed through — causing Query's collect loop to exit short. Reading
// from the buffered channel directly avoids that race.
events = buffer
} else {
events = make(chan ReqEvent)
go func() { go func() {
bufferedPipe(buffer, events) bufferedPipe(buffer, events)
close(events) close(events)
}() }()
}
req := &request{ req := &request{
id: id, id: id,
@@ -182,22 +179,10 @@ func (m *RequestManager) newStream(
isQuery: isQuery, isQuery: isQuery,
} }
m.mu.Lock()
if _, exists := m.reqs[id]; exists {
m.mu.Unlock()
close(buffer)
if !isQuery {
// drain bufferedPipe goroutine
for range events {
}
}
return "", nil, nil, fmt.Errorf("Stream: id %q already in use", id)
}
m.reqs[id] = req m.reqs[id] = req
if m.envoy.IsConnected() { if m.envoy.IsConnected() {
m.activate(req) m.activate(req)
} }
m.mu.Unlock()
return id, events, closed, nil return id, events, closed, nil
} }
@@ -356,14 +341,8 @@ func (m *RequestManager) dispatchInbox(msg InboxMessage) {
} }
} }
// routeEvent, routeEOSE, and routeClosed use blocking sends into req.buffer. // routeEvent, routeEOSE, and routeClosed use blocking sends into req.buffer,
// This preserves every event without dropping: req.buffer feeds either an // which reads eagerly into a slice buffer and cannot block the router.
// unbounded bufferedPipe (streams) that absorbs a slow external reader, or
// is read directly by Query's collect loop in the caller's goroutine.
// ---
// routeEvent, routeEOSE, and routeClosed are always called sequentially from
// the same routeInbox goroutine via dispatchInbox. This makes it safe for
// routeEOSE to close req.buffer: no concurrent routeEvent send can race it.
func (m *RequestManager) routeEvent(msg InboxMessage) { func (m *RequestManager) routeEvent(msg InboxMessage) {
subID, event, err := envelope.FindSubscriptionEvent(msg.Data) subID, event, err := envelope.FindSubscriptionEvent(msg.Data)
if err != nil { if err != nil {
@@ -382,6 +361,9 @@ func (m *RequestManager) routeEvent(msg InboxMessage) {
} }
} }
// routeEvent, routeEOSE, and routeClosed are always called sequentially from
// the same routeInbox goroutine via dispatchInbox. This makes it safe for
// routeEOSE to close req.buffer: no concurrent routeEvent send can race it.
func (m *RequestManager) routeEOSE(msg InboxMessage) { func (m *RequestManager) routeEOSE(msg InboxMessage) {
subID, err := envelope.FindEOSE(msg.Data) subID, err := envelope.FindEOSE(msg.Data)
if err != nil { if err != nil {