Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/db"
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/httpapi"
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/logredact"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetOutput(logredact.Writer(os.Stderr))
|
||||
slog.SetDefault(slog.New(logredact.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})))
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("config: %v", err)
|
||||
}
|
||||
if cfg.ShouldWarnRateLimits() {
|
||||
slog.Warn(cfg.RateLimitWarningMessage(),
|
||||
"rate_limit_replicas", cfg.RateLimitReplicas,
|
||||
"rate_limit_multi_replica", cfg.RateLimitMultiReplica,
|
||||
"rate_limit_backend", cfg.RateLimitBackend,
|
||||
"rate_limit_backend_requested", cfg.RateLimitBackendRequested,
|
||||
)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
pool, err := db.NewPool(ctx, cfg.DatabaseURL, db.PoolOptions{
|
||||
MaxConns: int32(cfg.DBMaxConns),
|
||||
MinConns: int32(cfg.DBMinConns),
|
||||
MaxConnLifetime: cfg.DBMaxConnLifetime,
|
||||
MaxConnLifetimeJitter: cfg.DBMaxConnLifetimeJitter,
|
||||
MaxConnIdleTime: cfg.DBMaxConnIdleTime,
|
||||
HealthCheckPeriod: cfg.DBHealthCheckPeriod,
|
||||
StatementTimeout: cfg.DBStatementTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("db: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
sessions := auth.NewSessionManager(pool, cfg.SessionCookieName, cfg.CookieSecure(), cfg.SessionIdleHours)
|
||||
srv := httpapi.NewServer(cfg, pool, sessions)
|
||||
|
||||
runCtx, runCancel := context.WithCancel(context.Background())
|
||||
defer runCancel()
|
||||
// Lightweight AI fallback poller so FAQ-miss tickets drain without a separate worker.
|
||||
if srv.Support != nil {
|
||||
go srv.Support.RunAutoJobsLoop(runCtx, 2*time.Second, 3)
|
||||
}
|
||||
|
||||
httpServer := newHTTPServer(cfg.HTTPAddr, srv.Router())
|
||||
|
||||
ln, err := net.Listen("tcp", cfg.HTTPAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("listen: %v", err)
|
||||
}
|
||||
slog.Info("api_listening", "addr", cfg.HTTPAddr, "maintenance", cfg.MaintenanceMode, "read_only", cfg.ReadOnlyMode)
|
||||
|
||||
go func() {
|
||||
if err := httpServer.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("serve: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-stop
|
||||
runCancel()
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
_ = httpServer.Shutdown(shutdownCtx)
|
||||
}
|
||||
|
||||
// newHTTPServer configures net/http timeouts for the API listener.
|
||||
//
|
||||
// Tradeoff — WriteTimeout vs long sync/export:
|
||||
// WriteTimeout bounds the whole ServeHTTP + response write. Feed export
|
||||
// streams and sync-style handlers can run for many minutes; a short
|
||||
// WriteTimeout aborts them mid-stream (clients see truncated/hanging
|
||||
// responses). We use a long WriteTimeout ceiling instead of 0 (unlimited)
|
||||
// so wedged handlers still release connections eventually. Finer per-route
|
||||
// deadlines belong on request contexts / middleware for normal JSON APIs.
|
||||
// Leaving WriteTimeout unset (0) would never reclaim a stuck writer.
|
||||
func newHTTPServer(addr string, handler http.Handler) *http.Server {
|
||||
return &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
// Headers-only Slowloris guard (independent of ReadTimeout).
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
// Full request read (headers + body). Above typical API JSON uploads.
|
||||
ReadTimeout: 60 * time.Second,
|
||||
// Long ceiling so streaming exports/sync can finish; see comment above.
|
||||
WriteTimeout: 15 * time.Minute,
|
||||
// Close keep-alive connections idle between requests.
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewHTTPServerTimeouts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
||||
srv := newHTTPServer(":0", handler)
|
||||
|
||||
if srv.Addr != ":0" {
|
||||
t.Fatalf("Addr = %q, want :0", srv.Addr)
|
||||
}
|
||||
if srv.Handler == nil {
|
||||
t.Fatal("Handler is nil")
|
||||
}
|
||||
if got, want := srv.ReadHeaderTimeout, 10*time.Second; got != want {
|
||||
t.Fatalf("ReadHeaderTimeout = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := srv.ReadTimeout, 60*time.Second; got != want {
|
||||
t.Fatalf("ReadTimeout = %v, want %v", got, want)
|
||||
}
|
||||
// Long WriteTimeout preserves streaming feed exports/sync; must stay >> typical JSON handlers.
|
||||
if got, want := srv.WriteTimeout, 15*time.Minute; got != want {
|
||||
t.Fatalf("WriteTimeout = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := srv.IdleTimeout, 120*time.Second; got != want {
|
||||
t.Fatalf("IdleTimeout = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user