diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0b7cacb --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +SERVICE_URI=postgresql://localhost:5432/postgres diff --git a/.gitignore b/.gitignore index aaadf73..f07f66b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +.idea +ca.pem + + + # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ac58105 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +ARG GO_VERSION=1.24.10 +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build +WORKDIR /src + +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,source=go.sum,target=go.sum \ + --mount=type=bind,source=go.mod,target=go.mod \ + go mod download -x + +ARG TARGETARCH + +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,target=. \ + CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /bin/server . + +FROM alpine:latest AS final + +RUN --mount=type=cache,target=/var/cache/apk \ + apk --update add \ + ca-certificates \ + tzdata \ + && \ + update-ca-certificates + +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + +RUN mkdir -p /app && chown -R appuser:appuser /app + +USER appuser + +COPY --from=build /bin/server /bin/ + +WORKDIR /app + +EXPOSE 1999 + +ENTRYPOINT [ "/bin/server" ] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c696244 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/tiennm99/postgresql-keepalive + +go 1.23.12 + +toolchain go1.24.10 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ecb9035 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..aad2144 --- /dev/null +++ b/init.sql @@ -0,0 +1,33 @@ +-- Drop and recreate the database +DROP DATABASE IF EXISTS keepalive; +CREATE DATABASE keepalive; + +-- Create user if not exists +DO +$do$ +BEGIN + IF NOT EXISTS ( + SELECT FROM pg_catalog.pg_roles WHERE rolname = 'keepalive' + ) THEN +CREATE ROLE keepalive LOGIN PASSWORD 'keepalive'; +END IF; +END +$do$; + +-- Grant full permissions on this database +GRANT ALL PRIVILEGES ON DATABASE keepalive TO keepalive; + +-- Connect to the database +\c keepalive; + +-- Create the table for key/value counters +CREATE TABLE IF NOT EXISTS keepalive ( + key VARCHAR(255) PRIMARY KEY, + value BIGINT NOT NULL + ); + +-- Initialize key/value +INSERT INTO keepalive (key, value) +VALUES ('counter', 0) + ON CONFLICT (key) DO UPDATE + SET value = EXCLUDED.value; diff --git a/main.go b/main.go new file mode 100644 index 0000000..05e1202 --- /dev/null +++ b/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "context" + "database/sql" + "log" + "net/url" + "os" + "os/signal" + "syscall" + "time" + + "github.com/joho/godotenv" + _ "github.com/lib/pq" +) + +func main() { + if err := godotenv.Load(); err != nil { + log.Println("Warning: .env file not found") + } + + serviceURI, isExist := os.LookupEnv("SERVICE_URI") + if !isExist { + log.Fatal("Warning: SERVICE_URI not set!") + return + } + conn, _ := url.Parse(serviceURI) + conn.RawQuery = "sslmode=verify-ca;sslrootcert=ca.pem" + + db, err := sql.Open("postgres", conn.String()) + if err != nil { + log.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := incrementCounter(ctx, db); err != nil { + log.Printf("Keepalive increment error: %v", err) + } + case <-ctx.Done(): + return + } + } + }() + + defer func() { + cancel() + if err := db.Close(); err != nil { + log.Printf("Close error: %v", err) + return + } + }() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh +} + +func incrementCounter(ctx context.Context, db *sql.DB) error { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) + if err != nil { + return err + } + + var value int64 + err = tx.QueryRowContext(ctx, + "UPDATE keepalive SET value = value + 1 WHERE key = 'counter' RETURNING value", + ).Scan(&value) + if err != nil { + tx.Rollback() + return err + } + + if err := tx.Commit(); err != nil { + return err + } + + log.Printf("Counter: %d\n", value) + return nil +}