You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

159 lines
5.3 KiB

// 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
}