hermes

package module
v1.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 12, 2020 License: MIT Imports: 13 Imported by: 0

README

Hermes 1.2.4

Hermes wraps the jmoiron/sqlx *sqlx.DB and *sqlx.Tx models in a common interface, hermes.Conn. Makes it easier to build small functions that can be aggregated and used in a single transaction, as well as for testing.

Godoc license

Usage

func Sample(conn hermes.Conn, name string) error {
    tx, err := conn.Begin()
    if err != nil {
        return err
    }
    
    // Will automatically rollback if an error short-circuits the return
    // before tx.Commit() is called...
    defer tx.Close() 

    res, err := conn.Exec("insert into samples (name) values ($1)", name)
    if err != nil {
        return err
    }

    check, err := res.RowsAffected()
    if check == 0 {
        return fmt.Errorf("Failed to insert row (%s)", err)
    }

    return tx.Commit()
}

func main() {
    // Create a connection pool with max 10 connections, min 2 idle connections...
    conn, err := hermes.Connect("postgres", 
        "postgres://postgres@127.0.0.1/engaged?sslmode=disable&connect_timeout=10", 
        10, 2)
    if err != nil {
        return err
    }

    // This works...
    if err := Sample(conn, "Bob"); err != nil {
        fmt.Println("Bob failed!", err.Error())
    }

    // So does this...
    tx, err := conn.Begin()
    if err != nil {
        panic(err)
    }

    // Will automatically rollback if call to sample fails...
    defer tx.Close() 

    if err := Sample(tx, "Frank"); err != nil {
        fmt.Println("Frank failed!", err.Error())
        return
    }

    // Don't forget to commit, or you'll automatically rollback on 
    // "defer tx.Close()" above!
    tx.Commit() 
}

Using a hermes.Conn parameter in a function also opens up in situ testing of database functionality. You can create a transaction in the test case and pass it to a function that takes a hermes.Conn, run any tests on the results of that function, and simply let the transaction rollback at the end of the test to clean up.

var Conn hermes.Conn

// We'll just open one database connection pool to speed up testing, so 
// we're not constantly opening and closing connections.
func TestMain(m *testing.M) {
    conn, err := hermes.Connect("postgres", DBTestURI, 5, 2)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open a database connection: %s\n", err)
        os.Exit(1)
	}
	defer conn.Close()
	
	Conn = conn
	
	os.Exit(m.Run())
}

// Test getting a user account from the database.  The signature for the
// function is:  `func GetUser(conn hermes.Conn, email string) (User, error)`
// 
// Passing a hermes.Conn value to the function means we can pass in either
// a reference to the database pool and really update the data, or we can
// pass in the same transaction reference to both the SaveUser and GetUser
// functions.  If we use a transaction, we can let the transaction roll back 
// after we test these functions, or at any failure point in the test case,
// and we know the data is cleaned up. 
func TestGetUser(t *testing.T) {
    u := User{
        Email: "jdoe@nowhere.com",
        Name: "John Doe",
    }
    
    tx, err := Conn.Begin()
    if err != nil {
        t.Fatal(err)
    }
    defer tx.Close()
    
    if err := db.SaveUser(tx, u); err != nil {
        t.Fatalf("Unable to create a new user account: %s", err)
    }
    
    check, err := db.GetUser(tx, u.Email)
    if err != nil {
        t.Fatalf("Failed to get user by email address: %s", err)
    }
    
    if check.Email != u.Email {
        t.Errorf("Expected user email to be %s; was %s", u.Email, check.Email)
    } 
    
    if check.Name != u.Name {
        t.Errorf("Expected user name to be %s; was %s", u.Name, check.Name)
    } 
    
    // Note:  do nothing...when the test case ends, the `defer tx.Close()`
    // is called, and all the data in this transaction is rolled back out.
}

Using transactions, even if a test case fails a returns prematurely, the database transaction is automatically closed, thanks to defer. The database is cleaned up without any fuss or need to remember to delete the data you created at any point in the test.

Confirm (1.2.3)

If the network environment is unstable, Hermes may be configured to retry connections from the connection pool if those pooled connections lose their connectivity to the database.

