hasql

package module
v2.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 8, 2025 License: Apache-2.0 Imports: 13 Imported by: 2

README

hasql

PkgGoDev GoDoc tests lint Go Report Card codecov

hasql provides simple and reliable way to access high-availability database setups with multiple hosts.

Status

hasql is production-ready and is actively used inside Yandex' production environment.

Prerequisites

Installation

With Go module support, simply add the following import

import "golang.yandex/hasql/v2"

to your code, and then go [build|run|test] will automatically fetch the necessary dependencies.

Otherwise, to install the hasql package, run the following command:

$ go get -u golang.yandex/hasql/v2

How does it work

hasql operates using standard database/sql connection pool objects. User creates *sql.DB-compatible objects for each node of database cluster and passes them to constructor. Library keeps up to date information on state of each node by 'pinging' them periodically. User is provided with a set of interfaces to retrieve database node object suitable for required operation.

dbFoo, _ := sql.Open("pgx", "host=foo")
dbBar, _ := sql.Open("pgx", "host=bar")

discoverer := NewStaticNodeDiscoverer(
    NewNode("foo", dbFoo),
    NewNode("bar", dbBar),
)

cl, err := hasql.NewCluster(discoverer, hasql.PostgreSQLChecker)
if err != nil { ... }

node := cl.Primary()
if node == nil {
    err := cl.Err() // most recent errors for all nodes in the cluster
}

fmt.Printf("got node %s\n", node)

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

if err = node.DB().PingContext(ctx); err != nil { ... }

hasql does not configure provided connection pools in any way. It is user's job to set them up properly. Library does handle their lifetime though - pools are closed when Cluster object is closed.

Concepts and entities

Cluster is a set of database/sql-compatible nodes that tracks their lifespan and provides access to each individual nodes.

Node is a single database instance in high-availability cluster.

Node discoverer provides nodes objects to cluster. This abstraction allows user to dynamically change set of cluster nodes, for example collect nodes list via Service Discovery (etcd, Consul).

Node checker collects information about current state of individual node in cluster, such as: cluster role, network latency, replication lag, etc.

Node picker picks node from cluster by given criterion using predefined algorithm: random, round-robin, lowest latency, etc.

Supported criteria

Alive primary|Alive standby|Any alive node or none otherwise

node := c.Node(hasql.Alive)

Alive primary or none otherwise

node := c.Node(hasql.Primary)

Alive standby or none otherwise

node := c.Node(hasql.Standby)

Alive primary|Alive standby or none otherwise

node := c.Node(hasql.PreferPrimary)

Alive standby|Alive primary or none otherwise

node := c.Node(hasql.PreferStandby)
Node pickers

When user asks Cluster object for a node a random one from a list of suitable nodes is returned. User can override this behavior by providing a custom node picker.

Library provides a couple of predefined pickers. For example if user wants 'closest' node (with lowest latency) LatencyNodePicker should be used.

cl, err := hasql.NewCluster(
    hasql.NewStaticNodeDiscoverer(hasql.NewNode("foo", dbFoo), hasql.NewNode("bar", dbBar)),
    hasql.PostgreSQLChecker,
    hasql.WithNodePicker(new(hasql.LatencyNodePicker[*sql.DB])),
)

Supported databases

Since library requires Querier interface, which describes a subset of database/sql.DB methods, it supports any database that has a database/sql driver. All it requires is a database-specific checker function that can provide node state info.

Check out node_checker.go file for more information.

Caveats

Node's state is transient at any given time. If Primary() returns a node it does not mean that node is still primary when you execute statement on it. All it means is that it was primary when it was last checked. Nodes can change their state at a whim or even go offline and hasql can't control it in any manner.

This is one of the reasons why nodes do not expose their perceived state to user.

Extensions

Instrumentation

You can add instrumentation via Tracer object similar to httptrace in standard library.

Documentation

Overview

Package hasql provides simple and reliable way to access high-availability database setups with multiple hosts.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CheckedNode

