Startup-Connectivity-Check: Fixen 15s-Delay durch aktive Erreichbarkeitsprüfung von API und SOGo ersetzt
This commit is contained in:
@@ -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))
|
||||
|
||||
121
cmd/mcbdd/startup.go
Normal file
121
cmd/mcbdd/startup.go
Normal file
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
Der Mailcow Birthday Daemon synchronisiert automatisch Geburtstagskalender für jede aktive Mailbox. Der gesamte Prozess läuft ohne Benutzereingriff ab.
|
||||
|
||||
## Startup-Connectivity-Check
|
||||
|
||||
Beim Start prüft der Daemon aktiv, ob die Mailcow-API und SOGo erreichbar sind, bevor die erste Synchronisation beginnt. Die Prüfung nutzt exponentielles Backoff (2 s → 4 s → … → max 30 s) und wiederholt sich, bis beide Dienste antworten. Im Log wird klar angegeben, welcher Dienst noch nicht bereit ist. Ein Shutdown-Signal bricht den Check sofort ab.
|
||||
|
||||
## App-Passwörter
|
||||
|
||||
- Über die Mailcow-API wird für jeden aktiven Benutzer ein App-Passwort mit Zugriff auf CardDAV und CalDAV erzeugt.
|
||||
@@ -58,7 +62,7 @@ Der Daemon reagiert auf `SIGTERM` und `SIGINT` (z. B. durch `docker stop`) und b
|
||||
2. Ungespeicherte App-Passwörter werden in die Zustandsdatei geschrieben.
|
||||
3. Erst danach beendet sich der Prozess.
|
||||
|
||||
Dadurch wird sichergestellt, dass das State-File konsistent bleibt und keine Daten verloren gehen. Auch die Startverzögerung (15 Sekunden) wird bei einem Shutdown-Signal sofort abgebrochen.
|
||||
Dadurch wird sichergestellt, dass das State-File konsistent bleibt und keine Daten verloren gehen. Auch der Startup-Connectivity-Check wird bei einem Shutdown-Signal sofort abgebrochen.
|
||||
|
||||
## Healthcheck
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ cd /opt/mailcow-dockerized
|
||||
docker compose pull birthdaydaemon && docker compose up -d --no-deps birthdaydaemon
|
||||
```
|
||||
|
||||
> **Hinweis:** Nach dem Start wartet der Daemon zunächst **15 Sekunden**, bevor die erste Synchronisation beginnt. Diese Verzögerung stellt sicher, dass abhängige Dienste (z. B. Nginx, SOGo) vollständig bereit sind.
|
||||
> **Hinweis:** Nach dem Start prüft der Daemon aktiv die Erreichbarkeit der Mailcow-API und SOGo, bevor die erste Synchronisation beginnt. Die Prüfung wiederholt sich mit steigendem Intervall (2 s → 4 s → … → max 30 s), bis beide Dienste antworten. Im Log wird klar angezeigt, welcher Dienst noch nicht bereit ist.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
|
||||
Reference in New Issue
Block a user