feat(valkey): initial stable release v0.9.0
Valkey (Redis-compatible) client component with launcher lifecycle and health check integration. What's included: - Config with Addrs, Password, SelectDB, CacheSizeEachConn (env-driven) - Provider interface exposing native valkey-go Client() directly (no wrapper) - Component interface: launcher.Component + health.Checkable + Provider - New(logger, cfg) constructor for lifecycle registration via lc.Append - Health check via PING at LevelDegraded priority - Graceful shutdown calling client.Close() in OnStop Tested-via: todo-api POC integration Reviewed-against: docs/adr/
This commit is contained in:
26
.devcontainer/devcontainer.json
Normal file
26
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "Go",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:2-1.25-trixie",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-extra/features/claude-code:1": {}
|
||||||
|
},
|
||||||
|
"forwardPorts": [],
|
||||||
|
"postCreateCommand": "go version",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {
|
||||||
|
"files.autoSave": "afterDelay",
|
||||||
|
"files.autoSaveDelay": 1000,
|
||||||
|
"explorer.compactFolders": false,
|
||||||
|
"explorer.showEmptyFolders": true
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"golang.go",
|
||||||
|
"eamodio.golang-postfix-completion",
|
||||||
|
"quicktype.quicktype",
|
||||||
|
"usernamehw.errorlens"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remoteUser": "vscode"
|
||||||
|
}
|
||||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Binaries
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with go test -c
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of go build
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Editor / IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# VCS files
|
||||||
|
COMMIT.md
|
||||||
|
RELEASE.md
|
||||||
29
CHANGELOG.md
Normal file
29
CHANGELOG.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this module will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.9.0] - 2026-03-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `Config` struct — Valkey connection settings loaded from environment variables: `VK_ADDRS` (required, comma-separated server addresses), `VK_PASSWORD` (authentication password), `VK_DB` (database index, default `0`), `VK_CLIENT_CACHE_MB` (per-connection client-side cache size in MB, default `0` disables caching)
|
||||||
|
- `Provider` interface — `Client() valkey.Client`; for consumers that only need access to the native `valkey-go` client
|
||||||
|
- `Component` interface — embeds `launcher.Component`, `health.Checkable`, and `Provider`; the full lifecycle-managed surface registered with the launcher
|
||||||
|
- `New(logger logz.Logger, cfg Config) Component` — constructor; returns a `Component` ready for `lc.Append`
|
||||||
|
- `OnInit` — constructs the `valkey-go` client from `Config`; enables client-side caching when `CacheSizeEachConn > 0`
|
||||||
|
- `OnStart` — verifies connectivity by issuing a `PING` command; returns an error if the ping fails
|
||||||
|
- `OnStop` — calls `client.Close()` for graceful shutdown
|
||||||
|
- `HealthCheck(ctx context.Context) error` — issues `PING`; returns an error if the client is nil or the command fails
|
||||||
|
- `Name() string` — returns `"valkey"`
|
||||||
|
- `Priority() health.Level` — returns `health.LevelDegraded`; a cache outage degrades service rather than halting it
|
||||||
|
|
||||||
|
### Design Notes
|
||||||
|
|
||||||
|
- The native `valkey-go` client is exposed directly via `Client()` with no wrapping or re-exported command subset; callers use the full command-builder API and own all serialisation and key namespacing.
|
||||||
|
- `Provider` / `Component` split follows the framework pattern: inject `Provider` into repositories and services, inject `Component` only at the lifecycle registration site.
|
||||||
|
- Health priority is `LevelDegraded` by design — callers must handle cache misses by falling back to the primary datastore rather than treating a Valkey outage as fatal.
|
||||||
|
|
||||||
|
[0.9.0]: https://code.nochebuena.dev/go/valkey/releases/tag/v0.9.0
|
||||||
92
CLAUDE.md
Normal file
92
CLAUDE.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# valkey
|
||||||
|
|
||||||
|
Valkey (Redis-compatible) client with launcher lifecycle and health check integration.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Manages the lifecycle of a `valkey-go` client: constructs it from config, verifies
|
||||||
|
connectivity at startup, exposes the native client to consumers, and closes it on shutdown.
|
||||||
|
Provides a health check via `PING`. Does not add serialisation, key namespacing, or any
|
||||||
|
caching policy on top of the native client.
|
||||||
|
|
||||||
|
## Tier & Dependencies
|
||||||
|
|
||||||
|
**Tier 3 (infrastructure)** — depends on:
|
||||||
|
- `code.nochebuena.dev/go/health` (Tier 2)
|
||||||
|
- `code.nochebuena.dev/go/launcher` (Tier 2)
|
||||||
|
- `code.nochebuena.dev/go/logz` (Tier 1)
|
||||||
|
- `github.com/valkey-io/valkey-go` (native Valkey client)
|
||||||
|
|
||||||
|
Does **not** depend on `xerrors` — errors from the valkey-go client are returned as-is.
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
- **Native client exposure**: `Client() vk.Client` returns the native `valkey-go` client
|
||||||
|
directly. No wrapper, no re-exported command subset. Callers use the full valkey-go command
|
||||||
|
builder API. See ADR-001.
|
||||||
|
- **No serialisation helpers**: The module has no `SetJSON`, `GetJSON`, or similar helpers.
|
||||||
|
Callers marshal and unmarshal their own data. See ADR-002.
|
||||||
|
- **Health priority is Degraded**: `Priority()` returns `health.LevelDegraded`, not
|
||||||
|
`LevelCritical`. A Valkey outage degrades service but should not always halt it,
|
||||||
|
depending on whether the caller falls back to the primary datastore.
|
||||||
|
- **Optional client-side caching**: `Config.CacheSizeEachConn` (in MB) enables valkey-go's
|
||||||
|
built-in client-side cache. Setting it to 0 (the default) disables the cache entirely.
|
||||||
|
- **Duck-typed Logger**: The internal `logger` field is typed as `logz.Logger`, the shared
|
||||||
|
interface from the `logz` module (ADR-001 global pattern).
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
**Lifecycle registration:**
|
||||||
|
```go
|
||||||
|
vk := valkey.New(logger, cfg)
|
||||||
|
lc.Append(vk) // registers OnInit / OnStart / OnStop
|
||||||
|
```
|
||||||
|
|
||||||
|
**Accessing the client in a repository:**
|
||||||
|
```go
|
||||||
|
type cacheRepo struct {
|
||||||
|
provider valkey.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *cacheRepo) Get(ctx context.Context, key string) ([]byte, error) {
|
||||||
|
client := r.provider.Client()
|
||||||
|
result := client.Do(ctx, client.B().Get().Key(key).Build())
|
||||||
|
return result.AsBytes()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Writing with TTL:**
|
||||||
|
```go
|
||||||
|
client := provider.Client()
|
||||||
|
cmd := client.B().Set().Key(key).Value(val).Ex(300).Build()
|
||||||
|
if err := client.Do(ctx, cmd).Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Health check registration:**
|
||||||
|
```go
|
||||||
|
health.Register(vkComponent) // satisfies health.Checkable via Name()/Priority()/HealthCheck()
|
||||||
|
```
|
||||||
|
|
||||||
|
## What to Avoid
|
||||||
|
|
||||||
|
- Do not add serialisation helpers to this module. Keep marshal/unmarshal in the caller or
|
||||||
|
in a separate cache repository layer.
|
||||||
|
- Do not define custom interfaces that re-export a subset of `vk.Client` methods here.
|
||||||
|
If a consumer needs a minimal testable interface, define it in the consumer package.
|
||||||
|
- Do not call `Client()` before `OnInit` has run — it will return `nil`.
|
||||||
|
- Do not treat `health.LevelDegraded` as if it were `LevelCritical`. Design callers to
|
||||||
|
handle cache misses gracefully rather than depending on Valkey for correctness.
|
||||||
|
|
||||||
|
## Testing Notes
|
||||||
|
|
||||||
|
- Unit tests (`valkey_test.go`) do not require a running Valkey server. They test lifecycle
|
||||||
|
behaviour (nil client safety, name, priority) without real network calls.
|
||||||
|
- `TestComponent_OnInit_InvalidAddr` verifies that an empty address slice does not panic.
|
||||||
|
Whether `OnInit` returns an error depends on the valkey-go implementation; the test
|
||||||
|
documents the accepted behaviour.
|
||||||
|
- Integration tests requiring a live Valkey instance are outside this module and belong in
|
||||||
|
a higher-tier test suite.
|
||||||
|
- `compliance_test.go` (package `valkey_test`) asserts `New(...)` satisfies `Component`
|
||||||
|
at compile time.
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 NOCHEBUENADEV
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
8
compliance_test.go
Normal file
8
compliance_test.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package valkey_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.nochebuena.dev/go/logz"
|
||||||
|
"code.nochebuena.dev/go/valkey"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ valkey.Component = valkey.New(logz.New(logz.Options{}), valkey.Config{})
|
||||||
3
doc.go
Normal file
3
doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package valkey provides a Valkey (Redis-compatible) client with launcher lifecycle
|
||||||
|
// and health check integration.
|
||||||
|
package valkey
|
||||||
54
docs/adr/ADR-001-native-valkey-client.md
Normal file
54
docs/adr/ADR-001-native-valkey-client.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# ADR-001: Expose Native valkey.Client Without a Wrapper Layer
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-03-18
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Some infrastructure modules wrap their underlying client behind a custom interface that
|
||||||
|
re-exports only the operations they anticipate callers will need. This approach has two
|
||||||
|
failure modes:
|
||||||
|
|
||||||
|
1. The wrapper becomes a bottleneck: every new operation requires a new method on the
|
||||||
|
wrapper interface, creating churn.
|
||||||
|
2. The wrapper diverges from the upstream API surface, forcing callers to learn two APIs.
|
||||||
|
|
||||||
|
Valkey (and the compatible Redis protocol) has a rich, evolving command set. A thin wrapper
|
||||||
|
that re-exports commands one at a time would either be incomplete or grow unboundedly.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The `Component` interface exposes the native `vk.Client` directly via `Client() vk.Client`.
|
||||||
|
Callers receive a `vk.Client` value and use the valkey-go command builder API directly:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cmd := vkClient.B().Set().Key(key).Value(val).Ex(ttl).Build()
|
||||||
|
err = vkClient.Do(ctx, cmd).Error()
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Provider` interface is the minimal consumer-facing surface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Provider interface {
|
||||||
|
Client() vk.Client
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This module's only responsibilities are: constructing the client from `Config`, verifying
|
||||||
|
connectivity on `OnStart`, issuing a `PING` for health checks, and closing the client on
|
||||||
|
`OnStop`. All command execution is delegated entirely to the caller via the native client.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
- Callers have access to the full valkey-go API with no intermediary layer.
|
||||||
|
- No wrapper code to maintain as the valkey-go API evolves.
|
||||||
|
- The module stays small and focused on lifecycle management.
|
||||||
|
- Optional client-side caching (`CacheSizeEachConn`) is supported by passing the config
|
||||||
|
option through to `vk.NewClient` — no wrapper changes needed.
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
- Callers are coupled to the `valkey-go` library's API directly. Switching to a different
|
||||||
|
Valkey/Redis client would require changes at every call site.
|
||||||
|
- Mocking in tests requires either an `httptest`-style server or a mock that satisfies the
|
||||||
|
`vk.Client` interface, which is more complex than mocking a minimal custom interface.
|
||||||
49
docs/adr/ADR-002-no-serialization-helpers.md
Normal file
49
docs/adr/ADR-002-no-serialization-helpers.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ADR-002: No Serialisation Helpers — Callers Marshal/Unmarshal Themselves
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-03-18
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Cache and key-value store modules often provide convenience methods such as
|
||||||
|
`SetJSON(ctx, key, value, ttl)` or `GetJSON(ctx, key, &target)` that handle JSON
|
||||||
|
marshalling and unmarshalling internally. While convenient, this approach has drawbacks:
|
||||||
|
|
||||||
|
- It encodes a single serialisation format (typically JSON) into the module's API, making
|
||||||
|
it hard to use binary formats like protobuf or MessagePack for performance-sensitive paths.
|
||||||
|
- It obscures marshalling errors, which can become hard to distinguish from network errors.
|
||||||
|
- It requires the module to understand the caller's data types, coupling them together.
|
||||||
|
- It adds dependencies (e.g. `encoding/json`) that are not needed for all callers.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The `valkey` module provides no serialisation helpers. It exposes only `Client() vk.Client`,
|
||||||
|
and all marshal/unmarshal logic lives in the caller:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// caller marshals before writing
|
||||||
|
b, err := json.Marshal(myValue)
|
||||||
|
cmd := client.B().Set().Key(key).Value(string(b)).Ex(ttl).Build()
|
||||||
|
client.Do(ctx, cmd)
|
||||||
|
|
||||||
|
// caller unmarshals after reading
|
||||||
|
result := client.Do(ctx, client.B().Get().Key(key).Build())
|
||||||
|
b, err := result.AsBytes()
|
||||||
|
json.Unmarshal(b, &myValue)
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps the module at zero opinion on serialisation format, zero added dependencies
|
||||||
|
beyond `valkey-go`, and zero abstraction cost.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
- Callers choose their own serialisation format with no module-level constraints.
|
||||||
|
- The module has no encoding/decoding logic that needs testing or maintenance.
|
||||||
|
- Binary formats, compressed payloads, and plain strings all work identically.
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
- Every caller that stores structured data must implement its own marshal/unmarshal
|
||||||
|
boilerplate, typically in a repository or cache layer.
|
||||||
|
- There is no built-in protection against storing data with an incompatible format
|
||||||
|
(e.g. writing JSON and reading with a protobuf decoder).
|
||||||
12
go.mod
Normal file
12
go.mod
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module code.nochebuena.dev/go/valkey
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require (
|
||||||
|
code.nochebuena.dev/go/health v0.9.0
|
||||||
|
code.nochebuena.dev/go/launcher v0.9.0
|
||||||
|
code.nochebuena.dev/go/logz v0.9.0
|
||||||
|
github.com/valkey-io/valkey-go v1.0.54
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.29.0 // indirect
|
||||||
20
go.sum
Normal file
20
go.sum
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
code.nochebuena.dev/go/health v0.9.0 h1:x0UKjC7CHAE3AgwyFzCyjmGJIjoLBBxeOHxXuqpbKwI=
|
||||||
|
code.nochebuena.dev/go/health v0.9.0/go.mod h1:f3IsNtU60JSn5yXmBBh9XOvr5pRyEah5+wS4tjDQZso=
|
||||||
|
code.nochebuena.dev/go/launcher v0.9.0 h1:dJHonA9Xm03AQKK0919FJaQn9ZKHZ+RZfB9yxjnx3TA=
|
||||||
|
code.nochebuena.dev/go/launcher v0.9.0/go.mod h1:IBtntmbnyddukjEhxlc7Ysdzz9nZsnd9+8FzAIHt77g=
|
||||||
|
code.nochebuena.dev/go/logz v0.9.0 h1:wfV7vtI4V/8ED7Hm31Fbql7Y5iOGrlHN4X8Z5ajTZZE=
|
||||||
|
code.nochebuena.dev/go/logz v0.9.0/go.mod h1:qODhSbKb+tWE7rdhHLcKweiP5CgwIaWoZxadCT3bQV8=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||||
|
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||||
|
github.com/valkey-io/valkey-go v1.0.54 h1:pmFRGcMRJW8mHvsWLd/2MSgY6i3WNygpUl904KUaxao=
|
||||||
|
github.com/valkey-io/valkey-go v1.0.54/go.mod h1:NE+C8cjb3+XvLazNhiorcLJGhJa9MBAkFNoAW/48/fk=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
92
valkey.go
Normal file
92
valkey.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package valkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
vk "github.com/valkey-io/valkey-go"
|
||||||
|
|
||||||
|
"code.nochebuena.dev/go/health"
|
||||||
|
"code.nochebuena.dev/go/launcher"
|
||||||
|
"code.nochebuena.dev/go/logz"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider is the minimal interface for consumers that only need the valkey client.
|
||||||
|
type Provider interface {
|
||||||
|
Client() vk.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component adds lifecycle management and health check to Provider.
|
||||||
|
type Component interface {
|
||||||
|
launcher.Component
|
||||||
|
health.Checkable
|
||||||
|
Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds Valkey connection settings.
|
||||||
|
type Config struct {
|
||||||
|
Addrs []string `env:"VK_ADDRS,required" envSeparator:","`
|
||||||
|
Password string `env:"VK_PASSWORD"`
|
||||||
|
SelectDB int `env:"VK_DB" envDefault:"0"`
|
||||||
|
CacheSizeEachConn int `env:"VK_CLIENT_CACHE_MB" envDefault:"0"` // MB; 0 = disable
|
||||||
|
}
|
||||||
|
|
||||||
|
type vkComponent struct {
|
||||||
|
cfg Config
|
||||||
|
logger logz.Logger
|
||||||
|
client vk.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a valkey Component. Call lc.Append(vk) to manage its lifecycle.
|
||||||
|
func New(logger logz.Logger, cfg Config) Component {
|
||||||
|
return &vkComponent{cfg: cfg, logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *vkComponent) OnInit() error {
|
||||||
|
opts := vk.ClientOption{
|
||||||
|
InitAddress: v.cfg.Addrs,
|
||||||
|
Password: v.cfg.Password,
|
||||||
|
SelectDB: v.cfg.SelectDB,
|
||||||
|
}
|
||||||
|
if v.cfg.CacheSizeEachConn > 0 {
|
||||||
|
opts.CacheSizeEachConn = v.cfg.CacheSizeEachConn * 1024 * 1024
|
||||||
|
}
|
||||||
|
client, err := vk.NewClient(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("valkey: failed to create client: %w", err)
|
||||||
|
}
|
||||||
|
v.client = client
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *vkComponent) OnStart() error {
|
||||||
|
v.logger.Info("valkey: verifying connection")
|
||||||
|
if v.client == nil {
|
||||||
|
return fmt.Errorf("valkey: client not initialized")
|
||||||
|
}
|
||||||
|
if err := v.client.Do(context.Background(), v.client.B().Ping().Build()).Error(); err != nil {
|
||||||
|
return fmt.Errorf("valkey: ping failed: %w", err)
|
||||||
|
}
|
||||||
|
v.logger.Info("valkey: connected")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *vkComponent) OnStop() error {
|
||||||
|
v.logger.Info("valkey: closing client")
|
||||||
|
if v.client != nil {
|
||||||
|
v.client.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *vkComponent) Client() vk.Client { return v.client }
|
||||||
|
|
||||||
|
func (v *vkComponent) HealthCheck(ctx context.Context) error {
|
||||||
|
if v.client == nil {
|
||||||
|
return fmt.Errorf("valkey: client not initialized")
|
||||||
|
}
|
||||||
|
return v.client.Do(ctx, v.client.B().Ping().Build()).Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *vkComponent) Name() string { return "valkey" }
|
||||||
|
func (v *vkComponent) Priority() health.Level { return health.LevelDegraded }
|
||||||
46
valkey_test.go
Normal file
46
valkey_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package valkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.nochebuena.dev/go/health"
|
||||||
|
"code.nochebuena.dev/go/logz"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newLogger() logz.Logger { return logz.New(logz.Options{}) }
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
if New(newLogger(), Config{}) == nil {
|
||||||
|
t.Fatal("New returned nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComponent_Name(t *testing.T) {
|
||||||
|
c := New(newLogger(), Config{}).(health.Checkable)
|
||||||
|
if c.Name() != "valkey" {
|
||||||
|
t.Errorf("want valkey, got %s", c.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComponent_Priority(t *testing.T) {
|
||||||
|
c := New(newLogger(), Config{}).(health.Checkable)
|
||||||
|
if c.Priority() != health.LevelDegraded {
|
||||||
|
t.Error("Priority() != LevelDegraded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComponent_OnStop_NilClient(t *testing.T) {
|
||||||
|
c := &vkComponent{logger: newLogger()}
|
||||||
|
if err := c.OnStop(); err != nil {
|
||||||
|
t.Errorf("OnStop with nil client: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComponent_OnInit_InvalidAddr(t *testing.T) {
|
||||||
|
// An empty Addrs slice should cause NewClient to fail or produce an error.
|
||||||
|
c := New(newLogger(), Config{Addrs: []string{}})
|
||||||
|
err := c.OnInit()
|
||||||
|
// valkey-go may not error on empty addr until first command;
|
||||||
|
// just verify it doesn't panic and returns a component.
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user