Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ import (

"github.com/acronis/go-dbkit"

// Import the `mysql` package for registering the retryable function for MySQL transient errors (like deadlocks).
// Import the `mysql` package for registering the retryable function for MySQL transient
// errors (like deadlocks).
_ "github.com/acronis/go-dbkit/mysql"
)

Expand Down Expand Up @@ -89,11 +90,13 @@ func main() {
}
defer db.Close()

// Execute a transaction with a custom retry policy (exponential backoff with 3 retries, starting from 10ms).
// Execute a transaction with a custom retry policy (exponential backoff with 3 retries,
// starting from 10ms).
retryPolicy := retry.NewConstantBackoffPolicy(10*time.Millisecond, 3)
if err = dbkit.DoInTx(context.Background(), db, func(tx *sql.Tx) error {
// Execute your transactional operations here.
// Example: _, err := tx.Exec("UPDATE users SET last_login = ? WHERE id = ?", time.Now(), 1)
// Example: _, err := tx.Exec("UPDATE users SET last_login = ? WHERE id = ?",
// time.Now(), 1)
return nil
}, dbkit.WithRetryPolicy(retryPolicy)); err != nil {
log.Fatal(err)
Expand All @@ -112,22 +115,23 @@ package main
import (
"context"
"database/sql"
"errors"
"fmt"
stdlog "log"
"net/http"
"os"
"time"

"github.com/acronis/go-appkit/log"
"github.com/gocraft/dbr/v2"

"github.com/acronis/go-dbkit"
"github.com/acronis/go-dbkit/dbrutil"
)

func main() {
logger, loggerClose := log.NewLogger(&log.Config{Output: log.OutputStderr, Level: log.LevelInfo})
logger, loggerClose := log.NewLogger(&log.Config{
Output: log.OutputStderr,
Level: log.LevelInfo,
})
defer loggerClose()

// Create a Prometheus metrics collector.
Expand All @@ -150,18 +154,21 @@ func main() {
txRunner := dbrutil.NewTxRunner(conn, &sql.TxOptions{Isolation: sql.LevelReadCommitted}, nil)

// Execute function in a transaction.
// The transaction will be automatically committed if the function returns nil, otherwise it will be rolled back.
// The transaction will be automatically committed if the function returns nil, otherwise it
// will be rolled back.
if dbErr := txRunner.DoInTx(context.Background(), func(tx dbr.SessionRunner) error {
var result int
return tx.Select("SLEEP(1)").
Comment(annotateQuery("long_operation")). // Annotate the query for Prometheus metrics and slow query log.
// Annotate the query for Prometheus metrics and slow query log.
Comment(annotateQuery("long_operation")).
LoadOne(&result)
}); dbErr != nil {
stdlog.Fatal(dbErr)
}

// The following log message will be printed:
// {"level":"warn","time":"2025-02-14T16:29:55.429257+02:00","msg":"slow SQL query","pid":14030,"annotation":"query:long_operation","duration_ms":1007}
// {"level":"warn","time":"2025-02-14T16:29:55.429257+02:00","msg":"slow SQL query",
// "pid":14030, "annotation":"query:long_operation","duration_ms":1007}

// Prometheus metrics will be collected:
// db_query_duration_seconds_bucket{query="query:long_operation",le="2.5"} 1
Expand All @@ -187,8 +194,10 @@ func openDB(eventReceiver dbr.EventReceiver) (*dbr.Connection, error) {
},
}

// Open database with instrumentation based on the provided event receiver (see github.com/gocraft/dbr doc for details).
// Opening includes configuring the max open/idle connections and their lifetime and pinging the database.
// Open database with instrumentation based on the provided event receiver
// (see github.com/gocraft/dbr doc for details).
// Opening includes configuring the max open/idle connections and their lifetime and
// pinging the database.
conn, err := dbrutil.Open(cfg, true, eventReceiver)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
Expand Down Expand Up @@ -238,11 +247,13 @@ func main() {
}

// Do some work exclusively.
const lockKey = "test-lock-key-1" // Unique key that will be used to ensure exclusive execution among multiple instances
err = distrlock.DoExclusively(ctx, db, dbkit.DialectMySQL, lockKey, func(ctx context.Context) error {
time.Sleep(10 * time.Second) // Simulate work.
return nil
})
// Unique key that will be used to ensure exclusive execution among multiple instances
const lockKey = "test-lock-key-1"
err = distrlock.DoExclusively(ctx, db, dbkit.DialectMySQL, lockKey,
func(ctx context.Context) error {
time.Sleep(10 * time.Second) // Simulate work.
return nil
})
if err != nil {
log.Fatal(err)
}
Expand All @@ -253,7 +264,7 @@ More examples and detailed usage instructions can be found in the `distrlock` pa

## License

Copyright © 2024 Acronis International GmbH.
Copyright © 2024-2025 Acronis International GmbH.

Licensed under [MIT License](./LICENSE).

Expand Down
9 changes: 4 additions & 5 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func NewConfig(supportedDialects []Dialect, options ...ConfigOption) *Config {

// NewConfigWithKeyPrefix creates a new instance of the Config with a key prefix.
// This prefix will be used by config.Loader.
//
// Deprecated: use NewConfig with WithKeyPrefix instead.
func NewConfigWithKeyPrefix(keyPrefix string, supportedDialects []Dialect) *Config {
if keyPrefix != "" {
Expand Down Expand Up @@ -266,14 +267,12 @@ func (c *Config) DriverNameAndDSN() (driverName, dsn string) {
}

func (c *Config) setDialectSpecificConfig(dp config.DataProvider) error {
var err error

var supportedDialectsStr []string
supportedDialectsStr := make([]string, 0, len(c.SupportedDialects()))
for _, dialect := range c.SupportedDialects() {
supportedDialectsStr = append(supportedDialectsStr, string(dialect))
}
var dialectStr string
if dialectStr, err = dp.GetStringFromSet(cfgKeyDialect, supportedDialectsStr, false); err != nil {
dialectStr, err := dp.GetStringFromSet(cfgKeyDialect, supportedDialectsStr, false)
if err != nil {
return err
}
c.Dialect = Dialect(dialectStr)
Expand Down
35 changes: 24 additions & 11 deletions dbrutil/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,23 @@ package main
import (
"context"
"database/sql"
"errors"
"fmt"
stdlog "log"
"net/http"
"os"
"time"

"github.com/acronis/go-appkit/log"
"github.com/gocraft/dbr/v2"

"github.com/acronis/go-dbkit"
"github.com/acronis/go-dbkit/dbrutil"
)

func main() {
logger, loggerClose := log.NewLogger(&log.Config{Output: log.OutputStderr, Level: log.LevelInfo})
logger, loggerClose := log.NewLogger(&log.Config{
Output: log.OutputStderr,
Level: log.LevelInfo,
})
defer loggerClose()

// Create a Prometheus metrics collector.
Expand All @@ -59,18 +60,21 @@ func main() {
txRunner := dbrutil.NewTxRunner(conn, &sql.TxOptions{Isolation: sql.LevelReadCommitted}, nil)

// Execute function in a transaction.
// The transaction will be automatically committed if the function returns nil, otherwise it will be rolled back.
// The transaction will be automatically committed if the function returns nil, otherwise it
// will be rolled back.
if dbErr := txRunner.DoInTx(context.Background(), func(tx dbr.SessionRunner) error {
var result int
return tx.Select("SLEEP(1)").
Comment(annotateQuery("long_operation")). // Annotate the query for Prometheus metrics and slow query log.
// Annotate the query for Prometheus metrics and slow query log.
Comment(annotateQuery("long_operation")).
LoadOne(&result)
}); dbErr != nil {
stdlog.Fatal(dbErr)
}

// The following log message will be printed:
// {"level":"warn","time":"2025-02-14T16:29:55.429257+02:00","msg":"slow SQL query","pid":14030,"annotation":"query:long_operation","duration_ms":1007}
// {"level":"warn","time":"2025-02-14T16:29:55.429257+02:00","msg":"slow SQL query",
// "pid":14030, "annotation":"query:long_operation","duration_ms":1007}

// Prometheus metrics will be collected:
// db_query_duration_seconds_bucket{query="query:long_operation",le="2.5"} 1
Expand All @@ -96,8 +100,10 @@ func openDB(eventReceiver dbr.EventReceiver) (*dbr.Connection, error) {
},
}

// Open database with instrumentation based on the provided event receiver (see github.com/gocraft/dbr doc for details).
// Opening includes configuring the max open/idle connections and their lifetime and pinging the database.
// Open database with instrumentation based on the provided event receiver
// (see github.com/gocraft/dbr doc for details).
// Opening includes configuring the max open/idle connections and their lifetime and
// pinging the database.
conn, err := dbrutil.Open(cfg, true, eventReceiver)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
Expand Down Expand Up @@ -184,7 +190,14 @@ func main() {
mux := http.NewServeMux()
mux.Handle("/long-operation", h)
mux.Handle("/metrics", promhttp.Handler())
if srvErr := http.ListenAndServe(":8080", mux); srvErr != nil && errors.Is(srvErr, http.ErrServerClosed) {
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
if srvErr := srv.ListenAndServe(); srvErr != nil && !errors.Is(srvErr, http.ErrServerClosed) {
stdlog.Fatalf("failed to start server: %v", srvErr)
}
}
Expand Down Expand Up @@ -241,7 +254,7 @@ db_query_duration_seconds_count{query="query:long_operation"} 1

## License

Copyright © 2024 Acronis International GmbH.
Copyright © 2025-2026 Acronis International GmbH.

Licensed under [MIT License](./../LICENSE).

Expand Down
6 changes: 4 additions & 2 deletions dbrutil/dbrutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (e *TxRollbackError) Error() string {
return fmt.Sprintf("error while transaction rollback: %s", e.Inner)
}

// TxBeginError is a error that may occur when begging transaction is failed.
// TxBeginError is an error that may occur when beginning a transaction fails.
type TxBeginError struct {
Inner error
}
Expand All @@ -81,7 +81,7 @@ func (e *TxBeginError) Unwrap() error {

// Error returns a string representation of TxBeginError.
func (e *TxBeginError) Error() string {
return fmt.Sprintf("error while begging transaction: %s", e.Inner)
return fmt.Sprintf("error while beginning transaction: %s", e.Inner)
}

// TxRunner can begin a new transaction and provides the ability to execute code inside already started one.
Expand Down Expand Up @@ -121,6 +121,8 @@ func (s *TxSession) BeginTx(ctx context.Context) (*dbr.Tx, error) {

// DoInTx begins a new transaction, calls passed function and do commit or rollback
// depending on whether the function returns an error or not.
//
//nolint:contextcheck // inherited context is replaced for sqlite3, see comment below
func (s *TxSession) DoInTx(ctx context.Context, fn func(runner dbr.SessionRunner) error) error {
if s.Dialect == dialect.SQLite3 {
// race of ctx cancel with transaction begin leads to 'cannot start a transaction within a transaction'
Expand Down
19 changes: 13 additions & 6 deletions dbrutil/examples/dbr-instrumentation-1/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright © 2025 Acronis International GmbH.
Copyright © 2025-2026 Acronis International GmbH.

Released under MIT license.
*/
Expand All @@ -22,7 +22,10 @@ import (
)

func main() {
logger, loggerClose := log.NewLogger(&log.Config{Output: log.OutputStderr, Level: log.LevelInfo})
logger, loggerClose := log.NewLogger(&log.Config{
Output: log.OutputStderr,
Level: log.LevelInfo,
})
defer loggerClose()

// Create a Prometheus metrics collector.
Expand All @@ -45,11 +48,13 @@ func main() {
txRunner := dbrutil.NewTxRunner(conn, &sql.TxOptions{Isolation: sql.LevelReadCommitted}, nil)

// Execute function in a transaction.
// The transaction will be automatically committed if the function returns nil, otherwise it will be rolled back.
// The transaction will be automatically committed if the function returns nil, otherwise it
// will be rolled back.
if dbErr := txRunner.DoInTx(context.Background(), func(tx dbr.SessionRunner) error {
var result int
return tx.Select("SLEEP(1)").
Comment(annotateQuery("long_operation")). // Annotate the query for Prometheus metrics and slow query log.
// Annotate the query for Prometheus metrics and slow query log.
Comment(annotateQuery("long_operation")).
LoadOne(&result)
}); dbErr != nil {
stdlog.Fatal(dbErr)
Expand Down Expand Up @@ -83,8 +88,10 @@ func openDB(eventReceiver dbr.EventReceiver) (*dbr.Connection, error) {
},
}

// Open database with instrumentation based on the provided event receiver (see github.com/gocraft/dbr doc for details).
// Opening includes configuring the max open/idle connections and their lifetime and pinging the database.
// Open database with instrumentation based on the provided event receiver
// (see github.com/gocraft/dbr doc for details).
// Opening includes configuring the max open/idle connections and their lifetime and
// pinging the database.
conn, err := dbrutil.Open(cfg, true, eventReceiver)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion dbrutil/examples/dbr-instrumentation-2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func main() {
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
if srvErr := srv.ListenAndServe(); srvErr != nil && errors.Is(srvErr, http.ErrServerClosed) {
if srvErr := srv.ListenAndServe(); srvErr != nil && !errors.Is(srvErr, http.ErrServerClosed) {
stdlog.Fatalf("failed to start server: %v", srvErr)
}
}
Expand Down
19 changes: 11 additions & 8 deletions distrlock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ func main() {
}

// Do some work exclusively.
const lockKey = "test-lock-key-1" // Unique key that will be used to ensure exclusive execution among multiple instances
err = distrlock.DoExclusively(ctx, db, dbkit.DialectMySQL, lockKey, func(ctx context.Context) error {
time.Sleep(10 * time.Second) // Simulate work.
return nil
})
// Unique key that will be used to ensure exclusive execution among multiple instances
const lockKey = "test-lock-key-1"
err = distrlock.DoExclusively(ctx, db, dbkit.DialectMySQL, lockKey,
func(ctx context.Context) error {
time.Sleep(10 * time.Second) // Simulate work.
return nil
})
if err != nil {
log.Fatal(err)
}
Expand Down Expand Up @@ -107,7 +109,8 @@ func main() {
log.Fatal(err)
}

const lockKey = "test-lock-key-2" // Unique key that will be used to ensure exclusive execution among multiple instances
// Unique key that will be used to ensure exclusive execution among multiple instances
const lockKey = "test-lock-key-2"

// Create lock.
lock, err := lockManager.NewLock(ctx, db, lockKey)
Expand All @@ -126,13 +129,13 @@ func main() {
}
}()

time.Sleep(10 * time.Second) // Simulate work
time.Sleep(11 * time.Second) // Simulate work
}
```

## License

Copyright © 2024 Acronis International GmbH.
Copyright © 2024-2025 Acronis International GmbH.

Licensed under [MIT License](./../LICENSE).

Expand Down
Loading