mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat(gateway): publish lifecycle runtime events
Emit gateway.start, gateway.ready, and gateway.shutdown on the shared runtime event bus, while keeping reload events on the same helper path. Update subturn architecture docs to refer to runtime event kinds instead of the removed agent EventBus names. Validation: GOCACHE=/tmp/picoclaw-go-cache go test ./pkg/gateway ./pkg/events; GOCACHE=/tmp/picoclaw-go-cache go test ./pkg/bus ./pkg/channels ./pkg/mcp ./pkg/tools/integration ./pkg/events ./pkg/gateway; make lint
This commit is contained in:
@@ -75,6 +75,12 @@ const (
|
||||
// KindBusCloseDrained is emitted when message bus close drains buffered messages.
|
||||
KindBusCloseDrained Kind = "bus.close.drained"
|
||||
|
||||
// KindGatewayStart is emitted when gateway startup reaches runtime bootstrap.
|
||||
KindGatewayStart Kind = "gateway.start"
|
||||
// KindGatewayReady is emitted when gateway services are started and ready.
|
||||
KindGatewayReady Kind = "gateway.ready"
|
||||
// KindGatewayShutdown is emitted when gateway shutdown starts.
|
||||
KindGatewayShutdown Kind = "gateway.shutdown"
|
||||
// KindGatewayReloadStarted is emitted when gateway reload starts.
|
||||
KindGatewayReloadStarted Kind = "gateway.reload.started"
|
||||
// KindGatewayReloadCompleted is emitted when gateway reload completes.
|
||||
|
||||
@@ -10,12 +10,12 @@ import (
|
||||
|
||||
const gatewayEventPublishTimeout = 100 * time.Millisecond
|
||||
|
||||
type gatewayReloadPayload struct {
|
||||
type gatewayEventPayload struct {
|
||||
DurationMS int64 `json:"duration_ms,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func publishGatewayReloadEvent(
|
||||
func publishGatewayEvent(
|
||||
al *agent.AgentLoop,
|
||||
kind runtimeevents.Kind,
|
||||
startedAt time.Time,
|
||||
@@ -26,7 +26,7 @@ func publishGatewayReloadEvent(
|
||||
}
|
||||
|
||||
severity := runtimeevents.SeverityInfo
|
||||
payload := gatewayReloadPayload{}
|
||||
payload := gatewayEventPayload{}
|
||||
if !startedAt.IsZero() {
|
||||
payload.DurationMS = time.Since(startedAt).Milliseconds()
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ func (p *startupBlockedProvider) GetDefaultModel() string {
|
||||
|
||||
// Run starts the gateway runtime using the configuration loaded from configPath.
|
||||
func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runErr error) {
|
||||
startedAt := time.Now()
|
||||
panicPath := filepath.Join(homePath, logPath, panicFile)
|
||||
panicFunc, err := logger.InitPanic(panicPath)
|
||||
if err != nil {
|
||||
@@ -199,6 +200,7 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
|
||||
msgBus := bus.NewMessageBus()
|
||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
||||
msgBus.SetEventPublisher(agentLoop.RuntimeEventBus())
|
||||
publishGatewayEvent(agentLoop, runtimeevents.KindGatewayStart, startedAt, nil)
|
||||
|
||||
fmt.Println("\n📦 Agent Status:")
|
||||
startupInfo := agentLoop.GetStartupInfo()
|
||||
@@ -218,6 +220,7 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) (runEr
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publishGatewayEvent(agentLoop, runtimeevents.KindGatewayReady, startedAt, nil)
|
||||
closeListeners = false
|
||||
|
||||
// Setup manual reload channel for /reload endpoint
|
||||
@@ -316,14 +319,14 @@ func executeReload(
|
||||
debug bool,
|
||||
) (err error) {
|
||||
startedAt := time.Now()
|
||||
publishGatewayReloadEvent(agentLoop, runtimeevents.KindGatewayReloadStarted, startedAt, nil)
|
||||
publishGatewayEvent(agentLoop, runtimeevents.KindGatewayReloadStarted, startedAt, nil)
|
||||
defer runningServices.reloading.Store(false)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
publishGatewayReloadEvent(agentLoop, runtimeevents.KindGatewayReloadFailed, startedAt, err)
|
||||
publishGatewayEvent(agentLoop, runtimeevents.KindGatewayReloadFailed, startedAt, err)
|
||||
return
|
||||
}
|
||||
publishGatewayReloadEvent(agentLoop, runtimeevents.KindGatewayReloadCompleted, startedAt, nil)
|
||||
publishGatewayEvent(agentLoop, runtimeevents.KindGatewayReloadCompleted, startedAt, nil)
|
||||
}()
|
||||
|
||||
err = handleConfigReload(ctx, agentLoop, newCfg, provider, runningServices, msgBus, allowEmptyStartup, debug)
|
||||
@@ -509,6 +512,8 @@ func shutdownGateway(
|
||||
provider providers.LLMProvider,
|
||||
fullShutdown bool,
|
||||
) {
|
||||
publishGatewayEvent(agentLoop, runtimeevents.KindGatewayShutdown, time.Time{}, nil)
|
||||
|
||||
if cp, ok := provider.(providers.StatefulProvider); ok && fullShutdown {
|
||||
cp.Close()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/agent"
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
runtimeevents "github.com/sipeed/picoclaw/pkg/events"
|
||||
)
|
||||
|
||||
func TestRun_StartupFailuresReturnErrorAndEmitStructuredLog(t *testing.T) {
|
||||
@@ -106,3 +111,64 @@ func TestGatewayRunStartupFailureHelper(t *testing.T) {
|
||||
fmt.Fprintln(os.Stdout, err.Error())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestPublishGatewayEvent(t *testing.T) {
|
||||
eventBus := runtimeevents.NewBus()
|
||||
t.Cleanup(func() {
|
||||
if err := eventBus.Close(); err != nil {
|
||||
t.Fatalf("Close runtime event bus: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
sub, eventsCh, err := eventBus.Channel().OfKind(runtimeevents.KindGatewayStart).SubscribeChan(
|
||||
ctx,
|
||||
runtimeevents.SubscribeOptions{Name: "gateway-test", Buffer: 4},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("SubscribeChan() error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := sub.Close(); err != nil {
|
||||
t.Fatalf("Close subscription: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
al := agent.NewAgentLoop(
|
||||
config.DefaultConfig(),
|
||||
bus.NewMessageBus(),
|
||||
&startupBlockedProvider{reason: "not used"},
|
||||
agent.WithRuntimeEvents(eventBus),
|
||||
)
|
||||
t.Cleanup(al.Close)
|
||||
|
||||
startedAt := time.Now().Add(-1500 * time.Millisecond)
|
||||
publishGatewayEvent(al, runtimeevents.KindGatewayStart, startedAt, nil)
|
||||
|
||||
evt := receiveGatewayRuntimeEvent(t, eventsCh)
|
||||
if evt.Kind != runtimeevents.KindGatewayStart ||
|
||||
evt.Source.Component != "gateway" ||
|
||||
evt.Severity != runtimeevents.SeverityInfo {
|
||||
t.Fatalf("gateway event = %+v", evt)
|
||||
}
|
||||
payload, ok := evt.Payload.(gatewayEventPayload)
|
||||
if !ok {
|
||||
t.Fatalf("payload type = %T, want gatewayEventPayload", evt.Payload)
|
||||
}
|
||||
if payload.DurationMS <= 0 {
|
||||
t.Fatalf("DurationMS = %d, want positive", payload.DurationMS)
|
||||
}
|
||||
}
|
||||
|
||||
func receiveGatewayRuntimeEvent(t *testing.T, ch <-chan runtimeevents.Event) runtimeevents.Event {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case evt := <-ch:
|
||||
return evt
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for gateway runtime event")
|
||||
return runtimeevents.Event{}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user