Startup-Connectivity-Check: Fixen 15s-Delay durch aktive Erreichbarkeitsprüfung von API und SOGo ersetzt
This commit is contained in:
+8
-12
@@ -99,18 +99,6 @@ func run() error {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
// Kurze Wartezeit beim Start, damit abhängige Dienste (z. B. nginx)
|
||||
// vollständig hochgefahren sind, bevor Verbindungen aufgebaut werden.
|
||||
const startupDelay = 15 * time.Second
|
||||
slog.Info("waiting for dependent services to become ready", "delay", startupDelay)
|
||||
select {
|
||||
case <-time.After(startupDelay):
|
||||
slog.Debug("startup delay completed, proceeding with initialization")
|
||||
case <-ctx.Done():
|
||||
slog.Warn("shutdown signal received during startup, exiting")
|
||||
return nil
|
||||
}
|
||||
|
||||
mailcowBase := os.Getenv("MAILCOW_BASE")
|
||||
if mailcowBase == "" {
|
||||
return fmt.Errorf("MAILCOW_BASE environment variable is not set")
|
||||
@@ -149,6 +137,14 @@ func run() error {
|
||||
"MAILBOX_EXCLUDE", os.Getenv("MAILBOX_EXCLUDE"),
|
||||
)
|
||||
|
||||
// Aktive Erreichbarkeitsprüfung: Mailcow-API und SOGo werden wiederholt
|
||||
// geprüft, bevor der erste Sync startet. Damit entfällt der frühere
|
||||
// fixe 15-Sekunden-Delay. Der Check nutzt exponentielles Backoff
|
||||
// (2 s → 4 s → … → max 30 s) und bricht bei Shutdown-Signal sofort ab.
|
||||
if err := waitForServices(ctx, &http.Client{Transport: buildTransport()}, mailcowBase, mailcowAPIKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
excludeMailboxes := parseMailboxExclude()
|
||||
if len(excludeMailboxes) > 0 {
|
||||
slog.Info("mailbox exclusion configured", "count", len(excludeMailboxes))
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.techniverse.net/scriptos/mailcow-birthday-daemon/pkg/mailcow"
|
||||
)
|
||||
|
||||
const (
|
||||
// startupBackoffInit ist das anfängliche Warteintervall zwischen
|
||||
// Erreichbarkeitsprüfungen (2 Sekunden).
|
||||
startupBackoffInit = 2 * time.Second
|
||||
|
||||
// startupBackoffMax begrenzt das maximale Warteintervall auf 30 Sekunden.
|
||||
startupBackoffMax = 30 * time.Second
|
||||
|
||||
// startupRequestTimeout begrenzt einzelne HTTP-Anfragen während der
|
||||
// Startphase auf 10 Sekunden.
|
||||
startupRequestTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// waitForServices prüft in einer Schleife, ob die Mailcow-API und SOGo
|
||||
// erreichbar sind. Erst wenn beide Dienste antworten, kehrt die Funktion
|
||||
// zurück. Bei einem Shutdown-Signal (ctx.Done) wird sofort nil
|
||||
// zurückgegeben.
|
||||
func waitForServices(ctx context.Context, httpClient *http.Client, baseURL, apiKey string) error {
|
||||
slog.Info("checking connectivity to mailcow API and SOGo")
|
||||
|
||||
backoff := startupBackoffInit
|
||||
|
||||
for {
|
||||
apiOK := checkMailcowAPI(ctx, httpClient, baseURL, apiKey)
|
||||
sogoOK := checkSOGo(ctx, httpClient, baseURL)
|
||||
|
||||
if apiOK && sogoOK {
|
||||
slog.Info("all services reachable, proceeding with initialization")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Klare Meldung, welcher Dienst noch nicht bereit ist.
|
||||
if !apiOK && !sogoOK {
|
||||
slog.Warn("mailcow API and SOGo not reachable yet, retrying", "backoff", backoff)
|
||||
} else if !apiOK {
|
||||
slog.Warn("mailcow API not reachable yet, retrying", "backoff", backoff)
|
||||
} else {
|
||||
slog.Warn("SOGo not reachable yet, retrying", "backoff", backoff)
|
||||
}
|
||||
|
||||
// Auf nächsten Versuch oder Shutdown-Signal warten.
|
||||
timer := time.NewTimer(backoff)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
slog.Warn("shutdown signal received during startup connectivity check, exiting")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exponentielles Backoff verdoppeln, aber nicht über das Maximum.
|
||||
backoff *= 2
|
||||
if backoff > startupBackoffMax {
|
||||
backoff = startupBackoffMax
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkMailcowAPI führt einen einfachen API-Aufruf (GetMailboxes) durch,
|
||||
// um zu prüfen, ob die Mailcow-API erreichbar ist und der API-Key gültig
|
||||
// ist. Bei Erfolg wird true zurückgegeben.
|
||||
func checkMailcowAPI(ctx context.Context, httpClient *http.Client, baseURL, apiKey string) bool {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, startupRequestTimeout)
|
||||
defer cancel()
|
||||
|
||||
mc := mailcow.New(httpClient, baseURL, apiKey)
|
||||
_, err := mc.GetMailboxes(reqCtx)
|
||||
if err != nil {
|
||||
slog.Debug("mailcow API check failed", "err", err)
|
||||
return false
|
||||
}
|
||||
slog.Debug("mailcow API is reachable")
|
||||
return true
|
||||
}
|
||||
|
||||
// checkSOGo prüft die Erreichbarkeit von SOGo über einen HTTP-GET auf den
|
||||
// SOGo-Basispfad. Jeder HTTP-Statuscode (auch Redirects oder 401) gilt als
|
||||
// "erreichbar", da SOGo in diesem Fall antwortet. Nur Netzwerkfehler
|
||||
// (connection refused, timeout) gelten als "nicht erreichbar".
|
||||
func checkSOGo(ctx context.Context, httpClient *http.Client, baseURL string) bool {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, startupRequestTimeout)
|
||||
defer cancel()
|
||||
|
||||
sogoURL, err := url.JoinPath(baseURL, "SOGo/")
|
||||
if err != nil {
|
||||
slog.Debug("SOGo URL construction failed", "err", err)
|
||||
return false
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, sogoURL, nil)
|
||||
if err != nil {
|
||||
slog.Debug("SOGo request creation failed", "err", err)
|
||||
return false
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
slog.Debug("SOGo check failed", "err", err)
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// SOGo ist erreichbar – der konkrete Statuscode ist irrelevant,
|
||||
// solange eine HTTP-Antwort zurückkommt.
|
||||
slog.Debug("SOGo is reachable", "status", resp.StatusCode)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user