mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* serverless sqldb use same env for test config as normal * rip boltdb implementation of controller backend out it was replaced by postgres and no longer works properly. This involved migrating a number of tests which only worked with boltdb, which exposed several ways in which the postgres implementation had slightly different behavior from the bolt one: 1. ordering of results in some cases, and 2. (more importantly) erroring when a record to delete was not found. The bolt implementation silently ignored it when things to delete weren't found, so we make some changes to match that behavior. Also stopped propagating CreatedAt and UpdatedAt from DB tables into dax types. These were breaking existing tests. Perhaps it would be better to actually use them, but for now they will only exist at the DB level. This change set also moves the insertion of the directive_versions record out of migrations and into the startup/connection code. Having this in the migrations was a bit ugly because you couldn't just truncate all the tables and have everything work from scratch. Inserting it during startup is fairly innocuous, and will just continue on if it already exists. * update directive_version test I changed the initial value to 0 so that the first version that gets sent out is 1
1225 lines
29 KiB
Go
1225 lines
29 KiB
Go
package controller_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
|
|
"sort"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/dax"
|
|
"github.com/featurebasedb/featurebase/v3/dax/controller"
|
|
"github.com/featurebasedb/featurebase/v3/dax/controller/sqldb"
|
|
|
|
daxtest "github.com/featurebasedb/featurebase/v3/dax/test"
|
|
"github.com/featurebasedb/featurebase/v3/errors"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var trans sqldb.Transactor
|
|
|
|
func TestMain(m *testing.M) {
|
|
os.Exit(run(m))
|
|
}
|
|
|
|
// run is a separate function so that we can defer cleanups. (deferred functions won't run if os.Exit is called)
|
|
func run(m *testing.M) int {
|
|
// We connect to a randomized database, create it, and run migrations. Then we drop it when tests are done.
|
|
conf := sqldb.GetTestConfigRandomDB("controller_test")
|
|
var err error
|
|
trans, err = sqldb.NewTransactor(conf, logger.StderrLogger)
|
|
if err != nil {
|
|
fmt.Printf("couldn't set up transactor: %v", err)
|
|
return -1
|
|
}
|
|
|
|
defer sqldb.DropDatabase(trans)
|
|
code := m.Run()
|
|
return code
|
|
}
|
|
|
|
func TestController(t *testing.T) {
|
|
ctx := context.Background()
|
|
qdbid := dax.NewQualifiedDatabaseID("acme", "db1")
|
|
|
|
t.Run("RegisterNode", func(t *testing.T) {
|
|
director := newTestDirector()
|
|
schemar := sqldb.NewSchemar(logger.StderrLogger)
|
|
err := trans.Start()
|
|
require.NoError(t, err, "starting transactor")
|
|
|
|
defer func() {
|
|
trans.TruncateAll()
|
|
trans.Close()
|
|
}()
|
|
|
|
cfg := controller.Config{}
|
|
con := controller.New(cfg)
|
|
con.Schemar = schemar
|
|
con.Transactor = trans
|
|
con.Director = director
|
|
|
|
// Register a node with an invalid role type.
|
|
node0 := &dax.Node{
|
|
Address: "10.0.0.1:80",
|
|
RoleTypes: []dax.RoleType{
|
|
"invalid-role-type",
|
|
},
|
|
}
|
|
err = con.RegisterNodes(ctx, node0)
|
|
if assert.Error(t, err) {
|
|
assert.True(t, errors.Is(err, controller.ErrCodeRoleTypeInvalid))
|
|
}
|
|
|
|
// Register a node with no role type.
|
|
node1 := &dax.Node{
|
|
Address: "10.0.0.1:81",
|
|
RoleTypes: []dax.RoleType{},
|
|
}
|
|
err = con.RegisterNodes(ctx, node1)
|
|
if assert.Error(t, err) {
|
|
assert.True(t, errors.Is(err, controller.ErrCodeRoleTypeInvalid))
|
|
}
|
|
})
|
|
|
|
t.Run("ComputeNodes", func(t *testing.T) {
|
|
director := newTestDirector()
|
|
schemar := sqldb.NewSchemar(logger.StderrLogger)
|
|
err := trans.Start()
|
|
require.NoError(t, err, "starting transactor")
|
|
|
|
defer func() {
|
|
trans.TruncateAll()
|
|
}()
|
|
|
|
cfg := controller.Config{}
|
|
con := controller.New(cfg)
|
|
con.Schemar = schemar
|
|
con.Balancer = sqldb.NewBalancer(logger.StderrLogger)
|
|
con.DirectiveVersion = sqldb.NewDirectiveVersion(logger.StderrLogger)
|
|
con.Director = director
|
|
con.Transactor = trans
|
|
|
|
var exp []*dax.Directive
|
|
|
|
// Register a node.
|
|
node0 := &dax.Node{
|
|
Address: "10.0.0.1:80",
|
|
RoleTypes: []dax.RoleType{
|
|
dax.RoleTypeCompute,
|
|
},
|
|
}
|
|
assert.NoError(t, con.RegisterNodes(ctx, node0))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodReset,
|
|
Tables: []*dax.QualifiedTable{},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 1,
|
|
},
|
|
}
|
|
got := director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
// Add a qualified database.
|
|
dbOptions := dax.DatabaseOptions{
|
|
WorkersMin: 1,
|
|
WorkersMax: 1,
|
|
}
|
|
qdb1 := daxtest.TestQualifiedDatabaseWithID(t, qdbid.OrganizationID, qdbid.DatabaseID, "dbname1", dbOptions)
|
|
assert.NoError(t, con.CreateDatabase(ctx, qdb1))
|
|
|
|
// tbls keeps the sorted list of tables used in tests
|
|
var tbls dax.QualifiedTables
|
|
|
|
// Add a non-keyed table.
|
|
tbl0 := daxtest.TestQualifiedTable(t, qdbid, "foo", 0, false)
|
|
assert.NoError(t, con.CreateTable(ctx, tbl0))
|
|
|
|
tbls = append(tbls, tbl0)
|
|
|
|
exp = []*dax.Directive{}
|
|
assert.Equal(t, exp, director.flush())
|
|
|
|
// Add a shard.
|
|
addShards(t, ctx, con, tbl0.QualifiedID(), 0)
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(0),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 2,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
// Set WorkersMin to 3 so we can used the added nodes that follow.
|
|
{
|
|
assert.NoError(t, con.SetDatabaseOption(ctx, qdb1.QualifiedID(), dax.DatabaseOptionWorkersMin, "3"))
|
|
}
|
|
|
|
// Register two more nodes.
|
|
node1 := &dax.Node{
|
|
Address: "10.0.0.1:81",
|
|
RoleTypes: []dax.RoleType{
|
|
dax.RoleTypeCompute,
|
|
},
|
|
}
|
|
assert.NoError(t, con.RegisterNodes(ctx, node1))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node1.Address,
|
|
Method: dax.DirectiveMethodReset,
|
|
Tables: []*dax.QualifiedTable{},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 3,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
node2 := &dax.Node{
|
|
Address: "10.0.0.1:82",
|
|
RoleTypes: []dax.RoleType{
|
|
dax.RoleTypeCompute,
|
|
},
|
|
}
|
|
assert.NoError(t, con.RegisterNodes(ctx, node2))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node2.Address,
|
|
Method: dax.DirectiveMethodReset,
|
|
Tables: []*dax.QualifiedTable{},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 4,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
// Add more shards.
|
|
addShards(t, ctx, con, tbl0.QualifiedID(), dax.NewShardNums(1, 2, 3, 5, 8)...)
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node1.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(1),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 5,
|
|
},
|
|
{
|
|
Address: node2.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(2),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 6,
|
|
},
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(0, 3),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 7,
|
|
},
|
|
{
|
|
Address: node1.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(1, 5),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 8,
|
|
},
|
|
{
|
|
Address: node2.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(2, 8),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 9,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
// Add another non-keyed table.
|
|
tbl1 := daxtest.TestQualifiedTable(t, qdbid, "bar", 0, false)
|
|
assert.NoError(t, con.CreateTable(ctx, tbl1))
|
|
|
|
tbls = append(tbls, tbl1)
|
|
sort.Sort(tbls)
|
|
|
|
// Add more shards.
|
|
addShards(t, ctx, con, tbl1.QualifiedID(), dax.NewShardNums(3, 5, 8, 13)...)
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(3),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(0, 3),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 10,
|
|
},
|
|
{
|
|
Address: node1.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(5),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(1, 5),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 11,
|
|
},
|
|
{
|
|
Address: node2.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(8),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(2, 8),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 12,
|
|
},
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(3, 13),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(0, 3),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 13,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
require.Equal(t, exp, got)
|
|
|
|
// Remove a node.
|
|
assert.NoError(t, con.DeregisterNodes(ctx, node1.Address))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(3, 13),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(0, 1, 3),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 14,
|
|
},
|
|
{
|
|
Address: node2.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(5, 8),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(2, 5, 8),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 15,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
// assert.Equal(t, exp, got) // TODO fails due to shards being allocated differently
|
|
|
|
// Remove another node.
|
|
assert.NoError(t, con.DeregisterNodes(ctx, node0.Address))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node2.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(3, 5, 8, 13),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(0, 1, 2, 3, 5, 8),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 16,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
// Remove final node.
|
|
assert.NoError(t, con.DeregisterNodes(ctx, node2.Address))
|
|
|
|
exp = []*dax.Directive{}
|
|
assert.Equal(t, exp, director.flush())
|
|
|
|
// Set WorkersMin to 1 so we can add a single node and have it be used
|
|
// (currently just adding 1 node won't satisfy the minimum of 3).
|
|
{
|
|
assert.NoError(t, con.SetDatabaseOption(ctx, qdb1.QualifiedID(), dax.DatabaseOptionWorkersMin, "1"))
|
|
}
|
|
|
|
// Add a new node and ensure that the free shards get assigned to it.
|
|
node3 := &dax.Node{
|
|
Address: "10.0.0.1:83",
|
|
RoleTypes: []dax.RoleType{
|
|
dax.RoleTypeCompute,
|
|
},
|
|
}
|
|
assert.NoError(t, con.RegisterNodes(ctx, node3))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node3.Address,
|
|
Method: dax.DirectiveMethodReset,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(3, 5, 8, 13),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(0, 1, 2, 3, 5, 8),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 17,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
// Remove shards.
|
|
assert.NoError(t, con.RemoveShards(ctx, tbl0.QualifiedID(), dax.NewShardNums(2, 5)...))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node3.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(3, 5, 8, 13),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(0, 1, 3, 8),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 18,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
// Remove shards, one which does not exist.
|
|
// Currently that doesn't result in an error, it simply no-ops on trying
|
|
// to remove 99.
|
|
assert.NoError(t, con.RemoveShards(ctx, tbl0.QualifiedID(), dax.NewShardNums(3, 99)...))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node3.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(3, 5, 8, 13),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Shards: dax.NewShardNums(0, 1, 8),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 19,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
// Remove a table.
|
|
assert.NoError(t, con.DropTable(ctx, tbl0.QualifiedID()))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node3.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Shards: dax.NewShardNums(3, 5, 8, 13),
|
|
},
|
|
},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 20,
|
|
},
|
|
}
|
|
got = director.flush()
|
|
require.Equal(t, len(exp), len(got))
|
|
assert.Equal(t, exp, got)
|
|
|
|
// Remove a node which doesn't exist.
|
|
assert.NoError(t, con.DeregisterNodes(ctx, "invalidNode"))
|
|
})
|
|
|
|
t.Run("TranslateNodes", func(t *testing.T) {
|
|
invalidQtid := dax.NewQualifiedTableID(
|
|
dax.NewQualifiedDatabaseID("", ""),
|
|
dax.TableID("invalidID"),
|
|
)
|
|
|
|
director := newTestDirector()
|
|
schemar := sqldb.NewSchemar(logger.StderrLogger)
|
|
err := trans.Start()
|
|
require.NoError(t, err, "starting transactor")
|
|
|
|
defer func() {
|
|
trans.TruncateAll()
|
|
}()
|
|
|
|
cfg := controller.Config{}
|
|
con := controller.New(cfg)
|
|
con.Schemar = schemar
|
|
con.Balancer = sqldb.NewBalancer(logger.StderrLogger)
|
|
con.DirectiveVersion = sqldb.NewDirectiveVersion(logger.StderrLogger)
|
|
con.Transactor = trans
|
|
con.Director = director
|
|
|
|
var exp []*dax.Directive
|
|
|
|
// Register a node.
|
|
node0 := &dax.Node{
|
|
Address: "10.0.0.1:80",
|
|
RoleTypes: []dax.RoleType{
|
|
dax.RoleTypeTranslate,
|
|
},
|
|
HasDirective: true,
|
|
}
|
|
assert.NoError(t, con.RegisterNodes(ctx, node0))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodReset,
|
|
Tables: []*dax.QualifiedTable{},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 1,
|
|
},
|
|
}
|
|
assert.Equal(t, exp, director.flush())
|
|
|
|
// Try registering the same node. This should be ok.
|
|
assert.NoError(t, con.RegisterNodes(ctx, node0))
|
|
|
|
exp = []*dax.Directive{}
|
|
assert.Equal(t, exp, director.flush())
|
|
|
|
// Add a qualified database.
|
|
dbOptions := dax.DatabaseOptions{
|
|
WorkersMin: 1,
|
|
WorkersMax: 1,
|
|
}
|
|
qdb1 := daxtest.TestQualifiedDatabaseWithID(t, qdbid.OrganizationID, qdbid.DatabaseID, "dbname1", dbOptions)
|
|
assert.NoError(t, con.CreateDatabase(ctx, qdb1))
|
|
|
|
// tbls keeps the sorted list of tables used in tests
|
|
var tbls dax.QualifiedTables
|
|
|
|
// Add a keyed table.
|
|
tbl0 := daxtest.TestQualifiedTable(t, qdbid, "foo", 8, true)
|
|
assert.NoError(t, con.CreateTable(ctx, tbl0))
|
|
|
|
tbls = append(tbls, tbl0)
|
|
|
|
// Check directives.
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(0, 1, 2, 3, 4, 5, 6, 7),
|
|
},
|
|
},
|
|
Version: 2,
|
|
},
|
|
}
|
|
assert.Equal(t, exp, director.flush())
|
|
|
|
// Set WorkersMin to 3 so we can used the two added nodes that follow.
|
|
{
|
|
assert.NoError(t, con.SetDatabaseOption(ctx, qdb1.QualifiedID(), dax.DatabaseOptionWorkersMin, "3"))
|
|
}
|
|
|
|
// Register two more nodes.
|
|
node1 := &dax.Node{
|
|
Address: "10.0.0.1:81",
|
|
RoleTypes: []dax.RoleType{
|
|
dax.RoleTypeTranslate,
|
|
},
|
|
}
|
|
assert.NoError(t, con.RegisterNodes(ctx, node1))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node1.Address,
|
|
Method: dax.DirectiveMethodReset,
|
|
Tables: []*dax.QualifiedTable{},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{},
|
|
Version: 3,
|
|
},
|
|
}
|
|
assert.Equal(t, exp, director.flush())
|
|
|
|
node2 := &dax.Node{
|
|
Address: "10.0.0.1:82",
|
|
RoleTypes: []dax.RoleType{
|
|
dax.RoleTypeTranslate,
|
|
},
|
|
}
|
|
assert.NoError(t, con.RegisterNodes(ctx, node2))
|
|
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(0, 1, 2),
|
|
},
|
|
},
|
|
Version: 4,
|
|
},
|
|
{
|
|
Address: node1.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(3, 5, 7),
|
|
},
|
|
},
|
|
Version: 5,
|
|
},
|
|
{
|
|
Address: node2.Address,
|
|
Method: dax.DirectiveMethodReset,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbl0,
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(4, 6),
|
|
},
|
|
},
|
|
Version: 6,
|
|
},
|
|
}
|
|
assert.Equal(t, exp, director.flush())
|
|
|
|
// Add another keyed table.
|
|
// Make PartitionN double digit to ensure that partition ints aren't
|
|
// sorted as strings. Also, it should be large enough to spill over
|
|
// onto node0.
|
|
tbl1 := daxtest.TestQualifiedTable(t, qdbid, "bar", 24, true)
|
|
assert.NoError(t, con.CreateTable(ctx, tbl1))
|
|
|
|
tbls = append(tbls, tbl1)
|
|
sort.Sort(tbls)
|
|
|
|
// Check directives.
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Partitions: dax.NewPartitionNums(1, 4, 7, 10, 13, 16, 19, 22),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Partitions: dax.NewPartitionNums(0, 1, 2),
|
|
},
|
|
},
|
|
Version: 7,
|
|
},
|
|
{
|
|
Address: node1.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Partitions: dax.NewPartitionNums(2, 5, 8, 11, 14, 17, 20, 23),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Partitions: dax.NewPartitionNums(3, 5, 7),
|
|
},
|
|
},
|
|
Version: 8,
|
|
},
|
|
{
|
|
Address: node2.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
tbls[1],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Partitions: dax.NewPartitionNums(0, 3, 6, 9, 12, 15, 18, 21),
|
|
},
|
|
{
|
|
TableKey: tbls[1].Key(),
|
|
Partitions: dax.NewPartitionNums(4, 6),
|
|
},
|
|
},
|
|
Version: 9,
|
|
},
|
|
}
|
|
assert.Equal(t, exp, director.flush())
|
|
|
|
// Remove a keyed table.
|
|
assert.NoError(t, con.DropTable(ctx, tbl0.QualifiedID()))
|
|
|
|
// Check directives.
|
|
exp = []*dax.Directive{
|
|
{
|
|
Address: node0.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Partitions: dax.NewPartitionNums(1, 4, 7, 10, 13, 16, 19, 22),
|
|
},
|
|
},
|
|
Version: 10,
|
|
},
|
|
{
|
|
Address: node1.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Partitions: dax.NewPartitionNums(2, 5, 8, 11, 14, 17, 20, 23),
|
|
},
|
|
},
|
|
Version: 11,
|
|
},
|
|
{
|
|
Address: node2.Address,
|
|
Method: dax.DirectiveMethodDiff,
|
|
Tables: []*dax.QualifiedTable{
|
|
tbls[0],
|
|
},
|
|
ComputeRoles: []dax.ComputeRole{},
|
|
TranslateRoles: []dax.TranslateRole{
|
|
{
|
|
TableKey: tbls[0].Key(),
|
|
Partitions: dax.NewPartitionNums(0, 3, 6, 9, 12, 15, 18, 21),
|
|
},
|
|
},
|
|
Version: 12,
|
|
},
|
|
}
|
|
assert.Equal(t, exp, director.flush())
|
|
|
|
// Remove a table which doesn't exist.
|
|
err = con.DropTable(ctx, invalidQtid)
|
|
if assert.Error(t, err) {
|
|
assert.True(t, errors.Is(err, dax.ErrTableIDDoesNotExist))
|
|
}
|
|
|
|
// Register an invalid node.
|
|
nodeX := &dax.Node{
|
|
Address: "",
|
|
}
|
|
err = con.RegisterNodes(ctx, nodeX)
|
|
if assert.Error(t, err) {
|
|
assert.True(t, errors.Is(err, controller.ErrCodeNodeKeyInvalid))
|
|
}
|
|
})
|
|
|
|
t.Run("GetNodes", func(t *testing.T) {
|
|
schemar := sqldb.NewSchemar(logger.StderrLogger)
|
|
err := trans.Start()
|
|
require.NoError(t, err, "starting transactor")
|
|
|
|
defer func() {
|
|
trans.TruncateAll()
|
|
}()
|
|
|
|
cfg := controller.Config{}
|
|
con := controller.New(cfg)
|
|
con.Schemar = schemar
|
|
con.Balancer = sqldb.NewBalancer(logger.StderrLogger)
|
|
con.DirectiveVersion = sqldb.NewDirectiveVersion(logger.StderrLogger)
|
|
con.Transactor = trans
|
|
|
|
// Register two nodes.
|
|
node0 := &dax.Node{
|
|
Address: "10.0.0.1:80",
|
|
RoleTypes: []dax.RoleType{
|
|
dax.RoleTypeCompute,
|
|
dax.RoleTypeTranslate,
|
|
},
|
|
}
|
|
assert.NoError(t, con.RegisterNodes(ctx, node0))
|
|
node1 := &dax.Node{
|
|
Address: "10.0.0.1:81",
|
|
RoleTypes: []dax.RoleType{
|
|
dax.RoleTypeCompute,
|
|
dax.RoleTypeTranslate,
|
|
},
|
|
}
|
|
assert.NoError(t, con.RegisterNodes(ctx, node1))
|
|
|
|
// Add a qualified database.
|
|
dbOptions := dax.DatabaseOptions{
|
|
WorkersMin: 2,
|
|
WorkersMax: 2,
|
|
}
|
|
qdb1 := daxtest.TestQualifiedDatabaseWithID(t, qdbid.OrganizationID, qdbid.DatabaseID, "dbname1", dbOptions)
|
|
assert.NoError(t, con.CreateDatabase(ctx, qdb1))
|
|
|
|
// Add a keyed table.
|
|
tbl0 := daxtest.TestQualifiedTable(t, qdbid, "foo", 12, true)
|
|
assert.NoError(t, con.CreateTable(ctx, tbl0))
|
|
|
|
// Add shards.
|
|
addShards(t, ctx, con, tbl0.QualifiedID(), 0, 1, 2, 3, 11, 12)
|
|
|
|
t.Run("ComputeNodes", func(t *testing.T) {
|
|
tests := []struct {
|
|
role *dax.ComputeRole
|
|
isWrite bool
|
|
exp []dax.ComputeNode
|
|
}{
|
|
{
|
|
role: &dax.ComputeRole{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(0, 1, 2, 3),
|
|
},
|
|
exp: []dax.ComputeNode{
|
|
{
|
|
Address: node0.Address,
|
|
Table: tbl0.Key(),
|
|
Shards: dax.NewShardNums(0, 2),
|
|
},
|
|
{
|
|
Address: node1.Address,
|
|
Table: tbl0.Key(),
|
|
Shards: dax.NewShardNums(1, 3),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
role: &dax.ComputeRole{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(1),
|
|
},
|
|
exp: []dax.ComputeNode{
|
|
{
|
|
Address: node1.Address,
|
|
Table: tbl0.Key(),
|
|
Shards: dax.NewShardNums(1),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Add unassigned shards.
|
|
role: &dax.ComputeRole{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(1, 888, 889),
|
|
},
|
|
isWrite: true,
|
|
exp: []dax.ComputeNode{
|
|
{
|
|
Address: node0.Address,
|
|
Table: tbl0.Key(),
|
|
Shards: dax.NewShardNums(888),
|
|
},
|
|
{
|
|
Address: node1.Address,
|
|
Table: tbl0.Key(),
|
|
Shards: dax.NewShardNums(1, 889),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Ensure shards are not returned sorted as strings.
|
|
role: &dax.ComputeRole{
|
|
TableKey: tbl0.Key(),
|
|
Shards: dax.NewShardNums(2, 11),
|
|
},
|
|
exp: []dax.ComputeNode{
|
|
{
|
|
Address: node0.Address,
|
|
Table: tbl0.Key(),
|
|
Shards: dax.NewShardNums(2, 11),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for i, test := range tests {
|
|
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
|
|
if test.isWrite {
|
|
addShards(t, ctx, con, test.role.TableKey.QualifiedTableID(), test.role.Shards...)
|
|
}
|
|
nodes, err := con.ComputeNodes(ctx, test.role.TableKey.QualifiedTableID(), test.role.Shards)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, test.exp, nodes)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("TranslateNodes", func(t *testing.T) {
|
|
tests := []struct {
|
|
role *dax.TranslateRole
|
|
isWrite bool
|
|
exp []dax.TranslateNode
|
|
expErrCode errors.Code
|
|
}{
|
|
{
|
|
role: &dax.TranslateRole{
|
|
TableKey: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(0),
|
|
},
|
|
isWrite: true,
|
|
exp: []dax.TranslateNode{
|
|
{
|
|
Address: node0.Address,
|
|
Table: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(0),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
role: &dax.TranslateRole{
|
|
TableKey: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(0, 1, 2, 3, 999),
|
|
},
|
|
isWrite: false,
|
|
exp: []dax.TranslateNode{
|
|
{
|
|
Address: node0.Address,
|
|
Table: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(0, 2),
|
|
},
|
|
{
|
|
Address: node1.Address,
|
|
Table: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(1, 3),
|
|
},
|
|
},
|
|
expErrCode: controller.ErrCodeUnassignedJobs,
|
|
},
|
|
{
|
|
role: &dax.TranslateRole{
|
|
TableKey: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(1),
|
|
},
|
|
isWrite: false,
|
|
exp: []dax.TranslateNode{
|
|
{
|
|
Address: node1.Address,
|
|
Table: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(1),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Ensure partitions are not returned sorted as strings.
|
|
role: &dax.TranslateRole{
|
|
TableKey: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(2, 10),
|
|
},
|
|
isWrite: false,
|
|
exp: []dax.TranslateNode{
|
|
{
|
|
Address: node0.Address,
|
|
Table: tbl0.Key(),
|
|
Partitions: dax.NewPartitionNums(2, 10),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for i, test := range tests {
|
|
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
|
|
if test.isWrite {
|
|
addPartitions(t, ctx, con, test.role.TableKey.QualifiedTableID(), test.role.Partitions...)
|
|
}
|
|
nodes, err := con.TranslateNodes(ctx, test.role.TableKey.QualifiedTableID(), test.role.Partitions)
|
|
|
|
if test.expErrCode != "" {
|
|
assert.True(t, errors.Is(err, test.expErrCode))
|
|
} else {
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, test.exp, nodes)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
//////////////////////////////////////////////////////
|
|
|
|
// Ensure type implements interface.
|
|
var _ controller.Director = &testDirector{}
|
|
|
|
// testDirector is an implementation of the Director interface used for testing.
|
|
type testDirector struct {
|
|
mu sync.Mutex
|
|
dirs []*dax.Directive
|
|
}
|
|
|
|
func newTestDirector() *testDirector {
|
|
return &testDirector{}
|
|
}
|
|
|
|
func (d *testDirector) SendDirective(ctx context.Context, dir *dax.Directive) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.dirs = append(d.dirs, dir)
|
|
return nil
|
|
}
|
|
|
|
func (d *testDirector) SendSnapshotShardDataRequest(ctx context.Context, req *dax.SnapshotShardDataRequest) error {
|
|
return nil
|
|
}
|
|
|
|
func (d *testDirector) SendSnapshotTableKeysRequest(ctx context.Context, req *dax.SnapshotTableKeysRequest) error {
|
|
return nil
|
|
}
|
|
|
|
func (d *testDirector) SendSnapshotFieldKeysRequest(ctx context.Context, req *dax.SnapshotFieldKeysRequest) error {
|
|
return nil
|
|
}
|
|
|
|
// flush returns all the directives that have been captured through the Send()
|
|
// method and then resets the internal list.
|
|
func (d *testDirector) flush() []*dax.Directive {
|
|
out := make([]*dax.Directive, len(d.dirs))
|
|
copy(out, d.dirs)
|
|
|
|
// Zero out the slice (but retain allocated memory).
|
|
d.dirs = d.dirs[:0]
|
|
|
|
// Since the directives can be received asyncronously, sort them here so
|
|
// that we can more easily compare them in tests.
|
|
sort.Sort(dax.Directives(out))
|
|
|
|
return out
|
|
}
|
|
|
|
func addShards(t *testing.T, ctx context.Context, con *controller.Controller, qtid dax.QualifiedTableID, shards ...dax.ShardNum) {
|
|
t.Helper()
|
|
for _, shard := range shards {
|
|
if _, err := con.IngestShard(ctx, qtid, shard); err != nil {
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func addPartitions(t *testing.T, ctx context.Context, con *controller.Controller, qtid dax.QualifiedTableID, partitions ...dax.PartitionNum) {
|
|
t.Helper()
|
|
for _, parition := range partitions {
|
|
if _, err := con.IngestPartition(ctx, qtid, parition); err != nil {
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
}
|