Send lists on from submit
list := r.Form["something[]"]
// Aggregate all form inputs with the same name attribute
Run tests a designated number of times, ignoring no file changes optimization
go test -count=1 ./...
This document explains how performance profiling was integrated into the BOMR Forecaster application using Go's runtime/pprof package.
The application now includes HTTP endpoints for runtime profiling, allowing you to identify CPU and memory hot paths in your codebase. This is enabled via the net/http/pprof package, which exposes profiling endpoints over HTTP.
router/router.go)Import Added:
import (
// ... existing imports
"net/http/pprof"
)
Handler Registration:
Added pprof endpoint registration in the New() function (lines 137-151):
// Enable pprof endpoints in local mode, dev environment, or when explicitly enabled
if config.EnablePprof {
// Register pprof handlers manually since we're using a custom ServeMux
r.mux.HandleFunc("/debug/pprof/", pprof.Index)
r.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
r.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
r.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
r.mux.HandleFunc("/debug/pprof/heap", pprof.Handler("heap").ServeHTTP)
r.mux.HandleFunc("/debug/pprof/goroutine", pprof.Handler("goroutine").ServeHTTP)
r.mux.HandleFunc("/debug/pprof/allocs", pprof.Handler("allocs").ServeHTTP)
r.mux.HandleFunc("/debug/pprof/block", pprof.Handler("block").ServeHTTP)
r.mux.HandleFunc("/debug/pprof/mutex", pprof.Handler("mutex").ServeHTTP)
log.Printf("pprof endpoints enabled at /debug/pprof/")
}
Key Points:
http.ServeMux instead of the defaultconfig.EnablePprof is true, ORconfig.LocalMode is true, ORconfig.Environment == "dev"config/config.go)New Field Added:
type Config struct {
// ... existing fields
EnablePprof bool `json:"ENABLE_PPROF"`
// ... rest of fields
}
Environment Variable Support:
Added check in New() function (lines 112-115):
// Check environment variable as override for pprof
if os.Getenv("ENABLE_PPROF") == "true" {
C.EnablePprof = true
}
Key Points:
EnablePprof can be set in the JSON config fileENABLE_PPROF=true overrides the config file settingMakefile)New Target Added: // note when starting up this server you may see this message:
Couldn't find a suitable web browser! Set the BROWSER environment variable to your desired browser.
This likely means that you are running on headless machine, which is fine because the only thing that failed was the browser auto-starting. You can still connect to the server.
p:
@echo "Starting application with pprof enabled..."
@echo "pprof endpoints will be available at: http://localhost:8081/debug/pprof/"
@echo "Example: go tool pprof http://localhost:8081/debug/pprof/profile?seconds=30"
$(eval include include .env.local)
ENABLE_PPROF=true go run .
Key Points:
make p starts the application with pprof enabledENABLE_PPROF=true environment variablemake p
This will:
/debug/pprof/Once running, the following endpoints are available:
http://localhost:8081/debug/pprof/ - Index page listing all profileshttp://localhost:8081/debug/pprof/profile - CPU profile (requires ?seconds=N parameter)http://localhost:8081/debug/pprof/heap - Memory/heap profilehttp://localhost:8081/debug/pprof/goroutine - Goroutine profilehttp://localhost:8081/debug/pprof/allocs - Allocation profilehttp://localhost:8081/debug/pprof/block - Blocking operations profilehttp://localhost:8081/debug/pprof/mutex - Mutex contention profilehttp://localhost:8081/debug/pprof/trace - Execution tracego tool pprof http://localhost:8081/debug/pprof/profile?seconds=30
Important: Generate load on your application while profiling is running, otherwise you'll get 0 samples.
go tool pprof http://localhost:8081/debug/pprof/heap
go tool pprof http://localhost:8081/debug/pprof/goroutine
Once in pprof interactive mode, use these commands:
top - Show top functions by CPU/memory usagetop10 - Show top 10 functionslist <function_name> - Show annotated source codeweb - Generate visual graph (requires graphviz)svg - Generate SVG visualizationpng - Generate PNG visualizationhelp - Show all available commandsFor the easiest analysis experience:
# Save profile to file first
curl "http://localhost:8081/debug/pprof/profile?seconds=30" > cpu.prof
# Open in web UI
go tool pprof -http=:8082 cpu.prof
# Store profile data in memory so you don't have to remember to clean up
go tool pprof -http=:8082 "http://localhost:8081/debug/pprof/profile?seconds=30"
This opens a web interface at http://localhost:8082 with:
Start application with profiling:
make p
In another terminal, generate load while profiling:
# Generate a 30-second CPU profile
go tool pprof http://localhost:8081/debug/pprof/profile?seconds=30
# While profiling is running, use your application normally
# or generate load with curl requests
Analyze the profile:
# If you saved it to a file
go tool pprof -http=:8082 cpu.prof
# Or use interactive mode
go tool pprof cpu.prof
(pprof) top10
(pprof) list <function_name>
(pprof) web
Ubuntu/Debian:
sudo apt-get update
sudo apt-get install graphviz
macOS:
brew install graphviz
Fedora/RHEL:
sudo dnf install graphviz
Verify installation:
dot -V
This means your application was idle during profiling. Solutions:
Generate load while profiling:
# Terminal 1: Start profiling
go tool pprof http://localhost:8081/debug/pprof/profile?seconds=30
# Terminal 2: Generate load
for i in {1..100}; do
curl -s http://localhost:8081/bomr/getForecast > /dev/null &
done
Use different profile types:
Enable block profiling (for I/O-bound apps):
Add to main.go:
import _ "runtime"
func main() {
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
// ... rest of code
}
CPU profile files (.pb.gz) are binary format. Use go tool pprof to read them:
go tool pprof cpu.prof
# or
go tool pprof -http=:8082 cpu.prof
If port 8081 is already in use, check your config file for the HTTP_PORT setting and adjust accordingly.
ENABLE_PPROF=true environment variable to enable explicitlyImportant: Never expose pprof endpoints in production without proper authentication, as they can reveal sensitive information about your application's internals.
The pprof integration provides:
make p command to enable profilingUse make p to start profiling, then use go tool pprof to analyze your application's performance characteristics and identify hot paths.
var wg sync.WaitGroup
errChan := make(chan error)
var results sync.Map
// loop example:
for key, value := range someMap {
wg.Add(1) // Increment the wait group counter
go func(k, v string) {
defer wg.Done() // Decrement the wait group counter when done
item, e := someMethod(k, v)
if e != nil {
errChan <- fmt.Errorf("error when someMethod for someParentMethod() of key: %s. Error: %v", k, e)
return
}
results.Store(k, item)
}(key, value)
}
// inline example:
wg.Add(1)
go func() {
defer wg.Done()
var e error
someResult, e = someMethod()
if e != nil {
errChan <- fmt.Errorf("error, when someMethod() for someParentMethod(). Error: %v", e)
return
}
results.Store(someMapKey, someResult)
}()
go func() {
wg.Wait()
close(errChan)
}()
if errChanError := <-errChan; errChanError != nil {
return fmt.Errorf("error, when attempting to perform async actions. Error: %v", errChanError)
}
func DoingSOmething() error {
done := make(chan error, 1)
go func() {
err := doingSomethingElse()
if err != nil {
done <- fmt.Errorf("error, when doingSomethingElse() for DoingSomething(). Error: %v", err)
return
}
done <- nil
}()
select {
case err := <-done:
return err
case <- time.After(20 * time.Seconds):
return errors.New("error, doingSOmethingElse() for DoingSomething() has timed out")
}
}
func StartDailyBackup(ctx context.Context, dbFileLocation string) error {
location, err := time.LoadLocation("America/Chicago") // familiar timezone for an easier schedule
if err != nil {
return fmt.Errorf("error, invalid location provided. Error: %v", err)
}
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
currentTime := time.Now()
localTime := currentTime.In(location)
h, _, _ := localTime.Clock()
if h == 20 { // 8 pm
err := backupDatabase(ctx, dbFileLocation)
if err != nil {
return fmt.Errorf("error, when backupDatabase() for StartDailyBackup(). Error: %v", err)
}
}
}
}
}
func backupDatabase(ctx context.Context) error {
log.Printf("starting database backup")
backupFile := "/data/database_backup"
statement := fmt.Sprintf("VACUUM main INTO '%s'", backupFile)
_, err := database.Exec(statement)
if err != nil {
return fmt.Errorf("error, when backing up the database. Error: %v", err)
}
fileBytes, err := os.ReadFile(backupFile)
if err != nil {
return fmt.Errorf("error, when reading database backup file. Error: %v", err)
}
uReq := UploadToBucketRequest{
bucketName: EnvironmentStateBucketName,
fileName: "database_backup",
data: fileBytes,
}
err = uploadToBucket(
ctx,
uReq,
)
if err != nil {
return fmt.Errorf("error, when uploadToBucket() for backupDatabase(). Error: %v", err)
}
log.Printf("database backup complete")
return nil
}
type UploadToBucketRequest struct {
bucketName string
fileName string
data []byte
}
// uploadToBucket assuming GCP for example
func uploadToBucket(
ctx context.Context,
req UploadToBucketRequest,
) error {
client, err := storage.NewClient(ctx)
if err != nil {
return fmt.Errorf("error, failed to create GCP storage client. Error: %v", err)
}
bucket := client.Bucket(req.bucketName)
object := bucket.Object(req.fileName)
customTime := time.Now()
writer := object.NewWriter(ctx)
writer.CustomTime = customTime
reader := bytes.NewReader(req.data)
if _, err = io.Copy(writer, reader); err != nil {
return fmt.Errorf("error, failed to upload %s to GCP bucket: %s. Error: %v", req.fileName, req.bucketName, err)
}
if err = writer.Close(); err != nil {
return fmt.Errorf("error, failed to close file writer. Error: %v", err)
}
return nil
}
// downloadFromBucket assuming GCP for example
func downloadFromBucket(
ctx context.Context,
bucketName,
objectName string,
generation int64,
) (bucketStuff []byte, err error) {
var closeErr error
// todo look into reusing the storage client
client, err := storage.NewClient(ctx)
if err != nil {
return nil, fmt.Errorf("error, when attempting to create new GCP storage client. Error: %v", err)
}
defer func(client *storage.Client) {
closeErr = client.Close()
if closeErr != nil {
err = fmt.Errorf("error, when attempting to close the GCP storage client. Error: %w", closeErr)
return
}
}(client)
bucket := client.Bucket(bucketName)
var object *storage.ObjectHandle
if generation == 0 {
object = bucket.Object(objectName)
} else {
object = bucket.Object(objectName).Generation(generation)
}
rc, err := object.NewReader(ctx)
if err != nil {
return nil, fmt.Errorf("error, when attempting to create new reader from gcp bucket object. Error: %w", err)
}
defer func(rc *storage.Reader) {
closeErr = rc.Close()
if closeErr != nil {
err = fmt.Errorf("error, when attempting to close object reader. Error: %w", closeErr)
return
}
}(rc)
data, err := io.ReadAll(rc)
if err != nil {
return nil, fmt.Errorf("error, when attempting to read from object reader. Error: %v", err)
}
return data, nil
}
// log exact query used for debugging queries
func FormatQuery(query string, args ...interface{}) string {
for _, arg := range args {
var formatted string
switch v := arg.(type) {
case string:
formatted = fmt.Sprintf("'%v'", v)
case sql.NullString:
if v.Valid {
formatted = fmt.Sprintf("'%v'", v.String)
} else {
formatted = "NULL"
}
default:
formatted = fmt.Sprintf("%v", v)
}
query = strings.Replace(query, "?", formatted, 1)
}
return base64.StdEncoding.EncodeToString([]byte(query))
}
// Example:
if err != nil {
theStatement = shared.FormatQuery(theStatement, args...)
return nil, fmt.Errorf("error, when attempting to retrieve records. Query: %s. Error: %v", theStatement, err)
}
ctx := context.TODO()
tx, err := _CONNECTION_POOL_.Begin(ctx)
if err != nil {
return fmt.Errorf("error, when attempting to start a transaction. Error: %v", err)
}
err = func() error {
_, err2 := tx.Exec(ctx, `_QUERY_HERE_`)
if err2 != nil {
return fmt.Errorf("error, when executing query to _do_something_. Error: %v", err2)
}
_, err2 = tx.Exec(ctx, `_QUERY_HERE_`)
if err2 != nil {
return fmt.Errorf("error, when executing query to _do_something_. Error: %v", err2)
}
_, err2 = tx.Exec(ctx, `_QUERY_HERE_`)
if err2 != nil {
return fmt.Errorf("error, when executing query to _do_something_. Error: %v", err2)
}
return nil
}()
if err != nil {
rollBackErr := tx.Rollback(ctx)
if rollBackErr != nil {
return fmt.Errorf("error, when attempting to roll back commit: Rollback Error: %v, Original Error: %v", rollBackErr, err)
}
return fmt.Errorf("error, when attempting to perform database transaction. Error: %v", err)
}
err = tx.Commit(ctx)
if err != nil {
return fmt.Errorf("error, when attempting to commit the transaction to the database. Error: %v", err)
}
var result string
err = _DATABASE_CONNECTION_POOL_.QueryRow(
`SELECT some_column
FROM some_table
WHERE some_column = ?
AND some_other_column IS NULL
ORDER BY id DESC
LIMIT 1`,
someArg,
).Scan(
&result,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// todo implement
} else {
return nil, fmt.Errorf("error, when attempting to execute sql statement. Error: %w", err)
}
}
rows, err := _DATABASE_CONNECTION_POOL_.Query(
`SELECT st.id, st.some_column
FROM some_table st
JOIN some_other_table sot ON st.id = sot.id
WHERE sot.some_column = ?`,
someArg,
)
defer rows.Close()
if err != nil {
return nil, fmt.Errorf("error, when attempting to retrieve records. Error: %v", err)
}
var result []_RESULT_STRUCT_
for rows.Next() {
var r _RESULT_STRUCT_
err = rows.Scan(
&r.Id,
&r._SOME_PROPERTY_,
)
if err != nil {
return nil, fmt.Errorf("error, when scanning database rows. Error: %v", err)
}
result = append(result, r)
}
err = rows.Err()
if err != nil {
return nil, fmt.Errorf("error, when iterating through database rows. Error: %v", err)
}
return result, nil
func SmartRedirect(w http.ResponseWriter, r *http.Request, u string) {
if r.Header.Get("HX-Request") == "true" { // was triggered from button press
w.Header().Set("HX-Redirect", u)
} else { // was triggered from page refresh
http.Redirect(w, r, u, http.StatusSeeOther)
}
}
package template_golang
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
// http.MethodPost
// http.MethodGet
// http.MethodPut
// http.MethodDelete
func HttpRequest(method, theURL string, reqBody any, dst any) error {
var body io.Reader
if reqBody != nil {
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(reqBody); err != nil {
return fmt.Errorf("error, %s %q: encoding request body: %w", method, theURL, err)
}
body = buf
}
request, err := http.NewRequest(method, theURL, body)
if err != nil {
return fmt.Errorf("error, when generating get request: %w", err)
}
request.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(request)
if err != nil {
return fmt.Errorf("error, request if http method %s failed: %w", method, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
const maxErrorBody = 4 << 10 // 4 KiB
rb, readErr := io.ReadAll(&io.LimitedReader{R: resp.Body, N: maxErrorBody})
if readErr != nil {
return fmt.Errorf("error, when reading error resp body: %w", readErr)
}
if resp.StatusCode == http.StatusNotFound {
log.Printf("received a 404 when attempting url. Url: %s", request.URL)
}
return fmt.Errorf(
"error, when performing get request. REQUEST METHOD: %s. RESPONSE CODE: %d. RESPONSE MESSAGE: %s",
method,
resp.StatusCode,
string(rb),
)
}
// Caller doesn't care about response body; just drain so connection can be reused.
if dst == nil {
_, _ = io.Copy(io.Discard, resp.Body)
return nil
}
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(dst); err != nil {
if err == io.EOF {
return fmt.Errorf("%s %q: empty response body", method, theURL)
}
return fmt.Errorf("error, %s %q: decoding JSON response: %w", method, theURL, err)
}
return nil
}
_, err := _DATABASE_CONNECTION_POOL_.Exec(
"INSERT INTO _SOME_TABLE_ (_SOME_COLUMN_, _SOME_COLUMN_) VALUES ($1, $2)",
r._SOME_PROPERTY_,
r._SOME_PROPERTY_,
)
if err != nil {
return fmt.Errorf("error, when attempting to persist a request for a stress test: %v", err)
}
// In migration file:
// -- Set busy_timeout to 5000 milliseconds (5 seconds)
// -- This ensures SQLite will wait up to 5 seconds for a lock before returning SQLITE_BUSY
// PRAGMA busy_timeout = 5000;
// PRAGMA journal_mode = WAL;
// -- Create a test table for health check lock testing
// -- This table is used to test database lock detection
// CREATE TABLE IF NOT EXISTS health_check_test (
// id INTEGER PRIMARY KEY,
// test_value TEXT
// );
// CheckDatabaseLock checks if the database is locked for more than 5 seconds.
// With PRAGMA busy_timeout = 5000, SQLite will only return SQLITE_BUSY after waiting 5 seconds.
// Returns an error if SQLITE_BUSY is encountered, indicating the database has been locked for more than 5 seconds.
func (m *HealthModel) CheckDatabaseLock(ctx context.Context, timeout time.Duration) error {
// Create a context with timeout for the database check
checkCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Attempt to acquire a write lock immediately
// With PRAGMA busy_timeout = 5000, this will wait up to 5 seconds before returning SQLITE_BUSY
_, err := m.database..ExecContext(checkCtx, "DELETE FROM health_check_test WHERE id = 999")
if err != nil {
// Check if the error is a SQLite error with BUSY_TIMEOUT or BUSY code
var sqliteErr *sqlite3.Error
if errors.As(err, &sqliteErr) {
// Check for BUSY_TIMEOUT (extended error code) or BUSY (base error code)
if sqliteErr.ExtendedCode() == sqlite3.BUSY_TIMEOUT || sqliteErr.Code() == sqlite3.BUSY {
msg := fmt.Sprintf("database has been locked for more than 5 seconds: %v", err)
log.Println(msg)
return errors.New(msg)
}
}
// Other errors (connection issues, etc.)
msg := fmt.Sprintf("database error: %v", err)
log.Println(msg)
return errors.New(msg)
}
// Successfully acquired the lock within 5 seconds
return nil
}
type ProfilerMutex struct {
mu sync.Mutex
startTime time.Time
queryName string
}
func (pm *ProfilerMutex) Lock(queryName string) {
pm.mu.Lock()
pm.startTime = time.Now()
pm.queryName = queryName
}
func (pm *ProfilerMutex) Unlock() {
log.Printf("%s. Duration: %s\n", pm.queryName, time.Since(pm.startTime))
pm.mu.Unlock()
}
import (
"github.com/nats-io/nats.go"
)
opts := []nats.Option{
nats.MaxReconnects(-1),
nats.Token(config.DbExpressAgentKey),
nats.ProxyPath("/nats/wss"), // only applicable to websockets (unconfirmed), but this configuration has to be on the client side for some reason. Cannot put the path in the URL, the nats url can only take host and port, no paths allowed
}
// url can be any of these as long as the server is hosting that protocol, your client config will use the specified protocal based on what is provided as the url:
// nats://localhost:4444
// wss://localhost:4444
// ws://localhost:4444
Conn, err = nats.Connect(url, opts...)
if err != nil {
panic(fmt.Errorf("error, when connecting to nats service for client init. Error: %v", err))
}
import (
"github.com/nats-io/nats-server/v2/server"
)
// When behind proxy make sure the clients configure nats.ProxyPath, see ./nats_client.go
opts := &server.Options{
Port: 3000,
Authorization: "some-super-secret-token",
Websocket: server.WebsocketOpts{
Host: "0.0.0.0",
Port: 4430,
NoTLS: true, // set to true if your terminating tls with proxy, false if not
},
}
ns, err := server.NewServer(opts)
if err != nil {
log.Fatalf("error, unable to start nats server. Error: %v", err)
}
go ns.Start()
if !ns.ReadyForConnections(10 * time.Second) {
log.Fatalf("error, nats failed to start due to timeout")
}
func printStackTrace() {
// Create a byte slice to hold the stack trace, size 1024 bytes
buf := make([]byte, 1024)
// Capture stack trace into buf, runtime.Stack returns the length of n bytes written
n := runtime.Stack(buf, false)
// Print the stack trace as a string
log.Printf("Stack trace:\n%s\n", buf[:n])
}
stageStart := time.Now()
logStage := func(stage string, stageStart time.Time) {
log.Printf("%s. Duration: %s\n", stage, time.Since(stageStart))
}
logStage("time spent doing XXXXXXX", stageStart)
// reference: https://en.wikipedia.org/wiki/Orders_of_magnitude_(time)
// nanosecond ns One billionth of one second 1 ns: The time needed to execute one machine cycle by a 1 GHz microprocessor
// microsecond μs One millionth of one second 1 μs: The time needed to execute one machine cycle by an Intel 80186 microprocessor
// millisecond ms One thousandth of one second
result, err := config.RedisConnectionPool.Get(ctx, key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, nil
}
return nil, fmt.Errorf("error, when attempting to get key %s from redis. Error %v", key, err)
}
rpl := _REDIS_CONNECTION_POOL_.Pool.Pipeline()
var results []model._SOME_STRUCT_
var cmds []*redis.StringCmd
// Prepare the pipeline commands
for _, key := range constants._REDIS_KEY_LIST_ {
cmds = append(cmds, rpl.Get(ctx, key))
}
// Execute the pipeline
_, err := rpl.Exec(ctx)
if err != nil {
return nil, fmt.Errorf("error, when executing Redis pipeline: %v", err)
}
// Collect and process results
for i, cmd := range cmds {
var redisResult string
redisResult, err = cmd.Result()
redisValueExists := !errors.Is(err, redis.Nil)
if err != nil && redisValueExists {
return nil, fmt.Errorf("error, when fetching value from redis: %v", err)
}
var parsedResult *model._SOME_STRUCT_
parsedResult = &model._SOME_STRUCT_{}
key := constants._REDIS_KEY_LIST_[i]
if redisValueExists {
err = json.Unmarshal([]byte(redisResult), parsedResult)
if err != nil {
return nil, fmt.Errorf("error, when attempting to unmarshal redis result. Error: %v", err)
}
}
results = append(results, *parsedResult)
}
return results, nil
err := config.RedisConnectionPool.Set(ctx, key, value, expirationDuration).Err()
if err != nil {
return fmt.Errorf("error, when attempting to set redis key: %s. Error: %v", key, err)
}
func RequestWithRetry(client *http.Client, request *http.Request, retryLimit int, retryBackoffInSeconds int) (*http.Response, error) {
var response *http.Response
retryBackoff := time.Duration(retryBackoffInSeconds) * time.Second
var err error
for i := 0; i < retryLimit; i++ {
response, err = func(client *http.Client, r *http.Request) (*http.Response, error) {
response, err = client.Do(r)
if response != nil && (response.StatusCode < 200 || response.StatusCode > 299) {
if response.StatusCode == http.StatusNotFound {
log.Printf("recieved a 404 when attempting url: %s", r.URL)
}
var rb []byte
rb, err = io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("error, when reading error response body: %v", err)
}
return nil, fmt.Errorf("error, when performing get request. ERROR: %v. RESPONSE CODE: %d. RESPONSE MESSAGE: %s", err, response.StatusCode, string(rb))
}
if err != nil {
if response != nil {
err = fmt.Errorf("error: %v. RESPONSE CODE: %d", err, response.StatusCode)
}
return nil, fmt.Errorf("error, when performing post request. ERROR: %v", err)
}
return response, nil
}(client, request)
if err != nil {
err = fmt.Errorf("error, when attempting to send request. Error: %v", err)
if response != nil {
err = fmt.Errorf("%v. Response code: %d", err, response.StatusCode)
}
log.Printf("error, request failed, retrying in %d seconds", retryBackoffInSeconds)
time.Sleep(retryBackoff)
} else {
break
}
}
if err != nil {
return nil, err
}
return response, nil
}
import (
"context"
"fmt"
"sync"
"time"
"golang.org/x/sync/singleflight"
)
type User struct {
ID string
Name string
}
type UserService struct {
group singleflight.Group
cache sync.Map
}
func (s *UserService) GetUser(ctx context.Context, userID string) (User, error) {
if cached, ok := s.cache.Load(userID); ok {
return cached.(User), nil
}
value, err, _ := s.group.Do(userID, func() (any, error) {
// Double check once we're inside the singleflight call in case
// another caller filled the cache while we were waiting.
if cached, ok := s.cache.Load(userID); ok {
return cached.(User), nil
}
user, err := fetchUserFromDB(ctx, userID)
if err != nil {
return nil, err
}
s.cache.Store(userID, user)
return user, nil
})
if err != nil {
return User{}, fmt.Errorf("get user %q: %w", userID, err)
}
return value.(User), nil
}
func (s *UserService) RefreshUser(ctx context.Context, userID string) (User, bool, error) {
value, err, shared := s.group.Do(userID, func() (any, error) {
user, err := fetchUserFromDB(ctx, userID)
if err != nil {
return nil, err
}
s.cache.Store(userID, user)
return user, nil
})
if err != nil {
return User{}, false, fmt.Errorf("refresh user %q: %w", userID, err)
}
return value.(User), shared, nil
}
func fetchUserFromDB(ctx context.Context, userID string) (User, error) {
select {
case <-ctx.Done():
return User{}, ctx.Err()
case <-time.After(250 * time.Millisecond):
fmt.Printf("expensive fetch for user %s\n", userID)
return User{ID: userID, Name: "Ada"}, nil
}
}
func exampleSingleflight() {
service := &UserService{}
var wg sync.WaitGroup
for range 5 {
wg.Add(1)
go func() {
defer wg.Done()
user, shared, err := service.RefreshUser(context.Background(), "user-123")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("user=%+v shared=%t\n", user, shared)
}()
}
wg.Wait()
}
package database
import (
"strconv"
"strings"
"sort"
"fmt"
"log"
"os"
"regexp"
"database/sql"
)
func (c *Client) migrate() error {
initExists, err := c.doesInitTableExist()
if err != nil {
return fmt.Errorf("error, when checking if database initialization is needed. Error: %v", err)
}
if !initExists {
log.Println("database is not initialized, creating init table")
err = c.createInitTable()
if err != nil {
return fmt.Errorf("error, when attempting to create init table. Error: %v", err)
}
log.Println("database initialization complete")
}
log.Println("checking for migrations")
dirEntries, err := os.ReadDir(c.migrationDir)
if err != nil {
return fmt.Errorf("error, when attempting to read database directory. Directory: %s. Error: %v", c.migrationDir, err)
}
var migrationFileCandidateFileNames []string
for _, entry := range dirEntries {
if !entry.IsDir() {
migrationFileCandidateFileNames = append(migrationFileCandidateFileNames, entry.Name())
}
}
migrationFiles := c.filterForMigrationFiles(migrationFileCandidateFileNames)
var migrationsCompleted []string
noMigrationsToProcessMessage := "no database migration files to process, skipping migrations"
if len(migrationFiles) == 0 {
log.Println(noMigrationsToProcessMessage)
return nil
} else {
migrationsCompleted, err = c.checkForCompletedMigrations()
if err != nil {
return fmt.Errorf("error, when checking for completed migrations: %v", err)
}
}
migrationsNeeded := c.determineMigrationsNeeded(migrationFiles, migrationsCompleted)
migrationsNeededSorted := c.sortMigrationsNeededFiles(migrationsNeeded)
for _, fileName := range migrationsNeededSorted {
log.Printf("attempting to perform database migration with %s", fileName)
filePath := fmt.Sprintf("%s/%s", c.migrationDir, fileName)
err = c.executeSQLFile(filePath)
if err != nil {
return fmt.Errorf("error, when executing sql script: Filename: %s. Error: %v", fileName, err)
}
err = c.recordSuccessfulMigration(fileName)
if err != nil {
return fmt.Errorf("error, when attempting to record a successful migration. Error: %v", err)
}
}
log.Println("finished database schema changes")
return nil
}
func (c *Client) createInitTable() error {
_, err := c.conn.Exec(
`CREATE TABLE init (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
migration_file_name TEXT NOT NULL UNIQUE
)`)
if err != nil {
return fmt.Errorf("error, when executing query to create init table. Error: %v", err)
}
return nil
}
func (c *Client) sortMigrationsNeededFiles(needed []string) []string {
re := regexp.MustCompile(`^(\d+)`)
sort.Slice(needed, func(i, j int) bool {
num1, _ := strconv.Atoi(re.FindStringSubmatch(needed[i])[1])
num2, _ := strconv.Atoi(re.FindStringSubmatch(needed[j])[1])
return num1 < num2
})
return needed
}
func (c *Client) determineMigrationsNeeded(migrationFiles []string, migrationsCompleted []string) []string {
var migrationsNeeded []string
migrationsCompletedMap := make(map[string]bool)
for _, value := range migrationsCompleted {
migrationsCompletedMap[value] = true
}
for _, value := range migrationFiles {
if !migrationsCompletedMap[value] {
migrationsNeeded = append(migrationsNeeded, value)
}
}
return migrationsNeeded
}
func (c *Client) filterForMigrationFiles(candidates []string) []string {
var migrationFiles []string
re := regexp.MustCompile(`^\d+`)
for _, fileName := range candidates {
if re.MatchString(fileName) {
migrationFiles = append(migrationFiles, fileName)
}
}
return migrationFiles
}
func (c *Client) recordSuccessfulMigration(fileName string) error {
_, err := c.conn.Exec(
`INSERT INTO init (migration_file_name)
VALUES (?)`,
fileName,
)
if err != nil {
return fmt.Errorf("error, when attempting to run sql command. Error: %v", err)
}
return nil
}
func (c *Client) checkForCompletedMigrations() (results []string, err error) {
var rows *sql.Rows
rows, err = c.conn.Query(
`SELECT migration_file_name
FROM init`,
)
defer func() {
err = rows.Err()
if err != nil {
err = fmt.Errorf("error, when reading rows. Error: %v", err)
}
rows.Close()
}()
if err != nil {
return nil, fmt.Errorf("error, when attempting to retrieve pending migrations. Error: %v", err)
}
for rows.Next() {
var result string
err = rows.Scan(
&result,
)
if err != nil {
return nil, fmt.Errorf("error, when scanning for pending migrations. Error: %v", err)
}
results = append(results, result)
}
return results, nil
}
func (c *Client) doesInitTableExist() (bool, error) {
var result bool
row := c.conn.QueryRow(
`SELECT count(*)
FROM sqlite_master
WHERE type='table'
AND name='init'`,
)
err := row.Scan(
&result,
)
if err != nil {
return false, fmt.Errorf("error, when checking to see if database had been initialized. Error: %v", err)
}
return result, nil
}
func (c *Client) executeSQLFile(filePath string) error {
content, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("error, failed to read SQL file. Error: %w", err)
}
sql := string(content)
queries := strings.Split(sql, ";")
for _, query := range queries {
query = strings.TrimSpace(query)
if query == "" {
continue
}
_, err = c.conn.Exec(query)
if err != nil {
return fmt.Errorf("error, failed to execute. Query: %s. Error: %v", query, err)
}
}
return nil
}
type Task struct {
Fn func(chan string)
Result chan string
}
type TaskQueue struct {
taskChan chan Task
}
var taskQueueInstance *TaskQueue
var once sync.Once
func GetTaskQueueInstance() *TaskQueue {
once.Do(func() {
taskQueueInstance = &TaskQueue{
taskChan: make(chan Task, 100),
}
taskQueueInstance.StartWorkers(10)
})
return taskQueueInstance
}
func (q *TaskQueue) StartWorkers(numWorkers int) {
for i := 0; i < numWorkers; i++ {
go func() {
for task := range q.taskChan {
task.Fn(task.Result)
}
}()
}
}
func (q *TaskQueue) AddTask(task Task) {
q.taskChan <- task
}
func exampleUsage() {
// Fetch the singleton instance and start 10 workers
queue := GetTaskQueueInstance()
// Create yer own channel to receive the result
resultChan := make(chan string)
// Add the task
queue.AddTask(Task{
Fn: func(result chan string) {
result <- "This task be complete, yarrr!"
},
Result: resultChan,
})
// Wait for result
result := <-resultChan
fmt.Println("Result: ", result)
// Yarrr, add more tasks as ye please!
}
package leader
import (
"context"
"errors"
"github.com/jackc/pgx/v5/pgxpool"
)
const leaderLockID int64 = 123456 // Pick a stable, unique value for this job.
func TryAcquire(ctx context.Context, pool *pgxpool.Pool) (*pgxpool.Conn, bool, error) {
conn, err := pool.Acquire(ctx)
if err != nil {
return nil, false, err
}
var acquired bool
err = conn.QueryRow(ctx,
`SELECT pg_try_advisory_lock($1)`,
// SELECT pg_try_advisory_lock(5391437);
// true = acquired
// false = another session owns it
// does not wait till lock is available
// SELECT pg_advisory_lock(5391437);
// waits until available; then returns
leaderLockID,
).Scan(&acquired)
if err != nil {
conn.Release()
return nil, false, err
}
if !acquired {
conn.Release()
return nil, false, nil
}
// Keep this conn checked out while this process is leader.
return conn, true, nil
}
func Release(ctx context.Context, conn *pgxpool.Conn) error {
var released bool
err := conn.QueryRow(ctx,
`SELECT pg_advisory_unlock($1)`,
leaderLockID,
).Scan(&released)
conn.Release()
if err != nil {
return err
}
if !released {
return errors.New("leader lock was not held")
}
return nil
}
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func main() {
os.Exit(run())
}
func run() int {
// Cancel when the process receives Ctrl-C (SIGINT) or a normal stop signal
// from Docker/Kubernetes/systemd (SIGTERM).
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(signals) // unregister signal handling / release resources
appCtx, cancelApp := context.WithCancelCause(context.Background())
defer cancelApp(nil)
go func() {
sig := <-signals
cancelApp(fmt.Errorf("received shutdown signal: %s", sig))
}()
var cleanupWg sync.WaitGroup
// Your normal application work here
// before your operations that require cleanup ensure you add cleanupWg.Add(1)
cleanupWg.Add(1)
go appStuffDo(appCtx, &cleanupWg, cancelApp)
cleanupWg.Add(1)
go otherAppStuffDo(appCtx, &cleanupWg)
<-appCtx.Done()
log.Printf("shutting down: %v", context.Cause(appCtx))
cleanupDone := make(chan struct{})
go func() {
cleanupWg.Wait()
close(cleanupDone)
}()
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
select {
case <-cleanupDone:
log.Println("cleanup succeeded")
return 0
case <-cleanupCtx.Done():
log.Println("cleanup deadline exceeded; exiting")
return 1
}
}
func appStuffDo(ctx context.Context, wg *sync.WaitGroup, cancelApp context.CancelCauseFunc) {
// if failure during init then use cancelApp. Init failures should never be ignored otherwise we won't discover a problem until a use-case is triggered
defer wg.Done()
// normal work until shutdown
<-ctx.Done()
// component-specific cleanup
}
func otherAppStuffDo(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
// normal work until shutdown
<-ctx.Done()
// component-specific cleanup
}
Use ANY($1::text[]) when comparing a column against a Go slice:
statuses := []string{"pending", "active"}
rows, err := pool.Query(
ctx,
`SELECT id, status
FROM tasks
WHERE status = ANY($1::text[])`,
statuses,
)
This is equivalent to:
WHERE status IN ('pending', 'active')
Key details:
The brackets matter: use text[], not text().
Concatenate a parameter with PostgreSQL’s % wildcard:
prefix := "intro"
rows, err := pool.Query(
ctx,
`SELECT id, title
FROM articles
WHERE title LIKE $1 || '%'`,
prefix,
)
This matches titles beginning with intro, such as:
introduction intro to databases introductory guide
Here:
Other common patterns:
-- Ends with the supplied value WHERE title LIKE '%' || $1
-- Contains the supplied value WHERE title LIKE '%' || $1 || '%'
-- Case-insensitive prefix match WHERE title ILIKE $1 || '%'
If the parameter itself can contain % or _, those characters also act as wildcards and may need escaping when literal matching is required.