To enable connection confirmations, set the hermes.Confirm global variable to
a number greater than 0:

// Create a connection pool with max 10 connections, min 2 idle connections...
conn, err := hermes.Connect("postgres", 
    "postgres://postgres@127.0.0.1/engaged?sslmode=disable&connect_timeout=10", 
    10, 2)
if err != nil {
    return err
}

// Check each database connection at least twice before panicking
hermes.Confirm = 2

When confirmation is enabled, Hermes pings the database prior to making any database requests (begin a transaction, select, insert, etc.). If the ping fails, Hermes waits a moment and tries again, up to the number of times specified in hermes.Confirm. Each try, the sql.Ping() function tries to reconnect to the database.

If Hermes can't open the database connection again after trying repeatedly, it panics and crashes the application. Ideally systemd, Kubernetes, or whatever monitor is watching the application will restart the app and clear up the cause of the problem, or at least alert someone there's a problem.

The hermes.Confirm functionality should be coupled with a connect_timeout value in the PostgreSQL configuration, or the equivalent for whatever database is being used.

This check is not performed with queries made within a transaction. If the connection is lost mid-transaction, there is no point trying to reconnect, as the transaction is lost. At that point, the transaction should simply fail.

There is the performance hit of an additional sql.Ping() request with nearly every database query. If you don't need this functionality, we recommend you don't enable it.

By default this functionality is disabled.

OnFailure (1.1.x)

Hermes supports an OnFailure function that may be called any time a database error appears to be an unrecoverable connection or server failure. This function is set on the database connection (hermes.DB), and may be customized to your environment with custom handling or logging functionality.

// Create a connection pool with max 10 connections, min 2 idle connections...
conn, err := hermes.Connect("postgres", 
    "postgres://postgres@127.0.0.1/engaged?sslmode=disable&connect_timeout=10", 
    10, 2)
if err != nil {
    return err
}

// In a Kubernetes deployment, this will cause the app to shutdown and let
// Kubernetes restart the pod...
conn.OnFailure = hermes.ExitOnFailure

// If the connection fails when conn.Exec is called, hermes.ExitOnFailure
// is called, the application exits, and Kubernetes restarts the app, 
// allowing the app to try to reconnect to the database.
if _, err := conn.Exec("...."); err != nil {
    return err
}
DidConnectionFail (1.1.x)

If OnFailure is not defined, Hermes simply returns the error as normal, expecting the application to do something with it. In these situations, there is a function in Hermes that can check if the error returned by lib/pq is a connection error: hermes.DidConnectionFail. Pass the error to that, and if it's a connection error, the function returns true.

Transaction Timers (1.2.x)

Hermes supports configurable transaction timers to watch transactions and warn the developer if the transaction was open longer than expected. This can be useful in testing for transactions that weren't properly cleaned up.

Simply call hermes.EnableTimeouts(time.Duration, bool) with the worst-case expected transaction duration (presumably less than a second).

func main() {
    // If a transaction takes longer than one second, you'll see an 
    // error message in stderr
    hermes.EnableTimeouts(time.Second, false)
    
    // Create a connection pool with max 10 connections, min 2 idle 
    // connections...
    conn, err := hermes.Connect("postgres", 
        "postgres://postgres@127.0.0.1/engaged?sslmode=disable&connect_timeout=10", 
        10, 2)
    if err != nil {
        return err
    }

    tx, err := conn.Begin()
    if err != nil {
        panic(err)
    }

    // Oops...we forgot tx.Close()!
   
    // This will cause an error message to print out to stderr
    time.Sleep(5 * time.Second)
}

If you pass in true to the hermes.EnableTimeouts function, the application will panic when a transaction times out.

You may disable transaction timers using the hermes.DisableTimeouts() call.

Do not run transaction timers in production! There is overhead with the timers enabled; enabling them in production could cause performance and memory issues under load (each transaction will get a time.Timer).

Savepoints (1.2.4)

Hermes 1.2.4 adds support for transaction "savepoints." A savepoint acts like a bookmark in a transaction that stays around until the transaction ends. It allows a transaction to partially rollback to the savepoint.

