mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-08 23:37:43 -04:00
* feat: add distributed mode (experimental) Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix data races, mutexes, transactions Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix events and tool stream in agent chat Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * use ginkgo Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(cron): compute correctly time boundaries avoiding re-triggering Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * enhancements, refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * do not flood of healthy checks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * do not list obvious backends as text backends Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * tests fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Drop redundant healthcheck Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * enhancements, refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
98 lines
2.7 KiB
Go
98 lines
2.7 KiB
Go
package jobs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// SSEBridge provides an HTTP handler that bridges NATS progress events to SSE.
|
|
// This follows the notetaker pattern: subscribe to NATS, forward to SSE client.
|
|
func (d *Dispatcher) SSEHandler() echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
jobID := c.Param("id")
|
|
if jobID == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "job ID required"})
|
|
}
|
|
|
|
// Check flusher support before writing any headers
|
|
flusher, ok := c.Response().Writer.(http.Flusher)
|
|
if !ok {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "streaming not supported"})
|
|
}
|
|
|
|
// Set SSE headers
|
|
c.Response().Header().Set("Content-Type", "text/event-stream")
|
|
c.Response().Header().Set("Cache-Control", "no-cache")
|
|
c.Response().Header().Set("Connection", "keep-alive")
|
|
c.Response().WriteHeader(http.StatusOK)
|
|
|
|
// Thread-safe event writer with close guard to prevent writes after handler returns
|
|
var mu sync.Mutex
|
|
var closed atomic.Bool
|
|
sendEvent := func(event string, data any) {
|
|
if closed.Load() {
|
|
return
|
|
}
|
|
jsonData, err := json.Marshal(data)
|
|
if err != nil {
|
|
return
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
fmt.Fprintf(c.Response(), "event: %s\ndata: %s\n\n", event, jsonData)
|
|
flusher.Flush()
|
|
}
|
|
|
|
// Send current job state first
|
|
job, err := d.store.GetJob(jobID)
|
|
if err == nil {
|
|
sendEvent("status", ProgressEvent{
|
|
JobID: jobID,
|
|
Status: job.Status,
|
|
})
|
|
// If already terminal, send done and close — no need to subscribe
|
|
if job.Status == "completed" || job.Status == "failed" || job.Status == "cancelled" {
|
|
sendEvent("done", ProgressEvent{
|
|
JobID: jobID,
|
|
Status: job.Status,
|
|
})
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// done is closed when a terminal state event is received, so the
|
|
// handler can return promptly instead of waiting for client disconnect.
|
|
done := make(chan struct{})
|
|
closeOnce := sync.Once{}
|
|
|
|
// Subscribe to progress events for this job
|
|
sub, err := d.SubscribeProgress(jobID, func(evt ProgressEvent) {
|
|
sendEvent("progress", evt)
|
|
|
|
// Close the stream on terminal states
|
|
if evt.Status == "completed" || evt.Status == "failed" || evt.Status == "cancelled" {
|
|
sendEvent("done", evt)
|
|
closeOnce.Do(func() { close(done) })
|
|
}
|
|
})
|
|
if err != nil {
|
|
// Headers already written as SSE — cannot send JSON error; use SSE event instead
|
|
sendEvent("error", map[string]string{"error": "failed to subscribe"})
|
|
return nil
|
|
}
|
|
// Wait for client disconnect or terminal state
|
|
select {
|
|
case <-c.Request().Context().Done():
|
|
case <-done:
|
|
}
|
|
closed.Store(true)
|
|
sub.Unsubscribe()
|
|
return nil
|
|
}
|
|
}
|