mirror of
https://github.com/tiennm99/keepalive.git
synced 2026-10-11 03:13:31 +00:00
feat(config): standardize keepalive env names
This commit is contained in:
1 parent
f5218ea327
commit
655db8dcbf
12 files changed
+132
-72
No files matched your search
+18
-15
@@ -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
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user