Skip to content

Commit

Permalink
Add load testing task and expose database metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
96malhar committed Dec 8, 2023
1 parent aa34ac5 commit 1330f8a
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 15 deletions.
38 changes: 25 additions & 13 deletions Taskfile.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
version: '3'

dotenv: [env/.dev.env]
dotenv: [ env/.dev.env ]

tasks:
docker:postgres:start:
Expand Down Expand Up @@ -42,14 +42,14 @@ tasks:
db:migrations:up:
desc: Runs up migrations
cmds:
- echo "Running up migrations..."
- migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up
- echo "Running up migrations..."
- migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} up

db:migrations:down:
desc: Runs down migrations
cmds:
- echo "Running down migrations..."
- migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} down
- echo "y" | migrate -path ./migrations -database ${GREENLIGHT_DB_DSN} down

server:help:
desc: Show help for the Greenlight API server
Expand All @@ -61,15 +61,6 @@ tasks:
cmds:
- go run ./cmd/api {{.CLI_ARGS}}

smoketest:
desc: Run the smoke test
dotenv: [env/.smoketest.env]
cmds:
- defer: go run ./cmd/db teardown
- go run ./cmd/db setup
- migrate -path ./migrations -database $GREENLIGHT_DB_DSN up
- ./smoke_test.sh

audit:
desc: Run quality control audits
cmds:
Expand All @@ -84,7 +75,28 @@ tasks:
- echo "Checking vulnerabilities..."
- govulncheck ./...

smoketest:
desc: Run the smoke test
dotenv: [ env/.smoketest.env ]
cmds:
- defer: go run ./cmd/db teardown
- go run ./cmd/db setup
- migrate -path ./migrations -database $GREENLIGHT_DB_DSN up
- ./smoke_test.sh

test:
desc: Run all tests
cmds:
- go test -json -v --coverprofile=coverage.out ./... 2>&1 | gotestfmt -hide "successful-tests, empty-packages"

loadtest:
desc: Run the load test
summary: Creates a fresh database, runs migrations, and runs the load test.
The command uses 'Hey' to perform the load test. It registers a new user and then repeatedly
invokes the /v1/tokens/authentication endpoint to generate authentication tokens.
dotenv: [ env/.smoketest.env ]
cmds:
- defer: go run ./cmd/db teardown
- go run ./cmd/db setup
- migrate -path ./migrations -database $GREENLIGHT_DB_DSN up
- ./load_test.sh
2 changes: 1 addition & 1 deletion cmd/api/greenlight.http
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ Content-Type: application/json
POST localhost:4000/v1/tokens/authentication
Content-Type: application/json

{"email":"bob@gmail.com", "password":"5ecret1234"}
{"email":"alice@example.com", "password":"pa55word"}
37 changes: 37 additions & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package main

