feat: mail service
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
// Package internalhttp provides the trusted internal HTTP listener used by the
|
||||
// runnable Mail Service process.
|
||||
package internalhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"galaxy/mail/internal/telemetry"
|
||||
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
// DeliveriesPath is the trusted operator list route reserved by the Stage 6
|
||||
// runnable skeleton.
|
||||
DeliveriesPath = "/api/v1/internal/deliveries"
|
||||
|
||||
// DeliveryByIDPath is the trusted operator get-delivery route reserved by
|
||||
// the Stage 6 runnable skeleton.
|
||||
DeliveryByIDPath = "/api/v1/internal/deliveries/{delivery_id}"
|
||||
|
||||
// DeliveryAttemptsPath is the trusted operator list-attempts route reserved
|
||||
// by the Stage 6 runnable skeleton.
|
||||
DeliveryAttemptsPath = "/api/v1/internal/deliveries/{delivery_id}/attempts"
|
||||
|
||||
// DeliveryResendPath is the trusted operator resend route reserved by the
|
||||
// Stage 6 runnable skeleton.
|
||||
DeliveryResendPath = "/api/v1/internal/deliveries/{delivery_id}/resend"
|
||||
)
|
||||
|
||||
// Config describes the trusted internal HTTP listener owned by Mail Service.
|
||||
type Config struct {
|
||||
// Addr is the TCP listen address used by the trusted internal HTTP server.
|
||||
Addr string
|
||||
|
||||
// ReadHeaderTimeout bounds how long the listener may spend reading request
|
||||
// headers before the server rejects the connection.
|
||||
ReadHeaderTimeout time.Duration
|
||||
|
||||
// ReadTimeout bounds how long the listener may spend reading one trusted
|
||||
// internal request.
|
||||
ReadTimeout time.Duration
|
||||
|
||||
// IdleTimeout bounds how long the listener keeps an idle keep-alive
|
||||
// connection open.
|
||||
IdleTimeout time.Duration
|
||||
}
|
||||
|
||||
// Validate reports whether cfg contains a usable internal HTTP listener
|
||||
// configuration.
|
||||
func (cfg Config) Validate() error {
|
||||
switch {
|
||||
case cfg.Addr == "":
|
||||
return errors.New("internal HTTP addr must not be empty")
|
||||
case cfg.ReadHeaderTimeout <= 0:
|
||||
return errors.New("internal HTTP read header timeout must be positive")
|
||||
case cfg.ReadTimeout <= 0:
|
||||
return errors.New("internal HTTP read timeout must be positive")
|
||||
case cfg.IdleTimeout <= 0:
|
||||
return errors.New("internal HTTP idle timeout must be positive")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Dependencies describes the collaborators used by the trusted internal HTTP
|
||||
// transport layer.
|
||||
type Dependencies struct {
|
||||
// Logger writes structured transport logs. When nil, slog.Default is used.
|
||||
Logger *slog.Logger
|
||||
|
||||
// Telemetry records low-cardinality transport and auth-delivery metrics.
|
||||
Telemetry *telemetry.Runtime
|
||||
|
||||
// AcceptLoginCodeDelivery handles the dedicated auth-delivery route when
|
||||
// provided.
|
||||
AcceptLoginCodeDelivery AcceptLoginCodeDeliveryUseCase
|
||||
|
||||
// ListDeliveries handles the trusted operator delivery-list route when
|
||||
// provided.
|
||||
ListDeliveries ListDeliveriesUseCase
|
||||
|
||||
// GetDelivery handles the trusted operator exact delivery-read route when
|
||||
// provided.
|
||||
GetDelivery GetDeliveryUseCase
|
||||
|
||||
// ListAttempts handles the trusted operator attempt-history route when
|
||||
// provided.
|
||||
ListAttempts ListAttemptsUseCase
|
||||
|
||||
// ResendDelivery handles the trusted operator resend route when provided.
|
||||
ResendDelivery ResendDeliveryUseCase
|
||||
|
||||
// OperatorRequestTimeout bounds one trusted operator use-case execution.
|
||||
OperatorRequestTimeout time.Duration
|
||||
}
|
||||
|
||||
// Server owns the trusted internal HTTP listener exposed by Mail Service.
|
||||
type Server struct {
|
||||
cfg Config
|
||||
|
||||
handler http.Handler
|
||||
logger *slog.Logger
|
||||
|
||||
stateMu sync.RWMutex
|
||||
server *http.Server
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewServer constructs one trusted internal HTTP server for cfg and deps.
|
||||
func NewServer(cfg Config, deps Dependencies) (*Server, error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("new internal HTTP server: %w", err)
|
||||
}
|
||||
|
||||
logger := deps.Logger
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
handler: newHandler(deps),
|
||||
logger: logger.With("component", "internal_http"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run binds the configured listener and serves the trusted internal HTTP
|
||||
// surface until Shutdown closes the server.
|
||||
func (server *Server) Run(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return errors.New("run internal HTTP server: nil context")
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", server.cfg.Addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("run internal HTTP server: listen on %q: %w", server.cfg.Addr, err)
|
||||
}
|
||||
|
||||
httpServer := &http.Server{
|
||||
Handler: server.handler,
|
||||
ReadHeaderTimeout: server.cfg.ReadHeaderTimeout,
|
||||
ReadTimeout: server.cfg.ReadTimeout,
|
||||
IdleTimeout: server.cfg.IdleTimeout,
|
||||
}
|
||||
|
||||
server.stateMu.Lock()
|
||||
server.server = httpServer
|
||||
server.listener = listener
|
||||
server.stateMu.Unlock()
|
||||
|
||||
server.logger.Info("internal HTTP server started", "addr", listener.Addr().String())
|
||||
|
||||
defer func() {
|
||||
server.stateMu.Lock()
|
||||
server.server = nil
|
||||
server.listener = nil
|
||||
server.stateMu.Unlock()
|
||||
}()
|
||||
|
||||
err = httpServer.Serve(listener)
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case errors.Is(err, http.ErrServerClosed):
|
||||
server.logger.Info("internal HTTP server stopped")
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("run internal HTTP server: serve on %q: %w", server.cfg.Addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown gracefully stops the trusted internal HTTP server within ctx.
|
||||
func (server *Server) Shutdown(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return errors.New("shutdown internal HTTP server: nil context")
|
||||
}
|
||||
|
||||
server.stateMu.RLock()
|
||||
httpServer := server.server
|
||||
server.stateMu.RUnlock()
|
||||
|
||||
if httpServer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := httpServer.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("shutdown internal HTTP server: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newHandler(deps Dependencies) http.Handler {
|
||||
logger := deps.Logger
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
loginCodeHandler := http.HandlerFunc(serviceUnavailableHandler)
|
||||
if deps.AcceptLoginCodeDelivery != nil {
|
||||
loginCodeHandler = newAcceptLoginCodeDeliveryHandler(deps.AcceptLoginCodeDelivery)
|
||||
}
|
||||
listDeliveriesHandler := http.HandlerFunc(serviceUnavailableHandler)
|
||||
if deps.ListDeliveries != nil {
|
||||
listDeliveriesHandler = newListDeliveriesHandler(deps.ListDeliveries, deps.OperatorRequestTimeout)
|
||||
}
|
||||
getDeliveryHandler := http.HandlerFunc(serviceUnavailableHandler)
|
||||
if deps.GetDelivery != nil {
|
||||
getDeliveryHandler = newGetDeliveryHandler(deps.GetDelivery, deps.OperatorRequestTimeout)
|
||||
}
|
||||
listAttemptsHandler := http.HandlerFunc(serviceUnavailableHandler)
|
||||
if deps.ListAttempts != nil {
|
||||
listAttemptsHandler = newListAttemptsHandler(deps.ListAttempts, deps.OperatorRequestTimeout)
|
||||
}
|
||||
resendDeliveryHandler := http.HandlerFunc(serviceUnavailableHandler)
|
||||
if deps.ResendDelivery != nil {
|
||||
resendDeliveryHandler = newResendDeliveryHandler(deps.ResendDelivery, deps.OperatorRequestTimeout)
|
||||
}
|
||||
|
||||
mux.Handle("POST "+LoginCodeDeliveriesPath, wrapObservedRoute(LoginCodeDeliveriesPath, logger, deps.Telemetry, loginCodeHandler))
|
||||
mux.Handle("GET "+DeliveriesPath, wrapObservedRoute(DeliveriesPath, logger, deps.Telemetry, listDeliveriesHandler))
|
||||
mux.Handle("GET "+DeliveryByIDPath, wrapObservedRoute(DeliveryByIDPath, logger, deps.Telemetry, getDeliveryHandler))
|
||||
mux.Handle("GET "+DeliveryAttemptsPath, wrapObservedRoute(DeliveryAttemptsPath, logger, deps.Telemetry, listAttemptsHandler))
|
||||
mux.Handle("POST "+DeliveryResendPath, wrapObservedRoute(DeliveryResendPath, logger, deps.Telemetry, resendDeliveryHandler))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func wrapObservedRoute(route string, logger *slog.Logger, telemetryRuntime *telemetry.Runtime, next http.Handler) http.Handler {
|
||||
handler := instrumentRoute(route, logger, telemetryRuntime, next)
|
||||
|
||||
options := []otelhttp.Option{}
|
||||
if telemetryRuntime != nil {
|
||||
options = append(options,
|
||||
otelhttp.WithTracerProvider(telemetryRuntime.TracerProvider()),
|
||||
otelhttp.WithMeterProvider(telemetryRuntime.MeterProvider()),
|
||||
)
|
||||
}
|
||||
|
||||
return otelhttp.NewHandler(handler, route, options...)
|
||||
}
|
||||
|
||||
func serviceUnavailableHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
_ = request
|
||||
writeErrorResponse(writer, http.StatusServiceUnavailable, ErrorCodeServiceUnavailable, "service is unavailable")
|
||||
}
|
||||
|
||||
func writeErrorResponse(writer http.ResponseWriter, statusCode int, code string, message string) {
|
||||
if recorder, ok := writer.(*observedResponseWriter); ok {
|
||||
recorder.SetErrorCode(code)
|
||||
}
|
||||
|
||||
payload := ErrorResponse{
|
||||
Error: ErrorBody{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(statusCode)
|
||||
_ = json.NewEncoder(writer).Encode(payload)
|
||||
}
|
||||
Reference in New Issue
Block a user