At any point in a transaction, use Conn.Savepoint to create a savepoint in the transaction. The savepoint is assigned a random identifer, which is the returned by the Conn.Savepoint function. When you wish to rollback to this savepoint, call Conn.RollbackTo(savepointID).

For example:

tx, err := db.Begin()
if err != nil {
    return err
}
defer tx.Close()

// ... do some work ...

savepoint, err := tx.Savepoint
if err != nil {
    // If the savepoint can't be created, rollback the entire transaction
    return err
}

// ... do additional work ...

// Whoops!  Something went wrong in the additional work!
//
// Also note that RollbackTo does return an error, which you should probably
// catch.
tx.RollbackTo(savepoint)

// Continue working; the transaction is still valid, but we just lost the 
// additional work.

Savepoints remain valid once created. You can create a savepoint, rollback to the savepoint, do more work, and rollback to the savepoint again.

Cursors created before a savepoint are unaffected by a rollback to the savepoint, even if they have been manipulated after the savepoint was created. Cursors created after a savepoint are closed when the savepoint is rolled back. See the documentation below for more details.

While Savepoint() and RollbackTo() are part of the hermes.Conn interface, when called on a hermes.DB object they do nothing.

Savepoints have only been tested with PostgreSQL, though they should also work with MySQL.

Usages

Savepoints can be very useful for database testing. For example, you can create a Hermes transaction (hermes.Tx) at the start of a test case containing multiple scenarios, setup your initial data, then create a savepoint before each scenario you're testing.

After each scenario, simply rollback to the savepoint and test the next scenario. At the end of the test case, allow the transaction to close (defer tx.Close()) and rollback all the data, leaving the database in a pristine state.

For example:

// Test uniqueness when saving users.  Based off the example above.  Both
// `User.Email` and `User.Name` must be unique. 
func TestUserUniquness(t *testing.T) {
    u := User{
        Email: "jdoe@nowhere.com",
        Name: "John Doe",
    }
    
    tx, err := Conn.Begin()
    if err != nil {
        t.Fatal(err)
    }
    defer tx.Close()
    
    // Create our valid user account
    if err := db.SaveUser(tx, u); err != nil {
        t.Fatalf("Unable to create a new user account: %s", err)
    }
    
    // Leave a savepoint to rollback to
    savepoint, err := tx.Savepoint()
    if err != nil {
        t.Fatalf("Couldn't create a savepoint: %s", err)
    }
    
    // Test email uniquness
    other := User{
        Email: "jdoe@nowhere.com,
        Name: "Another name",
    }
    
    if err = db.SaveUser(tx, other); err == nil {
        t.Error("Appears that user emails lack a uniqueness constraint in the database")
    }
    
    // Just to be safe, rollback to our valid user and try the name
    if err = tx.RollbackTo(savepoint); err != nil {
        t.Fatalf("Unable to rollback to savepoint: %s", err)
    }
    
    // Test name uniqueness
    other = User{
        Email: "another@nowhere.com",
        Name: "John Doe",
    }

    if err = db.SaveUser(tx, other); err == nil {
        t.Error("Appears that user names lack a uniqueness constraint in the database")
    }

    // Again, let the test case end and allow `defer tx.Close()` to wipe all
    // the data and savepoints created by the transaction; no need for any
    // delete calls.        
}
Additional information

For more information on savepoints, see the PostgreSQL documentation:

Or the MySQL documentation:

Testing

Testing requires the lib/pq library, a PostgreSQL database, and a test database called "hermes_test".

(A future release may mock a database driver.)

On a Mac...
$ brew install postgresql
$ createdb hermes_test
$ cd $GOPATH/src/github.com/sbowman/hermes
$ go get
$ go test

Documentation

Overview

Package hermes wraps the jmoiron/sqlx *sqlx.DB and *sqlx.Tx in a common interface, hermes.Conn.

Use hermes.Conn in functions to optionally support transactions in your database queries. It allows you to create database queries composed of other functions without having to worry about whether or not you're working off a database connection or an existing transaction.

Additionally, testing with the database becomes easier. Simply create a transaction at the beginning of every test with a `defer tx.Close()`, pass the transaction into your functions instead of the database connection, and don't commit the transaction at the end. Every database insert, select, update, and delete will function normally in your test, then rollback and clean out the database automatically at the end.

