Skip to content

Commit

Permalink
Merge pull request #236 from cole-miller/direct-sqlite3
Browse files Browse the repository at this point in the history
Add a `nosqlite3` build tag to disable linking libsqlite3
  • Loading branch information
cole-miller authored Mar 27, 2023
2 parents ab30b33 + 6f18e4d commit 251e831
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 509 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
golint
export GO_DQLITE_MULTITHREAD=1
go test -v -race -coverprofile=coverage.out ./...
go test -v -tags nosqlite3 ./...
VERBOSE=1 DISK=${{ matrix.disk }} ./test/dqlite-demo.sh
VERBOSE=1 DISK=${{ matrix.disk }} ./test/roles.sh
VERBOSE=1 DISK=${{ matrix.disk }} ./test/recover.sh
Expand Down
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,20 @@ Build

In order to use the go-dqlite package in your application, you'll need to have
the [dqlite](https://github.com/canonical/dqlite) C library installed on your
system, along with its dependencies. You then need to pass the ```-tags```
argument to the Go tools when building or testing your packages, for example:

```bash
export CGO_LDFLAGS_ALLOW="-Wl,-z,now"
go build -tags libsqlite3
go test -tags libsqlite3
```
system, along with its dependencies. You'll also need to put ``CGO_LDFLAGS_ALLOW="-Wl,-z,now"``
in the environment of any Go build commands (see [here](https://github.com/golang/go/wiki/InvalidFlag)
for the explanation).

By default, go-dqlite's `client` module supports storing a cache of the
cluster's state in a SQLite database, locally on each cluster member. (This is
not to be confused with any SQLite databases that are managed by dqlite.) In
order to do this, it imports https://github.com/mattn/go-sqlite3, and so you
can use the `libsqlite3` build tag to control whether go-sqlite3 links to a
system libsqlite3 or builds its own. You can also disable support for SQLite
node stores entirely with the `nosqlite3` build tag (unique to go-dqlite). If
you pass this tag, your application will not link *directly* to libsqlite3 (but
it will still link it *indirectly* via libdqlite, unless you've dropped the
sqlite3.c amalgamation into the dqlite build).

Documentation
-------------
Expand Down
157 changes: 157 additions & 0 deletions client/database_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// +build !nosqlite3

package client

import (
"context"
"database/sql"
"fmt"
"strings"

"github.com/pkg/errors"
_ "github.com/mattn/go-sqlite3" // Go SQLite bindings
)

// Option that can be used to tweak node store parameters.
type NodeStoreOption func(*nodeStoreOptions)

type nodeStoreOptions struct {
Where string
}

// DatabaseNodeStore persists a list addresses of dqlite nodes in a SQL table.
type DatabaseNodeStore struct {
db *sql.DB // Database handle to use.
schema string // Name of the schema holding the servers table.
table string // Name of the servers table.
column string // Column name in the servers table holding the server address.
where string // Optional WHERE filter
}

// DefaultNodeStore creates a new NodeStore using the given filename.
//
// If the filename ends with ".yaml" then the YamlNodeStore implementation will
// be used. Otherwise the SQLite-based one will be picked, with default names
// for the schema, table and column parameters.
//
// It also creates the table if it doesn't exist yet.
func DefaultNodeStore(filename string) (NodeStore, error) {
if strings.HasSuffix(filename, ".yaml") {
return NewYamlNodeStore(filename)
}

// Open the database.
db, err := sql.Open("sqlite3", filename)
if err != nil {
return nil, errors.Wrap(err, "failed to open database")
}

// Since we're setting SQLite single-thread mode, we need to have one
// connection at most.
db.SetMaxOpenConns(1)

// Create the servers table if it does not exist yet.
_, err = db.Exec("CREATE TABLE IF NOT EXISTS servers (address TEXT, UNIQUE(address))")
if err != nil {
return nil, errors.Wrap(err, "failed to create servers table")
}

store := NewNodeStore(db, "main", "servers", "address")

return store, nil
}

// NewNodeStore creates a new NodeStore.
func NewNodeStore(db *sql.DB, schema, table, column string, options ...NodeStoreOption) *DatabaseNodeStore {
o := &nodeStoreOptions{}
for _, option := range options {
option(o)
}

return &DatabaseNodeStore{
db: db,
schema: schema,
table: table,
column: column,
where: o.Where,
}
}

// WithNodeStoreWhereClause configures the node store to append the given
// hard-coded where clause to the SELECT query used to fetch nodes. Only the
// clause itself must be given, without the "WHERE" prefix.
func WithNodeStoreWhereClause(where string) NodeStoreOption {
return func(options *nodeStoreOptions) {
options.Where = where
}
}

// Get the current servers.
func (d *DatabaseNodeStore) Get(ctx context.Context) ([]NodeInfo, error) {
tx, err := d.db.Begin()
if err != nil {
return nil, errors.Wrap(err, "failed to begin transaction")
}
defer tx.Rollback()

query := fmt.Sprintf("SELECT %s FROM %s.%s", d.column, d.schema, d.table)
if d.where != "" {
query += " WHERE " + d.where
}
rows, err := tx.QueryContext(ctx, query)
if err != nil {
return nil, errors.Wrap(err, "failed to query servers table")
}
defer rows.Close()

servers := make([]NodeInfo, 0)
for rows.Next() {
var address string
err := rows.Scan(&address)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch server address")
}
servers = append(servers, NodeInfo{ID: 1, Address: address})
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "result set failure")
}

