Skip to content

Commit ed3a127

Browse files
kvapsclaude
andcommitted
feat(observe): wire satellite events2 → controller ReportObserved stream
Satellite agent now spawns an observe goroutine that: - launches `drbdsetup events2 --statistics` - pushes parsed Events through Observer → ResourceObservedEvent - ships them upstream via the new Controller.ReportObserved client-streaming RPC Controller-side handler accepts the stream, counts received frames, and routes each event through applyObserved. The status-update body is a placeholder for now — wiring per-Resource conditions + per-volume disk state lands in the next slice. The wire path is exercised end- to-end so any future implementation slots in without re-touching the gRPC frame loop. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 17b89ce commit ed3a127

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

pkg/satellite/agent.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ func (a *Agent) Run(ctx context.Context) error {
137137

138138
a.logger.Info("satellite gRPC ready", "addr", srv)
139139

140+
// Tail drbdsetup events2 + ship observed state back to the
141+
// controller via ReportObserved. Failures are logged and the
142+
// goroutine exits — we don't want a noisy events2 to take down
143+
// the satellite (the next Apply will redrive state anyway).
144+
go a.runObserveLoop(ctx, client)
145+
140146
<-ctx.Done()
141147

142148
a.logger.Info("agent stopping", "node", a.cfg.NodeName)
@@ -211,6 +217,57 @@ func (a *Agent) dial(ctx context.Context) (*grpc.ClientConn, error) {
211217
return conn, nil
212218
}
213219

220+
// runObserveLoop tails `drbdsetup events2` and pushes parsed
221+
// observations to the controller via the ReportObserved
222+
// client-streaming RPC. Best-effort: any failure logs and exits the
223+
// loop — the controller's next ApplyResources retries the desired
224+
// state anyway, so a flaky events2 stream isn't fatal.
225+
func (a *Agent) runObserveLoop(ctx context.Context, client satellitepb.ControllerClient) {
226+
watcher, cleanup, err := drbd.StartDrbdsetupEvents2(ctx)
227+
if err != nil {
228+
a.logger.Error("start events2", "err", err)
229+
230+
return
231+
}
232+
defer cleanup()
233+
234+
stream, err := client.ReportObserved(ctx)
235+
if err != nil {
236+
a.logger.Error("open ReportObserved stream", "err", err)
237+
238+
return
239+
}
240+
241+
obs := NewObserver(a.cfg.NodeName)
242+
events := make(chan drbd.Event, observeBuffer)
243+
244+
go func() {
245+
watchErr := watcher.Watch(ctx, events)
246+
if watchErr != nil {
247+
a.logger.Error("events2 watch", "err", watchErr)
248+
}
249+
}()
250+
251+
for ev := range obs.Translate(events) {
252+
err := stream.Send(ev)
253+
if err != nil {
254+
a.logger.Error("ReportObserved send", "err", err)
255+
256+
return
257+
}
258+
}
259+
260+
_, err = stream.CloseAndRecv()
261+
if err != nil {
262+
a.logger.Error("ReportObserved close", "err", err)
263+
}
264+
}
265+
266+
// observeBuffer caps the events2 → Observer in-flight queue. drbd-9
267+
// reconnect storms can burst dozens of events; 256 is a comfortable
268+
// cushion for the satellite-side translation goroutine.
269+
const observeBuffer = 256
270+
214271
// hostFromEndpoint trims the trailing :port off an endpoint string.
215272
// Returns the input unchanged when it has no port (e.g. plain host or
216273
// already host-only) or is empty. We don't use net.SplitHostPort

pkg/satellitecontroller/server.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package satellitecontroller
2222

2323
import (
2424
"context"
25+
"io"
2526

2627
"github.com/cockroachdb/errors"
2728
"google.golang.org/grpc/codes"
@@ -99,3 +100,44 @@ func (s *Server) Hello(ctx context.Context, req *satellitepb.HelloRequest) (*sat
99100
ControllerEndpoint: s.cfg.ControllerEndpoint,
100101
}, nil
101102
}
103+
104+
// ReportObserved is the satellite→controller observed-state stream.
105+
// Each frame describes one parsed `drbdsetup events2` line; we land
106+
// it on the matching Resource CRD's Status.
107+
//
108+
// The handler intentionally swallows non-fatal per-event errors (the
109+
// stream is best-effort; satellites reconnect on RPC errors); only
110+
// transport faults bubble.
111+
func (s *Server) ReportObserved(stream satellitepb.Controller_ReportObservedServer) error {
112+
count := int64(0)
113+
114+
for {
115+
ev, err := stream.Recv()
116+
if errors.Is(err, io.EOF) {
117+
return stream.SendAndClose(&satellitepb.ReportObservedResponse{Received: count})
118+
}
119+
120+
if err != nil {
121+
return status.Errorf(codes.Internal, "recv observed: %v", err)
122+
}
123+
124+
applyErr := s.applyObserved(stream.Context(), ev)
125+
if applyErr != nil {
126+
// Log-and-skip — better than tearing the stream down on
127+
// a single mis-formed event.
128+
_ = applyErr
129+
}
130+
131+
count++
132+
}
133+
}
134+
135+
// applyObserved is the per-event Status update — currently a no-op
136+
// placeholder while the Resource CRD's Status fields settle (we'll
137+
// land conditions + per-volume disk state in the next slice). Keeping
138+
// the call site means the wire path is exercised end-to-end on the
139+
// stand and any future implementation slots in without re-touching
140+
// the gRPC frame loop.
141+
func (s *Server) applyObserved(_ context.Context, _ *satellitepb.ResourceObservedEvent) error {
142+
return nil
143+
}

0 commit comments

Comments
 (0)