|
|
|
|
// tera-sessiond owns Direct Mode WebSocket attachments and bounded fan-out.
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"syscall"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"tera-clone/services/tera-api/internal/directstream"
|
|
|
|
|
"tera-clone/services/tera-api/internal/serial"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
listenAddress := sessiondGetenv("TERA_SESSIOND_LISTEN_ADDR", "127.0.0.1:8081")
|
|
|
|
|
ticketSockets := sessiondGetenvList("TERA_SESSIOND_TICKET_SOCKETS")
|
|
|
|
|
if len(ticketSockets) == 0 {
|
|
|
|
|
ticketSockets = []string{sessiondGetenv("TERA_SESSIOND_TICKET_SOCKET", directstream.DefaultTicketSocketPath)}
|
|
|
|
|
}
|
|
|
|
|
serialSocket := sessiondGetenv("TERA_SESSIOND_SERIAL_STREAM_SOCKET", serial.DefaultStreamSocketPath)
|
|
|
|
|
ticketClient := directstream.NewTicketSocketPool(ticketSockets)
|
|
|
|
|
transportFactory := serial.NewStreamSocketClient(serialSocket, "tera-sessiond")
|
|
|
|
|
broker, err := directstream.NewBroker(transportFactory, directstream.BrokerOptions{SharedPortSessions: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("tera-sessiond broker setup failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer broker.Close()
|
|
|
|
|
sharedListener, err := directstream.ListenTicketSocket(sessiondGetenv("TERA_SESSIOND_SHARED_SOCKET", directstream.DefaultSharedSocketPath))
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("shared stream socket setup failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer sharedListener.Close()
|
|
|
|
|
go func() {
|
|
|
|
|
if err := directstream.ServeSharedSocket(sharedListener, broker); err != nil {
|
|
|
|
|
log.Printf("shared stream socket stopped: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
webSocketMetrics := &directstream.WebSocketMetrics{}
|
|
|
|
|
streamHandler := directstream.NewWebSocketHandler(broker, ticketClient, ticketClient, directstream.WebSocketOptions{
|
|
|
|
|
AllowedOrigins: sessiondGetenvList("TERA_SESSIOND_ALLOWED_ORIGINS"),
|
|
|
|
|
Metrics: webSocketMetrics,
|
|
|
|
|
})
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
mux.Handle("/api/v1/direct/", streamHandler)
|
|
|
|
|
statusHandler := newSessiondStatusHandler(broker, webSocketMetrics, ticketClient, transportFactory)
|
|
|
|
|
mux.Handle("/healthz", statusHandler(false))
|
|
|
|
|
mux.Handle("/readyz", statusHandler(true))
|
|
|
|
|
server := &http.Server{
|
|
|
|
|
Addr: listenAddress,
|
|
|
|
|
Handler: mux,
|
|
|
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
|
|
|
IdleTimeout: 60 * time.Second,
|
|
|
|
|
}
|
|
|
|
|
log.Printf("tera-sessiond listening on http://%s", listenAddress)
|
|
|
|
|
serverErrors := make(chan error, 1)
|
|
|
|
|
go func() { serverErrors <- server.ListenAndServe() }()
|
|
|
|
|
signalContext, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
|
defer stopSignals()
|
|
|
|
|
select {
|
|
|
|
|
case <-signalContext.Done():
|
|
|
|
|
log.Printf("tera-sessiond stopping")
|
|
|
|
|
_ = broker.Close()
|
|
|
|
|
shutdownContext, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
if err := server.Shutdown(shutdownContext); err != nil {
|
|
|
|
|
log.Printf("tera-sessiond HTTP shutdown failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
case err := <-serverErrors:
|
|
|
|
|
if err != nil && err != http.ErrServerClosed {
|
|
|
|
|
log.Fatalf("tera-sessiond failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ticketStatusProvider interface {
|
|
|
|
|
Status(context.Context) (directstream.TicketServiceSnapshot, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type serialStatusProvider interface {
|
|
|
|
|
Status(context.Context) (serial.StreamSnapshot, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type sessiondStatus struct {
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
Broker directstream.BrokerSnapshot `json:"broker"`
|
|
|
|
|
WebSocket directstream.WebSocketSnapshot `json:"websocket"`
|
|
|
|
|
Tickets directstream.TicketServiceSnapshot `json:"tickets"`
|
|
|
|
|
Serial serial.StreamSnapshot `json:"serial"`
|
|
|
|
|
TicketError string `json:"ticket_error,omitempty"`
|
|
|
|
|
SerialError string `json:"serial_error,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newSessiondStatusHandler(broker *directstream.Broker, metrics *directstream.WebSocketMetrics, tickets ticketStatusProvider, seriald serialStatusProvider) func(bool) http.Handler {
|
|
|
|
|
return func(readiness bool) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
|
|
|
if request.Method != http.MethodGet {
|
|
|
|
|
response.Header().Set("Allow", http.MethodGet)
|
|
|
|
|
response.WriteHeader(http.StatusMethodNotAllowed)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx, cancel := context.WithTimeout(request.Context(), time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
status := sessiondStatus{Status: "ok", Broker: broker.Snapshot(), WebSocket: metrics.Snapshot()}
|
|
|
|
|
var wait sync.WaitGroup
|
|
|
|
|
wait.Add(2)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wait.Done()
|
|
|
|
|
value, err := tickets.Status(ctx)
|
|
|
|
|
status.Tickets = value
|
|
|
|
|
if err != nil {
|
|
|
|
|
status.TicketError = err.Error()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
go func() {
|
|
|
|
|
defer wait.Done()
|
|
|
|
|
value, err := seriald.Status(ctx)
|
|
|
|
|
status.Serial = value
|
|
|
|
|
if err != nil {
|
|
|
|
|
status.SerialError = err.Error()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
wait.Wait()
|
|
|
|
|
if status.TicketError != "" || status.SerialError != "" {
|
|
|
|
|
status.Status = "degraded"
|
|
|
|
|
}
|
|
|
|
|
response.Header().Set("Content-Type", "application/json")
|
|
|
|
|
if readiness && status.Status != "ok" {
|
|
|
|
|
response.WriteHeader(http.StatusServiceUnavailable)
|
|
|
|
|
}
|
|
|
|
|
_ = json.NewEncoder(response).Encode(status)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sessiondGetenv(key, fallback string) string {
|
|
|
|
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sessiondGetenvList(key string) []string {
|
|
|
|
|
value := os.Getenv(key)
|
|
|
|
|
result := make([]string, 0)
|
|
|
|
|
for _, item := range strings.Split(value, ",") {
|
|
|
|
|
item = strings.TrimSpace(item)
|
|
|
|
|
if item != "" {
|
|
|
|
|
result = append(result, item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|