return servers, nil
}

// Set the servers addresses.
func (d *DatabaseNodeStore) Set(ctx context.Context, servers []NodeInfo) error {
tx, err := d.db.Begin()
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}

query := fmt.Sprintf("DELETE FROM %s.%s", d.schema, d.table)
if _, err := tx.ExecContext(ctx, query); err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to delete existing servers rows")
}

query = fmt.Sprintf("INSERT INTO %s.%s(%s) VALUES (?)", d.schema, d.table, d.column)
stmt, err := tx.PrepareContext(ctx, query)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to prepare insert statement")
}
defer stmt.Close()

for _, server := range servers {
if _, err := stmt.ExecContext(ctx, server.Address); err != nil {
tx.Rollback()
return errors.Wrapf(err, "failed to insert server %s", server.Address)
}
}

if err := tx.Commit(); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}

return nil
}

20 changes: 20 additions & 0 deletions client/no_database_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// +build nosqlite3

package client

import (
"strings"

"github.com/pkg/errors"
)

// DefaultNodeStore creates a new NodeStore using the given filename.
//
// The filename must end with ".yaml".
func DefaultNodeStore(filename string) (NodeStore, error) {
if strings.HasSuffix(filename, ".yaml") {
return NewYamlNodeStore(filename)
}

return nil, errors.New("built without support for DatabaseNodeStore")
}
148 changes: 0 additions & 148 deletions client/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@ package client

import (
"context"
"database/sql"
"fmt"
"io/ioutil"
"os"
"strings"
"sync"

"github.com/google/renameio"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"

"github.com/canonical/go-dqlite/internal/protocol"
_ "github.com/mattn/go-sqlite3" // Go SQLite bindings
)

// NodeStore is used by a dqlite client to get an initial list of candidate
Expand All @@ -33,149 +28,6 @@ type InmemNodeStore = protocol.InmemNodeStore
// NewInmemNodeStore creates NodeStore which stores its data in-memory.
var NewInmemNodeStore = protocol.NewInmemNodeStore

// DatabaseNodeStore persists a list addresses of dqlite nodes in a SQL table.
type DatabaseNodeStore struct {
db *sql.DB // Database handle to use.
schema string // Name of the schema holding the servers table.
table string // Name of the servers table.
column string // Column name in the servers table holding the server address.
where string // Optional WHERE filter
}

