feat: Added ping command
Chart Release / chart (push) Successful in 18s
Agent Release / build (push) Successful in 39s
Server Deploy / deploy (push) Successful in 55s
Agent Release / msi (push) Successful in 40s

This commit is contained in:
2026-07-31 17:10:59 +01:00
parent 8699dc5b7e
commit 1fb9bd827f
6 changed files with 127 additions and 2 deletions
+6
View File
@@ -158,8 +158,14 @@ type ServerCommand struct {
RunStep *RunStepCmd `json:"run_step,omitempty"`
CleanupWorkspace *CleanupWorkspaceCmd `json:"cleanup_workspace,omitempty"`
OpenProxy *OpenProxyCmd `json:"open_proxy,omitempty"`
Ping *PingCmd `json:"ping,omitempty"`
}
// PingCmd is a server-originated liveness beat. It carries nothing and expects
// no reply: its arrival is the entire message. See the .proto for why gRPC
// keepalive is not sufficient on its own.
type PingCmd struct{}
type CleanupWorkspaceCmd struct {
WorkspaceId string `json:"workspace_id"`
}
+21
View File
@@ -212,11 +212,27 @@ func (s *vantageServer) CommandStream(stream pb.Vantage_CommandStreamServer) err
}
}()
// The heartbeat is what lets the agent tell a live stream from an orphaned
// one. gRPC keepalive cannot: behind an L7 proxy the agent's connection
// terminates at the proxy, which answers pings on its own behalf, so a dead
// pod leaves the agent blocked in Recv forever with commands vanishing into
// a stream nobody is serving. A message that originates here is the only
// thing that proves this process is still on the other end.
ping := time.NewTicker(pingInterval)
defer ping.Stop()
ctx := stream.Context()
for {
select {
case <-ctx.Done():
return nil
case <-ping.C:
// A failed send is the point: it is how this side learns the stream
// is gone, which runs the deferred release and frees the agent's
// presence claim for whichever pod it reconnects to.
if err := stream.Send(&pb.ServerCommand{Ping: &pb.PingCmd{}}); err != nil {
return err
}
case cmd, ok := <-ch:
if !ok {
return nil
@@ -228,6 +244,11 @@ func (s *vantageServer) CommandStream(stream pb.Vantage_CommandStreamServer) err
}
}
// How often the server beats on an idle command stream. Comfortably under the
// agent's staleness threshold, so a single dropped beat does not cost a
// reconnect.
const pingInterval = 20 * time.Second
// StartGRPC serves the agent API until stop is called.
//
// It returns a stop function rather than serving forever because an abrupt exit