Skip to content

Commit 251e831

Browse files
authored
Merge pull request #236 from cole-miller/direct-sqlite3
Add a `nosqlite3` build tag to disable linking libsqlite3
2 parents ab30b33 + 6f18e4d commit 251e831

File tree

13 files changed

+268
-509
lines changed

13 files changed

+268
-509
lines changed

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
golint
5151
export GO_DQLITE_MULTITHREAD=1
5252
go test -v -race -coverprofile=coverage.out ./...
53+
go test -v -tags nosqlite3 ./...
5354
VERBOSE=1 DISK=${{ matrix.disk }} ./test/dqlite-demo.sh
5455
VERBOSE=1 DISK=${{ matrix.disk }} ./test/roles.sh
5556
VERBOSE=1 DISK=${{ matrix.disk }} ./test/recover.sh

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,20 @@ Build
4141

4242
In order to use the go-dqlite package in your application, you'll need to have
4343
the [dqlite](https://github.com/canonical/dqlite) C library installed on your
44-
system, along with its dependencies. You then need to pass the ```-tags```
45-
argument to the Go tools when building or testing your packages, for example:
46-
47-
```bash
48-
export CGO_LDFLAGS_ALLOW="-Wl,-z,now"
49-
go build -tags libsqlite3
50-
go test -tags libsqlite3
51-
```
44+
system, along with its dependencies. You'll also need to put ``CGO_LDFLAGS_ALLOW="-Wl,-z,now"``
45+
in the environment of any Go build commands (see [here](https://github.com/golang/go/wiki/InvalidFlag)
46+
for the explanation).
47+
48+
By default, go-dqlite's `client` module supports storing a cache of the
49+
cluster's state in a SQLite database, locally on each cluster member. (This is
50+
not to be confused with any SQLite databases that are managed by dqlite.) In
51+
order to do this, it imports https://github.com/mattn/go-sqlite3, and so you
52+
can use the `libsqlite3` build tag to control whether go-sqlite3 links to a
53+
system libsqlite3 or builds its own. You can also disable support for SQLite
54+
node stores entirely with the `nosqlite3` build tag (unique to go-dqlite). If
55+
you pass this tag, your application will not link *directly* to libsqlite3 (but
56+
it will still link it *indirectly* via libdqlite, unless you've dropped the
57+
sqlite3.c amalgamation into the dqlite build).
5258

5359
Documentation
5460
-------------

client/database_store.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// +build !nosqlite3
2+
3+
package client
4+
5+
import (
6+
"context"
7+
"database/sql"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/pkg/errors"
12+
_ "github.com/mattn/go-sqlite3" // Go SQLite bindings
13+
)
14+
15+
// Option that can be used to tweak node store parameters.
16+
type NodeStoreOption func(*nodeStoreOptions)
17+
18+
type nodeStoreOptions struct {
19+
Where string
20+
}
21+
22+
// DatabaseNodeStore persists a list addresses of dqlite nodes in a SQL table.
23+
type DatabaseNodeStore struct {
24+
db *sql.DB // Database handle to use.
25+
schema string // Name of the schema holding the servers table.
26+
table string // Name of the servers table.
27+
column string // Column name in the servers table holding the server address.
28+
where string // Optional WHERE filter
29+
}
30+
31+
// DefaultNodeStore creates a new NodeStore using the given filename.
32+
//
33+
// If the filename ends with ".yaml" then the YamlNodeStore implementation will
34+
// be used. Otherwise the SQLite-based one will be picked, with default names
35+
// for the schema, table and column parameters.
36+
//
37+
// It also creates the table if it doesn't exist yet.
38+
func DefaultNodeStore(filename string) (NodeStore, error) {
39+
if strings.HasSuffix(filename, ".yaml") {
40+
return NewYamlNodeStore(filename)
41+
}
42+
43+
// Open the database.
44+
db, err := sql.Open("sqlite3", filename)
45+
if err != nil {
46+
return nil, errors.Wrap(err, "failed to open database")
47+
}
48+
49+
// Since we're setting SQLite single-thread mode, we need to have one
50+
// connection at most.
51+
db.SetMaxOpenConns(1)
52+
53+
// Create the servers table if it does not exist yet.
54+
_, err = db.Exec("CREATE TABLE IF NOT EXISTS servers (address TEXT, UNIQUE(address))")
55+
if err != nil {
56+
return nil, errors.Wrap(err, "failed to create servers table")
57+
}
58+
59+
store := NewNodeStore(db, "main", "servers", "address")
60+
61+
return store, nil
62+
}
63+
64+
// NewNodeStore creates a new NodeStore.
65+
func NewNodeStore(db *sql.DB, schema, table, column string, options ...NodeStoreOption) *DatabaseNodeStore {
66+
o := &nodeStoreOptions{}
67+
for _, option := range options {
68+
option(o)
69+
}
70+
71+
return &DatabaseNodeStore{
72+
db: db,
73+
schema: schema,
74+
table: table,
75+
column: column,
76+
where: o.Where,
77+
}
78+
}
79+
80+
// WithNodeStoreWhereClause configures the node store to append the given
81+
// hard-coded where clause to the SELECT query used to fetch nodes. Only the
82+
// clause itself must be given, without the "WHERE" prefix.
83+
func WithNodeStoreWhereClause(where string) NodeStoreOption {
84+
return func(options *nodeStoreOptions) {
85+
options.Where = where
86+
}
87+
}
88+
89+
// Get the current servers.
90+
func (d *DatabaseNodeStore) Get(ctx context.Context) ([]NodeInfo, error) {
91+
tx, err := d.db.Begin()
92+
if err != nil {
93+
return nil, errors.Wrap(err, "failed to begin transaction")
94+
}
95+
defer tx.Rollback()
96+
97+
query := fmt.Sprintf("SELECT %s FROM %s.%s", d.column, d.schema, d.table)
98+
if d.where != "" {
99+
query += " WHERE " + d.where
100+
}
101+
rows, err := tx.QueryContext(ctx, query)
102+
if err != nil {
103+
return nil, errors.Wrap(err, "failed to query servers table")
104+
}
105+
defer rows.Close()
106+
107+
servers := make([]NodeInfo, 0)
108+
for rows.Next() {
109+
var address string
110+
err := rows.Scan(&address)
111+
if err != nil {
112+
return nil, errors.Wrap(err, "failed to fetch server address")
113+
}
114+
servers = append(servers, NodeInfo{ID: 1, Address: address})
115+
}
116+
if err := rows.Err(); err != nil {
117+
return nil, errors.Wrap(err, "result set failure")
118+
}
119+
120+
return servers, nil
121+
}
122+
123+
// Set the servers addresses.
124+
func (d *DatabaseNodeStore) Set(ctx context.Context, servers []NodeInfo) error {
125+
tx, err := d.db.Begin()
126+
if err != nil {
127+
return errors.Wrap(err, "failed to begin transaction")
128+
}
129+
130+
query := fmt.Sprintf("DELETE FROM %s.%s", d.schema, d.table)
131+
if _, err := tx.ExecContext(ctx, query); err != nil {
132+
tx.Rollback()
133+
return errors.Wrap(err, "failed to delete existing servers rows")
134+
}
135+
136+
query = fmt.Sprintf("INSERT INTO %s.%s(%s) VALUES (?)", d.schema, d.table, d.column)
137+
stmt, err := tx.PrepareContext(ctx, query)
138+
if err != nil {
139+
tx.Rollback()
140+
return errors.Wrap(err, "failed to prepare insert statement")
141+
}
142+
defer stmt.Close()
143+
144+
for _, server := range servers {
145+
if _, err := stmt.ExecContext(ctx, server.Address); err != nil {
146+
tx.Rollback()
147+
return errors.Wrapf(err, "failed to insert server %s", server.Address)
148+
}
149+
}
150+
151+
if err := tx.Commit(); err != nil {
152+
return errors.Wrap(err, "failed to commit transaction")
153+
}
154+
155+
return nil
156+
}
157+

client/no_database_store.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// +build nosqlite3
2+
3+
package client
4+
5+
import (
6+
"strings"
7+
8+
"github.com/pkg/errors"
9+
)
10+
11+
// DefaultNodeStore creates a new NodeStore using the given filename.
12+
//
13+
// The filename must end with ".yaml".
14+
func DefaultNodeStore(filename string) (NodeStore, error) {
15+
if strings.HasSuffix(filename, ".yaml") {
16+
return NewYamlNodeStore(filename)
17+
}
18+
19+
return nil, errors.New("built without support for DatabaseNodeStore")
20+
}

client/store.go

Lines changed: 0 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@ package client
22

33
import (
44
"context"
5-
"database/sql"
6-
"fmt"
75
"io/ioutil"
86
"os"
9-
"strings"
107
"sync"
118

129
"github.com/google/renameio"
13-
"github.com/pkg/errors"
1410
"gopkg.in/yaml.v2"
1511

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

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

36-
// DatabaseNodeStore persists a list addresses of dqlite nodes in a SQL table.
37-
type DatabaseNodeStore struct {
38-
db *sql.DB // Database handle to use.
39-
schema string // Name of the schema holding the servers table.
40-
table string // Name of the servers table.
41-
column string // Column name in the servers table holding the server address.
42-
where string // Optional WHERE filter
43-
}
44-
45-
// DefaultNodeStore creates a new NodeStore using the given filename.
46-
//
47-
// If the filename ends with ".yaml" then the YamlNodeStore implementation will
48-
// be used. Otherwise the SQLite-based one will be picked, with default names
49-
// for the schema, table and column parameters.
50-
//
51-
// It also creates the table if it doesn't exist yet.
52-
func DefaultNodeStore(filename string) (NodeStore, error) {
53-
if strings.HasSuffix(filename, ".yaml") {
54-
return NewYamlNodeStore(filename)
55-
}
56-
57-
// Open the database.
58-
db, err := sql.Open("sqlite3", filename)
59-
if err != nil {
60-
return nil, errors.Wrap(err, "failed to open database")
61-
}
62-
63-
// Since we're setting SQLite single-thread mode, we need to have one
64-
// connection at most.
65-
db.SetMaxOpenConns(1)
66-
67-
// Create the servers table if it does not exist yet.
68-
_, err = db.Exec("CREATE TABLE IF NOT EXISTS servers (address TEXT, UNIQUE(address))")
69-
if err != nil {
70-
return nil, errors.Wrap(err, "failed to create servers table")
71-
}
72-
73-
store := NewNodeStore(db, "main", "servers", "address")
74-
75-
return store, nil
76-
}
77-
78-
// Option that can be used to tweak node store parameters.
79-
type NodeStoreOption func(*nodeStoreOptions)
80-
81-
type nodeStoreOptions struct {
82-
Where string
83-
}
84-
85-
// WithNodeStoreWhereClause configures the node store to append the given
86-
// hard-coded where clause to the SELECT query used to fetch nodes. Only the
87-
// clause itself must be given, without the "WHERE" prefix.
88-
func WithNodeStoreWhereClause(where string) NodeStoreOption {
89-
return func(options *nodeStoreOptions) {
90-
options.Where = where
91-
}
92-
}
93-
94-
// NewNodeStore creates a new NodeStore.
95-
func NewNodeStore(db *sql.DB, schema, table, column string, options ...NodeStoreOption) *DatabaseNodeStore {
96-
o := &nodeStoreOptions{}
97-
for _, option := range options {
98-
option(o)
99-
}
100-
101-
return &DatabaseNodeStore{
102-
db: db,
103-
schema: schema,
104-
table: table,
105-
column: column,
106-
where: o.Where,
107-
}
108-
}
109-
110-
// Get the current servers.
111-
func (d *DatabaseNodeStore) Get(ctx context.Context) ([]NodeInfo, error) {
112-
tx, err := d.db.Begin()
113-
if err != nil {
114-
return nil, errors.Wrap(err, "failed to begin transaction")
115-
}
116-
defer tx.Rollback()
117-
118-
query := fmt.Sprintf("SELECT %s FROM %s.%s", d.column, d.schema, d.table)
119-
if d.where != "" {
120-
query += " WHERE " + d.where
121-
}
122-
rows, err := tx.QueryContext(ctx, query)
123-
if err != nil {
124-
return nil, errors.Wrap(err, "failed to query servers table")
125-
}
126-
defer rows.Close()
127-
128-
servers := make([]NodeInfo, 0)
129-
for rows.Next() {
130-
var address string
131-
err := rows.Scan(&address)
132-
if err != nil {
133-
return nil, errors.Wrap(err, "failed to fetch server address")
134-
}
135-
servers = append(servers, NodeInfo{ID: 1, Address: address})
136-
}
137-
if err := rows.Err(); err != nil {
138-
return nil, errors.Wrap(err, "result set failure")
139-
}
140-
141-
return servers, nil
142-
}
143-
144-
// Set the servers addresses.
145-
func (d *DatabaseNodeStore) Set(ctx context.Context, servers []NodeInfo) error {
146-
tx, err := d.db.Begin()
147-
if err != nil {
148-
return errors.Wrap(err, "failed to begin transaction")
149-
}
150-
151-
query := fmt.Sprintf("DELETE FROM %s.%s", d.schema, d.table)
152-
if _, err := tx.ExecContext(ctx, query); err != nil {
153-
tx.Rollback()
154-
return errors.Wrap(err, "failed to delete existing servers rows")
155-
}
156-
157-
query = fmt.Sprintf("INSERT INTO %s.%s(%s) VALUES (?)", d.schema, d.table, d.column)
158-
stmt, err := tx.PrepareContext(ctx, query)
159-
if err != nil {
160-
tx.Rollback()
161-
return errors.Wrap(err, "failed to prepare insert statement")
162-
}
163-
defer stmt.Close()
164-
165-
for _, server := range servers {
166-
if _, err := stmt.ExecContext(ctx, server.Address); err != nil {
167-
tx.Rollback()
168-
return errors.Wrapf(err, "failed to insert server %s", server.Address)
169-
}
170-
}
171-
172-
if err := tx.Commit(); err != nil {
173-
return errors.Wrap(err, "failed to commit transaction")
174-
}
175-
176-
return nil
177-
}
178-
17931
// Persists a list addresses of dqlite nodes in a YAML file.
18032
type YamlNodeStore struct {
18133
path string

0 commit comments

Comments
 (0)