type CheckedNode[T Querier] struct {
	Node *Node[T]
	Info NodeInfoProvider
}

CheckedNode contains most recent state of single cluster node

type CheckedNodes

type CheckedNodes[T Querier] struct {
	// contains filtered or unexported fields
}

CheckedNodes holds references to all available cluster nodes

func (CheckedNodes[T]) Alive

func (c CheckedNodes[T]) Alive() []CheckedNode[T]

Alive returns a list of all successfully checked nodes irregarding their cluster role

func (CheckedNodes[T]) Discovered

func (c CheckedNodes[T]) Discovered() []*Node[T]

Discovered returns a list of nodes discovered in cluster

func (CheckedNodes[T]) Err

func (c CheckedNodes[T]) Err() error

Err holds information about cause of node check failure.

func (CheckedNodes[T]) Primaries

func (c CheckedNodes[T]) Primaries() []CheckedNode[T]

Primaries returns list of all successfully checked nodes with primary role

func (CheckedNodes[T]) Standbys

func (c CheckedNodes[T]) Standbys() []CheckedNode[T]

Standbys returns list of all successfully checked nodes with standby role

type Cluster

type Cluster[T Querier] struct {
	// contains filtered or unexported fields
}

Cluster consists of number of 'nodes' of a single SQL database.

Example

ExampleCluster shows how to setup basic hasql cluster with some custom settings

// open connections to database instances
db1, err := sql.Open("pgx", "host1.example.com")
if err != nil {
	panic(err)
}
db2, err := sql.Open("pgx", "host2.example.com")
if err != nil {
	panic(err)
}

// register connections as nodes with some additional information
nodes := []*hasql.Node[*sql.DB]{
	hasql.NewNode("bear", db1),
	hasql.NewNode("battlestar galactica", db2),
}

// create NodeDiscoverer instance
// here we use built-in StaticNodeDiscoverer which always returns all registered nodes
discoverer := hasql.NewStaticNodeDiscoverer(nodes...)
// use checker suitable for your database
checker := hasql.PostgreSQLChecker
// change default RandomNodePicker to RoundRobinNodePicker here
picker := new(hasql.RoundRobinNodePicker[*sql.DB])

// create cluster instance using previously created discoverer, checker
// and some additional options
cl, err := hasql.NewCluster(discoverer, checker,
	// set custom picker via funcopt
	hasql.WithNodePicker(picker),
	// change interval of cluster state check
	hasql.WithUpdateInterval[*sql.DB](500*time.Millisecond),
	// change cluster check timeout value
	hasql.WithUpdateTimeout[*sql.DB](time.Second),
)
if err != nil {
	panic(err)
}

// create context with timeout to wait for at least one alive node in cluster
// note that context timeout value must be greater than cluster update interval + update timeout
// otherwise you will always receive `context.DeadlineExceeded` error if one of cluster node is dead on startup
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

// wait for any alive node to guarantee that your application
// does not start without database dependency
// this step usually performed on application startup
node, err := cl.WaitForNode(ctx, hasql.Alive)
if err != nil {
	panic(err)
}

// pick standby node to perform query
// always check node for nilness to avoid nil pointer dereference error
// node object can be nil if no alive nodes for given criterion has been found
node = cl.Node(hasql.Standby)
if node == nil {
	panic("no alive standby available")
}

// get connection from node and perform desired action
if err := node.DB().PingContext(ctx); err != nil {
	panic(err)
}

func NewCluster

func NewCluster[T Querier](discoverer NodeDiscoverer[T], checker NodeChecker, opts ...ClusterOpt[T]) (*Cluster[T], error)

NewCluster returns object representing a single 'cluster' of SQL databases

func (*Cluster[T]) Close

func (cl *Cluster[T]) Close() (err error)

Close stops node updates. Close function must be called when cluster is not needed anymore. It returns combined error if multiple nodes returned errors

func (*Cluster[T]) Err

func (cl *Cluster[T]) Err() error

