Skip to content

Commit

Permalink
Implement PG version of ExecuteInTx which does fewer roundtrips to th…
Browse files Browse the repository at this point in the history
…e Server

PostgreSQL doesn't benefit from SAVEPOINT/ROLLBACK logic like CockroachDB
does. With this change Nakama checks server DB engine and enables CockroachDB
optimization only when necessary.

There are 2 behviour change in the PG version of ExecuteInTx:

- it retries on all "Class 40" (a.k.a retriable) codes, not just
  serialization error:

	40000 	transaction_rollback
	40002 	transaction_integrity_constraint_violation
	40001 	serialization_failure
	40003 	statement_completion_unknown
	40P01 	deadlock_detected

- It doesn't ignore COMMIT result code anymore
  • Loading branch information
redbaron committed Jul 3, 2023
1 parent e9cd3ba commit ede550b
Showing 1 changed file with 55 additions and 0 deletions.
55 changes: 55 additions & 0 deletions server/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import (

var ErrDatabaseDriverMismatch = errors.New("database driver mismatch")

var isCockroach bool

func DbConnect(ctx context.Context, logger *zap.Logger, config Config) (*sql.DB, string) {
rawURL := config.GetDatabase().Addresses[0]
if !(strings.HasPrefix(rawURL, "postgresql://") || strings.HasPrefix(rawURL, "postgres://")) {
Expand Down Expand Up @@ -89,6 +91,11 @@ func DbConnect(ctx context.Context, logger *zap.Logger, config Config) (*sql.DB,
if err = db.QueryRowContext(pingCtx, "SELECT version()").Scan(&dbVersion); err != nil {
logger.Fatal("Error querying database version", zap.Error(err))
}
if strings.Split(dbVersion, " ")[0] == "CockroachDB" {
isCockroach = true
} else {
isCockroach = false
}

// Periodically check database hostname for underlying address changes.
go func() {
Expand Down Expand Up @@ -226,6 +233,54 @@ func ExecuteRetryable(fn func() error) error {
// ExecuteInTx runs fn inside tx which should already have begun.
// fn is subject to the same restrictions as the fn passed to ExecuteTx.
func ExecuteInTx(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error {
if isCockroach {
return ExecuteInTxCockroach(ctx, db, fn)
} else {
return ExecuteInTxPostgres(ctx, db, fn)
}
}

// Retries fn() if transaction commit returned retryable error code
// Every call to fn() happens in its own transaction. On retry previous transaction
// is ROLLBACK'ed and new transaction is opened.
func ExecuteInTxPostgres(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) (err error) {
var tx *sql.Tx
defer func() {
if tx != nil {
_ = tx.Rollback()
}
}()
for {
tx, err = db.BeginTx(ctx, nil)
if err != nil { // Can fail only if undernath connection is broken
tx = nil
return err
}
if err = fn(tx); err == nil {
err = tx.Commit()
tx = nil
return err
} else {
var pgErr *pgconn.PgError
if errors.As(errorCause(err), &pgErr) && pgErr.Code[:2] == "40" {
// 40XXXX codes are retriable errors
if err = tx.Rollback(); err != nil {
tx = nil
return err
}
continue
} else {
return err
}
}
}
}

// CockroachDB has it's own way to resolve serialization conflicts.
// It has special optimization for `SAVEPOINT cockroach_restart`, called "retry savepoint",
// which increases transaction priority every time it has to ROLLBACK due to serialization conflicts.
// See: https://www.cockroachlabs.com/docs/stable/advanced-client-side-transaction-retries.html
func ExecuteInTxCockroach(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil { // Can fail only if undernath connection is broken
return err
Expand Down

0 comments on commit ede550b

Please sign in to comment.