import (
"context"
"expvar"
"flag"
"github.com/96malhar/greenlight/internal/data"
"github.com/96malhar/greenlight/internal/email"
"github.com/jackc/pgx/v5/pgxpool"
"log/slog"
"os"
"runtime"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -85,6 +87,8 @@ func main() {
modelStore: data.NewModelStore(db),
}

monitorMetrics(db)

err = app.serve()
if err != nil {
logger.Error(err.Error())
Expand Down Expand Up @@ -129,6 +133,7 @@ func openDB(cfg config) (*pgxpool.Pool, error) {
}
pgxConf.MaxConnIdleTime = cfg.db.maxIdleTime
pgxConf.MaxConns = int32(cfg.db.maxOpenConns)

db, err := pgxpool.NewWithConfig(context.Background(), pgxConf)
if err != nil {
return nil, err
Expand All @@ -144,3 +149,35 @@ func openDB(cfg config) (*pgxpool.Pool, error) {

return db, nil
}

func monitorMetrics(pool *pgxpool.Pool) {
expvar.NewString("version").Set(version)

// Publish the number of active goroutines.
expvar.Publish("goroutines", expvar.Func(func() any {
return runtime.NumGoroutine()
}))

//Publish the database connection pool statistics.
expvar.Publish("database", expvar.Func(func() any {
st := pool.Stat()
return map[string]any{
"maximum_pool_size": st.MaxConns(),
"current_pool_size": st.TotalConns(),
"current_idle_conns": st.IdleConns(),
"current_constructing_conns": st.ConstructingConns(),
"current_acquired_conns": st.AcquiredConns(),
"total_acquired_conns": st.AcquireCount(),
"total_starved_acquired_conns": st.EmptyAcquireCount(),
"total_acquire_cancelled": st.CanceledAcquireCount(),
"total_acquired_duration": st.AcquireDuration().Seconds(),
"total_idle_closed": st.MaxIdleDestroyCount(),
"total_lifetime_closed": st.MaxLifetimeDestroyCount(),
}
}))

// Publish the current Unix timestamp.
expvar.Publish("timestamp", expvar.Func(func() any {
return time.Now().Unix()
}))
}
3 changes: 3 additions & 0 deletions cmd/api/routes.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"expvar"
"github.com/go-chi/chi/v5"
"net/http"
)

// routes returns a new chi router containing the application routes.
Expand All @@ -28,6 +30,7 @@ func (app *application) routes() *chi.Mux {
})

r.Post("/v1/tokens/authentication", app.createAuthenticationTokenHandler)
r.Method(http.MethodGet, "/debug/vars", expvar.Handler())

return r
}
4 changes: 3 additions & 1 deletion cmd/api/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"errors"
"fmt"
"github.com/96malhar/greenlight/internal/data"
"github.com/96malhar/greenlight/internal/validator"
"net/http"
Expand Down Expand Up @@ -73,7 +74,8 @@ func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Reque
}
err = app.mailer.Send(user.Email, "user_welcome.tmpl", userData)
if err != nil {
app.logger.Error(err.Error())
msg := fmt.Sprintf("Failed to send welcome email for new user (%s). Err = %s", user.Email, err.Error())
app.logger.Error(msg)
}
})

Expand Down
79 changes: 79 additions & 0 deletions load_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/bin/bash

cleanup() {
# Stop the web server if it is still running
echo "Stopping server with PID: $serverPid"
kill "$serverPid"

# Clean up the binary
rm -rf ./bin
}


setup() {
# Build the Go binary
go build -o ./bin/server ./cmd/api

# Start the web server in the background and discard all output
./bin/server -limiter-enabled=false > /dev/null &

# Capture the PID of the server process so we can kill it later
serverPid=$!
echo "Server PID: $serverPid"

# wait for the server to start
sleep 2

# Register a new user
response=$(curl -X POST --location "http://localhost:4000/v1/users" \
-H "Content-Type: application/json" \
-d '{"name":"Alice", "email":"alice@example.com", "password":"pa55word"}' -D -)

# check that the response code is 202 Accepted
status=$(echo "$response" | head -n 1 | cut -d$' ' -f2)
if [[ "$status" -ne 202 ]]; then
echo "Load test failed. Unexpected response code."
echo "Expected: 202"
echo "Actual : $status"
exit 1
fi
}

runLoadTest() {
echo "=====LOAD TEST STARTED====="
hey -d '{"email": "alice@example.com", "password": "pa55word"}' \
-m "POST" http://localhost:4000/v1/tokens/authentication
echo "=====LOAD TEST COMPLETED====="
}

# Register cleanup function on exit or error
trap cleanup EXIT ERR

# Create global variable called PID
export serverPid=0

# Call the setup function
setup

# start load test
runLoadTest

while true; do
echo "Choose an option:"
echo "1. Exit the script"
echo "2. Re-run the load test"
read -r -p "Enter your choice (1 or 2): " choice
case $choice in
1)
echo "Stopping the server and exiting the script"
exit 0
;;
2)
echo "Re-running load test"
runLoadTest
;;
*)
echo "Invalid choice, please choose 1 or 2."
;;
esac
done

0 comments on commit 1330f8a

Please sign in to comment.