update
mega-ci / static-release-gates (push) Failing after 11s
release-tag / release-image (push) Successful in 6m46s
mega-ci / go-quality (services/control) (push) Successful in 10m7s
mega-ci / go-quality (platform/neuroforge) (push) Successful in 10m20s
mega-ci / go-quality (services/agent) (push) Successful in 11m1s
mega-ci / go-quality (services/knowledge) (push) Successful in 11m13s
mega-ci / docker-build (push) Has been skipped

This commit is contained in:
2026-09-09 11:10:31 +02:00
parent 9a4370e4df
commit e0bf42bf32
85 changed files with 8035 additions and 1138 deletions
+129 -20
View File
@@ -6,12 +6,14 @@ import (
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
@@ -101,6 +103,59 @@ func maxIntMain(a, b int) int {
return b
}
type bootstrapHandler struct {
mu sync.RWMutex
phase string
handler http.Handler
}
func newBootstrapHandler() *bootstrapHandler {
return &bootstrapHandler{phase: "process.start"}
}
func (b *bootstrapHandler) SetPhase(phase string) {
b.mu.Lock()
b.phase = phase
b.mu.Unlock()
}
func (b *bootstrapHandler) SetHandler(h http.Handler) {
b.mu.Lock()
b.handler = h
b.phase = "ready"
b.mu.Unlock()
}
func (b *bootstrapHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
b.mu.RLock()
h := b.handler
phase := b.phase
b.mu.RUnlock()
if h != nil {
h.ServeHTTP(w, r)
return
}
w.Header().Set("Cache-Control", "no-store")
switch r.URL.Path {
case "/livez", "/healthz":
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = fmt.Fprintf(w, `{"ok":true,"status":"starting","phase":%q}`, phase)
case "/readyz":
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = fmt.Fprintf(w, `{"ok":false,"status":"starting","phase":%q}`, phase)
case "/", "/admin":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = fmt.Fprintf(w, `<!doctype html><html><head><meta charset="utf-8"><title>NeuroForge starting</title><meta http-equiv="refresh" content="5"></head><body><h1>NeuroForge startet</h1><p>Recovery-/Startphase: <code>%s</code></p><p>Die autoritativen Daten werden geladen. Diese Seite aktualisiert sich automatisch.</p></body></html>`, phase)
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = fmt.Fprintf(w, `{"error":"neuroforge is starting","phase":%q}`, phase)
}
}
func main() {
if err := run(); err != nil {
log.Printf("fatal: %v", err)
@@ -117,7 +172,55 @@ func run() (retErr error) {
return err
}
s, err := store.New(*data)
var boot *bootstrapHandler
var srv *http.Server
var errCh chan error
// Container deployments pass an explicit -listen address. Bind it before
// opening the potentially large store so liveness and a minimal startup UI
// remain reachable during WAL/index recovery instead of looking like a dead
// container with no logs.
if strings.TrimSpace(*listen) != "" {
boot = newBootstrapHandler()
d := core.DefaultConfig()
h := d.HTTP
srv = &http.Server{
Addr: *listen,
Handler: boot,
ReadHeaderTimeout: time.Duration(h.ReadHeaderTimeoutSeconds) * time.Second,
ReadTimeout: time.Duration(h.ReadTimeoutSeconds) * time.Second,
WriteTimeout: time.Duration(h.WriteTimeoutSeconds) * time.Second,
IdleTimeout: time.Duration(h.IdleTimeoutSeconds) * time.Second,
MaxHeaderBytes: h.MaxHeaderBytes,
}
ln, err := net.Listen("tcp", *listen)
if err != nil {
return fmt.Errorf("bootstrap listen %s: %w", *listen, err)
}
errCh = make(chan error, 1)
go func() {
err := srv.Serve(ln)
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
errCh <- err
}()
log.Printf("NeuroForge bootstrap listener active on %s", *listen)
defer func() {
if retErr != nil && srv != nil {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}
}()
}
reportStartup := func(phase string) {
log.Printf("startup phase: %s", phase)
if boot != nil {
boot.SetPhase(phase)
}
}
s, err := store.NewWithProgress(*data, reportStartup)
if err != nil {
return err
}
@@ -258,7 +361,7 @@ func run() (retErr error) {
// and environment-owned only when set, preserving admin-managed config otherwise.
workerEnv := []string{
"NEUROFORGE_WORKER_LEASE_SECONDS", "NEUROFORGE_WORKER_HEARTBEAT_SECONDS", "NEUROFORGE_WORKER_STALE_AFTER_SECONDS",
"NEUROFORGE_WORKER_DEFAULT_MAX_ATTEMPTS", "NEUROFORGE_WORKER_RETRY_BACKOFF_SECONDS", "NEUROFORGE_WORKER_MAX_QUEUED_JOBS",
"NEUROFORGE_WORKER_DEFAULT_MAX_ATTEMPTS", "NEUROFORGE_WORKER_RETRY_BACKOFF_SECONDS", "NEUROFORGE_WORKER_MAX_QUEUED_JOBS", "NEUROFORGE_WORKER_MAX_QUEUED_PAYLOAD_MB",
"NEUROFORGE_WORKER_JOB_RETENTION_HOURS", "NEUROFORGE_WORKER_MAX_TERMINAL_JOBS",
"NEUROFORGE_WORKER_MASTER_APPLY_MAX_ATTEMPTS", "NEUROFORGE_WORKER_MASTER_APPLY_BACKOFF_SECONDS",
"NEUROFORGE_GRAPH_BACKFILL_ENABLED", "NEUROFORGE_GRAPH_BACKFILL_INTERVAL_SECONDS", "NEUROFORGE_GRAPH_BACKFILL_BATCH_SIZE",
@@ -294,6 +397,9 @@ func run() (retErr error) {
if v, ok := envInt("NEUROFORGE_WORKER_MAX_QUEUED_JOBS"); ok {
cfg.Worker.MaxQueuedJobs = v
}
if v, ok := envInt("NEUROFORGE_WORKER_MAX_QUEUED_PAYLOAD_MB"); ok {
cfg.Worker.MaxQueuedPayloadMB = v
}
if v, ok := envInt("NEUROFORGE_WORKER_JOB_RETENTION_HOURS"); ok {
cfg.Worker.JobRetentionHours = v
}
@@ -453,30 +559,33 @@ func run() (retErr error) {
}
h := cfg.HTTP
srv := &http.Server{
Addr: addr,
Handler: api.Handler(),
ReadHeaderTimeout: time.Duration(h.ReadHeaderTimeoutSeconds) * time.Second,
ReadTimeout: time.Duration(h.ReadTimeoutSeconds) * time.Second,
WriteTimeout: time.Duration(h.WriteTimeoutSeconds) * time.Second,
IdleTimeout: time.Duration(h.IdleTimeoutSeconds) * time.Second,
MaxHeaderBytes: h.MaxHeaderBytes,
if srv == nil {
srv = &http.Server{
Addr: addr,
Handler: api.Handler(),
ReadHeaderTimeout: time.Duration(h.ReadHeaderTimeoutSeconds) * time.Second,
ReadTimeout: time.Duration(h.ReadTimeoutSeconds) * time.Second,
WriteTimeout: time.Duration(h.WriteTimeoutSeconds) * time.Second,
IdleTimeout: time.Duration(h.IdleTimeoutSeconds) * time.Second,
MaxHeaderBytes: h.MaxHeaderBytes,
}
errCh = make(chan error, 1)
go func() {
err := srv.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
errCh <- err
}()
} else {
boot.SetHandler(api.Handler())
}
log.Printf("NeuroForge v0.8.2 listening on %s", addr)
log.Printf("NeuroForge v0.8.3 listening on %s", addr)
log.Printf("Admin dashboard: /admin · readiness: /readyz · metrics: /metrics")
if os.Getenv("NEUROFORGE_ADMIN_TOKEN") == "" {
log.Printf("Admin token is intentionally not printed; read it locally from %s or set NEUROFORGE_ADMIN_TOKEN", filepath.Join(*data, "secrets.json"))
}
errCh := make(chan error, 1)
go func() {
err := srv.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
errCh <- err
}()
var serveErr error
select {
case serveErr = <-errCh:
@@ -0,0 +1,38 @@
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestBootstrapHandlerExposesLivenessAndStartupUI(t *testing.T) {
b := newBootstrapHandler()
b.SetPhase("hnsw.rebuild")
rr := httptest.NewRecorder()
b.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/livez", nil))
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "hnsw.rebuild") {
t.Fatalf("livez=%d %s", rr.Code, rr.Body.String())
}
rr = httptest.NewRecorder()
b.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if rr.Code != http.StatusServiceUnavailable {
t.Fatalf("readyz=%d", rr.Code)
}
rr = httptest.NewRecorder()
b.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/admin", nil))
if rr.Code != http.StatusServiceUnavailable || !strings.Contains(rr.Body.String(), "hnsw.rebuild") {
t.Fatalf("admin=%d %s", rr.Code, rr.Body.String())
}
b.SetHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }))
rr = httptest.NewRecorder()
b.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/admin", nil))
if rr.Code != http.StatusNoContent {
t.Fatalf("delegated=%d", rr.Code)
}
}