package eventbus import ( "context" "errors" "strings" "testing" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/metrics" ) func TestPartitionedAsyncSinkRawQueueIsIsolatedFromDerivedBacklog(t *testing.T) { delegate := newBlockingDerivedSink() sink := NewPartitionedAsyncSink(delegate, PartitionedAsyncConfig{ RawQueueSize: 1, DerivedQueueSize: 1, RawWorkers: 1, DerivedWorkers: 1, EnqueueTimeout: 20 * time.Millisecond, OperationTimeout: time.Second, }) defer sink.Close() env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425"} if err := sink.PublishFields(context.Background(), env); err != nil { t.Fatalf("PublishFields() error = %v", err) } select { case <-delegate.fieldsStarted: case <-time.After(time.Second): t.Fatal("delegate fields publish was not started") } if err := sink.PublishUnified(context.Background(), env); err != nil { t.Fatalf("PublishUnified() error = %v", err) } start := time.Now() if err := sink.PublishRaw(context.Background(), env); err != nil { t.Fatalf("PublishRaw() error = %v", err) } if elapsed := time.Since(start); elapsed > 50*time.Millisecond { t.Fatalf("PublishRaw() blocked behind derived backlog for %s", elapsed) } delegate.release() } func TestPartitionedAsyncSinkRecordsPerQueueMetrics(t *testing.T) { registry := metrics.NewRegistry() delegate := newBlockingDerivedSink() sink := NewPartitionedAsyncSink(delegate, PartitionedAsyncConfig{ RawQueueSize: 2, DerivedQueueSize: 3, RawWorkers: 1, DerivedWorkers: 1, OperationTimeout: time.Second, Metrics: registry, Name: "nats", }) env := envelope.FrameEnvelope{Protocol: envelope.ProtocolGB32960, VIN: "LNBSCB3D4R1234567"} if err := sink.PublishRaw(context.Background(), env); err != nil { t.Fatalf("PublishRaw() error = %v", err) } if err := sink.PublishFields(context.Background(), env); err != nil { t.Fatalf("PublishFields() error = %v", err) } delegate.release() if err := sink.Close(); err != nil { t.Fatalf("Close() error = %v", err) } text := registry.Render() for _, want := range []string{ `vehicle_async_sink_queue_capacity{queue="raw",sink="nats"} 2`, `vehicle_async_sink_queue_capacity{queue="derived",sink="nats"} 3`, `vehicle_async_sink_workers{queue="raw",sink="nats"} 1`, `vehicle_async_sink_workers{queue="derived",sink="nats"} 1`, `vehicle_async_sink_enqueue_total{kind="raw",sink="nats",status="queued"} 1`, `vehicle_async_sink_enqueue_total{kind="fields",sink="nats",status="queued"} 1`, `vehicle_async_sink_publish_total{kind="raw",sink="nats",status="ok"} 1`, `vehicle_async_sink_publish_total{kind="fields",sink="nats",status="ok"} 1`, `vehicle_async_sink_queue_wait_duration_ms_histogram_count{kind="raw",queue="raw",sink="nats"} 1`, `vehicle_async_sink_queue_wait_recent_p99_ms{kind="raw",queue="raw",sink="nats"}`, `vehicle_async_sink_queue_wait_recent_samples{kind="raw",queue="raw",sink="nats"} 1`, `vehicle_async_sink_queue_wait_duration_ms_histogram_count{kind="fields",queue="derived",sink="nats"} 1`, } { if !strings.Contains(text, want) { t.Fatalf("partitioned async metric missing %s:\n%s", want, text) } } } func TestPartitionedAsyncSinkUsesIndependentDerivedEnqueueTimeout(t *testing.T) { delegate := newBlockingDerivedSink() sink := NewPartitionedAsyncSink(delegate, PartitionedAsyncConfig{ RawQueueSize: 1, DerivedQueueSize: 1, RawWorkers: 1, DerivedWorkers: 1, RawEnqueueTimeout: 200 * time.Millisecond, DerivedEnqueueTimeout: 10 * time.Millisecond, OperationTimeout: time.Second, }) defer sink.Close() env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425"} if err := sink.PublishFields(context.Background(), env); err != nil { t.Fatalf("PublishFields() error = %v", err) } select { case <-delegate.fieldsStarted: case <-time.After(time.Second): t.Fatal("delegate fields publish was not started") } if err := sink.PublishUnified(context.Background(), env); err != nil { t.Fatalf("first PublishUnified() error = %v", err) } start := time.Now() err := sink.PublishUnified(context.Background(), env) elapsed := time.Since(start) if !errors.Is(err, ErrAsyncSinkEnqueueTimeout) { t.Fatalf("second PublishUnified() error = %v, want ErrAsyncSinkEnqueueTimeout", err) } if elapsed > 100*time.Millisecond { t.Fatalf("derived enqueue timeout took %s, want quick failure", elapsed) } delegate.release() } func TestPartitionedAsyncSinkCloseUnblocksBlockedDerivedEnqueue(t *testing.T) { delegate := newBlockingDerivedSink() sink := NewPartitionedAsyncSink(delegate, PartitionedAsyncConfig{ RawQueueSize: 1, DerivedQueueSize: 1, RawWorkers: 1, DerivedWorkers: 1, EnqueueTimeout: -1, OperationTimeout: time.Second, }) env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425"} if err := sink.PublishFields(context.Background(), env); err != nil { t.Fatalf("PublishFields() error = %v", err) } select { case <-delegate.fieldsStarted: case <-time.After(time.Second): t.Fatal("delegate fields publish was not started") } if err := sink.PublishUnified(context.Background(), env); err != nil { t.Fatalf("PublishUnified() error = %v", err) } publishErr := make(chan error, 1) go func() { publishErr <- sink.PublishUnified(context.Background(), env) }() closeErr := make(chan error, 1) go func() { closeErr <- sink.Close() }() select { case err := <-publishErr: if !errors.Is(err, ErrAsyncSinkClosed) { t.Fatalf("blocked PublishUnified() error = %v, want ErrAsyncSinkClosed", err) } case <-time.After(time.Second): t.Fatal("blocked PublishUnified() was not released by Close") } delegate.release() select { case err := <-closeErr: if err != nil { t.Fatalf("Close() error = %v", err) } case <-time.After(time.Second): t.Fatal("Close() did not finish after delegate release") } } type blockingDerivedSink struct { fieldsStarted chan struct{} releaseFields chan struct{} } func newBlockingDerivedSink() *blockingDerivedSink { return &blockingDerivedSink{ fieldsStarted: make(chan struct{}), releaseFields: make(chan struct{}), } } func (s *blockingDerivedSink) PublishRaw(context.Context, envelope.FrameEnvelope) error { return nil } func (s *blockingDerivedSink) PublishUnified(context.Context, envelope.FrameEnvelope) error { return nil } func (s *blockingDerivedSink) PublishFields(context.Context, envelope.FrameEnvelope) error { s.signalFieldsStarted() <-s.releaseFields return nil } func (s *blockingDerivedSink) Close() error { s.release() return nil } func (s *blockingDerivedSink) signalFieldsStarted() { select { case <-s.fieldsStarted: default: close(s.fieldsStarted) } } func (s *blockingDerivedSink) release() { select { case <-s.releaseFields: default: close(s.releaseFields) } }