feat(config): standardize keepalive env names

This commit is contained in:
tiennm99 committed 2026-06-27 21:47:14 +07:00
1 parent f5218ea327
commit 655db8dcbf
12 files changed
+132 -72

No files matched your search

+18 -15
View File
@@ -1,28 +1,31 @@
# Pick one of: redis, valkey, postgresql, mysql, mongodb, couchbase
DB_TYPE=redis
KEEPALIVE_ADAPTER=redis
# Optional: tick interval (e.g. 30s, 1m, 5m). Defaults to 1m.
# INTERVAL=1m
# KEEPALIVE_INTERVAL=1m
# Optional: key/doc ID to increment. Defaults to counter.
# KEEPALIVE_COUNTER_KEY=counter
# --- redis / valkey ---
REDIS_URL=redis://default@127.0.0.1:6379
# VALKEY_URL=valkey://default@127.0.0.1:6379
KEEPALIVE_REDIS_URL=redis://default@127.0.0.1:6379
# KEEPALIVE_VALKEY_URL=valkey://default@127.0.0.1:6379
# --- postgresql ---
# SERVICE_URI=postgres://user:pass@host:5432/dbname?sslmode=require
# KEEPALIVE_POSTGRESQL_URL=postgres://user:pass@host:5432/dbname?sslmode=require
# --- mysql ---
# DATA_SOURCE_NAME=user:pass@tcp(host:3306)/dbname
# KEEPALIVE_MYSQL_DSN=user:pass@tcp(host:3306)/dbname
# --- mongodb ---
# MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net
# MONGODB_DATABASE=keepalive
# MONGODB_COLLECTION=counter
# KEEPALIVE_MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net
# KEEPALIVE_MONGODB_DATABASE=keepalive
# KEEPALIVE_MONGODB_COLLECTION=counter
# --- couchbase ---
# COUCHBASE_CONNECTION_STRING=couchbases://cb.host
# COUCHBASE_USERNAME=
# COUCHBASE_PASSWORD=
# COUCHBASE_BUCKET_NAME=
# COUCHBASE_SCOPE_NAME=_default
# COUCHBASE_COLLECTION_NAME=_default
# KEEPALIVE_COUCHBASE_CONNECTION_STRING=couchbases://cb.host
# KEEPALIVE_COUCHBASE_USERNAME=
# KEEPALIVE_COUCHBASE_PASSWORD=
# KEEPALIVE_COUCHBASE_BUCKET_NAME=
# KEEPALIVE_COUCHBASE_SCOPE_NAME=_default
# KEEPALIVE_COUCHBASE_COLLECTION_NAME=_default
+22 -20
View File
@@ -8,23 +8,25 @@ Successor to the `*-keepalive` family: one binary, one image, six datastore adap
## Supported adapters
| `DB_TYPE` | Driver | Env vars |
| ------------ | ----------------------------------- | -------- |
| `redis` | `github.com/redis/go-redis/v9` | `REDIS_URL` |
| `valkey` | `github.com/valkey-io/valkey-go` | `VALKEY_URL` |
| `postgresql` | `github.com/lib/pq` | `SERVICE_URI` |
| `mysql` | `github.com/go-sql-driver/mysql` | `DATA_SOURCE_NAME` |
| `mongodb` | `go.mongodb.org/mongo-driver/v2` | `MONGODB_URI`, `MONGODB_DATABASE`, `MONGODB_COLLECTION` |
| `couchbase` | `github.com/couchbase/gocb/v2` | `COUCHBASE_CONNECTION_STRING`, `COUCHBASE_USERNAME`, `COUCHBASE_PASSWORD`, `COUCHBASE_BUCKET_NAME`, `COUCHBASE_SCOPE_NAME`, `COUCHBASE_COLLECTION_NAME` |
Set `KEEPALIVE_ADAPTER` to one of the values below.
Optional: `INTERVAL` (e.g. `30s`, `5m`; default `1m`), `COUNTER_KEY` (default `counter`).
| `KEEPALIVE_ADAPTER` | Driver | Env vars |
| ------------------- | ----------------------------------- | ------------------ |
| `redis` | `github.com/redis/go-redis/v9` | `KEEPALIVE_REDIS_URL` |
| `valkey` | `github.com/valkey-io/valkey-go` | `KEEPALIVE_VALKEY_URL` |
| `postgresql` | `github.com/lib/pq` | `KEEPALIVE_POSTGRESQL_URL` |
| `mysql` | `github.com/go-sql-driver/mysql` | `KEEPALIVE_MYSQL_DSN` |
| `mongodb` | `go.mongodb.org/mongo-driver/v2` | `KEEPALIVE_MONGODB_URI`, `KEEPALIVE_MONGODB_DATABASE`, `KEEPALIVE_MONGODB_COLLECTION` |
| `couchbase` | `github.com/couchbase/gocb/v2` | `KEEPALIVE_COUCHBASE_CONNECTION_STRING`, `KEEPALIVE_COUCHBASE_USERNAME`, `KEEPALIVE_COUCHBASE_PASSWORD`, `KEEPALIVE_COUCHBASE_BUCKET_NAME`, `KEEPALIVE_COUCHBASE_SCOPE_NAME`, `KEEPALIVE_COUCHBASE_COLLECTION_NAME` |
Optional: `KEEPALIVE_INTERVAL` (e.g. `30s`, `5m`; default `1m`), `KEEPALIVE_COUNTER_KEY` (default `counter`).
## Quick start (Docker)
```bash
docker run -d --name keepalive --restart unless-stopped \
-e DB_TYPE=redis \
-e REDIS_URL='redis://default@host:6379' \
-e KEEPALIVE_ADAPTER=redis \
-e KEEPALIVE_REDIS_URL='redis://default@host:6379' \
ghcr.io/tiennm99/keepalive:latest
```
@@ -33,19 +35,19 @@ docker run -d --name keepalive --restart unless-stopped \
```bash
git clone https://github.com/tiennm99/keepalive
cd keepalive
cp .env.example .env # then edit DB_TYPE + the driver's env vars
cp .env.example .env # then edit KEEPALIVE_ADAPTER + the driver's env vars
go run .
```
## How it works
On every tick the chosen adapter performs the cheapest write that proves the cluster is alive:
On every tick the chosen adapter performs the cheapest write that proves the cluster is alive. `KEEPALIVE_COUNTER_KEY` selects the key/doc ID and defaults to `counter`.
- **Redis/Valkey** — `INCR counter`
- **PostgreSQL** — `UPDATE keepalive SET value = value + 1 WHERE key = 'counter' RETURNING value`
- **MySQL** — `UPDATE` + `SELECT` inside a transaction
- **MongoDB** — `FindOneAndUpdate({_id: "counter"}, {$inc: {count: 1}}, upsert)`
- **Couchbase** — `GET counter` → `++` → `UPSERT counter`
- **Redis/Valkey** — `INCR key`
- **PostgreSQL** — `UPDATE keepalive SET value = value + 1 WHERE key = $1 RETURNING value`
- **MySQL** — `UPDATE` + `SELECT` by key inside a transaction
- **MongoDB** — `FindOneAndUpdate({_id: key}, {$inc: {count: 1}}, upsert)`
- **Couchbase** — `GET key` -> `++` -> `UPSERT key`
The PostgreSQL and MySQL adapters expect a table:
@@ -54,7 +56,7 @@ CREATE TABLE keepalive (key TEXT PRIMARY KEY, value BIGINT NOT NULL DEFAULT 0);
INSERT INTO keepalive (key, value) VALUES ('counter', 0);
```
(MySQL uses backticked identifiers — see `adapter/mysql.go`.)
Seed the value with your configured `KEEPALIVE_COUNTER_KEY` when it is not `counter`. MySQL uses backticked identifiers — see `adapter/mysql.go`.
## Adding a new adapter
@@ -64,7 +66,7 @@ INSERT INTO keepalive (key, value) VALUES ('counter', 0);
```go
func init() { Registry["<name>"] = func() (Adapter, error) { return &myAdapter{}, nil } }
```
4. Add an `import _ "your driver"` if needed, and the env vars to `.env.example` and the table above.
4. Add an `import _ "your driver"` if needed, and the `KEEPALIVE_*` env vars to `.env.example` and the table above.
## Migrated from
+1 -9
View File
@@ -24,7 +24,7 @@ var Registry = map[string]Factory{}
func New(dbType string) (Adapter, error) {
f, ok := Registry[dbType]
if !ok {
return nil, fmt.Errorf("unknown DB_TYPE %q (known: %v)", dbType, Known())
return nil, fmt.Errorf("unknown adapter %q (known: %v)", dbType, Known())
}
return f()
}
@@ -36,11 +36,3 @@ func Known() []string {
}
return out
}
func envOrFail(name string) (string, error) {
v, ok := lookupEnv(name)
if !ok || v == "" {
return "", fmt.Errorf("env %s is required", name)
}
return v, nil
}
+7 -7
View File
@@ -18,27 +18,27 @@ type couchbaseAdapter struct {
}
func (a *couchbaseAdapter) Connect(_ context.Context) error {
conn, err := envOrFail("COUCHBASE_CONNECTION_STRING")
conn, err := envOrFail("KEEPALIVE_COUCHBASE_CONNECTION_STRING")
if err != nil {
return err
}
user, err := envOrFail("COUCHBASE_USERNAME")
user, err := envOrFail("KEEPALIVE_COUCHBASE_USERNAME")
if err != nil {
return err
}
pass, err := envOrFail("COUCHBASE_PASSWORD")
pass, err := envOrFail("KEEPALIVE_COUCHBASE_PASSWORD")
if err != nil {
return err
}
bucket, err := envOrFail("COUCHBASE_BUCKET_NAME")
bucket, err := envOrFail("KEEPALIVE_COUCHBASE_BUCKET_NAME")
if err != nil {
return err
}
scope, err := envOrFail("COUCHBASE_SCOPE_NAME")
scope, err := envOrFail("KEEPALIVE_COUCHBASE_SCOPE_NAME")
if err != nil {
return err
}
collName, err := envOrFail("COUCHBASE_COLLECTION_NAME")
collName, err := envOrFail("KEEPALIVE_COUCHBASE_COLLECTION_NAME")
if err != nil {
return err
}
@@ -59,7 +59,7 @@ func (a *couchbaseAdapter) Connect(_ context.Context) error {
}
a.cluster = cluster
a.coll = b.Scope(scope).Collection(collName)
a.docID = envOr("COUNTER_KEY", "counter")
a.docID = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return nil
}
+10 -3
View File
@@ -1,9 +1,16 @@
package adapter
import "os"
import (
"fmt"
"os"
)
func lookupEnv(name string) (string, bool) {
return os.LookupEnv(name)
func envOrFail(name string) (string, error) {
v, ok := os.LookupEnv(name)
if !ok || v == "" {
return "", fmt.Errorf("env %s is required", name)
}
return v, nil
}
func envOr(name, def string) string {
+44
View File
@@ -0,0 +1,44 @@
package adapter
import (
"strings"
"testing"
)
func TestEnvOrFailReturnsConfiguredValue(t *testing.T) {
t.Setenv("KEEPALIVE_TEST_REQUIRED", "configured")
got, err := envOrFail("KEEPALIVE_TEST_REQUIRED")
if err != nil {
t.Fatalf("envOrFail returned error: %v", err)
}
if got != "configured" {
t.Fatalf("envOrFail() = %q, want configured", got)
}
}
func TestEnvOrFailReportsMissingName(t *testing.T) {
_, err := envOrFail("KEEPALIVE_TEST_MISSING_REQUIRED")
if err == nil {
t.Fatal("envOrFail returned nil error")
}
if !strings.Contains(err.Error(), "KEEPALIVE_TEST_MISSING_REQUIRED") {
t.Fatalf("error %q does not include env name", err)
}
}
func TestEnvOrReturnsConfiguredValue(t *testing.T) {
t.Setenv("KEEPALIVE_TEST_OPTIONAL", "configured")
got := envOr("KEEPALIVE_TEST_OPTIONAL", "default")
if got != "configured" {
t.Fatalf("envOr() = %q, want configured", got)
}
}
func TestEnvOrUsesDefaultWhenNoNamesAreSet(t *testing.T) {
got := envOr("KEEPALIVE_TEST_MISSING_DEFAULT", "default")
if got != "default" {
t.Fatalf("envOr() = %q, want default", got)
}
}
+4 -4
View File
@@ -20,15 +20,15 @@ type mongoAdapter struct {
}
func (a *mongoAdapter) Connect(ctx context.Context) error {
uri, err := envOrFail("MONGODB_URI")
uri, err := envOrFail("KEEPALIVE_MONGODB_URI")
if err != nil {
return err
}
dbName, err := envOrFail("MONGODB_DATABASE")
dbName, err := envOrFail("KEEPALIVE_MONGODB_DATABASE")
if err != nil {
return err
}
collName, err := envOrFail("MONGODB_COLLECTION")
collName, err := envOrFail("KEEPALIVE_MONGODB_COLLECTION")
if err != nil {
return err
}
@@ -38,7 +38,7 @@ func (a *mongoAdapter) Connect(ctx context.Context) error {
}
a.client = client
a.coll = client.Database(dbName).Collection(collName)
a.docID = envOr("COUNTER_KEY", "counter")
a.docID = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return client.Ping(ctx, nil)
}
+8 -4
View File
@@ -13,11 +13,12 @@ func init() {
}
type mysqlAdapter struct {
db *sql.DB
db *sql.DB
key string
}
func (a *mysqlAdapter) Connect(ctx context.Context) error {
dsn, err := envOrFail("DATA_SOURCE_NAME")
dsn, err := envOrFail("KEEPALIVE_MYSQL_DSN")
if err != nil {
return err
}
@@ -29,6 +30,7 @@ func (a *mysqlAdapter) Connect(ctx context.Context) error {
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
a.db = db
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return a.db.PingContext(ctx)
}
@@ -38,14 +40,16 @@ func (a *mysqlAdapter) Increment(ctx context.Context) (int64, error) {
return 0, err
}
if _, err := tx.ExecContext(ctx,
"UPDATE `keepalive` SET `value` = `value` + 1 WHERE `key` = 'counter'",
"UPDATE `keepalive` SET `value` = `value` + 1 WHERE `key` = ?",
a.key,
); err != nil {
tx.Rollback()
return 0, err
}
var value int64
if err := tx.QueryRowContext(ctx,
"SELECT `value` FROM `keepalive` WHERE `key` = 'counter'",
"SELECT `value` FROM `keepalive` WHERE `key` = ?",
a.key,
).Scan(&value); err != nil {
tx.Rollback()
return 0, err
+6 -3
View File
@@ -13,11 +13,12 @@ func init() {
}
type postgresAdapter struct {
db *sql.DB
db *sql.DB
key string
}
func (a *postgresAdapter) Connect(ctx context.Context) error {
uri, err := envOrFail("SERVICE_URI")
uri, err := envOrFail("KEEPALIVE_POSTGRESQL_URL")
if err != nil {
return err
}
@@ -26,6 +27,7 @@ func (a *postgresAdapter) Connect(ctx context.Context) error {
return err
}
a.db = db
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return a.db.PingContext(ctx)
}
@@ -36,7 +38,8 @@ func (a *postgresAdapter) Increment(ctx context.Context) (int64, error) {
}
var value int64
if err := tx.QueryRowContext(ctx,
`UPDATE keepalive SET value = value + 1 WHERE key = 'counter' RETURNING value`,
`UPDATE keepalive SET value = value + 1 WHERE key = $1 RETURNING value`,
a.key,
).Scan(&value); err != nil {
tx.Rollback()
return 0, err
+2 -2
View File
@@ -16,7 +16,7 @@ type redisAdapter struct {
}
func (a *redisAdapter) Connect(ctx context.Context) error {
url, err := envOrFail("REDIS_URL")
url, err := envOrFail("KEEPALIVE_REDIS_URL")
if err != nil {
return err
}
@@ -25,7 +25,7 @@ func (a *redisAdapter) Connect(ctx context.Context) error {
return err
}
a.client = redis.NewClient(opt)
a.key = envOr("COUNTER_KEY", "counter")
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return a.client.Ping(ctx).Err()
}
+2 -2
View File
@@ -16,7 +16,7 @@ type valkeyAdapter struct {
}
func (a *valkeyAdapter) Connect(_ context.Context) error {
url, err := envOrFail("VALKEY_URL")
url, err := envOrFail("KEEPALIVE_VALKEY_URL")
if err != nil {
return err
}
@@ -29,7 +29,7 @@ func (a *valkeyAdapter) Connect(_ context.Context) error {
return err
}
a.client = client
a.key = envOr("COUNTER_KEY", "counter")
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return nil
}
+8 -3
View File
@@ -13,17 +13,22 @@ import (
"github.com/tiennm99/keepalive/adapter"
)
const (
envAdapter = "KEEPALIVE_ADAPTER"
envInterval = "KEEPALIVE_INTERVAL"
)
func main() {
if err := godotenv.Load(); err != nil {
log.Println("note: .env not loaded, relying on process env")
}
dbType := os.Getenv("DB_TYPE")
dbType := os.Getenv(envAdapter)
if dbType == "" {
log.Fatalf("DB_TYPE is required (known: %v)", adapter.Known())
log.Fatalf("%s is required (known: %v)", envAdapter, adapter.Known())
}
interval := parseInterval(os.Getenv("INTERVAL"), time.Minute)
interval := parseInterval(os.Getenv(envInterval), time.Minute)
a, err := adapter.New(dbType)
if err != nil {