// DefaultNodeStore creates a new NodeStore using the given filename.
//
// If the filename ends with ".yaml" then the YamlNodeStore implementation will
// be used. Otherwise the SQLite-based one will be picked, with default names
// for the schema, table and column parameters.
//
// It also creates the table if it doesn't exist yet.
func DefaultNodeStore(filename string) (NodeStore, error) {
if strings.HasSuffix(filename, ".yaml") {
return NewYamlNodeStore(filename)
}

// Open the database.
db, err := sql.Open("sqlite3", filename)
if err != nil {
return nil, errors.Wrap(err, "failed to open database")
}

// Since we're setting SQLite single-thread mode, we need to have one
// connection at most.
db.SetMaxOpenConns(1)

// Create the servers table if it does not exist yet.
_, err = db.Exec("CREATE TABLE IF NOT EXISTS servers (address TEXT, UNIQUE(address))")
if err != nil {
return nil, errors.Wrap(err, "failed to create servers table")
}

store := NewNodeStore(db, "main", "servers", "address")

return store, nil
}

// Option that can be used to tweak node store parameters.
type NodeStoreOption func(*nodeStoreOptions)

type nodeStoreOptions struct {
Where string
}

// WithNodeStoreWhereClause configures the node store to append the given
// hard-coded where clause to the SELECT query used to fetch nodes. Only the
// clause itself must be given, without the "WHERE" prefix.
func WithNodeStoreWhereClause(where string) NodeStoreOption {
return func(options *nodeStoreOptions) {
options.Where = where
}
}

// NewNodeStore creates a new NodeStore.
func NewNodeStore(db *sql.DB, schema, table, column string, options ...NodeStoreOption) *DatabaseNodeStore {
o := &nodeStoreOptions{}
for _, option := range options {
option(o)
}

return &DatabaseNodeStore{
db: db,
schema: schema,
table: table,
column: column,
where: o.Where,
}
}

// Get the current servers.
func (d *DatabaseNodeStore) Get(ctx context.Context) ([]NodeInfo, error) {
tx, err := d.db.Begin()
if err != nil {
return nil, errors.Wrap(err, "failed to begin transaction")
}
defer tx.Rollback()

query := fmt.Sprintf("SELECT %s FROM %s.%s", d.column, d.schema, d.table)
if d.where != "" {
query += " WHERE " + d.where
}
rows, err := tx.QueryContext(ctx, query)
if err != nil {
return nil, errors.Wrap(err, "failed to query servers table")
}
defer rows.Close()

servers := make([]NodeInfo, 0)
for rows.Next() {
var address string
err := rows.Scan(&address)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch server address")
}
servers = append(servers, NodeInfo{ID: 1, Address: address})
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "result set failure")
}

return servers, nil
}

// Set the servers addresses.
func (d *DatabaseNodeStore) Set(ctx context.Context, servers []NodeInfo) error {
tx, err := d.db.Begin()
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}

query := fmt.Sprintf("DELETE FROM %s.%s", d.schema, d.table)
if _, err := tx.ExecContext(ctx, query); err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to delete existing servers rows")
}

query = fmt.Sprintf("INSERT INTO %s.%s(%s) VALUES (?)", d.schema, d.table, d.column)
stmt, err := tx.PrepareContext(ctx, query)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to prepare insert statement")
}
defer stmt.Close()

for _, server := range servers {
if _, err := stmt.ExecContext(ctx, server.Address); err != nil {
tx.Rollback()
return errors.Wrapf(err, "failed to insert server %s", server.Address)
}
}

if err := tx.Commit(); err != nil {
return errors.Wrap(err, "failed to commit transaction")
}

return nil
}

// Persists a list addresses of dqlite nodes in a YAML file.
type YamlNodeStore struct {
path string
Expand Down
Loading

0 comments on commit 251e831

Please sign in to comment.