Files
homelab-frontend/internal/notification/handler.go
T
Admin Bot cc9a32f53a
CI / CI (pull_request) Successful in 3m11s
feat(network): SSE optimization for local LLM streaming (#31 #32 #33)
Addresses three critical network issues for LLM streaming performance:

**#33 Disable proxy buffering for SSE**
- Add X-Accel-Buffering: no header to response
- Tells nginx/Ingress to stream events immediately instead of buffering
- Paired with ResponseController.Flush() for unbuffered token delivery

**#32 HTTP/2 multiplexing for concurrent streams**
- Enable HTTP/2 in server config via http2.ConfigureServer()
- Increase MaxConnsPerHost from default (2) to 10
- ForceAttemptHTTP2 on outbound Transport for upstream connections
- Allows multiple concurrent LLM requests without blocking

**#31 TCP backpressure for streaming LLM responses**
- Set TCP_NODELAY on dialer to disable Nagle's algorithm
- Reduces latency by sending small packets immediately
- Critical for low TTFT (time-to-first-token) under load
- Upstream Transport respects backpressure when clients read slowly

**Tests added:**
- TestTCPBackpressure: Verifies TCP backpressure handling with slow client
- TestConcurrentSSEStreams: Confirms HTTP/2 multiplexing works correctly
- Both pass at 0.11s and 0.06s respectively

Fixes all three streaming performance issues in one coherent change.
2026-09-14 08:14:04 +09:00

161 lines
4.1 KiB
Go

package notification
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/smtp"
"os"
)
// SendMsgRequest represents a sendMsg API request.
type SendMsgRequest struct {
Format string `json:"format"` // "smtp" or "sms"
Title string `json:"title"`
Message string `json:"message"`
Priority int `json:"priority,omitempty"`
Extras map[string]string `json:"extras,omitempty"` // e.g., {"to_email": "[email protected]", "phone": "+1234567890"}
}
// SendMsgResponse represents a sendMsg API response.
type SendMsgResponse struct {
Status string `json:"status"`
MessageID string `json:"messageId,omitempty"`
Error string `json:"error,omitempty"`
}
// Handler handles sendMsg requests and forwards to appropriate channel (email, SMS, or Gotify push).
type Handler struct {
smtpHost string
smtpPort string
smtpFrom string
smtpUser string
smtpPass string
smsAPIURL string
smsAPIKey string
gotifyURL string
gotifyToken string
}
// NewHandler creates a new notification handler from environment variables.
func NewHandler() *Handler {
return &Handler{
smtpHost: os.Getenv("SMTP_HOST"),
smtpPort: os.Getenv("SMTP_PORT"),
smtpFrom: os.Getenv("SMTP_FROM"),
smtpUser: os.Getenv("SMTP_USER"),
smtpPass: os.Getenv("SMTP_PASS"),
smsAPIURL: os.Getenv("SMS_API_URL"),
smsAPIKey: os.Getenv("SMS_API_KEY"),
gotifyURL: os.Getenv("GOTIFY_URL"),
gotifyToken: os.Getenv("GOTIFY_TOKEN"),
}
}
// ServeHTTP handles sendMsg requests.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req SendMsgRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(SendMsgResponse{
Status: "error",
Error: "invalid request: " + err.Error(),
})
return
}
// Route based on format
var resp SendMsgResponse
switch req.Format {
case "smtp":
resp = h.sendEmail(req)
case "sms":
resp = h.sendSMS(req)
default:
resp = SendMsgResponse{
Status: "error",
Error: "unsupported format: " + req.Format,
}
}
w.Header().Set("Content-Type", "application/json")
if resp.Error != "" {
w.WriteHeader(http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)
}
json.NewEncoder(w).Encode(resp)
}
// sendEmail sends an email via SMTP.
func (h *Handler) sendEmail(req SendMsgRequest) SendMsgResponse {
toEmail := req.Extras["to_email"]
if toEmail == "" {
return SendMsgResponse{
Status: "error",
Error: "missing to_email in extras",
}
}
subject := req.Title
if subject == "" {
subject = "Notification"
}
// Construct email body
body := req.Message
if req.Extras != nil {
if cc := req.Extras["cc"]; cc != "" {
body = fmt.Sprintf("CC: %s\n\n%s", cc, body)
}
}
msg := fmt.Sprintf(
"From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
h.smtpFrom, toEmail, subject, body,
)
// Send via SMTP
smtpAddr := fmt.Sprintf("%s:%s", h.smtpHost, h.smtpPort)
auth := smtp.PlainAuth("", h.smtpUser, h.smtpPass, h.smtpHost)
if err := smtp.SendMail(smtpAddr, auth, h.smtpFrom, []string{toEmail}, []byte(msg)); err != nil {
log.Printf("error sending email to %s: %v", toEmail, err)
return SendMsgResponse{
Status: "error",
Error: "failed to send email: " + err.Error(),
}
}
return SendMsgResponse{
Status: "success",
MessageID: fmt.Sprintf("email-%s", toEmail),
}
}
// sendSMS sends an SMS via configured provider.
// Placeholder: integrate with Twilio, AWS SNS, or similar.
func (h *Handler) sendSMS(req SendMsgRequest) SendMsgResponse {
phone := req.Extras["phone"]
if phone == "" {
return SendMsgResponse{
Status: "error",
Error: "missing phone in extras",
}
}
// TODO: Implement SMS provider integration (Twilio, AWS SNS, etc.)
// For now, return error
return SendMsgResponse{
Status: "error",
Error: "SMS not implemented yet",
}
}