227 lines
6.6 KiB
Go
227 lines
6.6 KiB
Go
// nolint:revive
|
|
package common
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"allaboutapps.dev/aw/go-starter/internal/util"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func ProbeReadiness(ctx context.Context, database *sql.DB, writeablePaths []string) (string, []error) {
|
|
var str strings.Builder
|
|
|
|
// slice collects all errors from probes
|
|
errs := make([]error, 0, 1+len(writeablePaths))
|
|
|
|
// DB readable?
|
|
dbPingStr, dbPingErr := probeDatabasePingable(ctx, database)
|
|
str.WriteString(dbPingStr)
|
|
|
|
if dbPingErr != nil {
|
|
errs = append(errs, dbPingErr)
|
|
}
|
|
|
|
// FS (potentially) writeable?
|
|
for _, writeablePath := range writeablePaths {
|
|
fsPermStr, fsPermErr := probePathWriteablePermission(ctx, writeablePath)
|
|
str.WriteString(fsPermStr)
|
|
|
|
if fsPermErr != nil {
|
|
errs = append(errs, fsPermErr)
|
|
}
|
|
}
|
|
|
|
// Feel free to add additional probes here...
|
|
|
|
return str.String(), errs
|
|
}
|
|
|
|
func ProbeLiveness(ctx context.Context, database *sql.DB, writeablePaths []string, touch string) (string, []error) {
|
|
// fail immediately if any readiness probes above have already failed.
|
|
readinessProbeStr, readinessProbeErrs := ProbeReadiness(ctx, database, writeablePaths)
|
|
|
|
if len(readinessProbeErrs) != 0 {
|
|
return readinessProbeStr, readinessProbeErrs
|
|
}
|
|
|
|
var str strings.Builder
|
|
|
|
// include previous readiness probe results in final string
|
|
str.WriteString(readinessProbeStr)
|
|
|
|
// slice collects all errors from probes
|
|
errs := make([]error, 0, 1+len(writeablePaths))
|
|
|
|
// DB writeable?
|
|
dbHealthStr, dbHealthErr := probeDatabaseNextHealthSequence(ctx, database)
|
|
str.WriteString(dbHealthStr)
|
|
|
|
if dbHealthErr != nil {
|
|
errs = append(errs, dbHealthErr)
|
|
}
|
|
|
|
// FS writeable?
|
|
for _, writeablePath := range writeablePaths {
|
|
fsTouchStr, fsTouchErr := probePathWriteableTouch(ctx, writeablePath, touch)
|
|
str.WriteString(fsTouchStr)
|
|
if fsTouchErr != nil {
|
|
errs = append(errs, fsTouchErr)
|
|
}
|
|
}
|
|
|
|
// Feel free to add additional probes here...
|
|
|
|
return str.String(), errs
|
|
}
|
|
|
|
// FS (especially hard mounted NFS paths) or PostgreSQL calls may be blocking or running for too long and thus need to run detached
|
|
// We additionally want them to timeout (e.g. useful for hard mounted NFS paths)
|
|
// Typically a any context used here will already have a deadline associated
|
|
// If not we will explicitly return a short one here.
|
|
func ensureProbeDeadlineFromContext(ctx context.Context) time.Time {
|
|
ctxDeadline, hasDeadline := ctx.Deadline()
|
|
if !hasDeadline {
|
|
ctxDeadline = time.Now().Add(1 * time.Second)
|
|
}
|
|
|
|
return ctxDeadline
|
|
}
|
|
|
|
func probeDatabasePingable(ctx context.Context, database *sql.DB) (string, error) {
|
|
var str strings.Builder
|
|
ctxDeadline := ensureProbeDeadlineFromContext(ctx)
|
|
|
|
dbPingStart := time.Now()
|
|
|
|
var dbPingWg sync.WaitGroup
|
|
var dbErr error
|
|
|
|
dbPingWg.Add(1)
|
|
go func() {
|
|
dbErr = database.PingContext(ctx)
|
|
dbPingWg.Done()
|
|
}()
|
|
|
|
if err := util.WaitTimeout(&dbPingWg, time.Until(ctxDeadline)); err != nil {
|
|
fmt.Fprintf(&str, "Probe db: Ping deadline after %s, error=%v.\n", time.Since(dbPingStart), err.Error())
|
|
return str.String(), err
|
|
}
|
|
|
|
if dbErr != nil {
|
|
fmt.Fprintf(&str, "Probe db: Ping errored after %s, error=%v.\n", time.Since(dbPingStart), dbErr.Error())
|
|
return str.String(), dbErr
|
|
}
|
|
|
|
fmt.Fprintf(&str, "Probe db: Ping succeeded in %s.\n", time.Since(dbPingStart))
|
|
|
|
return str.String(), nil
|
|
}
|
|
|
|
func probeDatabaseNextHealthSequence(ctx context.Context, database *sql.DB) (string, error) {
|
|
var str strings.Builder
|
|
ctxDeadline := ensureProbeDeadlineFromContext(ctx)
|
|
|
|
dbWriteStart := time.Now()
|
|
|
|
var seqVal int
|
|
var dbWriteWg sync.WaitGroup
|
|
var dbErr error
|
|
|
|
dbWriteWg.Add(1)
|
|
go func() {
|
|
dbErr = database.QueryRowContext(ctx, "SELECT nextval('seq_health');").Scan(&seqVal)
|
|
dbWriteWg.Done()
|
|
}()
|
|
|
|
if err := util.WaitTimeout(&dbWriteWg, time.Until(ctxDeadline)); err != nil {
|
|
fmt.Fprintf(&str, "Probe db: Next health sequence deadline after %s, error=%v.\n", time.Since(dbWriteStart), err.Error())
|
|
return str.String(), err
|
|
}
|
|
|
|
if dbErr != nil {
|
|
fmt.Fprintf(&str, "Probe db: Next health sequence errored after %s, error=%v.\n", time.Since(dbWriteStart), dbErr.Error())
|
|
return str.String(), dbErr
|
|
}
|
|
|
|
fmt.Fprintf(&str, "Probe db: Next health sequence succeeded in %s, seq_health=%v.\n", time.Since(dbWriteStart), seqVal)
|
|
|
|
return str.String(), nil
|
|
}
|
|
|
|
func probePathWriteablePermission(ctx context.Context, writeablePath string) (string, error) {
|
|
var str strings.Builder
|
|
ctxDeadline := ensureProbeDeadlineFromContext(ctx)
|
|
|
|
fsWriteStart := time.Now()
|
|
|
|
if ctx.Err() != nil {
|
|
fmt.Fprintf(&str, "Probe path '%s': W_OK check cancelled after %s, error=%v.\n", writeablePath, time.Since(fsWriteStart), ctx.Err())
|
|
return str.String(), ctx.Err()
|
|
}
|
|
|
|
var fsWriteWg sync.WaitGroup
|
|
var fsWriteErr error
|
|
fsWriteWg.Add(1)
|
|
go func(wp string) {
|
|
fsWriteErr = unix.Access(wp, unix.W_OK)
|
|
fsWriteWg.Done()
|
|
}(writeablePath)
|
|
|
|
if err := util.WaitTimeout(&fsWriteWg, time.Until(ctxDeadline)); err != nil {
|
|
fmt.Fprintf(&str, "Probe path '%s': W_OK check deadline after %s, error=%v.\n", writeablePath, time.Since(fsWriteStart), err)
|
|
return str.String(), err
|
|
}
|
|
|
|
if fsWriteErr != nil {
|
|
fmt.Fprintf(&str, "Probe path '%s': W_OK check errored after %s, error=%v.\n", writeablePath, time.Since(fsWriteStart), fsWriteErr.Error())
|
|
return str.String(), fsWriteErr
|
|
}
|
|
|
|
fmt.Fprintf(&str, "Probe path '%s': W_OK check succeeded in %s.\n", writeablePath, time.Since(fsWriteStart))
|
|
|
|
return str.String(), nil
|
|
}
|
|
|
|
func probePathWriteableTouch(ctx context.Context, writeablePath string, touch string) (string, error) {
|
|
var str strings.Builder
|
|
ctxDeadline := ensureProbeDeadlineFromContext(ctx)
|
|
|
|
fsTouchStart := time.Now()
|
|
fsTouchNameAbs := path.Join(writeablePath, touch)
|
|
|
|
if ctx.Err() != nil {
|
|
fmt.Fprintf(&str, "Probe path '%s': Touch cancelled after %s, error=%v.\n", fsTouchNameAbs, time.Since(fsTouchStart), ctx.Err())
|
|
return str.String(), ctx.Err()
|
|
}
|
|
|
|
var fsTouchWg sync.WaitGroup
|
|
var fsTouchErr error
|
|
var fsTouchModTime time.Time
|
|
fsTouchWg.Add(1)
|
|
go func(tn string) {
|
|
fsTouchModTime, fsTouchErr = util.TouchFile(tn)
|
|
fsTouchWg.Done()
|
|
}(fsTouchNameAbs)
|
|
|
|
if err := util.WaitTimeout(&fsTouchWg, time.Until(ctxDeadline)); err != nil {
|
|
fmt.Fprintf(&str, "Probe path '%s': Touch deadline after %s, error=%v.\n", fsTouchNameAbs, time.Since(fsTouchStart), err)
|
|
return str.String(), err
|
|
}
|
|
|
|
if fsTouchErr != nil {
|
|
fmt.Fprintf(&str, "Probe path '%s': Touch errored after %s, error=%v.\n", fsTouchNameAbs, time.Since(fsTouchStart), fsTouchErr.Error())
|
|
return str.String(), fsTouchErr
|
|
}
|
|
|
|
fmt.Fprintf(&str, "Probe path '%s': Touch succeeded in %s, modTime=%v.\n", fsTouchNameAbs, time.Since(fsTouchStart), fsTouchModTime.Unix())
|
|
|
|
return str.String(), nil
|
|
}
|