Index

Constants

This section is empty.

Variables

View Source
var (
	// TxTimeout configures the transaction timer, which warns you about
	// long-lived transactions.  This can be used in development to ensure
	// all transactions are closed correctly.
	//
	// Enabling transaction timeouts should not be used in production.  If
	// enabled, a timer is created for each transaction, adding measurable
	// overhead to database processing.
	TxTimeout struct {
		// Enabled must be set to true to enable transaction timers.
		Enabled bool

		// Duration is the time to wait in milliseconds before reporting
		// a transaction being left open.
		Duration time.Duration

		// Panic set to true causes Hermes to panic if the transaction
		// remains open past its duration.  When false, Hermes simply
		// writes a message to os.Stderr.
		Panic bool
	}

	// ErrTooManyClients matches the error returned by PostgreSQL when the
	// number of client connections exceeds that allowed by the server.
	ErrTooManyClients = errors.New("pq: sorry, too many clients already")

	// Confirm indicates whether or not to test each database connection
	// for validity before attempting a query, i.e. issue a Ping first.
	// Set to the number of retries before failing.  Default is to not
	// confirm the connection, i.e. zero retries.
	Confirm int
)
View Source
var (
	// ErrBadContext returned when the caller attempts to reset the context.
	ErrBadContext = errors.New("context mismatch")

	// ErrTxRolledBack returned by calls to the transaction if it has been
	// rolled back.
	ErrTxRolledBack = errors.New("transaction rolled back")

	// ErrTxCommitted returned if the caller tries to rollback then
	// commit a transaction in the same function.
	ErrTxCommitted = errors.New("already committed")
)

Functions

func DidConnectionFail added in v1.1.0

func DidConnectionFail(err error) bool

DidConnectionFail checks the error message returned from a database request Used by hermes.PanicDB in several instances. May be used by applications with other connection types, or to test queries not covered by PanicDB, such as scanning row results.

If exit is nil, simply returns the error, skipping the check.

func DisableTimeouts added in v1.2.1

func DisableTimeouts()

DisableTimeouts disables transaction timeouts. Transaction timers may be disabled at any time. Any existing timers will simply clean themselves up quietly.

func EnableTimeouts added in v1.2.1

func EnableTimeouts(dur time.Duration, panic bool)

EnableTimeouts enables the transaction timer, which will display an error message or panic if a transaction exceeds the precribed duration. Useful during development for tracking down transactions that weren't properly cleaned up.

Transaction timers may be enabled and disabled at will without requiring a restart.

Do not use in production! The overhead will measurably slow down your application.

func ExitOnFailure added in v1.1.0

func ExitOnFailure(db *DB, err error)

ExitOnFailure forces an `os.Exit(2)` when the connection fails. This can be useful in applications that trap panics, such as in HTTP middleware.

func GenerateSavepointID added in v1.2.4

func GenerateSavepointID() string

GenerateSavepointID generates a globally unique ID to use with a savepoint. Note that savepoint identifiers are prefixed with "point_" just in case the generated ID starts with a number.

func PanicOnFailure added in v1.1.0

func PanicOnFailure(db *DB, err error)

PanicOnFailure panics when the connection fails or the database server errors. If your application traps panics, see ExitOnFailure.

Types

type Conn

