modelRT/cmd/manualsyncmock/server.go

169 lines
5.3 KiB
Go
Raw Normal View History

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"modelRT/client/manualsync"
)
type listenFunc func(network, address string) (net.Listener, error)
const (
cl3611Address = ":9001"
protocol104Address = ":9002"
manualAPIPath = "/api/manual"
healthPath = "/healthz"
maxRequestBody = 1 << 20
shutdownTimeout = 5 * time.Second
)
func newProtocolServer(protocol, address string, logger *slog.Logger) *http.Server {
mux := http.NewServeMux()
mux.HandleFunc("POST "+manualAPIPath, manualHandler(protocol, strings.TrimPrefix(address, ":"), logger))
mux.HandleFunc("GET "+healthPath, healthHandler)
return &http.Server{
Addr: address,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
}
func run(ctx context.Context, logger *slog.Logger, listen listenFunc) error {
cl3611Listener, err := listen("tcp", cl3611Address)
if err != nil {
return fmt.Errorf("listen on %s: %w", cl3611Address, err)
}
protocol104Listener, err := listen("tcp", protocol104Address)
if err != nil {
_ = cl3611Listener.Close()
return fmt.Errorf("listen on %s: %w", protocol104Address, err)
}
servers := []struct {
protocol string
server *http.Server
listener net.Listener
}{
{protocol: "cl3611", server: newProtocolServer("cl3611", cl3611Address, logger), listener: cl3611Listener},
{protocol: "104", server: newProtocolServer("104", protocol104Address, logger), listener: protocol104Listener},
}
serveErrors := make(chan error, len(servers))
var serversWaitGroup sync.WaitGroup
for _, configuredServer := range servers {
serversWaitGroup.Add(1)
go func() {
defer serversWaitGroup.Done()
logger.Info("manual sync mock server started",
"protocol", configuredServer.protocol,
"listen_addr", configuredServer.listener.Addr().String(),
)
if err := configuredServer.server.Serve(configuredServer.listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
serveErrors <- fmt.Errorf("%s server failed: %w", configuredServer.protocol, err)
}
}()
}
var runError error
select {
case <-ctx.Done():
case runError = <-serveErrors:
}
shutdownContext, cancelShutdown := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancelShutdown()
for _, configuredServer := range servers {
if err := configuredServer.server.Shutdown(shutdownContext); err != nil {
runError = errors.Join(runError, fmt.Errorf("shut down %s server: %w", configuredServer.protocol, err))
_ = configuredServer.server.Close()
}
}
serversWaitGroup.Wait()
return runError
}
func healthHandler(response http.ResponseWriter, _ *http.Request) {
response.Header().Set("Content-Type", "text/plain; charset=utf-8")
response.WriteHeader(http.StatusOK)
_, _ = response.Write([]byte("ok\n"))
}
func manualHandler(protocol, listenPort string, logger *slog.Logger) http.HandlerFunc {
return func(response http.ResponseWriter, request *http.Request) {
request.Body = http.MaxBytesReader(response, request.Body, maxRequestBody)
var payload manualsync.Request
decoder := json.NewDecoder(request.Body)
if err := decoder.Decode(&payload); err != nil {
logger.LogAttrs(request.Context(), slog.LevelWarn, "invalid manual sync request",
slog.String("protocol", protocol),
slog.String("listen_port", listenPort),
slog.String("remote_addr", request.RemoteAddr),
slog.Any("error", err),
)
var maxBytesError *http.MaxBytesError
if errors.As(err, &maxBytesError) {
http.Error(response, "request body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(response, "invalid JSON request body", http.StatusBadRequest)
return
}
var trailing any
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
if err == nil {
err = errors.New("request body contains multiple JSON documents")
}
logger.LogAttrs(request.Context(), slog.LevelWarn, "invalid manual sync request",
slog.String("protocol", protocol),
slog.String("listen_port", listenPort),
slog.String("remote_addr", request.RemoteAddr),
slog.Any("error", err),
)
http.Error(response, "invalid JSON request body", http.StatusBadRequest)
return
}
logger.LogAttrs(request.Context(), slog.LevelInfo, "manual sync request received",
slog.String("protocol", protocol),
slog.String("listen_port", listenPort),
slog.String("remote_addr", request.RemoteAddr),
slog.Int("mode", int(payload.Mode)),
slog.Group("target",
slog.Int("type", payload.Target.Type),
slog.String("station", payload.Target.Station),
slog.String("main_pos", payload.Target.MainPos),
slog.String("sub_pos", payload.Target.SubPos),
slog.String("option", payload.Target.Option),
),
slog.Attr{Key: "data", Value: samplesLogValue(payload.Data)},
)
response.WriteHeader(http.StatusNoContent)
}
}
func samplesLogValue(samples []manualsync.SyntheticData) slog.Value {
attributes := make([]slog.Attr, 0, len(samples))
for index, sample := range samples {
attributes = append(attributes, slog.Group(strconv.Itoa(index),
slog.Int64("time", sample.Time),
slog.Float64("value", sample.Value),
))
}
return slog.GroupValue(attributes...)
}