-
Notifications
You must be signed in to change notification settings - Fork 96
feat: add go-memory-load-mysql sample app #225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # Go build outputs | ||
| /bin/ | ||
| *.exe | ||
| *.exe~ | ||
| *.dll | ||
| *.so | ||
| *.dylib | ||
|
|
||
| # Test artifacts | ||
| *.test | ||
| *.out | ||
| coverage.txt | ||
| coverage.html | ||
|
|
||
| # Go vendor | ||
| vendor/ | ||
|
|
||
| # IDE | ||
| .idea/ | ||
| .vscode/ | ||
| *.swp | ||
| *.swo | ||
|
|
||
| # OS | ||
| .DS_Store | ||
| Thumbs.db | ||
|
|
||
| # Docker | ||
| **/.git |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| APP_PORT=8080 | ||
| MYSQL_DSN=app_user:app_password@tcp(localhost:3306)/orderdb?parseTime=true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| FROM golang:1.26-alpine AS build | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY go.mod go.sum* ./ | ||
| RUN go mod download | ||
|
|
||
| COPY . . | ||
| RUN go build -o /bin/api ./cmd/api | ||
|
|
||
| FROM alpine:3.22 | ||
|
|
||
| WORKDIR /app | ||
| COPY --from=build /bin/api /app/api | ||
|
|
||
| EXPOSE 8080 | ||
|
|
||
| CMD ["/app/api"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // Package main is the entry point for the load-test MySQL API server. | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "log/slog" | ||
| "net/http" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
| "time" | ||
|
|
||
| "loadtestmysqlapi/internal/config" | ||
| "loadtestmysqlapi/internal/database" | ||
| "loadtestmysqlapi/internal/httpapi" | ||
| "loadtestmysqlapi/internal/store" | ||
| ) | ||
|
|
||
| func main() { | ||
| logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ | ||
| Level: slog.LevelInfo, | ||
| })) | ||
|
|
||
| cfg, err := config.Load() | ||
| if err != nil { | ||
| logger.Error("load config", "error", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| db, err := database.Open(ctx, cfg.MySQLDSN) | ||
| if err != nil { | ||
| logger.Error("connect mysql", "error", err) | ||
| os.Exit(1) | ||
| } | ||
| defer db.Close() | ||
|
|
||
| if err := database.EnsureRuntimeSchema(ctx, db); err != nil { | ||
| logger.Error("ensure schema", "error", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| st := store.New(db) | ||
|
|
||
| handler := httpapi.New(st, logger) | ||
|
|
||
| server := &http.Server{ | ||
| Addr: ":" + cfg.Port, | ||
| Handler: handler, | ||
| ReadHeaderTimeout: 3 * time.Second, | ||
| ReadTimeout: 30 * time.Second, | ||
| WriteTimeout: 60 * time.Second, | ||
| IdleTimeout: 60 * time.Second, | ||
| } | ||
|
|
||
| go func() { | ||
| logger.Info("api listening", "addr", server.Addr) | ||
| if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | ||
| logger.Error("listen and serve", "error", err) | ||
| stop() | ||
| } | ||
| }() | ||
|
|
||
| <-ctx.Done() | ||
| logger.Info("shutdown signal received") | ||
|
|
||
| shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||
| defer cancel() | ||
|
|
||
| if err := server.Shutdown(shutdownCtx); err != nil { | ||
| logger.Error("graceful shutdown", "error", err) | ||
| os.Exit(1) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| services: | ||
| db: | ||
| image: mysql:8.0 | ||
| container_name: load-test-mysql-db | ||
| environment: | ||
| MYSQL_DATABASE: orderdb | ||
| MYSQL_USER: app_user | ||
| MYSQL_PASSWORD: app_password | ||
| MYSQL_ROOT_PASSWORD: rootpassword | ||
| ports: | ||
| - "3306:3306" | ||
| volumes: | ||
| - mysql_data:/var/lib/mysql | ||
| healthcheck: | ||
| test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "app_user", "--password=app_password"] | ||
| interval: 5s | ||
| timeout: 5s | ||
| retries: 20 | ||
|
|
||
| api: | ||
| build: | ||
| context: . | ||
| container_name: load-test-mysql-api | ||
| environment: | ||
| APP_PORT: "8080" | ||
| MYSQL_DSN: "app_user:app_password@tcp(db:3306)/orderdb?parseTime=true&multiStatements=true&interpolateParams=true" | ||
| ports: | ||
| - "8080:8080" | ||
| depends_on: | ||
| db: | ||
| condition: service_healthy | ||
|
|
||
| k6: | ||
| image: grafana/k6:0.49.0 | ||
| profiles: ["loadtest"] | ||
| environment: | ||
| BASE_URL: http://api:8080 | ||
| volumes: | ||
| - ./loadtest:/scripts:ro | ||
| depends_on: | ||
| api: | ||
| condition: service_started | ||
| entrypoint: ["k6"] | ||
|
|
||
| volumes: | ||
| mysql_data: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| module loadtestmysqlapi | ||
|
|
||
| go 1.26 | ||
|
|
||
| require github.com/go-sql-driver/mysql v1.9.2 | ||
|
|
||
| require filippo.io/edwards25519 v1.1.0 // indirect |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| 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.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= | ||
| github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // Package config loads runtime configuration from environment variables. | ||
| package config | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| ) | ||
|
|
||
| // Config holds all runtime configuration for the MySQL load-test API. | ||
| type Config struct { | ||
| Port string | ||
| MySQLDSN string | ||
| } | ||
|
|
||
| // Load reads configuration from environment variables and returns Config. | ||
| // Required: MYSQL_DSN. | ||
| func Load() (Config, error) { | ||
| dsn := os.Getenv("MYSQL_DSN") | ||
| if dsn == "" { | ||
| return Config{}, fmt.Errorf("MYSQL_DSN environment variable is required") | ||
| } | ||
|
|
||
| port := os.Getenv("APP_PORT") | ||
| if port == "" { | ||
| port = "8080" | ||
| } | ||
|
|
||
| return Config{ | ||
| Port: port, | ||
| MySQLDSN: dsn, | ||
| }, nil | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,115 @@ | ||||||||||||||||||||||||||
| // Package database provides MySQL connection and schema helpers. | ||||||||||||||||||||||||||
| package database | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||
| "database/sql" | ||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| _ "github.com/go-sql-driver/mysql" // register mysql driver | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Open creates a *sql.DB, verifies connectivity with retries, and applies the | ||||||||||||||||||||||||||
| // runtime schema. It returns the open DB handle; the caller must call db.Close(). | ||||||||||||||||||||||||||
|
Comment on lines
+13
to
+14
|
||||||||||||||||||||||||||
| // Open creates a *sql.DB, verifies connectivity with retries, and applies the | |
| // runtime schema. It returns the open DB handle; the caller must call db.Close(). | |
| // Open creates a *sql.DB, configures it, and verifies connectivity with retries. | |
| // It returns the open DB handle; the caller must call db.Close(). Apply the | |
| // runtime schema separately with EnsureRuntimeSchema. |
Copilot
AI
Apr 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Several db.Close() calls ignore the returned error. With errcheck enabled in this repo, prefer defer db.Close() //nolint:errcheck or _ = db.Close() (and similarly in the retry failure paths) to avoid lint failures and make intent explicit.
| db.Close() | |
| return nil, fmt.Errorf("mysql did not become ready after %d attempts: %w", maxAttempts, pingErr) | |
| } | |
| select { | |
| case <-ctx.Done(): | |
| db.Close() | |
| _ = db.Close() | |
| return nil, fmt.Errorf("mysql did not become ready after %d attempts: %w", maxAttempts, pingErr) | |
| } | |
| select { | |
| case <-ctx.Done(): | |
| _ = db.Close() |
Copilot
AI
Apr 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Open() calls db.PingContext(ctx) directly in the retry loop without a per-attempt timeout; if a ping blocks, a single attempt can stall until the parent context is canceled. Consider using a short context.WithTimeout per attempt (similar to the Postgres sample) and canceling it each loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defer db.Close()ignores the returned error. Other samples in this repo annotate this with//nolint:errcheck(or use_ = db.Close()), which avoids errcheck lint failures and makes the choice explicit.