From 0faff1d481c219fb99f96626a0c5fcc72fbbfc1d Mon Sep 17 00:00:00 2001 From: Peter Kurfer Date: Mon, 4 Dec 2023 16:59:10 +0100 Subject: [PATCH] chore: cleanup, add docs, refactor some quirks and prepare release --- .forgejo/workflows/go.yaml | 11 +++++ .goreleaser.yaml | 13 +++-- README.md | 88 +++++++++++++++++++++++++++++++++- check/api.go | 4 +- check/context.go | 20 +++++--- cmd/app.go | 47 +++++++++++++----- cmd/server.go | 15 ++++-- go.mod | 2 +- go.sum | 2 - internal/retry/retry.go | 42 ++++++++++++++++ protocols/http/get.go | 40 +++++++++------- protocols/redis/checks.go | 3 ++ protocols/redis/checks_test.go | 5 ++ protocols/redis/get.go | 34 +++++-------- protocols/redis/ping.go | 39 ++++++--------- protocols/redis/set.go | 69 ++++++++++++++++++++++++++ protocols/redis/validation.go | 12 ++++- protocols/sql/select.go | 44 +++++++---------- 18 files changed, 366 insertions(+), 124 deletions(-) create mode 100644 internal/retry/retry.go create mode 100644 protocols/redis/set.go diff --git a/.forgejo/workflows/go.yaml b/.forgejo/workflows/go.yaml index d4b14bd..8d87c95 100644 --- a/.forgejo/workflows/go.yaml +++ b/.forgejo/workflows/go.yaml @@ -16,19 +16,24 @@ jobs: fetch-depth: '0' lfs: 'true' fetch-tags: 'true' + - name: Setup Go 1.21.x uses: actions/setup-go@v4 with: # Semantic version range syntax or exact version of Go go-version: '1.21.x' + - name: golangci-lint uses: golangci/golangci-lint-action@v3 + - name: Install Task uses: arduino/setup-task@v1 + - name: Run tests run: | go install gotest.tools/gotestsum@latest gotestsum --junitfile out/results.xml --format pkgname-and-test-fails -- -race -shuffle=on ./... + - uses: goreleaser/goreleaser-action@v5 if: ${{ !startsWith(github.ref, 'refs/tags/v') }} with: @@ -36,3 +41,9 @@ jobs: version: latest args: release --clean --snapshot + - uses: goreleaser/goreleaser-action@v5 + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + with: + distribution: goreleaser + version: latest + args: release --clean diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 719f479..fb0015a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,6 +1,3 @@ -before: - hooks: - - go mod tidy builds: - id: nurse binary: nurse @@ -26,6 +23,16 @@ changelog: - '^docs:' - '^test:' +release: + gitea: + owner: prskr + repo: nurse + ids: + - nurse + mode: replace + extra_files: + - glob: ./nurse.yaml + dockers: - ids: - nurse diff --git a/README.md b/README.md index 4bee667..1868f0e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,89 @@ # Nurse -A generic service health sidecar +## Usage + +Nurse comes currently with 2 different operation modes: + +- server +- CLI + +The server starts an HTTP server with configurable endpoints which you can use e.g. in Kubernetes environments to +distinguish between: + +- startup +- readiness +- liveness + +probes. +Every endpoint has a distinguished set of checks that are executed when you hit the endpoint. +Currently, there is no caching in place (and there are also no plans to change that). + +The CLI operation mode on the other hand executes all checks that are provided as arguments e.g. in Docker Swarm +environment where the container image has to ship the health check CLI. + +### Primer about checks + +All checks are executed **in parallel** which means you shouldn't rely on a certain execution + +### Global config/options + +Nurse comes with the following global options: + +| Switch | Environment variable | Default value | Description | +|--------------------|------------------------|--------------------------------------------------------------|---------------------------------------------------------------------| +| `--config` | `NURSE_CONFIG` | `$HOME/.nurse.yaml`, `/etc/nurse/config.yaml`,`./nurse.yaml` | path to the config file | +| `--check-timeout` | `NURSE_CHECK_TIMEOUT` | `500ms` | Timeout for executing all checks | +| `--check-attempts` | `NURSE_CHECK_ATTEMPTS` | `20` | How often checks should be retried before they're considered failed | +| `--log.level` | | `info` | Default log level | +| `--servers` | `NURSE_SERVER_` | | Configure server URLs via environment variables | + +The individual sub-commands come with additional options, like for example configuring endpoints via environment +variables as well. + +The [nurse.yaml](./nurse.yaml) describes how to configure Nurse via a configuration file. + +The most interesting root nodes are: + +- servers +- endpoints + +Within `servers` you can configure different servers for further usage in checks. +For example, to configure a Redis server: `redis://localhost:6379/0`. +Depending on the individual protocols there are further configuration options. + +Within `endpoints` you can configure different HTTP endpoints the server exposes and which checks should be executed for +which endpoint. + +### Server + +The `server` sub-command comes with the following additional config options: + +| Switch | Environment variable | Default value | Description | +|-----------------------------|----------------------------------|---------------|------------------------------------------------------------| +| `--endpoints` | `NURSE_ENDPOINT_` | | Configure HTTP endpoints via environment variables | +| `--http.address` | `NURSE_HTTP_ADDRESS` | `:8080` | IP and port the server will be listening on | +| `--http.read-header-timout` | `NURSE_HTTP_READ_HEADER_TIMEOUT` | `100ms` | Timeout until when the client has to have sent the headers | + +To configure an endpoint via an environment variable, set it like this: + +``` +NURSE_ENDPOINT_HEALTHZ='http.GET("https://api.chucknorris.io/jokes/random")=>Status(200);redis.PING("local-redis")' +``` + +The server will print the configured routes when it is starting up. +In the aforementioned case you should see something like: + +``` +{"time":"xxxxx","level":"INFO","msg":"Configuring route","route":"/healthz"} +``` + +Multiple checks can be configured by separating them with a `;` into multiple 'expressions'. + +### CLI + +The CLI has no additional config options compared to the server. +It simply takes all arguments you pass to it, tries to parse them as checks and executes them with the given time limit. +If one of the check fails it will exit with a non-zero exit code. + +Multiple checks can either be passed as single argument in `''` separated with a `;` just like in the environment variables, or you can pass multiple arguments. +The result will be the same. \ No newline at end of file diff --git a/check/api.go b/check/api.go index aa33974..9e0b229 100644 --- a/check/api.go +++ b/check/api.go @@ -3,6 +3,7 @@ package check import ( "context" "errors" + "time" "code.icb4dc0.de/prskr/nurse/config" "code.icb4dc0.de/prskr/nurse/grammar" @@ -21,7 +22,8 @@ type ( Context interface { context.Context - AttemptContext() (context.Context, context.CancelFunc) + AttemptCount() uint + AttemptTimeout() time.Duration WithParent(ctx context.Context) Context } diff --git a/check/context.go b/check/context.go index c366a19..906543c 100644 --- a/check/context.go +++ b/check/context.go @@ -12,23 +12,29 @@ func AttemptsContext(parent context.Context, numberOfAttempts uint, attemptTimeo base, cancel := context.WithTimeout(parent, finalTimeout) return &checkContext{ - Context: base, - attemptTimeout: attemptTimeout, + Context: base, + attemptTimeout: attemptTimeout, + numberOfAttempts: numberOfAttempts, }, cancel } type checkContext struct { - attemptTimeout time.Duration + attemptTimeout time.Duration + numberOfAttempts uint context.Context } +func (c *checkContext) AttemptCount() uint { + return c.numberOfAttempts +} + +func (c *checkContext) AttemptTimeout() time.Duration { + return c.attemptTimeout +} + func (c *checkContext) WithParent(ctx context.Context) Context { return &checkContext{ Context: ctx, attemptTimeout: c.attemptTimeout, } } - -func (c *checkContext) AttemptContext() (context.Context, context.CancelFunc) { - return context.WithTimeout(c, c.attemptTimeout) -} diff --git a/cmd/app.go b/cmd/app.go index 1640fe3..d2f314d 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -6,12 +6,13 @@ import ( "os" "time" + "github.com/urfave/cli/v2" + "code.icb4dc0.de/prskr/nurse/check" "code.icb4dc0.de/prskr/nurse/config" "code.icb4dc0.de/prskr/nurse/protocols/http" "code.icb4dc0.de/prskr/nurse/protocols/redis" "code.icb4dc0.de/prskr/nurse/protocols/sql" - "github.com/urfave/cli/v2" ) const ( @@ -19,6 +20,16 @@ const ( defaultAttemptCount = 20 ) +const ( + logLevelFlag = "log.level" + httpAddressFlag = "http.address" + httpReadHeaderTimeout = "http.read-header-timeout" + maxCheckAttemptsFlag = "check-attempts" + checkTimeoutFlag = "check-timeout" + serversFlag = "servers" + configFlag = "config" +) + func NewApp() (*cli.App, error) { app := &app{ registry: check.NewRegistry(), @@ -46,30 +57,30 @@ func NewApp() (*cli.App, error) { Before: app.init, Flags: []cli.Flag{ &cli.StringFlag{ - Name: "config", + Name: configFlag, Usage: "Config file to load, if not set `$HOME/.nurse.yaml`, `/etc/nurse/config.yaml` and `./nurse.yaml` are tried - optional", Aliases: []string{"c"}, EnvVars: []string{"NURSE_CONFIG"}, }, &cli.DurationFlag{ - Name: "check-timeout", + Name: checkTimeoutFlag, Usage: "Timeout when running checks", Value: defaultCheckTimeout, EnvVars: []string{"NURSE_CHECK_TIMEOUT"}, }, &cli.UintFlag{ - Name: "check-attempts", + Name: maxCheckAttemptsFlag, Usage: "Number of attempts for a check", Value: defaultAttemptCount, EnvVars: []string{"NURSE_CHECK_ATTEMPTS"}, }, &cli.StringFlag{ - Name: "log-level", + Name: logLevelFlag, Usage: "Log level to use", Value: "info", }, &cli.StringSliceFlag{ - Name: "servers", + Name: serversFlag, Usage: "", Aliases: []string{"s"}, }, @@ -83,9 +94,21 @@ func NewApp() (*cli.App, error) { Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "endpoints", - Usage: "", + Usage: "Endpoints to expose in the HTTP server", Aliases: []string{"ep"}, }, + &cli.StringFlag{ + Name: httpAddressFlag, + Usage: "HTTP server address", + Value: ":8080", + EnvVars: []string{"NURSE_HTTP_ADDRESS"}, + }, + &cli.DurationFlag{ + Name: httpReadHeaderTimeout, + Usage: "Timeout for reading headers in the HTTP server", + Value: 100 * time.Millisecond, + EnvVars: []string{"NURSE_HTTP_READ_HEADER_TIMEOUT"}, + }, }, }, { @@ -109,16 +132,16 @@ type app struct { } func (a *app) init(ctx *cli.Context) (err error) { - if err = a.configureLogging(ctx.String("log-level")); err != nil { + if err = a.configureLogging(ctx.String(logLevelFlag)); err != nil { return err } a.nurseInstance, err = config.New( - config.WithCheckAttempts(ctx.Uint("check-attempts")), - config.WithCheckDuration(ctx.Duration("check-timeout")), - config.WithConfigFile(ctx.String("config")), + config.WithCheckAttempts(ctx.Uint(maxCheckAttemptsFlag)), + config.WithCheckDuration(ctx.Duration(checkTimeoutFlag)), + config.WithConfigFile(ctx.String(configFlag)), config.WithServersFromEnv(), - config.WithServersFromArgs(ctx.StringSlice("servers")), + config.WithServersFromArgs(ctx.StringSlice(serversFlag)), config.WithEndpointsFromEnv(), ) diff --git a/cmd/server.go b/cmd/server.go index 7bbf52a..089da83 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -1,13 +1,15 @@ package cmd import ( + "context" "errors" "log/slog" + "net" "net/http" - "time" + + "github.com/urfave/cli/v2" "code.icb4dc0.de/prskr/nurse/api" - "github.com/urfave/cli/v2" ) type server struct { @@ -23,9 +25,12 @@ func (a *server) RunServer(ctx *cli.Context) error { } srv := http.Server{ - Addr: ":8080", - Handler: mux, - ReadHeaderTimeout: 100 * time.Millisecond, + Addr: ctx.String(httpAddressFlag), + ReadHeaderTimeout: ctx.Duration(httpReadHeaderTimeout), + BaseContext: func(listener net.Listener) context.Context { + return ctx.Context + }, + Handler: mux, } if err := srv.ListenAndServe(); err != nil { diff --git a/go.mod b/go.mod index cfcd029..154d5c7 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/testcontainers/testcontainers-go v0.26.0 github.com/urfave/cli/v2 v2.26.0 github.com/valyala/bytebufferpool v1.0.0 - golang.org/x/exp v0.0.0-20231127185646-65229373498e golang.org/x/sync v0.5.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -70,6 +69,7 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect golang.org/x/crypto v0.16.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect diff --git a/go.sum b/go.sum index da0dd65..e08b2d8 100644 --- a/go.sum +++ b/go.sum @@ -172,8 +172,6 @@ github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0h github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= -github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI= github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/internal/retry/retry.go b/internal/retry/retry.go new file mode 100644 index 0000000..52d9379 --- /dev/null +++ b/internal/retry/retry.go @@ -0,0 +1,42 @@ +package retry + +import ( + "context" + "errors" + "time" +) + +// Retry executes a function with the given number of attempts and attempt timeouts. +// It returns the last error encountered during the attempts. +// If the context is canceled, it returns the context error (if there is no previous error), +// or the joined error of the last error and the context error (otherwise). +func Retry(ctx context.Context, numberOfAttempts uint, attemptTimeout time.Duration, f func(ctx context.Context, attempt int) error) (lastErr error) { + baseCtx, baseCancel := context.WithTimeout(ctx, time.Duration(numberOfAttempts)*attemptTimeout) + defer baseCancel() + + for i := uint(0); i < numberOfAttempts; i++ { + select { + case <-ctx.Done(): + if lastErr == nil { + return ctx.Err() + } + return errors.Join(lastErr, ctx.Err()) + default: + attemptCtx, attemptCancel := context.WithTimeout(baseCtx, attemptTimeout) + + lastErr = f(attemptCtx, int(i)) + if lastErr == nil { + attemptCancel() + return nil + } + + if attemptCtx.Err() == nil { + <-attemptCtx.Done() + } + + attemptCancel() + } + } + + return lastErr +} diff --git a/protocols/http/get.go b/protocols/http/get.go index 4550d38..23f611c 100644 --- a/protocols/http/get.go +++ b/protocols/http/get.go @@ -2,6 +2,7 @@ package http import ( "bytes" + "context" "io" "log/slog" "net/http" @@ -9,6 +10,7 @@ import ( "code.icb4dc0.de/prskr/nurse/check" "code.icb4dc0.de/prskr/nurse/config" "code.icb4dc0.de/prskr/nurse/grammar" + "code.icb4dc0.de/prskr/nurse/internal/retry" "code.icb4dc0.de/prskr/nurse/validation" ) @@ -38,31 +40,35 @@ func (g *GenericCheck) SetClient(client *http.Client) { } func (g *GenericCheck) Execute(ctx check.Context) error { - slog.Default().Debug("Execute check", + logger := slog.Default().With( slog.String("check", "http"), slog.String("method", g.Method), slog.String("url", g.URL), ) - var body io.Reader - if len(g.Body) > 0 { - body = bytes.NewReader(g.Body) - } - req, err := http.NewRequestWithContext(ctx, g.Method, g.URL, body) - if err != nil { - return err - } + return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error { + logger.Debug("Execute check", slog.Int("attempt", attempt)) - resp, err := g.Do(req) - if err != nil { - return err - } + var body io.Reader + if len(g.Body) > 0 { + body = bytes.NewReader(g.Body) + } + req, err := http.NewRequestWithContext(ctx, g.Method, g.URL, body) + if err != nil { + return err + } - defer func() { - _ = resp.Body.Close() - }() + resp, err := g.Do(req) + if err != nil { + return err + } - return g.validators.Validate(resp) + defer func() { + _ = resp.Body.Close() + }() + + return g.validators.Validate(resp) + }) } func (g *GenericCheck) UnmarshalCheck(c grammar.Check, _ config.ServerLookup) error { diff --git a/protocols/redis/checks.go b/protocols/redis/checks.go index 519ed28..85256ac 100644 --- a/protocols/redis/checks.go +++ b/protocols/redis/checks.go @@ -13,6 +13,9 @@ func Module() *check.Module { check.WithCheck("get", check.FactoryFunc(func() check.SystemChecker { return new(GetCheck) })), + check.WithCheck("set", check.FactoryFunc(func() check.SystemChecker { + return new(SetCheck) + })), ) if err != nil { panic(err) diff --git a/protocols/redis/checks_test.go b/protocols/redis/checks_test.go index ff6aeb8..224e21a 100644 --- a/protocols/redis/checks_test.go +++ b/protocols/redis/checks_test.go @@ -56,6 +56,11 @@ func TestChecks_Execute(t *testing.T) { check: `redis.PING("%s", "Hello, Redis!")`, wantErr: false, }, + { + name: "SET check", + check: `redis.SET("%s", "Hello", "World!")`, + wantErr: false, + }, } for _, tt := range tests { tt := tt diff --git a/protocols/redis/get.go b/protocols/redis/get.go index e0458ba..f1810dc 100644 --- a/protocols/redis/get.go +++ b/protocols/redis/get.go @@ -6,6 +6,8 @@ import ( "github.com/redis/go-redis/v9" + "code.icb4dc0.de/prskr/nurse/internal/retry" + "code.icb4dc0.de/prskr/nurse/check" "code.icb4dc0.de/prskr/nurse/config" "code.icb4dc0.de/prskr/nurse/grammar" @@ -21,34 +23,22 @@ type GetCheck struct { } func (g *GetCheck) Execute(ctx check.Context) error { - slog.Default().Debug("Execute check", + logger := slog.Default().With( slog.String("check", "redis.GET"), slog.String("key", g.Key), ) - for { - select { - case <-ctx.Done(): - return ctx.Err() - default: - attemptCtx, cancel := ctx.AttemptContext() - err := g.executeAttempt(attemptCtx) - cancel() - if err == nil { - return nil - } + return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error { + logger.Debug("Execute check", slog.Int("attempt", attempt)) + + cmd := g.Get(ctx, g.Key) + + if err := cmd.Err(); err != nil { + return err } - } -} -func (g *GetCheck) executeAttempt(ctx context.Context) error { - cmd := g.Get(ctx, g.Key) - - if err := cmd.Err(); err != nil { - return err - } - - return g.validators.Validate(cmd) + return g.validators.Validate(cmd) + }) } func (g *GetCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) error { diff --git a/protocols/redis/ping.go b/protocols/redis/ping.go index 05f674a..83fb6eb 100644 --- a/protocols/redis/ping.go +++ b/protocols/redis/ping.go @@ -10,6 +10,7 @@ import ( "code.icb4dc0.de/prskr/nurse/check" "code.icb4dc0.de/prskr/nurse/config" "code.icb4dc0.de/prskr/nurse/grammar" + "code.icb4dc0.de/prskr/nurse/internal/retry" "code.icb4dc0.de/prskr/nurse/validation" ) @@ -21,39 +22,27 @@ type PingCheck struct { Message string } -func (p PingCheck) Execute(ctx check.Context) error { - slog.Default().Debug("Execute check", +func (p *PingCheck) Execute(ctx check.Context) error { + logger := slog.Default().With( slog.String("check", "redis.PING"), slog.String("msg", p.Message), ) - if p.Message == "" { - return p.Ping(ctx).Err() - } + return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error { + logger.Debug("Execute check", slog.Int("attempt", attempt)) - for { - select { - case <-ctx.Done(): - return ctx.Err() - default: - attemptCtx, cancel := ctx.AttemptContext() - err := p.executeAttempt(attemptCtx) - cancel() - if err == nil { - return nil - } + if p.Message == "" { + return p.Ping(ctx).Err() } - } -} -func (p PingCheck) executeAttempt(ctx context.Context) error { - if resp, err := p.Do(ctx, "PING", p.Message).Text(); err != nil { - return err - } else if resp != p.Message { - return fmt.Errorf("expected value %s got %s", p.Message, resp) - } + if resp, err := p.Do(ctx, "PING", p.Message).Text(); err != nil { + return err + } else if resp != p.Message { + return fmt.Errorf("expected value %s got %s", p.Message, resp) + } - return nil + return nil + }) } func (p *PingCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) error { diff --git a/protocols/redis/set.go b/protocols/redis/set.go new file mode 100644 index 0000000..9533c5d --- /dev/null +++ b/protocols/redis/set.go @@ -0,0 +1,69 @@ +package redis + +import ( + "context" + "log/slog" + + "github.com/redis/go-redis/v9" + + "code.icb4dc0.de/prskr/nurse/check" + "code.icb4dc0.de/prskr/nurse/config" + "code.icb4dc0.de/prskr/nurse/grammar" + "code.icb4dc0.de/prskr/nurse/internal/retry" + "code.icb4dc0.de/prskr/nurse/validation" +) + +var _ check.SystemChecker = (*SetCheck)(nil) + +type SetCheck struct { + redis.UniversalClient + validators validation.Validator[redis.Cmder] + Key, Value string +} + +func (s *SetCheck) Execute(ctx check.Context) error { + logger := slog.Default().With( + slog.String("check", "redis.SET"), + slog.String("key", s.Key), + slog.String("value", s.Value), + ) + + return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error { + logger.Debug("Execute check", slog.Int("attempt", attempt)) + + cmd := s.Set(ctx, s.Key, s.Value, -1) + + if err := cmd.Err(); err != nil { + return err + } + + return s.validators.Validate(cmd) + }) +} + +func (s *SetCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup) error { + const serverKeyAndValueArgsNumber = 3 + inst := c.Initiator + if err := grammar.ValidateParameterCount(inst.Params, serverKeyAndValueArgsNumber); err != nil { + return err + } + + var err error + if s.UniversalClient, err = clientFromParam(inst.Params[0], lookup); err != nil { + return err + } + + if s.Key, err = inst.Params[1].AsString(); err != nil { + return err + } + + if s.Value, err = inst.Params[2].AsString(); err != nil { + return err + } + + if s.validators, err = registry.ValidatorsForFilters(c.Validators); err != nil { + return err + } + + return nil +} diff --git a/protocols/redis/validation.go b/protocols/redis/validation.go index eac827c..d0724e0 100644 --- a/protocols/redis/validation.go +++ b/protocols/redis/validation.go @@ -75,12 +75,22 @@ func (g *GenericCmdValidator) Validate(cmder redis.Cmder) error { return err } - if in, ok := cmder.(*redis.StringCmd); ok { + switch in := cmder.(type) { + case *redis.StringCmd: + if err := in.Err(); err != nil { + return err + } + res, err := in.Result() if err != nil { return err } + return g.comparator.Equals(res) + case *redis.StatusCmd: + if err := in.Err(); err != nil { + return err + } } return nil diff --git a/protocols/sql/select.go b/protocols/sql/select.go index b31d228..b752c44 100644 --- a/protocols/sql/select.go +++ b/protocols/sql/select.go @@ -9,6 +9,7 @@ import ( "code.icb4dc0.de/prskr/nurse/check" "code.icb4dc0.de/prskr/nurse/config" "code.icb4dc0.de/prskr/nurse/grammar" + "code.icb4dc0.de/prskr/nurse/internal/retry" "code.icb4dc0.de/prskr/nurse/validation" ) @@ -44,36 +45,25 @@ func (s *SelectCheck) UnmarshalCheck(c grammar.Check, lookup config.ServerLookup } func (s *SelectCheck) Execute(ctx check.Context) error { - slog.Default().Debug("Execute check", + logger := slog.Default().With( slog.String("check", "sql.SELECT"), slog.String("query", s.Query), ) - for { - select { - case <-ctx.Done(): - return ctx.Err() - default: - attemptCtx, cancel := ctx.AttemptContext() - err := s.executeAttempt(attemptCtx) - cancel() - if err == nil { - return nil - } + return retry.Retry(ctx, ctx.AttemptCount(), ctx.AttemptTimeout(), func(ctx context.Context, attempt int) error { + logger.Debug("Execute check", slog.Int("attempt", attempt)) + + logger.Debug("") + + rows, err := s.QueryContext(ctx, s.Query) + if err != nil { + return err } - } -} - -func (s *SelectCheck) executeAttempt(ctx context.Context) (err error) { - var rows *sql.Rows - rows, err = s.QueryContext(ctx, s.Query) - if err != nil { - return err - } - - defer func() { - err = errors.Join(rows.Close(), rows.Err()) - }() - - return s.validators.Validate(rows) + + defer func() { + err = errors.Join(rows.Close(), rows.Err()) + }() + + return s.validators.Validate(rows) + }) }