|
@@ -43,6 +43,11 @@ type oliveTinAPI struct {
|
|
|
streamingClientsMutex sync.RWMutex
|
|
streamingClientsMutex sync.RWMutex
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Caps concurrent EventStream connections to limit memory/FD/goroutine exhaustion.
|
|
|
|
|
+const maxEventStreamClients = 16
|
|
|
|
|
+
|
|
|
|
|
+var errEventStreamClientLimit = errors.New("too many concurrent event stream clients")
|
|
|
|
|
+
|
|
|
// This is used to avoid race conditions when iterating over the connectedClients map.
|
|
// This is used to avoid race conditions when iterating over the connectedClients map.
|
|
|
// and holds the lock for as minimal time as possible to avoid blocking the API for too long.
|
|
// and holds the lock for as minimal time as possible to avoid blocking the API for too long.
|
|
|
func (api *oliveTinAPI) copyOfStreamingClients() []*streamingClient {
|
|
func (api *oliveTinAPI) copyOfStreamingClients() []*streamingClient {
|
|
@@ -1028,14 +1033,14 @@ func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.
|
|
|
heartbeatDone: make(chan struct{}),
|
|
heartbeatDone: make(chan struct{}),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if err := api.registerStreamingClient(client); err != nil {
|
|
|
|
|
+ return connect.NewError(connect.CodeResourceExhausted, err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
log.WithFields(log.Fields{
|
|
log.WithFields(log.Fields{
|
|
|
"authenticatedUser": user.Username,
|
|
"authenticatedUser": user.Username,
|
|
|
}).Debugf("EventStream: client connected")
|
|
}).Debugf("EventStream: client connected")
|
|
|
|
|
|
|
|
- api.streamingClientsMutex.Lock()
|
|
|
|
|
- api.streamingClients[client] = struct{}{}
|
|
|
|
|
- api.streamingClientsMutex.Unlock()
|
|
|
|
|
-
|
|
|
|
|
go api.sendEventStreamHeartbeats(client)
|
|
go api.sendEventStreamHeartbeats(client)
|
|
|
|
|
|
|
|
// loop over client channel and send events to connectedClient
|
|
// loop over client channel and send events to connectedClient
|
|
@@ -1054,6 +1059,21 @@ func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+func (api *oliveTinAPI) registerStreamingClient(client *streamingClient) error {
|
|
|
|
|
+ api.streamingClientsMutex.Lock()
|
|
|
|
|
+ defer api.streamingClientsMutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ if len(api.streamingClients) >= maxEventStreamClients {
|
|
|
|
|
+ log.WithFields(log.Fields{
|
|
|
|
|
+ "limit": maxEventStreamClients,
|
|
|
|
|
+ }).Warn("EventStream: rejecting client; concurrent client limit reached")
|
|
|
|
|
+ return errEventStreamClientLimit
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ api.streamingClients[client] = struct{}{}
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (api *oliveTinAPI) sendEventStreamHeartbeats(client *streamingClient) {
|
|
func (api *oliveTinAPI) sendEventStreamHeartbeats(client *streamingClient) {
|
|
|
defer close(client.heartbeatDone)
|
|
defer close(client.heartbeatDone)
|
|
|
|
|
|