feat: run gateway runtime reporter loop
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run

This commit is contained in:
youfak 2026-07-31 13:07:37 +08:00
parent 5a1873a9f0
commit de2c9ce9b6
2 changed files with 80 additions and 1 deletions

View File

@ -134,10 +134,40 @@ func (reporter *RuntimeReporter) Report(ctx context.Context) error {
return nil
}
// Run keeps the Controller session alive through bounded, periodic Runtime
// reports. The snapshot stream may start after Register, so an absent local
// snapshot is a normal waiting state rather than a process failure.
func (reporter *RuntimeReporter) Run(ctx context.Context) error {
registration, err := reporter.Register(ctx)
if err != nil {
return err
}
ticker := time.NewTicker(registration.HeartbeatInterval)
defer ticker.Stop()
for {
if err := reporter.reportIfSnapshot(ctx); err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
func (reporter *RuntimeReporter) reportIfSnapshot(ctx context.Context) error {
err := reporter.Report(ctx)
if errors.Is(err, snapshot.ErrInvalidRuntimeReport) {
return nil
}
return err
}
func validateRegistration(workerID string, response *controlplanev1.RegisterWorkerResponse) (Registration, error) {
if response == nil || response.GetWorkerId() != workerID || !workerruntime.ValidIdentifier(response.GetSessionId()) ||
response.GetOwnershipEpoch() == 0 || response.GetHeartbeatInterval() == nil || response.GetMaxStaleAge() == nil ||
response.GetHeartbeatInterval().AsDuration() <= 0 || response.GetMaxStaleAge().AsDuration() <= 0 {
response.GetHeartbeatInterval().AsDuration() <= 0 || response.GetMaxStaleAge().AsDuration() < response.GetHeartbeatInterval().AsDuration() {
return Registration{}, ErrInvalidOptions
}
return Registration{

View File

@ -88,6 +88,39 @@ func TestRuntimeReporterRejectsInvalidState(t *testing.T) {
}
}
func TestRuntimeReporterRunWaitsForSnapshotThenReports(t *testing.T) {
store := snapshot.NewStore("cluster-a", "worker-a")
client := &runClient{runtime: make(chan *controlplanev1.ReportRuntimeRequest, 8)}
reporter, err := NewRuntimeReporter(client, store, Options{
WorkerID: "worker-a", InstanceID: "instance-a", Zone: "zone-a", ProtocolVersion: 1, Now: time.Now,
})
if err != nil {
t.Fatalf("NewRuntimeReporter(): %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() { result <- reporter.Run(ctx) }()
time.Sleep(15 * time.Millisecond)
select {
case request := <-client.runtime:
t.Fatalf("ReportRuntime() before snapshot = %+v", request)
default:
}
applySnapshot(t, store)
select {
case request := <-client.runtime:
if request.GetReportSequence() != 1 {
t.Fatalf("report sequence = %d, want 1", request.GetReportSequence())
}
case <-time.After(time.Second):
t.Fatal("ReportRuntime() was not called after applying a snapshot")
}
cancel()
if err := <-result; !errors.Is(err, context.Canceled) {
t.Fatalf("Run() error = %v, want context canceled", err)
}
}
type clientStub struct {
register *controlplanev1.RegisterWorkerRequest
registration *controlplanev1.RegisterWorkerResponse
@ -118,3 +151,19 @@ func applySnapshot(t *testing.T, store *snapshot.Store) {
t.Fatalf("Apply(): %v", err)
}
}
type runClient struct {
runtime chan *controlplanev1.ReportRuntimeRequest
}
func (*runClient) RegisterWorker(context.Context, *controlplanev1.RegisterWorkerRequest) (*controlplanev1.RegisterWorkerResponse, error) {
return &controlplanev1.RegisterWorkerResponse{
WorkerId: "worker-a", SessionId: "session-a", OwnershipEpoch: 7,
HeartbeatInterval: durationpb.New(10 * time.Millisecond), MaxStaleAge: durationpb.New(time.Second),
}, nil
}
func (client *runClient) ReportRuntime(_ context.Context, request *controlplanev1.ReportRuntimeRequest) (*controlplanev1.ReportRuntimeResponse, error) {
client.runtime <- request
return &controlplanev1.ReportRuntimeResponse{AcceptedOwnershipEpoch: 7}, nil
}