Err returns cause of nodes most recent check failures. In most cases error is a list of errors of type CheckNodeErrors, original errors could be extracted using `errors.As`. Example:

var cerrs NodeCheckErrors
if errors.As(err, &cerrs) {
    for _, cerr := range cerrs {
        fmt.Printf("node: %s, err: %s\n", cerr.Node(), cerr.Err())
    }
}

func (*Cluster[T]) Node

func (cl *Cluster[T]) Node(criterion NodeStateCriterion) *Node[T]

Node returns cluster node with specified status

func (*Cluster[T]) NodesIter added in v2.1.0

func (cl *Cluster[T]) NodesIter(criterion NodeStateCriterion) iter.Seq[*Node[T]]

NodesIter returns iterator over a set of nodes. Set content is determined by given criterion. For example PreferStandby criterion will return next sequence: [standby1, standby2, standby3, primary1]. Nodes order in sequence is determined by cluster NodePicker. It is guaranteed that iterator will never return nil Node.

func (*Cluster[T]) WaitForNode

func (cl *Cluster[T]) WaitForNode(ctx context.Context, criterion NodeStateCriterion) (*Node[T], error)

WaitForNode with specified status to appear or until context is canceled

type ClusterOpt

type ClusterOpt[T Querier] func(*Cluster[T])

ClusterOpt is a functional option type for Cluster constructor

func WithNodePicker

func WithNodePicker[T Querier](picker NodePicker[T]) ClusterOpt[T]

WithNodePicker sets algorithm for node selection (e.g. random, round robin etc.)

func WithTracer

func WithTracer[T Querier](tracer Tracer[T]) ClusterOpt[T]

WithTracer sets tracer for actions happening in the background

func WithUpdateInterval

func WithUpdateInterval[T Querier](d time.Duration) ClusterOpt[T]

WithUpdateInterval sets interval between cluster state updates

func WithUpdateTimeout

func WithUpdateTimeout[T Querier](d time.Duration) ClusterOpt[T]

WithUpdateTimeout sets timeout for update of each node in cluster

type LatencyNodePicker

type LatencyNodePicker[T Querier] struct{}

LatencyNodePicker returns node with least latency and sorts checked nodes by reported latency ascending. WARNING: This picker requires that NodeInfoProvider can report node's network latency otherwise code will panic!

func (*LatencyNodePicker[T]) CompareNodes

func (*LatencyNodePicker[T]) CompareNodes(a, b CheckedNode[T]) int

CompareNodes performs nodes comparison based on reported network latency

func (*LatencyNodePicker[T]) PickNode

func (*LatencyNodePicker[T]) PickNode(nodes []CheckedNode[T]) CheckedNode[T]

PickNode returns node with least network latency

type Node

type Node[T Querier] struct {
	// contains filtered or unexported fields
}

Node holds reference to database connection pool with some additional data

func NewNode

func NewNode[T Querier](name string, db T) *Node[T]

NewNode constructs node with given SQL querier

func (*Node[T]) DB

func (n *Node[T]) DB() T

DB returns node's database connection

func (*Node[T]) String

func (n *Node[T]) String() string

String implements Stringer. It uses name provided at construction to uniquely identify a single node

type NodeCheckError

type NodeCheckError[T Querier] struct {
	// contains filtered or unexported fields
}

NodeCheckError implements `error` and contains information about unsuccessful node check

func (NodeCheckError[T]) Error

func (n NodeCheckError[T]) Error() string

Error implements `error` interface

func (NodeCheckError[T]) Node

func (n NodeCheckError[T]) Node() *Node[T]

Node returns dead node instance

func (NodeCheckError[T]) Unwrap

func (n NodeCheckError[T]) Unwrap() error

Unwrap returns underlying error

type NodeCheckErrors

type NodeCheckErrors[T Querier] []NodeCheckError[T]

NodeCheckErrors is a set of checked nodes errors. This type can be used in errors.As/Is as it implements errors.Unwrap method