type Conn interface {
	// DB returns the base database connection.
	BaseDB() *sqlx.DB

	// Tx returns the base database transaction, or nil if there is no
	// transaction.
	BaseTx() *sqlx.Tx

	// Context returns the context associated with this transaction, or nil
	// if a context is not associated.
	Context() context.Context

	// Begin a new transaction.  Returns a Conn wrapping the transaction
	// (*sqlx.Tx).
	Begin() (Conn, error)

	// Begin a new transaction in context.  The Conn will have the context
	// associated with it and use it for all subsequent commands.
	BeginCtx(ctx context.Context) (Conn, error)

	// Exec executes a database statement with no results..
	Exec(query string, args ...interface{}) (sql.Result, error)

	// Query the databsae.
	Query(query string, args ...interface{}) (*sqlx.Rows, error)

	// Row queries for a single row.
	Row(query string, args ...interface{}) (*sqlx.Row, error)

	// Prepare a database query.
	Prepare(query string) (*sqlx.Stmt, error)

	// Get a single record from the database, e.g. "SELECT ... LIMIT 1".
	Get(dest interface{}, query string, args ...interface{}) error

	// Select a collection of results.
	Select(dest interface{}, query string, args ...interface{}) error

	// Commit the transaction.
	Commit() error

	// Rollback the transaction.  This will rollback any parent transactions
	// as well.
	Rollback() error

	// Close rolls back a transaction (and all its parent transactions) if
	// it hasn't been committed.  Useful in a defer.
	Close() error

	// Is this connection in a rollback state?
	RolledBack() bool

	// The data source name for this connection
	Name() string

	// Savepoint creates a savepoint in a transaction.  On a database
	// connection does nothing.
	Savepoint() (string, error)

	// RollbackTo a savepoint ID.  On a database connection does nothing.
	RollbackTo(savepointID string) error
}

Conn masks the *sqlx.DB and *sqlx.Tx.

type DB

type DB struct {
	// OnFailure, if defined, is called when the database connection returns
	// a connection failed or other server-related error.  May be used to
	// reset the database pool connections.  Optional.
	OnFailure FailureFn
	// contains filtered or unexported fields
}

DB represents a database connection. Implements the hermes.Conn interface.

func Connect

func Connect(driverName, dataSourceName string, maxOpen, maxIdle int) (*DB, error)

Connect opens a connection to the database and pings it.

func ConnectUnchecked added in v1.1.0

func ConnectUnchecked(driverName, dataSourceName string, maxOpen, maxIdle int) (*DB, error)

ConnectUnchecked connects to the database, but does not test the connection before returning.

func NewDB added in v1.1.0

func NewDB(name string, internal *sqlx.DB, fn FailureFn) *DB

NewDB creates a new database connection. Primary used for testing.

func (*DB) BaseDB

func (db *DB) BaseDB() *sqlx.DB

BaseDB returns the base database connection.

func (*DB) BaseTx

func (db *DB) BaseTx() *sqlx.Tx

BaseTx returns nil.

func (*DB) Begin

func (db *DB) Begin() (Conn, error)

Begin a new transaction. Returns a Conn wrapping the transaction (*sqlx.Tx).

func (*DB) BeginCtx

func (db *DB) BeginCtx(ctx context.Context) (Conn, error)

BeginCtx begins a new transaction in context. The Conn will have the context associated with it and use it for all subsequent commands.

func (*DB) Close

func (db *DB) Close() error

Close closes the database connection and returns it to the pool.

func (*DB) Commit

func (db *DB) Commit() error

Commit does nothing in a raw connection.

func (*DB) Context

func (db *DB) Context() context.Context

Context returns the context associated with this transaction.

func (*DB) Exec

func (db *DB) Exec(query string, args ...interface{}) (sql.Result, error)

Exec executes a database statement with no results..

func (*DB) Get

func (db *DB) Get(dest interface{}, query string, args ...interface{}) error

Get a single record from the database, e.g. "SELECT ... LIMIT 1".

func (*DB) MaxIdle

func (db *DB) MaxIdle(n int)

MaxIdle set the maximum number of idle connections to leave in the pool.

func (*DB) MaxOpen

func (db *DB) MaxOpen(n int)

MaxOpen sets the maximum number of database connections to pool.

func (*DB) Name

func (db *DB) Name() string

Name returns the datasource name for this connection

func (*DB) Ping

func (db *DB) Ping() error

Ping the database to ensure it's alive.

func (*DB) Prepare

func (db *DB) Prepare(query string) (*sqlx.Stmt, error)

Prepare a database query.

func (*DB) Query

func (db *DB) Query(query string, args ...interface{}) (*sqlx.Rows, error)

Query the databsae.

func (*DB) Rollback

func (db *DB) Rollback() error

Rollback does nothing in a raw connection.

func (*DB) RollbackTo added in v1.2.4

func (db *DB) RollbackTo(savepointID string) error

