fix: Race condition and speedup in accessing streaming clients
This commit is contained in:
parent
e0fd10a6ec
commit
61fc771ac3
|
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||||
auth "github.com/OliveTin/OliveTin/internal/auth"
|
auth "github.com/OliveTin/OliveTin/internal/auth"
|
||||||
|
|
@ -28,10 +29,26 @@ type oliveTinAPI struct {
|
||||||
executor *executor.Executor
|
executor *executor.Executor
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
|
|
||||||
connectedClients []*connectedClients
|
// streamingClients is a set of currently connected clients.
|
||||||
|
// The empty struct value models set semantics (keys only) and keeps add/remove O(1).
|
||||||
|
// We use a map for efficient membership and deletion; ordering is not required.
|
||||||
|
streamingClients map[*streamingClient]struct{}
|
||||||
|
streamingClientsMutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type connectedClients struct {
|
// 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.
|
||||||
|
func (api *oliveTinAPI) copyOfStreamingClients() []*streamingClient {
|
||||||
|
api.streamingClientsMutex.RLock()
|
||||||
|
defer api.streamingClientsMutex.RUnlock()
|
||||||
|
clients := make([]*streamingClient, 0, len(api.streamingClients))
|
||||||
|
for client := range api.streamingClients {
|
||||||
|
clients = append(clients, client)
|
||||||
|
}
|
||||||
|
return clients
|
||||||
|
}
|
||||||
|
|
||||||
|
type streamingClient struct {
|
||||||
channel chan *apiv1.EventStreamResponse
|
channel chan *apiv1.EventStreamResponse
|
||||||
AuthenticatedUser *acl.AuthenticatedUser
|
AuthenticatedUser *acl.AuthenticatedUser
|
||||||
}
|
}
|
||||||
|
|
@ -629,14 +646,16 @@ func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &connectedClients{
|
client := &streamingClient{
|
||||||
channel: make(chan *apiv1.EventStreamResponse, 10), // Buffered channel to hold Events
|
channel: make(chan *apiv1.EventStreamResponse, 10), // Buffered channel to hold Events
|
||||||
AuthenticatedUser: user,
|
AuthenticatedUser: user,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("EventStream: client connected: %v", client.AuthenticatedUser.Username)
|
log.Infof("EventStream: client connected: %v", client.AuthenticatedUser.Username)
|
||||||
|
|
||||||
api.connectedClients = append(api.connectedClients, client)
|
api.streamingClientsMutex.Lock()
|
||||||
|
api.streamingClients[client] = struct{}{}
|
||||||
|
api.streamingClientsMutex.Unlock()
|
||||||
|
|
||||||
// loop over client channel and send events to connectedClient
|
// loop over client channel and send events to connectedClient
|
||||||
for msg := range client.channel {
|
for msg := range client.channel {
|
||||||
|
|
@ -654,22 +673,16 @@ func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) removeClient(clientToRemove *connectedClients) {
|
func (api *oliveTinAPI) removeClient(clientToRemove *streamingClient) {
|
||||||
api.connectedClients = func() []*connectedClients {
|
api.streamingClientsMutex.Lock()
|
||||||
var filtered []*connectedClients
|
delete(api.streamingClients, clientToRemove)
|
||||||
for _, client := range api.connectedClients {
|
api.streamingClientsMutex.Unlock()
|
||||||
if client != clientToRemove {
|
|
||||||
filtered = append(filtered, client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) OnActionMapRebuilt() {
|
func (api *oliveTinAPI) OnActionMapRebuilt() {
|
||||||
toRemove := []*connectedClients{}
|
toRemove := []*streamingClient{}
|
||||||
|
|
||||||
for _, client := range api.connectedClients {
|
for _, client := range api.copyOfStreamingClients() {
|
||||||
select {
|
select {
|
||||||
case client.channel <- &apiv1.EventStreamResponse{
|
case client.channel <- &apiv1.EventStreamResponse{
|
||||||
Event: &apiv1.EventStreamResponse_ConfigChanged{
|
Event: &apiv1.EventStreamResponse_ConfigChanged{
|
||||||
|
|
@ -688,9 +701,9 @@ func (api *oliveTinAPI) OnActionMapRebuilt() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
|
func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
|
||||||
toRemove := []*connectedClients{}
|
toRemove := []*streamingClient{}
|
||||||
|
|
||||||
for _, client := range api.connectedClients {
|
for _, client := range api.copyOfStreamingClients() {
|
||||||
select {
|
select {
|
||||||
case client.channel <- &apiv1.EventStreamResponse{
|
case client.channel <- &apiv1.EventStreamResponse{
|
||||||
Event: &apiv1.EventStreamResponse_ExecutionStarted{
|
Event: &apiv1.EventStreamResponse_ExecutionStarted{
|
||||||
|
|
@ -711,9 +724,9 @@ func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
|
func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
|
||||||
toRemove := []*connectedClients{}
|
toRemove := []*streamingClient{}
|
||||||
|
|
||||||
for _, client := range api.connectedClients {
|
for _, client := range api.copyOfStreamingClients() {
|
||||||
select {
|
select {
|
||||||
case client.channel <- &apiv1.EventStreamResponse{
|
case client.channel <- &apiv1.EventStreamResponse{
|
||||||
Event: &apiv1.EventStreamResponse_ExecutionFinished{
|
Event: &apiv1.EventStreamResponse_ExecutionFinished{
|
||||||
|
|
@ -834,9 +847,9 @@ func buildAdditionalLinks(links []*config.NavigationLink) []*apiv1.AdditionalLin
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string) {
|
func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string) {
|
||||||
toRemove := []*connectedClients{}
|
toRemove := []*streamingClient{}
|
||||||
|
|
||||||
for _, client := range api.connectedClients {
|
for _, client := range api.copyOfStreamingClients() {
|
||||||
select {
|
select {
|
||||||
case client.channel <- &apiv1.EventStreamResponse{
|
case client.channel <- &apiv1.EventStreamResponse{
|
||||||
Event: &apiv1.EventStreamResponse_OutputChunk{
|
Event: &apiv1.EventStreamResponse_OutputChunk{
|
||||||
|
|
@ -971,6 +984,7 @@ func newServer(ex *executor.Executor) *oliveTinAPI {
|
||||||
server := oliveTinAPI{}
|
server := oliveTinAPI{}
|
||||||
server.cfg = ex.Cfg
|
server.cfg = ex.Cfg
|
||||||
server.executor = ex
|
server.executor = ex
|
||||||
|
server.streamingClients = make(map[*streamingClient]struct{})
|
||||||
|
|
||||||
ex.AddListener(&server)
|
ex.AddListener(&server)
|
||||||
return &server
|
return &server
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue