diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 83846189..9c84553d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -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 diff --git a/README.md b/README.md index ade94f27..8b6d1c75 100644 --- a/README.md +++ b/README.md @@ -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 ------------- diff --git a/client/database_store.go b/client/database_store.go new file mode 100644 index 00000000..665bb02e --- /dev/null +++ b/client/database_store.go @@ -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 +} + diff --git a/client/no_database_store.go b/client/no_database_store.go new file mode 100644 index 00000000..ddcbe04b --- /dev/null +++ b/client/no_database_store.go @@ -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") +} diff --git a/client/store.go b/client/store.go index 1a39c17f..6e12646d 100644 --- a/client/store.go +++ b/client/store.go @@ -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 @@ -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 diff --git a/client/store_test.go b/client/store_test.go index ee55b91e..61ee95ef 100644 --- a/client/store_test.go +++ b/client/store_test.go @@ -1,10 +1,15 @@ +// +build !nosqlite3 + package client_test import ( "context" + "database/sql" "testing" + dqlite "github.com/canonical/go-dqlite" "github.com/canonical/go-dqlite/client" + "github.com/canonical/go-dqlite/driver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -53,3 +58,24 @@ func TestDefaultNodeStore(t *testing.T) { {ID: uint64(1), Address: "9.9.9.9:666"}}, servers) } + +func TestConfigMultiThread(t *testing.T) { + cleanup := dummyDBSetup(t) + defer cleanup() + + err := dqlite.ConfigMultiThread() + assert.EqualError(t, err, "SQLite is already initialized") +} + +func dummyDBSetup(t *testing.T) func() { + store := client.NewInmemNodeStore() + driver, err := driver.New(store) + require.NoError(t, err) + sql.Register("dummy", driver) + db, err := sql.Open("dummy", "test.db") + require.NoError(t, err) + cleanup := func() { + require.NoError(t, db.Close()) + } + return cleanup +} diff --git a/config.go b/config.go index 64361605..c98d775a 100644 --- a/config.go +++ b/config.go @@ -1,3 +1,5 @@ +// +build !nosqlite3 + package dqlite import ( diff --git a/driver/driver_test.go b/driver/driver_test.go index 07355f31..01036f0d 100644 --- a/driver/driver_test.go +++ b/driver/driver_test.go @@ -586,9 +586,7 @@ func newDriver(t *testing.T) (*dqlitedriver.Driver, func()) { func newStore(t *testing.T, address string) client.NodeStore { t.Helper() - store, err := client.DefaultNodeStore(":memory:") - require.NoError(t, err) - + store := client.NewInmemNodeStore() server := client.NodeInfo{Address: address} require.NoError(t, store.Set(context.Background(), []client.NodeInfo{server})) @@ -631,312 +629,3 @@ func newDir(t *testing.T) (string, func()) { return dir, cleanup } - -/* -import ( - "bytes" - "fmt" - "io/ioutil" - "log" - "os" - "path/filepath" - "testing" - - "github.com/canonical/go-dqlite" - "github.com/CanonicalLtd/go-sqlite3" - "github.com/CanonicalLtd/raft-test" - "github.com/hashicorp/raft" - "github.com/mpvl/subtest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// Using invalid paths in Config.Dir results in an error. -func TestNewDriver_DirErrors(t *testing.T) { - cases := []struct { - title string - dir string // Dir to pass to the new driver. - error string // Expected message - }{ - { - `no path given at all`, - "", - "no data dir provided in config", - }, - { - `non-existing path that can't be created`, - "/cant/create/anything/here/", - "failed to create data dir", - }, - { - `path that can't be accessed`, - "/proc/1/root/", - "failed to access data dir", - }, - { - `path that is not a directory`, - "/etc/fstab", - "data dir '/etc/fstab' is not a directory", - }, - } - for _, c := range cases { - subtest.Run(t, c.title, func(t *testing.T) { - registry := dqlite.NewRegistry(c.dir) - driver, err := dqlite.NewDriver(registry, nil, dqlite.DriverConfig{}) - assert.Nil(t, driver) - require.Error(t, err) - assert.Contains(t, err.Error(), c.error) - }) - } -} - -func TestNewDriver_CreateDir(t *testing.T) { - dir, cleanup := newDir(t) - defer cleanup() - - dir = filepath.Join(dir, "does", "not", "exist") - registry := dqlite.NewRegistry(dir) - _, err := dqlite.NewDriver(registry, &raft.Raft{}, dqlite.DriverConfig{}) - assert.NoError(t, err) -} - -func DISABLE_TestDriver_SQLiteLogging(t *testing.T) { - output := bytes.NewBuffer(nil) - logger := log.New(output, "", 0) - config := dqlite.DriverConfig{Logger: logger} - - driver, cleanup := newDriverWithConfig(t, config) - defer cleanup() - - conn, err := driver.Open("test.db") - require.NoError(t, err) - - _, err = conn.Prepare("CREATE FOO") - require.Error(t, err) - assert.Contains(t, output.String(), `[ERR] near "FOO": syntax error (1)`) -} - -func TestDriver_OpenClose(t *testing.T) { - driver, cleanup := newDriver(t) - defer cleanup() - - conn, err := driver.Open("test.db") - require.NoError(t, err) - assert.NoError(t, conn.Close()) -} - -func TestDriver_OpenInvalidURI(t *testing.T) { - driver, cleanup := newDriver(t) - defer cleanup() - - conn, err := driver.Open("/foo/test.db") - assert.Nil(t, conn) - assert.EqualError(t, err, "invalid URI /foo/test.db: directory segments are invalid") -} - -func TestDriver_OpenError(t *testing.T) { - dir, cleanup := newDir(t) - defer cleanup() - - registry := dqlite.NewRegistry(dir) - fsm := dqlite.NewFSM(registry) - raft, cleanup := rafttest.Node(t, fsm) - defer cleanup() - config := dqlite.DriverConfig{} - - driver, err := dqlite.NewDriver(registry, raft, config) - require.NoError(t, err) - require.NoError(t, os.RemoveAll(dir)) - - conn, err := driver.Open("test.db") - assert.Nil(t, conn) - - expected := fmt.Sprintf("open error for %s: unable to open database file", filepath.Join(dir, "test.db")) - assert.EqualError(t, err, expected) -} - -// If the driver is not the current leader, all APIs return an error. -func TestDriver_NotLeader_Errors(t *testing.T) { - cases := []struct { - title string - f func(*testing.T, *dqlite.Conn) error - }{ - { - `open`, - func(t *testing.T, conn *dqlite.Conn) error { - _, err := conn.Prepare("CREATE TABLE foo (n INT)") - return err - }, - }, - { - `exec`, - func(t *testing.T, conn *dqlite.Conn) error { - _, err := conn.Exec("CREATE TABLE foo (n INT)", nil) - return err - }, - }, - { - `begin`, - func(t *testing.T, conn *dqlite.Conn) error { - _, err := conn.Begin() - return err - }, - }, - } - - for _, c := range cases { - t.Run(c.title, func(t *testing.T) { - dir, cleanup := newDir(t) - defer cleanup() - - registry1 := dqlite.NewRegistry(dir) - registry2 := dqlite.NewRegistry(dir) - fsm1 := dqlite.NewFSM(registry1) - fsm2 := dqlite.NewFSM(registry2) - rafts, control := rafttest.Cluster(t, []raft.FSM{fsm1, fsm2}, rafttest.Latency(1000.0)) - defer control.Close() - - config := dqlite.DriverConfig{} - - driver, err := dqlite.NewDriver(registry1, rafts["0"], config) - require.NoError(t, err) - - conn, err := driver.Open("test.db") - require.NoError(t, err) - - err = c.f(t, conn.(*dqlite.Conn)) - require.Error(t, err) - erri, ok := err.(sqlite3.Error) - require.True(t, ok) - assert.Equal(t, sqlite3.ErrIoErrNotLeader, erri.ExtendedCode) - }) - } -} - -// Return the address of the current raft leader. -func TestDriver_Leader(t *testing.T) { - driver, cleanup := newDriver(t) - defer cleanup() - - assert.Equal(t, "0", driver.Leader()) -} - -// Return the addresses of all current raft servers. -func TestDriver_Nodes(t *testing.T) { - driver, cleanup := newDriver(t) - defer cleanup() - - servers, err := driver.Nodes() - require.NoError(t, err) - assert.Equal(t, []string{"0"}, servers) -} - -func TestStmt_Exec(t *testing.T) { - driver, cleanup := newDriver(t) - defer cleanup() - - conn, err := driver.Open("test.db") - require.NoError(t, err) - defer conn.Close() - - stmt, err := conn.Prepare("CREATE TABLE foo (n INT)") - require.NoError(t, err) - _, err = stmt.Exec(nil) - assert.NoError(t, err) -} - -func TestStmt_Query(t *testing.T) { - driver, cleanup := newDriver(t) - defer cleanup() - - conn, err := driver.Open("test.db") - require.NoError(t, err) - defer conn.Close() - - stmt, err := conn.Prepare("SELECT name FROM sqlite_master") - require.NoError(t, err) - assert.Equal(t, 0, stmt.NumInput()) - rows, err := stmt.Query(nil) - assert.NoError(t, err) - defer rows.Close() - -} - -func TestTx_Commit(t *testing.T) { - driver, cleanup := newDriver(t) - defer cleanup() - - conn, err := driver.Open("test.db") - require.NoError(t, err) - defer conn.Close() - - tx, err := conn.Begin() - require.NoError(t, err) - - _, err = conn.(*dqlite.Conn).Exec("CREATE TABLE test (n INT)", nil) - require.NoError(t, err) - - assert.NoError(t, tx.Commit()) - - // The transaction ID has been saved in the committed buffer. - token := tx.(*dqlite.Tx).Token() - assert.Equal(t, uint64(5), token) - assert.NoError(t, driver.Recover(token)) -} - -func TestTx_Rollback(t *testing.T) { - driver, cleanup := newDriver(t) - defer cleanup() - - conn, err := driver.Open("test.db") - require.NoError(t, err) - defer conn.Close() - - tx, err := conn.Begin() - require.NoError(t, err) - assert.NoError(t, tx.Rollback()) -} - -// Create a new test dqlite.Driver. -func newDriver(t *testing.T) (*dqlite.Driver, func()) { - config := dqlite.DriverConfig{Logger: newTestingLogger(t, 0)} - return newDriverWithConfig(t, config) -} - -// Create a new test dqlite.Driver with custom configuration. -func newDriverWithConfig(t *testing.T, config dqlite.DriverConfig) (*dqlite.Driver, func()) { - dir, dirCleanup := newDir(t) - - registry := dqlite.NewRegistry(dir) - fsm := dqlite.NewFSM(registry) - raft, raftCleanup := rafttest.Node(t, fsm) - - driver, err := dqlite.NewDriver(registry, raft, config) - require.NoError(t, err) - - cleanup := func() { - raftCleanup() - dirCleanup() - } - - return driver, cleanup -} - -// Create a new test directory and return it, along with a function that can be -// used to remove it. -func newDir(t *testing.T) (string, func()) { - dir, err := ioutil.TempDir("", "dqlite-driver-test-") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - cleanup := func() { - _, err := os.Stat(dir) - if err != nil { - assert.True(t, os.IsNotExist(err)) - } else { - assert.NoError(t, os.RemoveAll(dir)) - } - } - return dir, cleanup -} -*/ diff --git a/driver/integration_test.go b/driver/integration_test.go index 6849735b..99dea847 100644 --- a/driver/integration_test.go +++ b/driver/integration_test.go @@ -12,11 +12,13 @@ import ( "github.com/canonical/go-dqlite/client" "github.com/canonical/go-dqlite/driver" "github.com/canonical/go-dqlite/logging" - "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// https://sqlite.org/rescode.html#constraint_unique +const SQLITE_CONSTRAINT_UNIQUE = 2067 + func TestIntegration_DatabaseSQL(t *testing.T) { db, _, cleanup := newDB(t, 3) defer cleanup() @@ -92,7 +94,7 @@ func TestIntegration_ConstraintError(t *testing.T) { _, err = db.Exec("INSERT INTO test (n) VALUES (1)") if err, ok := err.(driver.Error); ok { - assert.Equal(t, int(sqlite3.ErrConstraintUnique), err.Code) + assert.Equal(t, SQLITE_CONSTRAINT_UNIQUE, err.Code) assert.Equal(t, "UNIQUE constraint failed: test.n", err.Message) } else { t.Fatalf("expected diver error, got %+v", err) @@ -126,14 +128,6 @@ func TestIntegration_QueryBindError(t *testing.T) { assert.EqualError(t, err, "bind parameters") } -func TestIntegration_ConfigMultiThread(t *testing.T) { - _, _, cleanup := newDB(t, 1) - defer cleanup() - - err := dqlite.ConfigMultiThread() - assert.EqualError(t, err, "SQLite is already initialized") -} - func TestIntegration_LargeQuery(t *testing.T) { db, _, cleanup := newDB(t, 3) defer cleanup() @@ -297,10 +291,9 @@ func TestIntegration_LeadershipTransfer_Tx(t *testing.T) { func TestOptions(t *testing.T) { // make sure applying all options doesn't break anything - store, err := client.DefaultNodeStore(":memory:") - require.NoError(t, err) + store := client.NewInmemNodeStore() log := logging.Test(t) - _, err = driver.New( + _, err := driver.New( store, driver.WithLogFunc(log), driver.WithContext(context.Background()), @@ -327,8 +320,7 @@ func newDB(t *testing.T, n int) (*sql.DB, []*nodeHelper, func()) { func newDBWithInfos(t *testing.T, infos []client.NodeInfo) (*sql.DB, []*nodeHelper, func()) { helpers, helpersCleanup := newNodeHelpers(t, infos) - store, err := client.DefaultNodeStore(":memory:") - require.NoError(t, err) + store := client.NewInmemNodeStore() require.NoError(t, store.Set(context.Background(), infos)) diff --git a/go.sum b/go.sum index 061d1022..728b79d1 100644 --- a/go.sum +++ b/go.sum @@ -578,6 +578,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/bindings/build.go b/internal/bindings/build.go index 078bc0d8..558c1e14 100644 --- a/internal/bindings/build.go +++ b/internal/bindings/build.go @@ -1,6 +1,6 @@ package bindings /* -#cgo linux LDFLAGS: -lsqlite3 -lraft -ldqlite -Wl,-z,now +#cgo linux LDFLAGS: -lraft -ldqlite -Wl,-z,now */ import "C" diff --git a/internal/bindings/server.go b/internal/bindings/server.go index b878faf4..a06f55fd 100644 --- a/internal/bindings/server.go +++ b/internal/bindings/server.go @@ -10,7 +10,6 @@ package bindings #include #include -#include #define EMIT_BUF_LEN 1024 @@ -61,16 +60,6 @@ static void setInfo(dqlite_node_info_ext *infos, unsigned i, dqlite_node_id id, info->dqlite_role = role; } -static int sqlite3ConfigSingleThread() -{ - return sqlite3_config(SQLITE_CONFIG_SINGLETHREAD); -} - -static int sqlite3ConfigMultiThread() -{ - return sqlite3_config(SQLITE_CONFIG_MULTITHREAD); -} - */ import "C" import ( @@ -102,20 +91,6 @@ func init() { C.signal(C.SIGPIPE, C.SIG_IGN) } -func ConfigSingleThread() error { - if rc := C.sqlite3ConfigSingleThread(); rc != 0 { - return protocol.Error{Message: C.GoString(C.sqlite3_errstr(rc)), Code: int(rc)} - } - return nil -} - -func ConfigMultiThread() error { - if rc := C.sqlite3ConfigMultiThread(); rc != 0 { - return protocol.Error{Message: C.GoString(C.sqlite3_errstr(rc)), Code: int(rc)} - } - return nil -} - // NewNode creates a new Node instance. func NewNode(ctx context.Context, id uint64, address string, dir string) (*Node, error) { var server *C.dqlite_node diff --git a/internal/bindings/sqlite3.go b/internal/bindings/sqlite3.go new file mode 100644 index 00000000..59ca273c --- /dev/null +++ b/internal/bindings/sqlite3.go @@ -0,0 +1,38 @@ +// +build !nosqlite3 + +package bindings + +import ( + "github.com/canonical/go-dqlite/internal/protocol" +) + +/* +#cgo linux LDFLAGS: -lsqlite3 + +#include + +static int sqlite3ConfigSingleThread() +{ + return sqlite3_config(SQLITE_CONFIG_SINGLETHREAD); +} + +static int sqlite3ConfigMultiThread() +{ + return sqlite3_config(SQLITE_CONFIG_MULTITHREAD); +} +*/ +import "C" + +func ConfigSingleThread() error { + if rc := C.sqlite3ConfigSingleThread(); rc != 0 { + return protocol.Error{Message: C.GoString(C.sqlite3_errstr(rc)), Code: int(rc)} + } + return nil +} + +func ConfigMultiThread() error { + if rc := C.sqlite3ConfigMultiThread(); rc != 0 { + return protocol.Error{Message: C.GoString(C.sqlite3_errstr(rc)), Code: int(rc)} + } + return nil +}