RollbackTo does nothng on DB.

func (*DB) RolledBack

func (db *DB) RolledBack() bool

RolledBack always returns false.

func (*DB) Row

func (db *DB) Row(query string, args ...interface{}) (*sqlx.Row, error)

Row returns the results for a single row.

func (*DB) Savepoint added in v1.2.4

func (db *DB) Savepoint() (string, error)

Savepoint does nothing on the DB.

func (*DB) Select

func (db *DB) Select(dest interface{}, query string, args ...interface{}) error

Select a collection of records from the database.

type FailureFn added in v1.1.0

type FailureFn func(db *DB, err error)

FailureFn defines the template for the check function called when the database action returns a connection-related error. Useful for trapping connection failures and resetting the database connection pool.

type MockDB

type MockDB struct{ *DB }

func Mock

func Mock(driverName, dataSourceName string, maxOpen, maxIdle int) (*MockDB, error)

Create a Mock Connection. This connection will ignore all calls to Commit and always rollback on close

func (*MockDB) Begin

func (db *MockDB) Begin() (Conn, error)

type MockTx

type MockTx struct{ *Tx }

func (*MockTx) Close

func (tx *MockTx) Close() error

always rollback on close

func (*MockTx) Commit

func (tx *MockTx) Commit() error

ignore all commits

type Tx

type Tx struct {
	// contains filtered or unexported fields
}

Tx wraps a sqlx.Tx transaction. Tracks context.

func (*Tx) BaseDB

func (tx *Tx) BaseDB() *sqlx.DB

BaseDB returns the base database connection.

func (*Tx) BaseTx

func (tx *Tx) BaseTx() *sqlx.Tx

BaseTx returns the internal sqlx transaction.

func (*Tx) Begin

func (tx *Tx) Begin() (Conn, error)

Begin a new transaction. Returns a Conn wrapping the transaction (*sqlx.Tx).

func (*Tx) BeginCtx

func (tx *Tx) BeginCtx(ctx context.Context) (Conn, error)

BeginCtx begins a new transaction in context. The Conn will have the context associated with it and use it for all subsequent commands.

func (*Tx) Close

func (tx *Tx) Close() error

Close will automatically rollback a transaction if it hasn't been committed.

func (*Tx) Commit

func (tx *Tx) Commit() error

Commit the current transaction. Returns ErrTxRolledBack if the transaction was already rolled back, or ErrTxCommitted if it was committed.

func (*Tx) Context

func (tx *Tx) Context() context.Context

Context returns the context associated with this transaction.

func (*Tx) Exec

func (tx *Tx) Exec(query string, args ...interface{}) (sql.Result, error)

Exec executes a database statement with no results..

func (*Tx) Get

func (tx *Tx) Get(dest interface{}, query string, args ...interface{}) error

Get a single record from the database, e.g. "SELECT ... LIMIT 1".

func (*Tx) Name

func (tx *Tx) Name() string

Name returns the datasource name for this connection

func (*Tx) Prepare

func (tx *Tx) Prepare(query string) (*sqlx.Stmt, error)

Prepare a database query.

func (*Tx) Query

func (tx *Tx) Query(query string, args ...interface{}) (*sqlx.Rows, error)

Query the database.

func (*Tx) Rollback

func (tx *Tx) Rollback() error

Rollback the transaction. Ignored if the transaction is already in a rollback. Returns ErrTxCommitted if the transaction was committed.

func (*Tx) RollbackTo added in v1.2.4

func (tx *Tx) RollbackTo(savepointID string) error

RollbackTo rolls back to the savepoint.

func (*Tx) RolledBack

func (tx *Tx) RolledBack() bool

RolledBack returns true if the transaction was rolled back.

func (*Tx) Row

func (tx *Tx) Row(query string, args ...interface{}) (*sqlx.Row, error)

Row queries the databsae for a single row.

func (*Tx) Savepoint added in v1.2.4

func (tx *Tx) Savepoint() (string, error)

Savepoint creates a new savepoint that can be rolled back to.

func (*Tx) Select

func (tx *Tx) Select(dest interface{}, query string, args ...interface{}) error

Select a collection record from the database.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL