featurebase/dax/controller/controller_test.go
Travis Turner 8fca15e936
RetryWithTx (#2348)
* First pass at RetryWithTx

* Refactor RetryWithTx to take a writable bool (instead of reads, writes)

* Implment DirectiveMethodDiff

This commit adds support for a Directive to contain only the diffs (as
opposed to the full Directive).

* Update controller tests to allow for DirectiveMethodDiff (over Full)

* Update RetryWithTx to retry on duplicate key constraint.

If two concurrent processes call IngestShard() for the same shard, both
were trying to insert the same job into the jobs table. That resulted in
a duplicate key error from the database. We want to include that error
in the list of errors for which RetryWithTx should retry.

* Remove unused method: Directive.TranslatePartitions()

* Replace query in a loop with a single query

We had a query which was looking to see if a job already existed. That
query was inside a loop, and could potentially generate 256 queries (for
example). This commit replaces that logic so that we use a single query
wiht an `IN ()` clause.

* Convert to directive version-by-address

This commit uses a separate directive version per address. It moves the
version get/increment back inside the buildDirective method so that if
two concurrent processes are building a directive for the same address,
one of them will get rolled back trying to commit the version update.

* Migration for directive version by address

* Add a comment about DirectiveVersion lock/unlock logic

* Remove AddLastWins

* fix linter

* handle error in walkdir

* fix test failures from removing AddLastWins
2023-03-30 20:54:37 -05:00

1248 lines
31 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,
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(0),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []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: 1,
},
}
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: 1,
},
}
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Add more shards.
// Because addShards is a helper function which actually adds each shard
// one at a time, the controller is actually building separate
// directives for each call to IngestShard. In other words, this test is
// ensuring that the directive which are sent are what you would get if
// you added one shard at a time. So here, we just send in 3 at a time.
// We don't want more that one directive per address in the same test
// check, otherwise we can't guarantee an order.
addShards(t, ctx, con, tbl0.QualifiedID(), dax.NewShardNums(1, 2, 3)...)
exp = []*dax.Directive{
{
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(3),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 3,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(1),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 2,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(2),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 2,
},
}
assert.Equal(t, exp, director.flush())
addShards(t, ctx, con, tbl0.QualifiedID(), dax.NewShardNums(5, 8)...)
exp = []*dax.Directive{
{
Address: node1.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(5),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 3,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(8),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 3,
},
}
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))
exp = []*dax.Directive{}
assert.Equal(t, exp, director.flush())
tbls = append(tbls, tbl1)
sort.Sort(tbls)
// Add more shards.
addShards(t, ctx, con, tbl1.QualifiedID(), dax.NewShardNums(3, 5, 8)...)
exp = []*dax.Directive{
{
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbls[0],
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(3),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 4,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbls[0],
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(5),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 4,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbls[0],
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(8),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 4,
},
}
got = director.flush()
require.Equal(t, len(exp), len(got))
require.Equal(t, exp, got)
addShards(t, ctx, con, tbl1.QualifiedID(), dax.NewShardNums(13)...)
exp = []*dax.Directive{
{
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbls[0],
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(13),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 5,
},
}
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[1],
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[1].Key(),
Shards: dax.NewShardNums(1),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 6,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(5),
},
{
TableKey: tbls[1].Key(),
Shards: dax.NewShardNums(5),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 5,
},
}
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.DirectiveMethodFull,
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: 6,
},
}
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: 1,
},
}
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.DirectiveMethodFull,
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: 2,
},
}
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.DirectiveMethodFull,
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: 3,
},
}
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.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
},
ComputeRoles: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(3, 5, 8, 13),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 4,
},
}
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.DirectiveMethodFull,
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: 1,
},
}
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.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRoles: []dax.ComputeRole{},
TranslateRoles: []dax.TranslateRole{
{
TableKey: tbl0.Key(),
Partitions: dax.NewPartitionNums(0, 1, 2),
},
},
Version: 3,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRoles: []dax.ComputeRole{},
TranslateRoles: []dax.TranslateRole{
{
TableKey: tbl0.Key(),
Partitions: dax.NewPartitionNums(3, 5, 7),
},
},
Version: 2,
},
{
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: 1,
},
}
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.DirectiveMethodFull,
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: 4,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodFull,
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: 3,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodFull,
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: 2,
},
}
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.DirectiveMethodFull,
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: 5,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodFull,
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: 4,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodFull,
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: 3,
},
}
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)
}
}
}