diff --git a/cmd/unified/main.go b/cmd/unified/main.go index 63e4b31..080ddfe 100644 --- a/cmd/unified/main.go +++ b/cmd/unified/main.go @@ -59,6 +59,8 @@ func loadConfig() AppConfig { m.HelloInterval = parseDuration(os.Getenv("MESH_HELLO_INTERVAL"), 20*time.Second) m.HelloFanout = parseIntEnv(os.Getenv("MESH_HELLO_FANOUT"), 8) + m.BlobTimeout = parseDuration(os.Getenv("MESH_BLOB_TIMEOUT"), 0) + // Wenn keine AdvertURL gesetzt ist, versuche eine sinnvolle Herleitung: if strings.TrimSpace(m.AdvertURL) == "" { m.AdvertURL = inferAdvertURL(m.BindAddr) diff --git a/internal/mesh/mesh.go b/internal/mesh/mesh.go index 7b03fb9..f742b95 100644 --- a/internal/mesh/mesh.go +++ b/internal/mesh/mesh.go @@ -41,6 +41,8 @@ type Config struct { // NEU: HelloInterval time.Duration // wie oft pingen HelloFanout int // wie viele Peers pro Tick + + BlobTimeout time.Duration } type Peer struct { @@ -714,31 +716,31 @@ func (n *Node) blobHandler(w http.ResponseWriter, r *http.Request) { // sendBlobRequest schickt eine signierte Anfrage an /mesh/blob und liefert die Response // zurück (Caller MUSS resp.Body schließen!). Bei HTTP 200 wird der Peer als gesehen markiert. func (n *Node) sendBlobRequest(url, id string) (*http.Response, error) { - body, _ := json.Marshal(struct { + b, _ := json.Marshal(struct { ID string `json:"id"` }{ID: id}) - req, _ := http.NewRequest(http.MethodPost, strings.TrimRight(url, "/")+"/mesh/blob", bytes.NewReader(body)) - req.Header.Set("X-Mesh-Sig", n.sign(body)) + req, _ := http.NewRequest(http.MethodPost, strings.TrimRight(url, "/")+"/mesh/blob", bytes.NewReader(b)) + req.Header.Set("X-Mesh-Sig", n.sign(b)) req.Header.Set("Content-Type", "application/json") - // kurzer Timeout für Blob-Requests (anpassbar) - ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) - defer cancel() + // ❗ WICHTIG: kein kurzer Timeout. Optional: großer Timeout aus Config + ctx := context.Background() + if n.cfg.BlobTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), n.cfg.BlobTimeout) + defer cancel() + } req = req.WithContext(ctx) resp, err := n.client.Do(req) if err != nil { return nil, err } - - // OK → Peer als "gesehen" markieren und Response (für Streaming) zurückgeben if resp.StatusCode == http.StatusOK { - n.touchPeer(url) - return resp, nil // Caller liest/streamt Body und schließt ihn + n.touchPeer(url) // ausgehender Erfolg zählt als "gesehen" + return resp, nil } - - // Nicht-OK → Body leeren/schließen und Fehler zurück io.Copy(io.Discard, resp.Body) resp.Body.Close() return nil, fmt.Errorf("blob %s from %s: %s", id, url, resp.Status)