hasql

repository·master·Indexed 41 days ago

https://github.com/yandex/go-hasql

A Go library for managing high-availability database clusters by wrapping standard database/sql connection pools. It provides mechanisms to discover, check, and select the most appropriate database node based on roles (Primary/Standby) and health. Includes built-in checkers for PostgreSQL (v10+), MySQL, and MSSQL, as well as customizable node pickers for selection criteria like lowest latency.

Tokens
7.5K
Snippets
36
Records
40
Agent score
99%

What's inside hasql

  1. Important caveats regarding node state

    master

    Node state is transient.

    If Primary() returns a node, it does not guarantee that the node is still the primary when you execute a statement on it. It only means the node was primary during the last check. Nodes can change roles or go offline at any time, and hasql cannot control these external changes.

    Because of this, nodes do not expose their perceived state to the user.

  2. Core concepts of hasql

    master

    Understanding the primary abstractions in hasql:

    • Cluster: A set of database/sql-compatible nodes that tracks their lifespan and provides access to individual nodes.
    • Node: A single database instance within a high-availability cluster.
    • Node discoverer: An abstraction that provides node objects to the cluster. This allows for dynamic node sets (e.g., via etcd or Consul).
    • Node checker: Collects information about the current state of individual nodes, such as cluster role, network latency, or replication lag.
    • Node picker: Selects a node from the cluster based on a given criterion using a predefined algorithm (e.g., random, round-robin, or lowest latency).
  3. Install the hasql package

    master

    You can install hasql using Go modules by adding the import to your code and running go build, go run, or go test. Alternatively, you can install it manually using go get.

    Prerequisites

    • Go: Any of the two latest major releases.
    import "golang.yandex/hasql/v2"
    $ go get -u golang.yandex/hasql/v2
  4. How hasql works and basic usage

    master

    hasql manages high-availability database clusters by wrapping standard database/sql connection pools.

    Workflow:

    1. Create *sql.DB objects for each database node.
    2. Use a NodeDiscoverer to provide these nodes to a Cluster.
    3. Initialize a Cluster with a NodeChecker (e.g., hasql.PostgreSQLChecker).
    4. Retrieve a suitable node using methods like Primary() or Node(criterion).

    Note: hasql does not configure your connection pools; you must set them up properly. However, hasql manages their lifetime and will close the pools when the Cluster object is closed.

    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 { ... }
  5. Customize node selection with Node Pickers

    master

    By default, when a user asks for a node matching a criterion, a random suitable node is returned. You can override this behavior by providing a custom node picker during cluster initialization using hasql.WithNodePicker.

    For example, to select the node with the lowest latency, use hasql.LatencyNodePicker.

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

    master

    You can request a node from a Cluster object using specific criteria. When a criterion is used, the library returns a random node from the list of suitable nodes. If no nodes match, it returns nil.

    CriterionDescription
    hasql.AliveAny alive node, or none otherwise
    hasql.PrimaryAn alive primary node, or none otherwise
    hasql.StandbyAn alive standby node, or none otherwise
    hasql.PreferPrimaryAn alive primary OR an alive standby, or none otherwise
    hasql.PreferStandbyAn alive standby OR an alive primary, or none otherwise
    // Example: Get any alive node
    node := c.Node(hasql.Alive)
    
    // Example: Get an alive primary
    node := c.Node(hasql.Primary)
  7. Initialize a Cluster with NewCluster

    master

    Use NewCluster to create a managed group of SQL database nodes. You must provide a NodeDiscoverer to find nodes and a NodeChecker to verify their health. You can optionally provide ClusterOpt functions to configure the cluster (e.g., update intervals).

    Note: NewCluster starts a background routine that periodically updates the cluster state. You must call Close() when the cluster is no longer needed to stop updates and close underlying connection pools.

    // T must implement the Querier interface
    cluster, err := hasql.NewCluster[MyQuerierType](discoverer, checker, hasql.WithUpdateInterval(10 * time.Second))
    if err != nil {
        log.Fatal(err)
    }
    defer cluster.Close()
  8. Configure a Cluster using functional options

    master

    When initializing a Cluster, you can use functional options of type ClusterOpt[T] to customize its behavior. These options allow you to control update intervals, timeouts, node selection algorithms, and background tracing. The options are generic over any type T that implements the Querier interface.

    // Example usage of Cluster options
    cluster := NewCluster(
        WithUpdateInterval(30 * time.Second),
        WithUpdateTimeout(5 * time.Second),
        WithNodePicker(myCustomPicker),
        WithTracer(myTracer),
    )
  9. Clean up a Cluster with Close

    master
    The Close method stops the background node update routine and attempts to close the underlying connection pools for all discovered nodes. It returns a joined error if multiple nodes fail to close. This method must be called to prevent resource leaks.
  10. Monitor SQL cluster nodes with NodeChecker

    master

    A NodeChecker is a function type used to query a SQL node and retrieve its current state, including its role (primary or standby), network latency, and replication lag. You can use built-in checkers for specific database engines to implement node monitoring.

    Available checkers:

    • PostgreSQLChecker: Works with PostgreSQL version 10 and higher.
    • MySQLChecker: Requires the database user to have REPLICATION CLIENT privileges.
    • MSSQLChecker: Checks the state of MSSQL nodes.
    // Example usage of PostgreSQLChecker
    info, err := hasql.PostgreSQLChecker(ctx, db)
    if err != nil {
        // handle error
    }
    
    role := info.Role()
    fmt.Printf("Node role: %s\n", role)
  11. Retrieve a node by status using Node

    master

    The Node method returns a single *Node[T] that matches the provided NodeStateCriterion. The specific node returned is determined by the cluster's configured NodePicker (defaults to RandomNodePicker).

    // Get a node that matches a specific criterion (e.g., Primary)
    node := cluster.Node(hasql.Primary)
    if node != nil {
        // Use node.DB() to access the database connection
        err := node.DB().Query("SELECT 1")
    }
  12. Implement or use NodeDiscoverer to provide cluster nodes

    master

    The NodeDiscoverer[T Querier] interface is used to provide a list of database nodes registered in a cluster. Implementations of this interface are responsible for returning the list of nodes but are not required to check node liveness or roles.

    If you need a fixed, unchanging list of nodes, use the StaticNodeDiscoverer[T Querier] implementation.

    // Example of creating a static node discoverer with a predefined list of nodes
    // Assuming Node[T] is already initialized
    discoverer := hasql.NewStaticNodeDiscoverer(node1, node2)
    
    // To use it:
    nodes, err := discoverer.DiscoverNodes(ctx)