style: gofmt + fix errcheck lint warning
All checks were successful
CI / Test (push) Successful in 3m2s
CI / Lint (push) Successful in 3m7s
CI / Release (push) Successful in 1m55s
CI / Notify Downstream (stt-module) (push) Successful in 1s
CI / Notify Downstream (voice-assistant) (push) Successful in 1s
CI / Notify (push) Successful in 2s
CI / Notify Downstream (chat-handler) (push) Successful in 1s
CI / Notify Downstream (pipeline-bridge) (push) Successful in 1s
CI / Notify Downstream (tts-module) (push) Successful in 1s
All checks were successful
CI / Test (push) Successful in 3m2s
CI / Lint (push) Successful in 3m7s
CI / Release (push) Successful in 1m55s
CI / Notify Downstream (stt-module) (push) Successful in 1s
CI / Notify Downstream (voice-assistant) (push) Successful in 1s
CI / Notify (push) Successful in 2s
CI / Notify Downstream (chat-handler) (push) Successful in 1s
CI / Notify Downstream (pipeline-bridge) (push) Successful in 1s
CI / Notify Downstream (tts-module) (push) Successful in 1s
This commit is contained in:
@@ -2,21 +2,21 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"github.com/nats-io/nats.go"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/config"
|
||||
pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/health"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/natsutil"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/telemetry"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/config"
|
||||
pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/health"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/natsutil"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/telemetry"
|
||||
)
|
||||
|
||||
// TypedMessageHandler processes the raw NATS message.
|
||||
@@ -32,36 +32,36 @@ type TeardownFunc func(ctx context.Context) error
|
||||
|
||||
// Handler is the base service runner that wires NATS, health, and telemetry.
|
||||
type Handler struct {
|
||||
Settings *config.Settings
|
||||
NATS *natsutil.Client
|
||||
Telemetry *telemetry.Provider
|
||||
Subject string
|
||||
QueueGroup string
|
||||
Settings *config.Settings
|
||||
NATS *natsutil.Client
|
||||
Telemetry *telemetry.Provider
|
||||
Subject string
|
||||
QueueGroup string
|
||||
|
||||
onSetup SetupFunc
|
||||
onTeardown TeardownFunc
|
||||
onTypedMessage TypedMessageHandler
|
||||
running bool
|
||||
onSetup SetupFunc
|
||||
onTeardown TeardownFunc
|
||||
onTypedMessage TypedMessageHandler
|
||||
running bool
|
||||
}
|
||||
|
||||
// New creates a Handler for the given NATS subject.
|
||||
func New(subject string, settings *config.Settings) *Handler {
|
||||
if settings == nil {
|
||||
settings = config.Load()
|
||||
}
|
||||
queueGroup := settings.NATSQueueGroup
|
||||
if settings == nil {
|
||||
settings = config.Load()
|
||||
}
|
||||
queueGroup := settings.NATSQueueGroup
|
||||
|
||||
natsOpts := []nats.Option{}
|
||||
if settings.NATSUser != "" && settings.NATSPassword != "" {
|
||||
natsOpts = append(natsOpts, nats.UserInfo(settings.NATSUser, settings.NATSPassword))
|
||||
}
|
||||
natsOpts := []nats.Option{}
|
||||
if settings.NATSUser != "" && settings.NATSPassword != "" {
|
||||
natsOpts = append(natsOpts, nats.UserInfo(settings.NATSUser, settings.NATSPassword))
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
Settings: settings,
|
||||
Subject: subject,
|
||||
QueueGroup: queueGroup,
|
||||
NATS: natsutil.New(settings.NATSURL, natsOpts...),
|
||||
}
|
||||
return &Handler{
|
||||
Settings: settings,
|
||||
Subject: subject,
|
||||
QueueGroup: queueGroup,
|
||||
NATS: natsutil.New(settings.NATSURL, natsOpts...),
|
||||
}
|
||||
}
|
||||
|
||||
// OnSetup registers the setup callback.
|
||||
@@ -75,101 +75,101 @@ func (h *Handler) OnTypedMessage(fn TypedMessageHandler) { h.onTypedMessage = fn
|
||||
|
||||
// Run starts the handler: telemetry, health server, NATS subscription, and blocks until SIGTERM/SIGINT.
|
||||
func (h *Handler) Run() error {
|
||||
// Structured logging
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})))
|
||||
slog.Info("starting service", "name", h.Settings.ServiceName, "version", h.Settings.ServiceVersion)
|
||||
// Structured logging
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})))
|
||||
slog.Info("starting service", "name", h.Settings.ServiceName, "version", h.Settings.ServiceVersion)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Telemetry
|
||||
tp, shutdown, err := telemetry.Setup(ctx, telemetry.Config{
|
||||
ServiceName: h.Settings.ServiceName,
|
||||
ServiceVersion: h.Settings.ServiceVersion,
|
||||
ServiceNamespace: h.Settings.ServiceNamespace,
|
||||
DeploymentEnv: h.Settings.DeploymentEnv,
|
||||
Enabled: h.Settings.OTELEnabled,
|
||||
Endpoint: h.Settings.OTELEndpoint,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("telemetry setup: %w", err)
|
||||
}
|
||||
defer shutdown(ctx)
|
||||
h.Telemetry = tp
|
||||
// Telemetry
|
||||
tp, shutdown, err := telemetry.Setup(ctx, telemetry.Config{
|
||||
ServiceName: h.Settings.ServiceName,
|
||||
ServiceVersion: h.Settings.ServiceVersion,
|
||||
ServiceNamespace: h.Settings.ServiceNamespace,
|
||||
DeploymentEnv: h.Settings.DeploymentEnv,
|
||||
Enabled: h.Settings.OTELEnabled,
|
||||
Endpoint: h.Settings.OTELEndpoint,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("telemetry setup: %w", err)
|
||||
}
|
||||
defer shutdown(ctx)
|
||||
h.Telemetry = tp
|
||||
|
||||
// Health server
|
||||
healthSrv := health.New(
|
||||
h.Settings.HealthPort,
|
||||
h.Settings.HealthPath,
|
||||
h.Settings.ReadyPath,
|
||||
func() bool { return h.running && h.NATS.IsConnected() },
|
||||
)
|
||||
healthSrv.Start()
|
||||
defer healthSrv.Stop(ctx)
|
||||
// Health server
|
||||
healthSrv := health.New(
|
||||
h.Settings.HealthPort,
|
||||
h.Settings.HealthPath,
|
||||
h.Settings.ReadyPath,
|
||||
func() bool { return h.running && h.NATS.IsConnected() },
|
||||
)
|
||||
healthSrv.Start()
|
||||
defer healthSrv.Stop(ctx)
|
||||
|
||||
// Connect to NATS
|
||||
if err := h.NATS.Connect(); err != nil {
|
||||
return fmt.Errorf("nats: %w", err)
|
||||
}
|
||||
defer h.NATS.Close()
|
||||
// Connect to NATS
|
||||
if err := h.NATS.Connect(); err != nil {
|
||||
return fmt.Errorf("nats: %w", err)
|
||||
}
|
||||
defer h.NATS.Close()
|
||||
|
||||
// User setup
|
||||
if h.onSetup != nil {
|
||||
slog.Info("running service setup")
|
||||
if err := h.onSetup(ctx); err != nil {
|
||||
return fmt.Errorf("setup: %w", err)
|
||||
}
|
||||
}
|
||||
// User setup
|
||||
if h.onSetup != nil {
|
||||
slog.Info("running service setup")
|
||||
if err := h.onSetup(ctx); err != nil {
|
||||
return fmt.Errorf("setup: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe
|
||||
if h.onTypedMessage == nil {
|
||||
return fmt.Errorf("no message handler registered")
|
||||
}
|
||||
if err := h.NATS.Subscribe(h.Subject, h.wrapHandler(ctx), h.QueueGroup); err != nil {
|
||||
return fmt.Errorf("subscribe: %w", err)
|
||||
}
|
||||
// Subscribe
|
||||
if h.onTypedMessage == nil {
|
||||
return fmt.Errorf("no message handler registered")
|
||||
}
|
||||
if err := h.NATS.Subscribe(h.Subject, h.wrapHandler(ctx), h.QueueGroup); err != nil {
|
||||
return fmt.Errorf("subscribe: %w", err)
|
||||
}
|
||||
|
||||
h.running = true
|
||||
slog.Info("handler ready", "subject", h.Subject)
|
||||
h.running = true
|
||||
slog.Info("handler ready", "subject", h.Subject)
|
||||
|
||||
// Wait for shutdown signal
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
|
||||
<-sigCh
|
||||
// Wait for shutdown signal
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
|
||||
<-sigCh
|
||||
|
||||
slog.Info("shutting down")
|
||||
h.running = false
|
||||
slog.Info("shutting down")
|
||||
h.running = false
|
||||
|
||||
// Teardown
|
||||
if h.onTeardown != nil {
|
||||
if err := h.onTeardown(ctx); err != nil {
|
||||
slog.Warn("teardown error", "error", err)
|
||||
}
|
||||
}
|
||||
// Teardown
|
||||
if h.onTeardown != nil {
|
||||
if err := h.onTeardown(ctx); err != nil {
|
||||
slog.Warn("teardown error", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("shutdown complete")
|
||||
return nil
|
||||
slog.Info("shutdown complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapHandler creates a nats.MsgHandler that dispatches to the registered callback.
|
||||
func (h *Handler) wrapHandler(ctx context.Context) nats.MsgHandler {
|
||||
return func(msg *nats.Msg) {
|
||||
response, err := h.onTypedMessage(ctx, msg)
|
||||
if err != nil {
|
||||
slog.Error("handler error", "subject", msg.Subject, "error", err)
|
||||
if msg.Reply != "" {
|
||||
_ = h.NATS.Publish(msg.Reply, &pb.ErrorResponse{
|
||||
Error: true,
|
||||
Message: err.Error(),
|
||||
Type: fmt.Sprintf("%T", err),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if response != nil && msg.Reply != "" {
|
||||
if err := h.NATS.Publish(msg.Reply, response); err != nil {
|
||||
slog.Error("failed to publish reply", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return func(msg *nats.Msg) {
|
||||
response, err := h.onTypedMessage(ctx, msg)
|
||||
if err != nil {
|
||||
slog.Error("handler error", "subject", msg.Subject, "error", err)
|
||||
if msg.Reply != "" {
|
||||
_ = h.NATS.Publish(msg.Reply, &pb.ErrorResponse{
|
||||
Error: true,
|
||||
Message: err.Error(),
|
||||
Type: fmt.Sprintf("%T", err),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if response != nil && msg.Reply != "" {
|
||||
if err := h.NATS.Publish(msg.Reply, response); err != nil {
|
||||
slog.Error("failed to publish reply", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"github.com/nats-io/nats.go"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/config"
|
||||
pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/natsutil"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/config"
|
||||
pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/natsutil"
|
||||
)
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -17,75 +17,75 @@ pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb"
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestNewHandler(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
cfg.ServiceName = "test-handler"
|
||||
cfg.NATSQueueGroup = "test-group"
|
||||
cfg := config.Load()
|
||||
cfg.ServiceName = "test-handler"
|
||||
cfg.NATSQueueGroup = "test-group"
|
||||
|
||||
h := New("ai.test.subject", cfg)
|
||||
if h.Subject != "ai.test.subject" {
|
||||
t.Errorf("Subject = %q", h.Subject)
|
||||
}
|
||||
if h.QueueGroup != "test-group" {
|
||||
t.Errorf("QueueGroup = %q", h.QueueGroup)
|
||||
}
|
||||
if h.Settings.ServiceName != "test-handler" {
|
||||
t.Errorf("ServiceName = %q", h.Settings.ServiceName)
|
||||
}
|
||||
h := New("ai.test.subject", cfg)
|
||||
if h.Subject != "ai.test.subject" {
|
||||
t.Errorf("Subject = %q", h.Subject)
|
||||
}
|
||||
if h.QueueGroup != "test-group" {
|
||||
t.Errorf("QueueGroup = %q", h.QueueGroup)
|
||||
}
|
||||
if h.Settings.ServiceName != "test-handler" {
|
||||
t.Errorf("ServiceName = %q", h.Settings.ServiceName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHandlerNilSettings(t *testing.T) {
|
||||
h := New("ai.test", nil)
|
||||
if h.Settings == nil {
|
||||
t.Fatal("Settings should be loaded automatically")
|
||||
}
|
||||
if h.Settings.ServiceName != "handler" {
|
||||
t.Errorf("ServiceName = %q, want default", h.Settings.ServiceName)
|
||||
}
|
||||
h := New("ai.test", nil)
|
||||
if h.Settings == nil {
|
||||
t.Fatal("Settings should be loaded automatically")
|
||||
}
|
||||
if h.Settings.ServiceName != "handler" {
|
||||
t.Errorf("ServiceName = %q, want default", h.Settings.ServiceName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackRegistration(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
|
||||
setupCalled := false
|
||||
h.OnSetup(func(ctx context.Context) error {
|
||||
setupCalled = true
|
||||
return nil
|
||||
})
|
||||
setupCalled := false
|
||||
h.OnSetup(func(ctx context.Context) error {
|
||||
setupCalled = true
|
||||
return nil
|
||||
})
|
||||
|
||||
teardownCalled := false
|
||||
h.OnTeardown(func(ctx context.Context) error {
|
||||
teardownCalled = true
|
||||
return nil
|
||||
})
|
||||
teardownCalled := false
|
||||
h.OnTeardown(func(ctx context.Context) error {
|
||||
teardownCalled = true
|
||||
return nil
|
||||
})
|
||||
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, nil
|
||||
})
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
if h.onSetup == nil || h.onTeardown == nil || h.onTypedMessage == nil {
|
||||
t.Error("callbacks should not be nil after registration")
|
||||
}
|
||||
if h.onSetup == nil || h.onTeardown == nil || h.onTypedMessage == nil {
|
||||
t.Error("callbacks should not be nil after registration")
|
||||
}
|
||||
|
||||
// Verify setup/teardown work when called directly.
|
||||
_ = h.onSetup(context.Background())
|
||||
_ = h.onTeardown(context.Background())
|
||||
if !setupCalled || !teardownCalled {
|
||||
t.Error("callbacks should have been invoked")
|
||||
}
|
||||
// Verify setup/teardown work when called directly.
|
||||
_ = h.onSetup(context.Background())
|
||||
_ = h.onTeardown(context.Background())
|
||||
if !setupCalled || !teardownCalled {
|
||||
t.Error("callbacks should have been invoked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypedMessageRegistration(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return &pb.ChatResponse{Response: "ok"}, nil
|
||||
})
|
||||
})
|
||||
|
||||
if h.onTypedMessage == nil {
|
||||
t.Error("onTypedMessage should not be nil after registration")
|
||||
}
|
||||
if h.onTypedMessage == nil {
|
||||
t.Error("onTypedMessage should not be nil after registration")
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -93,101 +93,101 @@ t.Error("onTypedMessage should not be nil after registration")
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestWrapHandler_ValidMessage(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
|
||||
var receivedReq pb.ChatRequest
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
if err := natsutil.Decode(msg.Data, &receivedReq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var receivedReq pb.ChatRequest
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
if err := natsutil.Decode(msg.Data, &receivedReq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.ChatResponse{Response: "ok", UserId: receivedReq.GetUserId()}, nil
|
||||
})
|
||||
})
|
||||
|
||||
// Encode a message the same way services would.
|
||||
encoded, err := proto.Marshal(&pb.ChatRequest{
|
||||
RequestId: "test-001",
|
||||
Message: "hello",
|
||||
Premium: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Encode a message the same way services would.
|
||||
encoded, err := proto.Marshal(&pb.ChatRequest{
|
||||
RequestId: "test-001",
|
||||
Message: "hello",
|
||||
Premium: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Call wrapHandler directly without NATS.
|
||||
handler := h.wrapHandler(context.Background())
|
||||
handler(&nats.Msg{
|
||||
Subject: "ai.test.user.42.message",
|
||||
Data: encoded,
|
||||
})
|
||||
// Call wrapHandler directly without NATS.
|
||||
handler := h.wrapHandler(context.Background())
|
||||
handler(&nats.Msg{
|
||||
Subject: "ai.test.user.42.message",
|
||||
Data: encoded,
|
||||
})
|
||||
|
||||
if receivedReq.GetRequestId() != "test-001" {
|
||||
t.Errorf("request_id = %v", receivedReq.GetRequestId())
|
||||
}
|
||||
if receivedReq.GetPremium() != true {
|
||||
t.Errorf("premium = %v", receivedReq.GetPremium())
|
||||
}
|
||||
if receivedReq.GetRequestId() != "test-001" {
|
||||
t.Errorf("request_id = %v", receivedReq.GetRequestId())
|
||||
}
|
||||
if receivedReq.GetPremium() != true {
|
||||
t.Errorf("premium = %v", receivedReq.GetPremium())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapHandler_InvalidMessage(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
|
||||
handlerCalled := false
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
handlerCalled = true
|
||||
var req pb.ChatRequest
|
||||
if err := natsutil.Decode(msg.Data, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.ChatResponse{}, nil
|
||||
})
|
||||
handlerCalled := false
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
handlerCalled = true
|
||||
var req pb.ChatRequest
|
||||
if err := natsutil.Decode(msg.Data, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.ChatResponse{}, nil
|
||||
})
|
||||
|
||||
handler := h.wrapHandler(context.Background())
|
||||
handler(&nats.Msg{
|
||||
Subject: "ai.test",
|
||||
Data: []byte{0xFF, 0xFE, 0xFD}, // invalid protobuf
|
||||
})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
handler(&nats.Msg{
|
||||
Subject: "ai.test",
|
||||
Data: []byte{0xFF, 0xFE, 0xFD}, // invalid protobuf
|
||||
})
|
||||
|
||||
// The handler IS called (wrapHandler doesn't pre-decode), but it should
|
||||
// return an error from Decode. Either way no panic.
|
||||
_ = handlerCalled
|
||||
// The handler IS called (wrapHandler doesn't pre-decode), but it should
|
||||
// return an error from Decode. Either way no panic.
|
||||
_ = handlerCalled
|
||||
}
|
||||
|
||||
func TestWrapHandler_HandlerError(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, context.DeadlineExceeded
|
||||
})
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, context.DeadlineExceeded
|
||||
})
|
||||
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "err-test"})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "err-test"})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
|
||||
// Should not panic even when handler returns error.
|
||||
handler(&nats.Msg{
|
||||
Subject: "ai.test",
|
||||
Data: encoded,
|
||||
})
|
||||
// Should not panic even when handler returns error.
|
||||
handler(&nats.Msg{
|
||||
Subject: "ai.test",
|
||||
Data: encoded,
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrapHandler_NilResponse(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, nil // fire-and-forget style
|
||||
})
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, nil // fire-and-forget style
|
||||
})
|
||||
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "nil-resp"})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "nil-resp"})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
|
||||
// Should not panic with nil response and no reply subject.
|
||||
handler(&nats.Msg{
|
||||
Subject: "ai.test",
|
||||
Data: encoded,
|
||||
})
|
||||
// Should not panic with nil response and no reply subject.
|
||||
handler(&nats.Msg{
|
||||
Subject: "ai.test",
|
||||
Data: encoded,
|
||||
})
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -195,59 +195,59 @@ Data: encoded,
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestWrapHandler_Typed(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
|
||||
var received pb.ChatRequest
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
if err := natsutil.Decode(msg.Data, &received); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var received pb.ChatRequest
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
if err := natsutil.Decode(msg.Data, &received); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.ChatResponse{UserId: received.GetUserId(), Response: "ok"}, nil
|
||||
})
|
||||
})
|
||||
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{
|
||||
RequestId: "typed-001",
|
||||
Message: "hello typed",
|
||||
})
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{
|
||||
RequestId: "typed-001",
|
||||
Message: "hello typed",
|
||||
})
|
||||
|
||||
handler := h.wrapHandler(context.Background())
|
||||
handler(&nats.Msg{Subject: "ai.test", Data: encoded})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
handler(&nats.Msg{Subject: "ai.test", Data: encoded})
|
||||
|
||||
if received.GetRequestId() != "typed-001" {
|
||||
t.Errorf("RequestId = %q", received.GetRequestId())
|
||||
}
|
||||
if received.GetMessage() != "hello typed" {
|
||||
t.Errorf("Message = %q", received.GetMessage())
|
||||
}
|
||||
if received.GetRequestId() != "typed-001" {
|
||||
t.Errorf("RequestId = %q", received.GetRequestId())
|
||||
}
|
||||
if received.GetMessage() != "hello typed" {
|
||||
t.Errorf("Message = %q", received.GetMessage())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapHandler_TypedError(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, context.DeadlineExceeded
|
||||
})
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, context.DeadlineExceeded
|
||||
})
|
||||
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "err"})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "err"})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
|
||||
// Should not panic.
|
||||
handler(&nats.Msg{Subject: "ai.test", Data: encoded})
|
||||
// Should not panic.
|
||||
handler(&nats.Msg{Subject: "ai.test", Data: encoded})
|
||||
}
|
||||
|
||||
func TestWrapHandler_TypedNilResponse(t *testing.T) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, nil
|
||||
})
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "nil"})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
handler(&nats.Msg{Subject: "ai.test", Data: encoded})
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "nil"})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
handler(&nats.Msg{Subject: "ai.test", Data: encoded})
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -255,25 +255,25 @@ handler(&nats.Msg{Subject: "ai.test", Data: encoded})
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func BenchmarkWrapHandler(b *testing.B) {
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
var req pb.ChatRequest
|
||||
_ = natsutil.Decode(msg.Data, &req)
|
||||
return &pb.ChatResponse{Response: "ok"}, nil
|
||||
})
|
||||
cfg := config.Load()
|
||||
h := New("ai.test", cfg)
|
||||
h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) {
|
||||
var req pb.ChatRequest
|
||||
_ = natsutil.Decode(msg.Data, &req)
|
||||
return &pb.ChatResponse{Response: "ok"}, nil
|
||||
})
|
||||
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{
|
||||
RequestId: "bench-001",
|
||||
Message: "What is the capital of France?",
|
||||
Premium: true,
|
||||
TopK: 10,
|
||||
})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
msg := &nats.Msg{Subject: "ai.test", Data: encoded}
|
||||
encoded, _ := proto.Marshal(&pb.ChatRequest{
|
||||
RequestId: "bench-001",
|
||||
Message: "What is the capital of France?",
|
||||
Premium: true,
|
||||
TopK: 10,
|
||||
})
|
||||
handler := h.wrapHandler(context.Background())
|
||||
msg := &nats.Msg{Subject: "ai.test", Data: encoded}
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
handler(msg)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
handler(msg)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user