mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
- Added multi-execution view to display multiple execution statuses. - Improved error handling in agent triggering to surface failures. - Updated execution view to support live updates via SSE and added a log panel for node logs. - Enhanced pipeline canvas to display node statuses during execution. - Introduced new fields in the build node form for Docker configurations. - Updated API to support execution streaming and enhanced node catalog descriptions.
108 lines
2.8 KiB
Go
108 lines
2.8 KiB
Go
// Package execevents provides an in-memory pub/sub bus for workflow
|
|
// execution events. The engine emits events through engine.Emitter; the API
|
|
// server fans them out to SSE subscribers.
|
|
//
|
|
// This is single-process — fine for the current self-hosted topology.
|
|
// Multi-instance deployments will swap this for Postgres LISTEN/NOTIFY or
|
|
// Redis Streams (the engine.Emitter interface stays the same).
|
|
package execevents
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/lyzrai/flow/pkg/engine"
|
|
)
|
|
|
|
// MemoryBus is a single-process publish/subscribe bus keyed by execution ID.
|
|
// Subscribers receive every event published for their execution until they
|
|
// unsubscribe or the channel buffer fills (slow subscribers are dropped to
|
|
// keep the publisher non-blocking).
|
|
type MemoryBus struct {
|
|
mu sync.RWMutex
|
|
subscribers map[string][]*subscription
|
|
// terminal stores the final event per exec so a subscriber that arrives
|
|
// late still gets a "done" / "error" event and closes cleanly.
|
|
terminal map[string]engine.ExecutionEvent
|
|
}
|
|
|
|
type subscription struct {
|
|
ch chan engine.ExecutionEvent
|
|
closed bool
|
|
once sync.Once
|
|
}
|
|
|
|
// NewMemoryBus returns a fresh in-memory bus.
|
|
func NewMemoryBus() *MemoryBus {
|
|
return &MemoryBus{
|
|
subscribers: map[string][]*subscription{},
|
|
terminal: map[string]engine.ExecutionEvent{},
|
|
}
|
|
}
|
|
|
|
// Emit implements engine.Emitter. Non-blocking: if a subscriber's channel is
|
|
// full we drop the event for that subscriber (publisher must not stall).
|
|
func (b *MemoryBus) Emit(_ context.Context, execID string, e engine.ExecutionEvent) {
|
|
if execID == "" {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
subs := append([]*subscription(nil), b.subscribers[execID]...)
|
|
if isTerminal(e.Type) {
|
|
b.terminal[execID] = e
|
|
}
|
|
b.mu.Unlock()
|
|
|
|
for _, s := range subs {
|
|
select {
|
|
case s.ch <- e:
|
|
default:
|
|
// drop — slow subscriber
|
|
}
|
|
}
|
|
}
|
|
|
|
// Subscribe returns a channel that receives every event for execID. The
|
|
// caller must call the returned cancel func when done. If a terminal event
|
|
// was already published before subscribe, it is replayed once so the caller
|
|
// can shut down cleanly.
|
|
func (b *MemoryBus) Subscribe(execID string) (<-chan engine.ExecutionEvent, func()) {
|
|
s := &subscription{ch: make(chan engine.ExecutionEvent, 32)}
|
|
|
|
b.mu.Lock()
|
|
b.subscribers[execID] = append(b.subscribers[execID], s)
|
|
term, hadTerm := b.terminal[execID]
|
|
b.mu.Unlock()
|
|
|
|
if hadTerm {
|
|
// non-blocking — buffer is fresh
|
|
s.ch <- term
|
|
}
|
|
|
|
cancel := func() {
|
|
s.once.Do(func() {
|
|
b.mu.Lock()
|
|
cur := b.subscribers[execID]
|
|
out := cur[:0]
|
|
for _, x := range cur {
|
|
if x != s {
|
|
out = append(out, x)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
delete(b.subscribers, execID)
|
|
} else {
|
|
b.subscribers[execID] = out
|
|
}
|
|
b.mu.Unlock()
|
|
s.closed = true
|
|
close(s.ch)
|
|
})
|
|
}
|
|
return s.ch, cancel
|
|
}
|
|
|
|
func isTerminal(t engine.EventType) bool {
|
|
return t == engine.EventDone
|
|
}
|