mirror of
https://github.com/tiennm99/keepalive.git
synced 2026-10-15 02:17:09 +00:00
fix(runner): retry failed services after one minute and fail fast on bad config
A failed connect or tick now closes the connection and reconnects after one minute, re-running initialization so a dropped table or row is recreated. Before, a failing tick logged every interval forever and connect retries logged every 10 seconds. Connection URLs that fail to parse no longer appear in logs, since their error repeats the password. An unknown adapter or a missing required config key now stops keepalive at startup instead of silently idling one service.
This commit is contained in:
1 parent
30fd62a46c
commit
42c8a871cf
5 files changed
+168
-32
No files matched your search
@@ -2,14 +2,19 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/keepalive/adapter"
|
||||
)
|
||||
|
||||
var reconnectDelay = 10 * time.Second
|
||||
// retryDelay is how long a service waits after any failure (connect or tick)
|
||||
// before reconnecting, so a broken service logs at most once per delay.
|
||||
var retryDelay = time.Minute
|
||||
|
||||
type runningService struct {
|
||||
config serviceConfig
|
||||
@@ -24,46 +29,50 @@ func runService(ctx context.Context, wg *sync.WaitGroup, config serviceConfig) {
|
||||
for {
|
||||
a, err := adapter.New(config.AdapterType, config.Config)
|
||||
if err != nil {
|
||||
log.Printf("[%s] init adapter: %v", config.Name, err)
|
||||
log.Printf("[%s] init adapter: %v", config.Name, redactURLError(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.Connect(ctx); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
closeService(ctx, config.Name, a)
|
||||
return
|
||||
}
|
||||
log.Printf("[%s] connect: %v", config.Name, err)
|
||||
closeService(ctx, config.Name, a)
|
||||
if !waitContext(ctx, reconnectDelay) {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
log.Printf("[%s] connect: %v; retrying in %s", config.Name, redactURLError(err), retryDelay)
|
||||
} else {
|
||||
log.Printf("[%s] keepalive: %s every %s", config.Name, config.AdapterType, config.Interval)
|
||||
err := runConnectedService(ctx, runningService{config: config, adapter: a})
|
||||
closeService(ctx, config.Name, a)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
// Reconnecting re-runs each adapter's initialization, which
|
||||
// recreates a dropped table, row, or collection.
|
||||
log.Printf("[%s] increment: %v; reconnecting in %s", config.Name, redactURLError(err), retryDelay)
|
||||
}
|
||||
if !waitContext(ctx, retryDelay) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[%s] keepalive: %s every %s", config.Name, config.AdapterType, config.Interval)
|
||||
runConnectedService(ctx, runningService{config: config, adapter: a})
|
||||
closeService(ctx, config.Name, a)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func runConnectedService(ctx context.Context, svc runningService) {
|
||||
// runConnectedService ticks until the context ends (returning nil) or an
|
||||
// increment fails (returning that error).
|
||||
func runConnectedService(ctx context.Context, svc runningService) error {
|
||||
ticker := time.NewTicker(svc.config.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
tickCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
count, err := svc.adapter.Increment(tickCtx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
log.Printf("[%s] increment: %v", svc.config.Name, err)
|
||||
continue
|
||||
return err
|
||||
}
|
||||
log.Printf("[%s] counter: %d", svc.config.Name, count)
|
||||
}
|
||||
@@ -81,6 +90,22 @@ func closeService(_ context.Context, name string, a adapter.Adapter) {
|
||||
}
|
||||
}
|
||||
|
||||
// redactURLError drops the raw URL from a *url.Error. Drivers return one when
|
||||
// a connection URL fails to parse, and its message repeats the URL, password
|
||||
// included.
|
||||
func redactURLError(err error) error {
|
||||
var urlErr *url.Error
|
||||
if !errors.As(err, &urlErr) {
|
||||
return err
|
||||
}
|
||||
var escapeErr url.EscapeError
|
||||
if errors.As(urlErr.Err, &escapeErr) {
|
||||
// The message quotes the bad escape, which may be part of a password.
|
||||
return errors.New("invalid connection URL: invalid percent-escape; percent-encode special characters in the user name and password")
|
||||
}
|
||||
return fmt.Errorf("invalid connection URL: %w", urlErr.Err)
|
||||
}
|
||||
|
||||
func waitContext(ctx context.Context, d time.Duration) bool {
|
||||
timer := time.NewTimer(d)
|
||||
defer timer.Stop()
|
||||
|
||||
Reference in new issue
Block a user