func (NodeCheckErrors[T]) Error

func (n NodeCheckErrors[T]) Error() string

func (NodeCheckErrors[T]) Unwrap

func (n NodeCheckErrors[T]) Unwrap() []error

Unwrap is a helper for errors.Is/errors.As functions

type NodeChecker

type NodeChecker func(context.Context, Querier) (NodeInfoProvider, error)

NodeChecker is a function that can perform request to SQL node and retrieve various information

type NodeDiscoverer

type NodeDiscoverer[T Querier] interface {
	// DiscoverNodes returns list of nodes registered in cluster
	DiscoverNodes(context.Context) ([]*Node[T], error)
}

NodeDiscoverer represents a provider of cluster nodes list. NodeDiscoverer must node check nodes liveness or role, just return all nodes registered in cluster

type NodeInfo

type NodeInfo struct {
	// Role contains determined node's role in cluster
	ClusterRole NodeRole
	// Latency stores time that has been spent to send check request
	// and receive response from server
	NetworkLatency time.Duration
	// ReplicaLag represents how far behind is data on standby
	// in comparison to primary. As determination of real replication
	// lag is a tricky task and value type vary from one DBMS to another
	// (e.g. bytes count lag, time delta lag etc.) this field contains
	// abstract value for sorting purposes only
	ReplicaLag int
}

NodeInfo contains various information about single cluster node

func (NodeInfo) Latency

func (n NodeInfo) Latency() time.Duration

Latency reports time spend on query execution from client's point of view. It can be used in LatencyNodePicker to determine node with fastest response time

func (NodeInfo) ReplicationLag

func (n NodeInfo) ReplicationLag() int

ReplicationLag reports data replication delta on standby. It can be used in ReplicationNodePicker to determine node with most up-to-date data

func (NodeInfo) Role

func (n NodeInfo) Role() NodeRole

Role reports determined role of node in cluster

type NodeInfoProvider

type NodeInfoProvider interface {
	// Role reports role of node in cluster.
	// For SQL servers it is usually either primary or standby
	Role() NodeRole
}

NodeInfoProvider information about single cluster node

func MSSQLChecker

func MSSQLChecker(ctx context.Context, db Querier) (NodeInfoProvider, error)

MSSQLChecker checks state of MSSQL node

func MySQLChecker

func MySQLChecker(ctx context.Context, db Querier) (NodeInfoProvider, error)

MySQLChecker checks state of MySQL node. ATTENTION: database user must have REPLICATION CLIENT privilege to perform underlying query.

func PostgreSQLChecker

func PostgreSQLChecker(ctx context.Context, db Querier) (NodeInfoProvider, error)

PostgreSQLChecker checks state on PostgreSQL node. It reports appropriate information for PostgreSQL nodes version 10 and higher

type NodePicker

type NodePicker[T Querier] interface {
	// PickNode returns a single node from given set
	PickNode(nodes []CheckedNode[T]) CheckedNode[T]
	// CompareNodes is a comparison function to be used to sort checked nodes
	CompareNodes(a, b CheckedNode[T]) int
}

NodePicker decides which node must be used from given set. It also provides a comparer to be used to pre-sort nodes for better performance

type NodeRole

type NodeRole uint8

NodeRole represents role of node in SQL cluster (usually primary/standby)

const (
	// NodeRoleUnknown used to report node with unconventional role in cluster
	NodeRoleUnknown NodeRole = iota
	// NodeRolePrimary used to report node with primary role in cluster
	NodeRolePrimary
	// NodeRoleStandby used to report node with standby role in cluster
	NodeRoleStandby
)

type NodeStateCriterion

type NodeStateCriterion uint8

NodeStateCriterion represents a node selection criterion

const (
	// Alive is a criterion to choose any alive node
	Alive NodeStateCriterion = iota + 1
	// Primary is a criterion to choose primary node
	Primary
	// Standby is a criterion to choose standby node
	Standby
	// PreferPrimary is a criterion to choose primary or any alive node
	PreferPrimary
	// PreferStandby is a criterion to choose standby or any alive node
	PreferStandby
)

