diff --git a/mysql/.env.example b/mysql/.env.example new file mode 100644 index 0000000..acf4449 --- /dev/null +++ b/mysql/.env.example @@ -0,0 +1,2 @@ +DRIVER_NAME=mysql +DATA_SOURCE_NAME=keepalive:keepalive@tcp(localhost:3306)/keepalive?tls=custom diff --git a/mysql/.gitignore b/mysql/.gitignore index aaadf73..f07f66b 100644 --- a/mysql/.gitignore +++ b/mysql/.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/mysql/Dockerfile b/mysql/Dockerfile new file mode 100644 index 0000000..00895c4 --- /dev/null +++ b/mysql/Dockerfile @@ -0,0 +1,40 @@ +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 +USER appuser + +COPY --from=build /bin/server /bin/ + +EXPOSE 1999 + +ENTRYPOINT [ "/bin/server" ] diff --git a/mysql/go.mod b/mysql/go.mod new file mode 100644 index 0000000..3b38f05 --- /dev/null +++ b/mysql/go.mod @@ -0,0 +1,12 @@ +module github.com/tiennm99/mysql-keepalive + +go 1.23.12 + +toolchain go1.24.10 + +require ( + github.com/go-sql-driver/mysql v1.9.3 + github.com/joho/godotenv v1.5.1 +) + +require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/mysql/go.sum b/mysql/go.sum new file mode 100644 index 0000000..b380070 --- /dev/null +++ b/mysql/go.sum @@ -0,0 +1,6 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/mysql/init.sql b/mysql/init.sql new file mode 100644 index 0000000..db29172 --- /dev/null +++ b/mysql/init.sql @@ -0,0 +1,35 @@ +-- Drop and recreate the database +DROP +DATABASE IF EXISTS keepalive; +CREATE +DATABASE keepalive; + +-- Create user if not exists +CREATE +USER IF NOT EXISTS 'keepalive'@'%' IDENTIFIED BY 'keepalive'; + +-- Grant full permissions on this database +GRANT ALL PRIVILEGES ON keepalive.* TO +'keepalive'@'%'; + +FLUSH +PRIVILEGES; + +-- Create the table for key/value counters +USE +keepalive; + +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 DUPLICATE KEY +UPDATE `value` = 0; diff --git a/mysql/main.go b/mysql/main.go new file mode 100644 index 0000000..41e6773 --- /dev/null +++ b/mysql/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "database/sql" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/go-sql-driver/mysql" + "github.com/joho/godotenv" +) + +func main() { + if err := godotenv.Load(); err != nil { + log.Println("Warning: .env file not found") + } + + driverName, isExist := os.LookupEnv("DRIVER_NAME") + if !isExist { + log.Fatal("Warning: DRIVER_NAME not set!") + return + } + + dataSourceName, isExist := os.LookupEnv("DATA_SOURCE_NAME") + if !isExist { + log.Fatal("Warning: DATA_SOURCE_NAME not set!") + return + } + + rootCertPool := x509.NewCertPool() + pem, err := os.ReadFile("ca.pem") + if err != nil { + log.Fatal(err) + } + rootCertPool.AppendCertsFromPEM(pem) + + err = mysql.RegisterTLSConfig("custom", &tls.Config{ + RootCAs: rootCertPool, + }) + if err != nil { + log.Fatal(err) + } + + db, err := sql.Open(driverName, dataSourceName) + if err != nil { + panic(err) + } + db.SetConnMaxLifetime(time.Minute * 3) + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(10) + + 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 + } + + _, err = tx.ExecContext(ctx, + "UPDATE `keepalive` SET `value` = `value` + 1 WHERE `key` = 'counter'", + ) + if err != nil { + tx.Rollback() + return err + } + + var value int64 + err = tx.QueryRowContext(ctx, + "SELECT `value` FROM `keepalive` WHERE `key` = 'counter'", + ).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 +}