344 lines
10 KiB
Go
344 lines
10 KiB
Go
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 TestAsyncSinkPublishRawReturnsAfterQueueing(t *testing.T) {
|
|
delegate := newBlockingSink()
|
|
sink := NewAsyncSink(delegate, AsyncConfig{QueueSize: 1, Workers: 1, OperationTimeout: time.Second})
|
|
|
|
start := time.Now()
|
|
if err := sink.PublishRaw(context.Background(), envelope.FrameEnvelope{Protocol: envelope.ProtocolGB32960, VIN: "LNBSCB3D4R1234567"}); err != nil {
|
|
t.Fatalf("PublishRaw() error = %v", err)
|
|
}
|
|
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
|
t.Fatalf("PublishRaw() blocked for %s, want quick queueing", elapsed)
|
|
}
|
|
|
|
select {
|
|
case <-delegate.rawStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("delegate raw publish was not started")
|
|
}
|
|
delegate.release()
|
|
if err := sink.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAsyncSinkPublishesInFIFOOrderWithSingleWorker(t *testing.T) {
|
|
delegate := &orderedSink{}
|
|
sink := NewAsyncSink(delegate, AsyncConfig{QueueSize: 4, Workers: 1, OperationTimeout: time.Second})
|
|
|
|
env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425"}
|
|
if err := sink.PublishRaw(context.Background(), env); err != nil {
|
|
t.Fatalf("PublishRaw() error = %v", err)
|
|
}
|
|
if err := sink.PublishUnified(context.Background(), env); err != nil {
|
|
t.Fatalf("PublishUnified() error = %v", err)
|
|
}
|
|
if err := sink.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
|
|
if got, want := delegate.calls, []string{"raw", "unified"}; len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
|
|
t.Fatalf("calls = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestAsyncSinkRecordsQueueMetrics(t *testing.T) {
|
|
registry := metrics.NewRegistry()
|
|
delegate := newBlockingSink()
|
|
sink := NewAsyncSink(delegate, AsyncConfig{
|
|
QueueSize: 2,
|
|
Workers: 1,
|
|
OperationTimeout: time.Second,
|
|
Metrics: registry,
|
|
Name: "nats",
|
|
})
|
|
|
|
env := envelope.FrameEnvelope{Protocol: envelope.ProtocolGB32960, VIN: "VIN001"}
|
|
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_enqueue_total{kind="raw",sink="nats",status="queued"} 1`,
|
|
`vehicle_async_sink_enqueue_total{kind="fields",sink="nats",status="queued"} 1`,
|
|
`vehicle_async_sink_queue_capacity{sink="nats"} 2`,
|
|
`vehicle_async_sink_queue_depth{sink="nats"}`,
|
|
`vehicle_async_sink_workers{queue="default",sink="nats"} 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_publish_duration_ms_histogram_bucket{le="+Inf",kind="raw",sink="nats",status="ok"} 1`,
|
|
`vehicle_async_sink_publish_duration_ms_histogram_count{kind="raw",sink="nats",status="ok"} 1`,
|
|
`vehicle_async_sink_publish_duration_ms_histogram_sum{kind="raw",sink="nats",status="ok"}`,
|
|
`vehicle_async_sink_queue_wait_duration_ms_histogram_count{kind="raw",queue="default",sink="nats"} 1`,
|
|
`vehicle_async_sink_queue_wait_recent_p99_ms{kind="raw",queue="default",sink="nats"}`,
|
|
`vehicle_async_sink_queue_wait_recent_samples{kind="raw",queue="default",sink="nats"} 1`,
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("async sink metric missing %s:\n%s", want, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAsyncSinkRecordsEnqueueTimeoutWhenQueueIsFull(t *testing.T) {
|
|
registry := metrics.NewRegistry()
|
|
delegate := newBlockingSink()
|
|
sink := NewAsyncSink(delegate, AsyncConfig{
|
|
QueueSize: 1,
|
|
Workers: 1,
|
|
OperationTimeout: time.Second,
|
|
Metrics: registry,
|
|
Name: "nats",
|
|
})
|
|
defer sink.Close()
|
|
|
|
env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425"}
|
|
if err := sink.PublishRaw(context.Background(), env); err != nil {
|
|
t.Fatalf("first PublishRaw() error = %v", err)
|
|
}
|
|
select {
|
|
case <-delegate.rawStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("delegate raw publish was not started")
|
|
}
|
|
if err := sink.PublishFields(context.Background(), env); err != nil {
|
|
t.Fatalf("second PublishFields() error = %v", err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
|
defer cancel()
|
|
if err := sink.PublishUnified(ctx, env); err == nil {
|
|
t.Fatal("third PublishUnified() error = nil, want context deadline exceeded")
|
|
}
|
|
|
|
text := registry.Render()
|
|
for _, want := range []string{
|
|
`vehicle_async_sink_enqueue_total{kind="unified",sink="nats",status="timeout"} 1`,
|
|
`vehicle_async_sink_queue_depth{sink="nats"} 1`,
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("async sink timeout metric missing %s:\n%s", want, text)
|
|
}
|
|
}
|
|
delegate.release()
|
|
}
|
|
|
|
func TestAsyncSinkEnqueueTimeoutDoesNotRequireCallerDeadline(t *testing.T) {
|
|
registry := metrics.NewRegistry()
|
|
delegate := newBlockingSink()
|
|
sink := NewAsyncSink(delegate, AsyncConfig{
|
|
QueueSize: 1,
|
|
Workers: 1,
|
|
EnqueueTimeout: 10 * time.Millisecond,
|
|
OperationTimeout: time.Second,
|
|
Metrics: registry,
|
|
Name: "nats",
|
|
})
|
|
defer sink.Close()
|
|
|
|
env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425"}
|
|
if err := sink.PublishRaw(context.Background(), env); err != nil {
|
|
t.Fatalf("first PublishRaw() error = %v", err)
|
|
}
|
|
select {
|
|
case <-delegate.rawStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("delegate raw publish was not started")
|
|
}
|
|
if err := sink.PublishFields(context.Background(), env); err != nil {
|
|
t.Fatalf("second PublishFields() error = %v", err)
|
|
}
|
|
if err := sink.PublishUnified(context.Background(), env); !errors.Is(err, ErrAsyncSinkEnqueueTimeout) {
|
|
t.Fatalf("third PublishUnified() error = %v, want ErrAsyncSinkEnqueueTimeout", err)
|
|
}
|
|
|
|
text := registry.Render()
|
|
for _, want := range []string{
|
|
`vehicle_async_sink_enqueue_total{kind="unified",sink="nats",status="timeout"} 1`,
|
|
`vehicle_async_sink_queue_depth{sink="nats"} 1`,
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("async sink enqueue timeout metric missing %s:\n%s", want, text)
|
|
}
|
|
}
|
|
delegate.release()
|
|
}
|
|
|
|
func TestAsyncSinkCloseDoesNotPanicWithConcurrentBlockedEnqueue(t *testing.T) {
|
|
delegate := newBlockingSink()
|
|
sink := NewAsyncSink(delegate, AsyncConfig{
|
|
QueueSize: 1,
|
|
Workers: 1,
|
|
EnqueueTimeout: 20 * time.Millisecond,
|
|
OperationTimeout: time.Second,
|
|
})
|
|
|
|
env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425"}
|
|
if err := sink.PublishRaw(context.Background(), env); err != nil {
|
|
t.Fatalf("first PublishRaw() error = %v", err)
|
|
}
|
|
select {
|
|
case <-delegate.rawStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("delegate raw publish was not started")
|
|
}
|
|
if err := sink.PublishFields(context.Background(), env); err != nil {
|
|
t.Fatalf("second PublishFields() error = %v", err)
|
|
}
|
|
|
|
publishErr := make(chan error, 1)
|
|
go func() {
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
publishErr <- errors.New("publish panicked")
|
|
}
|
|
}()
|
|
publishErr <- sink.PublishUnified(context.Background(), env)
|
|
}()
|
|
closeErr := make(chan error, 1)
|
|
go func() {
|
|
closeErr <- sink.Close()
|
|
}()
|
|
|
|
if err := <-publishErr; !errors.Is(err, ErrAsyncSinkEnqueueTimeout) && !errors.Is(err, ErrAsyncSinkClosed) {
|
|
t.Fatalf("concurrent PublishUnified() error = %v, want timeout or closed", err)
|
|
}
|
|
delegate.release()
|
|
if err := <-closeErr; err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
if err := sink.PublishRaw(context.Background(), env); !errors.Is(err, ErrAsyncSinkClosed) {
|
|
t.Fatalf("PublishRaw() after Close error = %v, want ErrAsyncSinkClosed", err)
|
|
}
|
|
}
|
|
|
|
func TestAsyncSinkCloseUnblocksPublishWhenEnqueueTimeoutIsDisabled(t *testing.T) {
|
|
delegate := newBlockingSink()
|
|
sink := NewAsyncSink(delegate, AsyncConfig{
|
|
QueueSize: 1,
|
|
Workers: 1,
|
|
EnqueueTimeout: -1,
|
|
OperationTimeout: time.Second,
|
|
})
|
|
|
|
env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425"}
|
|
if err := sink.PublishRaw(context.Background(), env); err != nil {
|
|
t.Fatalf("first PublishRaw() error = %v", err)
|
|
}
|
|
select {
|
|
case <-delegate.rawStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("delegate raw publish was not started")
|
|
}
|
|
if err := sink.PublishFields(context.Background(), env); err != nil {
|
|
t.Fatalf("second PublishFields() 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 blockingSink struct {
|
|
rawStarted chan struct{}
|
|
releaseRaw chan struct{}
|
|
}
|
|
|
|
func newBlockingSink() *blockingSink {
|
|
return &blockingSink{
|
|
rawStarted: make(chan struct{}),
|
|
releaseRaw: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (s *blockingSink) PublishRaw(context.Context, envelope.FrameEnvelope) error {
|
|
close(s.rawStarted)
|
|
<-s.releaseRaw
|
|
return nil
|
|
}
|
|
|
|
func (s *blockingSink) PublishUnified(context.Context, envelope.FrameEnvelope) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *blockingSink) PublishFields(context.Context, envelope.FrameEnvelope) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *blockingSink) Close() error {
|
|
s.release()
|
|
return nil
|
|
}
|
|
|
|
func (s *blockingSink) release() {
|
|
select {
|
|
case <-s.releaseRaw:
|
|
default:
|
|
close(s.releaseRaw)
|
|
}
|
|
}
|
|
|
|
type orderedSink struct {
|
|
calls []string
|
|
}
|
|
|
|
func (s *orderedSink) PublishRaw(context.Context, envelope.FrameEnvelope) error {
|
|
s.calls = append(s.calls, "raw")
|
|
return nil
|
|
}
|
|
|
|
func (s *orderedSink) PublishUnified(context.Context, envelope.FrameEnvelope) error {
|
|
s.calls = append(s.calls, "unified")
|
|
return nil
|
|
}
|
|
|
|
func (s *orderedSink) PublishFields(context.Context, envelope.FrameEnvelope) error {
|
|
s.calls = append(s.calls, "fields")
|
|
return nil
|
|
}
|
|
|
|
func (s *orderedSink) Close() error {
|
|
return nil
|
|
}
|