feat: coordinate gateway control plane session
This commit is contained in:
parent
6b6fb54075
commit
27035947dd
@ -138,10 +138,29 @@ func (reporter *RuntimeReporter) Report(ctx context.Context) error {
|
|||||||
// reports. The snapshot stream may start after Register, so an absent local
|
// reports. The snapshot stream may start after Register, so an absent local
|
||||||
// snapshot is a normal waiting state rather than a process failure.
|
// snapshot is a normal waiting state rather than a process failure.
|
||||||
func (reporter *RuntimeReporter) Run(ctx context.Context) error {
|
func (reporter *RuntimeReporter) Run(ctx context.Context) error {
|
||||||
registration, err := reporter.Register(ctx)
|
if _, err := reporter.Register(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return reporter.RunRegistered(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunRegistered reports Runtime for the session established by Register.
|
||||||
|
// It is useful when a SnapshotWatcher and a RuntimeReporter share one worker
|
||||||
|
// lifecycle and registration must occur exactly once.
|
||||||
|
func (reporter *RuntimeReporter) RunRegistered(ctx context.Context) error {
|
||||||
|
if reporter == nil || ctx == nil {
|
||||||
|
return ErrInvalidOptions
|
||||||
|
}
|
||||||
|
reporter.mu.Lock()
|
||||||
|
registration := reporter.session
|
||||||
|
reporter.mu.Unlock()
|
||||||
|
if registration.SessionID == "" {
|
||||||
|
return ErrNotRegistered
|
||||||
|
}
|
||||||
|
return reporter.runRegistered(ctx, registration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (reporter *RuntimeReporter) runRegistered(ctx context.Context, registration Registration) error {
|
||||||
ticker := time.NewTicker(registration.HeartbeatInterval)
|
ticker := time.NewTicker(registration.HeartbeatInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
|
|||||||
52
internal/gateway/controlplane/session_runner.go
Normal file
52
internal/gateway/controlplane/session_runner.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrSnapshotStreamClosed = errors.New("worker snapshot stream closed")
|
||||||
|
|
||||||
|
type SessionRunner struct {
|
||||||
|
reporter *RuntimeReporter
|
||||||
|
watcher *SnapshotWatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionRunner(reporter *RuntimeReporter, watcher *SnapshotWatcher) (*SessionRunner, error) {
|
||||||
|
if reporter == nil || watcher == nil {
|
||||||
|
return nil, ErrInvalidOptions
|
||||||
|
}
|
||||||
|
return &SessionRunner{reporter: reporter, watcher: watcher}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run registers exactly once, then runs snapshot intake and Runtime reporting
|
||||||
|
// against that session. A stream ending without context cancellation is a
|
||||||
|
// failure so the owning process can apply its reconnect policy.
|
||||||
|
func (runner *SessionRunner) Run(ctx context.Context) error {
|
||||||
|
if runner == nil || ctx == nil {
|
||||||
|
return ErrInvalidOptions
|
||||||
|
}
|
||||||
|
registration, err := runner.reporter.Register(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
groupCtx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
results := make(chan error, 2)
|
||||||
|
go func() { results <- runner.watcher.Watch(groupCtx, registration.SessionID) }()
|
||||||
|
go func() { results <- runner.reporter.RunRegistered(groupCtx) }()
|
||||||
|
|
||||||
|
first := <-results
|
||||||
|
cancel()
|
||||||
|
second := <-results
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if first == nil {
|
||||||
|
return ErrSnapshotStreamClosed
|
||||||
|
}
|
||||||
|
if errors.Is(first, context.Canceled) && second != nil {
|
||||||
|
return second
|
||||||
|
}
|
||||||
|
return first
|
||||||
|
}
|
||||||
109
internal/gateway/controlplane/session_runner_test.go
Normal file
109
internal/gateway/controlplane/session_runner_test.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
controlplanev1 "proxy-pool/gen/controlplane/v1"
|
||||||
|
"proxy-pool/internal/gateway/snapshot"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSessionRunnerRegistersOnceWatchesAndReports(t *testing.T) {
|
||||||
|
store := snapshot.NewStore("cluster-a", "worker-a")
|
||||||
|
full := &controlplanev1.WorkerSnapshot{
|
||||||
|
Version: 1, OwnershipEpoch: 7, GeneratedAt: timestamppb.New(time.Now()), ValidUntil: timestamppb.New(time.Now().Add(time.Minute)),
|
||||||
|
}
|
||||||
|
setSnapshotChecksum(t, full)
|
||||||
|
runtime := make(chan *controlplanev1.ReportRuntimeRequest, 8)
|
||||||
|
reporterClient := &sessionReporterClient{runtime: runtime}
|
||||||
|
reporter, err := NewRuntimeReporter(reporterClient, store, Options{
|
||||||
|
WorkerID: "worker-a", InstanceID: "instance-a", Zone: "zone-a", ProtocolVersion: 1, Now: time.Now,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRuntimeReporter(): %v", err)
|
||||||
|
}
|
||||||
|
watcherClient := &sessionWatcherClient{full: full}
|
||||||
|
watcher, err := NewSnapshotWatcher(watcherClient, store, SnapshotWatcherOptions{ClusterID: "cluster-a", WorkerID: "worker-a"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSnapshotWatcher(): %v", err)
|
||||||
|
}
|
||||||
|
runner, err := NewSessionRunner(reporter, watcher)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSessionRunner(): %v", err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
result := make(chan error, 1)
|
||||||
|
go func() { result <- runner.Run(ctx) }()
|
||||||
|
select {
|
||||||
|
case request := <-runtime:
|
||||||
|
if request.GetSessionId() != "session-a" || request.GetSnapshotVersion() != 1 || request.GetReportSequence() != 1 {
|
||||||
|
t.Fatalf("runtime request = %+v", request)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("runtime report was not sent")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
if err := <-result; !errors.Is(err, context.Canceled) {
|
||||||
|
t.Fatalf("Run() error = %v, want context canceled", err)
|
||||||
|
}
|
||||||
|
if reporterClient.registers != 1 || watcherClient.ack == nil || !watcherClient.ack.GetApplied() {
|
||||||
|
t.Fatalf("registers=%d ack=%+v", reporterClient.registers, watcherClient.ack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionReporterClient struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
registers int
|
||||||
|
runtime chan *controlplanev1.ReportRuntimeRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *sessionReporterClient) RegisterWorker(context.Context, *controlplanev1.RegisterWorkerRequest) (*controlplanev1.RegisterWorkerResponse, error) {
|
||||||
|
client.mu.Lock()
|
||||||
|
client.registers++
|
||||||
|
client.mu.Unlock()
|
||||||
|
return &controlplanev1.RegisterWorkerResponse{
|
||||||
|
WorkerId: "worker-a", SessionId: "session-a", OwnershipEpoch: 7,
|
||||||
|
HeartbeatInterval: durationpb.New(5 * time.Millisecond), MaxStaleAge: durationpb.New(time.Second),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *sessionReporterClient) ReportRuntime(_ context.Context, request *controlplanev1.ReportRuntimeRequest) (*controlplanev1.ReportRuntimeResponse, error) {
|
||||||
|
client.runtime <- request
|
||||||
|
return &controlplanev1.ReportRuntimeResponse{AcceptedOwnershipEpoch: 7}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionWatcherClient struct {
|
||||||
|
full *controlplanev1.WorkerSnapshot
|
||||||
|
ack *controlplanev1.AcknowledgeSnapshotRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *sessionWatcherClient) Watch(ctx context.Context, _ *controlplanev1.WatchSnapshotsRequest) (SnapshotStream, error) {
|
||||||
|
return &sessionSnapshotStream{ctx: ctx, full: client.full}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *sessionWatcherClient) Acknowledge(_ context.Context, acknowledgement *controlplanev1.AcknowledgeSnapshotRequest) (*emptypb.Empty, error) {
|
||||||
|
client.ack = acknowledgement
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionSnapshotStream struct {
|
||||||
|
ctx context.Context
|
||||||
|
full *controlplanev1.WorkerSnapshot
|
||||||
|
sent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stream *sessionSnapshotStream) Recv() (*controlplanev1.SnapshotEnvelope, error) {
|
||||||
|
if !stream.sent {
|
||||||
|
stream.sent = true
|
||||||
|
return &controlplanev1.SnapshotEnvelope{Payload: &controlplanev1.SnapshotEnvelope_Full{Full: stream.full}}, nil
|
||||||
|
}
|
||||||
|
<-stream.ctx.Done()
|
||||||
|
return nil, stream.ctx.Err()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user