type Querier

type Querier interface {
	// QueryRowContext executes a query that is expected to return at most one row
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
	// QueryContext executes a query that returns rows, typically a SELECT
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}

Querier describes abstract base SQL client such as database/sql.DB. Most of database/sql compatible third-party libraries already implement it

type RandomNodePicker

type RandomNodePicker[T Querier] struct{}

RandomNodePicker returns random node on each call and does not sort checked nodes

func (*RandomNodePicker[T]) CompareNodes

func (*RandomNodePicker[T]) CompareNodes(_, _ CheckedNode[T]) int

CompareNodes always treats nodes as equal, effectively not changing nodes order

func (*RandomNodePicker[T]) PickNode

func (*RandomNodePicker[T]) PickNode(nodes []CheckedNode[T]) CheckedNode[T]

PickNode returns random node from picker

type ReplicationNodePicker

type ReplicationNodePicker[T Querier] struct{}

ReplicationNodePicker returns node with smallest replication lag and sorts checked nodes by reported replication lag ascending. Note that replication lag reported by checkers can vastly differ from the real situation on standby server. WARNING: This picker requires that NodeInfoProvider can report node's replication lag otherwise code will panic!

func (*ReplicationNodePicker[T]) CompareNodes

func (*ReplicationNodePicker[T]) CompareNodes(a, b CheckedNode[T]) int

CompareNodes performs nodes comparison based on reported replication lag

func (*ReplicationNodePicker[T]) PickNode

func (*ReplicationNodePicker[T]) PickNode(nodes []CheckedNode[T]) CheckedNode[T]

PickNode returns node with lowest replication lag value

type RoundRobinNodePicker

type RoundRobinNodePicker[T Querier] struct {
	// contains filtered or unexported fields
}

RoundRobinNodePicker returns next node based on Round Robin algorithm and tries to preserve nodes order across checks

func (*RoundRobinNodePicker[T]) CompareNodes

func (r *RoundRobinNodePicker[T]) CompareNodes(a, b CheckedNode[T]) int

CompareNodes performs lexicographical comparison of two nodes

func (*RoundRobinNodePicker[T]) PickNode

func (r *RoundRobinNodePicker[T]) PickNode(nodes []CheckedNode[T]) CheckedNode[T]

PickNode returns next node in Round-Robin sequence

type StaticNodeDiscoverer

type StaticNodeDiscoverer[T Querier] struct {
	// contains filtered or unexported fields
}

StaticNodeDiscoverer always returns list of provided nodes

func NewStaticNodeDiscoverer

func NewStaticNodeDiscoverer[T Querier](nodes ...*Node[T]) StaticNodeDiscoverer[T]

NewStaticNodeDiscoverer returns new staticNodeDiscoverer instance

func (StaticNodeDiscoverer[T]) DiscoverNodes

func (s StaticNodeDiscoverer[T]) DiscoverNodes(_ context.Context) ([]*Node[T], error)

DiscoverNodes returns static list of nodes from StaticNodeDiscoverer

type Tracer

type Tracer[T Querier] struct {
	// UpdateNodes is called when before updating nodes status.
	UpdateNodes func()
	// NodesUpdated is called after all nodes are updated. The nodes is a list of currently alive nodes.
	NodesUpdated func(nodes CheckedNodes[T])
	// NodeDead is called when it is determined that specified node is dead.
	NodeDead func(err error)
	// NodeAlive is called when it is determined that specified node is alive.
	NodeAlive func(node CheckedNode[T])
	// WaitersNotified is called when callers of 'WaitForNode' function have been notified.
	WaitersNotified func()
}

Tracer is a set of hooks to be called at various stages of background nodes status update. Any particular hook may be nil. Functions may be called concurrently from different goroutines.

Jump to

Keyboard shortcuts

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