package balancer_test import ( "context" "fmt" "os" "testing" "github.com/featurebasedb/featurebase/v3/dax" daxbolt "github.com/featurebasedb/featurebase/v3/dax/boltdb" "github.com/featurebasedb/featurebase/v3/dax/controller" "github.com/featurebasedb/featurebase/v3/dax/controller/balancer/boltdb" schemardb "github.com/featurebasedb/featurebase/v3/dax/controller/schemar/boltdb" daxtest "github.com/featurebasedb/featurebase/v3/dax/test" testbolt "github.com/featurebasedb/featurebase/v3/dax/test/boltdb" "github.com/featurebasedb/featurebase/v3/logger" "github.com/stretchr/testify/assert" ) func newBoltBalancer(t *testing.T) (*daxbolt.DB, func()) { db := testbolt.MustOpenDB(t) assert.NoError(t, db.InitializeBuckets(boltdb.BalancerBuckets...)) assert.NoError(t, db.InitializeBuckets(schemardb.SchemarBuckets...)) return db, func() { testbolt.MustCloseDB(t, db) testbolt.CleanupDB(t, db.Path()) } } type runner interface { run(tx dax.Transaction, bal controller.Balancer) ([]dax.WorkerDiff, error) } type addWorker struct { node *dax.Node } func (r *addWorker) run(tx dax.Transaction, bal controller.Balancer) ([]dax.WorkerDiff, error) { return bal.AddWorker(tx, r.node) } type removeWorker struct { addr dax.Address } func (r *removeWorker) run(tx dax.Transaction, bal controller.Balancer) ([]dax.WorkerDiff, error) { return bal.RemoveWorker(tx, r.addr) } type addJob struct { roleType dax.RoleType qtid dax.QualifiedTableID job dax.Job } func (r *addJob) run(tx dax.Transaction, bal controller.Balancer) ([]dax.WorkerDiff, error) { return bal.AddJobs(tx, r.roleType, r.qtid, r.job) } type removeJob struct { roleType dax.RoleType qtid dax.QualifiedTableID job dax.Job } func (r *removeJob) run(tx dax.Transaction, bal controller.Balancer) ([]dax.WorkerDiff, error) { return bal.RemoveJobs(tx, r.roleType, r.qtid, r.job) } type balanceDatabase struct { qdbid dax.QualifiedDatabaseID } func (r *balanceDatabase) run(tx dax.Transaction, bal controller.Balancer) ([]dax.WorkerDiff, error) { return bal.BalanceDatabase(tx, r.qdbid) } func TestBalancer(t *testing.T) { ctx := context.Background() orgID := dax.OrganizationID("acme") dbID := dax.DatabaseID("db1") dbName := dax.DatabaseName("db1name") tableID := dax.TableID("tbl1") qtid := dax.NewQualifiedTableID(dax.NewQualifiedDatabaseID(orgID, dbID), tableID) t.Run("SingleWorker", func(t *testing.T) { qdb := &dax.QualifiedDatabase{ OrganizationID: orgID, Database: dax.Database{ ID: dbID, Name: dbName, Options: dax.DatabaseOptions{ WorkersMin: 1, WorkersMax: 1, }, }, } schemar, cleanup := daxtest.NewSchemar(t) defer cleanup() db, cleanup := newBoltBalancer(t) defer cleanup() bal := boltdb.NewBalancer(db, schemar, logger.NewStandardLogger(os.Stderr)) tx, err := db.BeginTx(ctx, true) assert.NoError(t, err) defer tx.Rollback() assert.NoError(t, schemar.CreateDatabase(tx, qdb)) role := dax.RoleTypeCompute tests := []struct { runner runner expDiff []dax.WorkerDiff expState []dax.WorkerInfo }{ { // Add job. runner: &addJob{ roleType: role, qtid: qtid, job: "s2", }, expDiff: []dax.WorkerDiff{}, expState: []dax.WorkerInfo{}, }, { // Add worker. runner: &addWorker{ node: &dax.Node{ Address: "w1", RoleTypes: dax.RoleTypes{role}, }, }, expDiff: []dax.WorkerDiff{ { Address: "w1", AddedJobs: []dax.Job{"s2"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w1", Jobs: []dax.Job{"s2"}, }, }, }, { // Add another job out of order. runner: &addJob{ roleType: role, qtid: qtid, job: "s1", }, expDiff: []dax.WorkerDiff{ { Address: "w1", AddedJobs: []dax.Job{"s1"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w1", Jobs: []dax.Job{"s1", "s2"}, }, }, }, { // Add another job. runner: &addJob{ roleType: role, qtid: qtid, job: "s3", }, expDiff: []dax.WorkerDiff{ { Address: "w1", AddedJobs: []dax.Job{"s3"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w1", Jobs: []dax.Job{"s1", "s2", "s3"}, }, }, }, { // Add a duplicate job. runner: &addJob{ roleType: role, qtid: qtid, job: "s2", }, expDiff: []dax.WorkerDiff{}, expState: []dax.WorkerInfo{ { Address: "w1", Jobs: []dax.Job{"s1", "s2", "s3"}, }, }, }, } for i, test := range tests { t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) { diff, err := test.runner.run(tx, bal) assert.NoError(t, err) assert.Equal(t, test.expDiff, diff) state, err := bal.CurrentState(tx, role, qdb.QualifiedID()) assert.NoError(t, err) assert.Equal(t, test.expState, state) }) } assert.NoError(t, tx.Commit()) }) t.Run("MultipleWorkers", func(t *testing.T) { dbOptions := dax.DatabaseOptions{ WorkersMin: 2, WorkersMax: 2, } qdb := &dax.QualifiedDatabase{ OrganizationID: orgID, Database: dax.Database{ ID: dbID, Name: dbName, Options: dbOptions, }, } schemar, cleanup := daxtest.NewSchemar(t) defer cleanup() db, cleanup := newBoltBalancer(t) defer cleanup() bal := boltdb.NewBalancer(db, schemar, logger.NewStandardLogger(os.Stderr)) tx, err := db.BeginTx(ctx, true) assert.NoError(t, err) defer tx.Rollback() assert.NoError(t, schemar.CreateDatabase(tx, qdb)) role := dax.RoleTypeCompute type testPart struct { name string runner runner expDiff []dax.WorkerDiff expState []dax.WorkerInfo } runTestPart := func(tp testPart) { t.Run(fmt.Sprintf("test-%s", tp.name), func(t *testing.T) { diff, err := tp.runner.run(tx, bal) assert.NoError(t, err) assert.Equal(t, tp.expDiff, diff) state, err := bal.CurrentState(tx, role, qdb.QualifiedID()) assert.NoError(t, err) assert.Equal(t, tp.expState, state) }) } runTestPart(testPart{ name: "balance when empty", runner: &balanceDatabase{ qdbid: qdb.QualifiedID(), }, expDiff: []dax.WorkerDiff{}, expState: []dax.WorkerInfo{}, }) runTestPart(testPart{ name: "add worker", runner: &addWorker{ node: &dax.Node{ Address: "w2", RoleTypes: dax.RoleTypes{role}, }, }, expDiff: []dax.WorkerDiff{}, expState: []dax.WorkerInfo{}, }) runTestPart(testPart{ name: "add worker again", runner: &addWorker{ node: &dax.Node{ Address: "w2", RoleTypes: dax.RoleTypes{role}, }, }, expDiff: []dax.WorkerDiff{}, expState: []dax.WorkerInfo{}, }) runTestPart(testPart{ name: "add a second worker", runner: &addWorker{ node: &dax.Node{ Address: "w1", RoleTypes: dax.RoleTypes{role}, }, }, expDiff: []dax.WorkerDiff{}, expState: []dax.WorkerInfo{}, }) runTestPart(testPart{ name: "add job 2", runner: &addJob{ roleType: role, qtid: qtid, job: "s2", }, expDiff: []dax.WorkerDiff{ { Address: "w1", AddedJobs: []dax.Job{"s2"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w1", Jobs: []dax.Job{"s2"}, }, { Address: "w2", Jobs: []dax.Job{}, }, }, }) runTestPart(testPart{ name: "add job 3", runner: &addJob{ roleType: role, qtid: qtid, job: "s3", }, expDiff: []dax.WorkerDiff{ { Address: "w2", AddedJobs: []dax.Job{"s3"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w1", Jobs: []dax.Job{"s2"}, }, { Address: "w2", Jobs: []dax.Job{"s3"}, }, }, }) runTestPart(testPart{ name: "add job 1", runner: &addJob{ roleType: role, qtid: qtid, job: "s1", }, expDiff: []dax.WorkerDiff{ { Address: "w1", AddedJobs: []dax.Job{"s1"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w1", Jobs: []dax.Job{"s1", "s2"}, }, { Address: "w2", Jobs: []dax.Job{"s3"}, }, }, }) // Update database options on schemar so min worker for db is 3. Here we // are updating the database option first, and then adding a worker to // satisfy those options. t.Run(fmt.Sprintf("test-%s", "set database options db min 3"), func(t *testing.T) { assert.NoError(t, schemar.SetDatabaseOption(tx, qdb.QualifiedID(), dax.DatabaseOptionWorkersMin, "3")) }) runTestPart(testPart{ name: "add a third worker", runner: &addWorker{ node: &dax.Node{ Address: "w0", RoleTypes: dax.RoleTypes{role}, }, }, expDiff: []dax.WorkerDiff{ { Address: "w0", AddedJobs: []dax.Job{"s2"}, RemovedJobs: []dax.Job{}, }, { Address: "w1", AddedJobs: []dax.Job{}, RemovedJobs: []dax.Job{"s2"}, }, }, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2"}, }, { Address: "w1", Jobs: []dax.Job{"s1"}, }, { Address: "w2", Jobs: []dax.Job{"s3"}, }, }, }) runTestPart(testPart{ name: "add job 4", runner: &addJob{ roleType: role, qtid: qtid, job: "s4", }, expDiff: []dax.WorkerDiff{ { Address: "w0", AddedJobs: []dax.Job{"s4"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4"}, }, { Address: "w1", Jobs: []dax.Job{"s1"}, }, { Address: "w2", Jobs: []dax.Job{"s3"}, }, }, }) runTestPart(testPart{ name: "add job 5", runner: &addJob{ roleType: role, qtid: qtid, job: "s5", }, expDiff: []dax.WorkerDiff{ { Address: "w1", AddedJobs: []dax.Job{"s5"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4"}, }, { Address: "w1", Jobs: []dax.Job{"s1", "s5"}, }, { Address: "w2", Jobs: []dax.Job{"s3"}, }, }, }) runTestPart(testPart{ name: "add job 0", runner: &addJob{ roleType: role, qtid: qtid, job: "s0", }, expDiff: []dax.WorkerDiff{ { Address: "w2", AddedJobs: []dax.Job{"s0"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4"}, }, { Address: "w1", Jobs: []dax.Job{"s1", "s5"}, }, { Address: "w2", Jobs: []dax.Job{"s0", "s3"}, }, }, }) runTestPart(testPart{ name: "add job 6", runner: &addJob{ roleType: role, qtid: qtid, job: "s6", }, expDiff: []dax.WorkerDiff{ { Address: "w0", AddedJobs: []dax.Job{"s6"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4", "s6"}, }, { Address: "w1", Jobs: []dax.Job{"s1", "s5"}, }, { Address: "w2", Jobs: []dax.Job{"s0", "s3"}, }, }, }) runTestPart(testPart{ name: "add job 7", runner: &addJob{ roleType: role, qtid: qtid, job: "s7", }, expDiff: []dax.WorkerDiff{ { Address: "w1", AddedJobs: []dax.Job{"s7"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4", "s6"}, }, { Address: "w1", Jobs: []dax.Job{"s1", "s5", "s7"}, }, { Address: "w2", Jobs: []dax.Job{"s0", "s3"}, }, }, }) //////////////////// Remove ///////////////////////// runTestPart(testPart{ name: "remove nonexistent worker", runner: &removeWorker{ addr: "nonexistent", }, expDiff: []dax.WorkerDiff{}, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4", "s6"}, }, { Address: "w1", Jobs: []dax.Job{"s1", "s5", "s7"}, }, { Address: "w2", Jobs: []dax.Job{"s0", "s3"}, }, }, }) runTestPart(testPart{ name: "remove worker", runner: &removeWorker{ addr: "w1", }, expDiff: []dax.WorkerDiff{ { Address: "w0", AddedJobs: []dax.Job{"s5"}, RemovedJobs: []dax.Job{}, }, { Address: "w1", AddedJobs: []dax.Job{}, RemovedJobs: []dax.Job{"s1", "s5", "s7"}, }, { Address: "w2", AddedJobs: []dax.Job{"s1", "s7"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4", "s5", "s6"}, }, { Address: "w2", Jobs: []dax.Job{"s0", "s1", "s3", "s7"}, }, }, }) runTestPart(testPart{ name: "remove active job", runner: &removeJob{ roleType: role, qtid: qtid, job: "s0", }, expDiff: []dax.WorkerDiff{ { Address: "w2", AddedJobs: []dax.Job{}, RemovedJobs: []dax.Job{"s0"}, }, }, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4", "s5", "s6"}, }, { Address: "w2", Jobs: []dax.Job{"s1", "s3", "s7"}, }, }, }) // Set WorkersMin back to 2 so we can test the change to 3 again. t.Run(fmt.Sprintf("test-%s", "set database options db min back to 2"), func(t *testing.T) { assert.NoError(t, schemar.SetDatabaseOption(tx, qdb.QualifiedID(), dax.DatabaseOptionWorkersMin, "2")) }) runTestPart(testPart{ name: "add a fourth worker", runner: &addWorker{ node: &dax.Node{ Address: "w3", RoleTypes: dax.RoleTypes{role}, }, }, expDiff: []dax.WorkerDiff{}, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4", "s5", "s6"}, }, { Address: "w2", Jobs: []dax.Job{"s1", "s3", "s7"}, }, }, }) // Update database options on schemar so min worker for db is 4. Here we // have added a worker which will satisfy this option, and then updated // the option. t.Run(fmt.Sprintf("test-%s", "set database options db min back to 3"), func(t *testing.T) { assert.NoError(t, schemar.SetDatabaseOption(tx, qdb.QualifiedID(), dax.DatabaseOptionWorkersMin, "3")) }) // This implies that there is a condition where the database does not // get repaired to its workerMin: if workers per database has dropped // below its min, and there are no available workers to replace the // missing workers (and bring it back to min), then the database will // operate below min. If, then, a worker is added to the pool, we do not // currently have a process to automatically assign that worker to a // database under min. That happens with an explicit call to // assignMinWorkers or BalanceDatabase. runTestPart(testPart{ name: "balance to include new third worker", runner: &balanceDatabase{ qdbid: qdb.QualifiedID(), }, expDiff: []dax.WorkerDiff{ { Address: "w0", AddedJobs: []dax.Job{}, RemovedJobs: []dax.Job{"s6"}, }, { Address: "w2", AddedJobs: []dax.Job{}, RemovedJobs: []dax.Job{"s7"}, }, { Address: "w3", AddedJobs: []dax.Job{"s6", "s7"}, RemovedJobs: []dax.Job{}, }, }, expState: []dax.WorkerInfo{ { Address: "w0", Jobs: []dax.Job{"s2", "s4", "s5"}, }, { Address: "w2", Jobs: []dax.Job{"s1", "s3"}, }, { Address: "w3", Jobs: []dax.Job{"s6", "s7"}, }, }, }) assert.NoError(t, tx.Commit()) }) t.Run("WorkersForJobs", func(t *testing.T) { qdb := &dax.QualifiedDatabase{ OrganizationID: orgID, Database: dax.Database{ ID: dbID, Name: dbName, Options: dax.DatabaseOptions{ WorkersMin: 2, WorkersMax: 2, }, }, } qdbid := qdb.QualifiedID() schemar, scleanup := daxtest.NewSchemar(t) defer scleanup() role := dax.RoleTypeCompute db, cleanup := newBoltBalancer(t) defer cleanup() bal := boltdb.NewBalancer(db, schemar, logger.NewStandardLogger(os.Stderr)) tx, err := db.BeginTx(ctx, true) assert.NoError(t, err) defer tx.Rollback() assert.NoError(t, schemar.CreateDatabase(tx, qdb)) node1 := &dax.Node{ Address: "n1", RoleTypes: []dax.RoleType{role}, } node2 := &dax.Node{ Address: "n2", RoleTypes: []dax.RoleType{role}, } _, err = bal.AddWorker(tx, node1) assert.NoError(t, err) _, err = bal.AddWorker(tx, node2) assert.NoError(t, err) for i := 0; i < 12; i++ { _, err = bal.AddJobs(tx, role, qtid, dax.Job(fmt.Sprintf("s%d", i))) assert.NoError(t, err) } exp := dax.WorkerInfo{ Address: "n1", Jobs: []dax.Job{"s0", "s10", "s2", "s4", "s6", "s8"}, } ws, err := bal.WorkerState(tx, role, "n1") assert.NoError(t, err) assert.Equal(t, exp, ws) exp = dax.WorkerInfo{ Address: "n2", Jobs: []dax.Job{"s1", "s11", "s3", "s5", "s7", "s9"}, } ws, err = bal.WorkerState(tx, role, "n2") assert.NoError(t, err) assert.Equal(t, exp, ws) tests := []struct { jobs []dax.Job exp []dax.WorkerInfo }{ { jobs: []dax.Job{"s0"}, exp: []dax.WorkerInfo{ {Address: "n1", Jobs: []dax.Job{"s0"}}, }, }, { jobs: []dax.Job{"s0", "s4"}, exp: []dax.WorkerInfo{ {Address: "n1", Jobs: []dax.Job{"s0", "s4"}}, }, }, { jobs: []dax.Job{"s0", "s4", "s999"}, exp: []dax.WorkerInfo{ {Address: "n1", Jobs: []dax.Job{"s0", "s4"}}, }, }, { jobs: []dax.Job{"s0", "s1"}, exp: []dax.WorkerInfo{ {Address: "n1", Jobs: []dax.Job{"s0"}}, {Address: "n2", Jobs: []dax.Job{"s1"}}, }, }, { jobs: []dax.Job{"s5", "s0", "s1", "s8"}, exp: []dax.WorkerInfo{ {Address: "n1", Jobs: []dax.Job{"s0", "s8"}}, {Address: "n2", Jobs: []dax.Job{"s1", "s5"}}, }, }, } for i, test := range tests { t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) { workers, err := bal.WorkersForJobs(tx, role, qdbid, test.jobs...) assert.NoError(t, err) assert.Equal(t, test.exp, workers) }) } assert.NoError(t, tx.Commit()) }) t.Run("WorkersForTable", func(t *testing.T) { qdb := &dax.QualifiedDatabase{ OrganizationID: orgID, Database: dax.Database{ ID: dbID, Name: dbName, Options: dax.DatabaseOptions{ WorkersMin: 2, WorkersMax: 2, }, }, } qdbid := dax.NewQualifiedDatabaseID(orgID, dbID) // Table 1. tbl1 := &dax.Table{ ID: "id1", Name: "table1", } qtbl1 := dax.NewQualifiedTable(qdbid, tbl1) qtid1 := qtbl1.QualifiedID() // Table 2. tbl2 := &dax.Table{ ID: "id2", Name: "table2", } qtbl2 := dax.NewQualifiedTable(qdbid, tbl2) qtid2 := qtbl2.QualifiedID() schemar, scleanup := daxtest.NewSchemar(t) defer scleanup() role := dax.RoleTypeCompute db, cleanup := newBoltBalancer(t) defer cleanup() bal := boltdb.NewBalancer(db, schemar, logger.NewStandardLogger(os.Stderr)) tx, err := db.BeginTx(ctx, true) assert.NoError(t, err) defer tx.Rollback() assert.NoError(t, schemar.CreateDatabase(tx, qdb)) node1 := &dax.Node{ Address: "n1", RoleTypes: []dax.RoleType{role}, } node2 := &dax.Node{ Address: "n2", RoleTypes: []dax.RoleType{role}, } _, err = bal.AddWorker(tx, node1) assert.NoError(t, err) _, err = bal.AddWorker(tx, node2) assert.NoError(t, err) for i := 0; i < 12; i++ { job := fmt.Sprintf("%s|s%d", qtid1.Key(), i) _, err = bal.AddJobs(tx, role, qtid1, dax.Job(job)) assert.NoError(t, err) } for i := 9; i < 16; i++ { job := fmt.Sprintf("%s|s%d", qtid2.Key(), i) _, err = bal.AddJobs(tx, role, qtid2, dax.Job(job)) assert.NoError(t, err) } // fn1 and fn2 are just helper functions used to make the tests easier // to read. fn1 := func(s string) dax.Job { return dax.Job(fmt.Sprintf("tbl__acme__db1__id1|%s", s)) } fn2 := func(s string) dax.Job { return dax.Job(fmt.Sprintf("tbl__acme__db1__id2|%s", s)) } // Table 1 workers, err := bal.WorkersForTable(tx, role, qtid1) assert.NoError(t, err) assert.ElementsMatch(t, []dax.WorkerInfo{ {Address: "n1", Jobs: []dax.Job{fn1("s0"), fn1("s10"), fn1("s2"), fn1("s4"), fn1("s6"), fn1("s8")}}, {Address: "n2", Jobs: []dax.Job{fn1("s1"), fn1("s11"), fn1("s3"), fn1("s5"), fn1("s7"), fn1("s9")}}, }, workers) // Table 2 workers, err = bal.WorkersForTable(tx, role, qtid2) assert.NoError(t, err) assert.ElementsMatch(t, []dax.WorkerInfo{ {Address: "n1", Jobs: []dax.Job{fn2("s11"), fn2("s13"), fn2("s15"), fn2("s9")}}, {Address: "n2", Jobs: []dax.Job{fn2("s10"), fn2("s12"), fn2("s14")}}, }, workers) // No match qtid0 := dax.NewQualifiedTableID(qdbid, "bad") workers, err = bal.WorkersForTable(tx, role, qtid0) assert.NoError(t, err) assert.ElementsMatch(t, []dax.WorkerInfo{}, workers) assert.NoError(t, tx.Commit()) }) t.Run("Balance", func(t *testing.T) { dbOptions := dax.DatabaseOptions{ WorkersMin: 2, WorkersMax: 2, } qdb := &dax.QualifiedDatabase{ OrganizationID: orgID, Database: dax.Database{ ID: dbID, Name: dbName, Options: dbOptions, }, } qdbid := qdb.QualifiedID() schemar, scleanup := daxtest.NewSchemar(t) defer scleanup() role := dax.RoleTypeCompute db, cleanup := newBoltBalancer(t) defer cleanup() bal := boltdb.NewBalancer(db, schemar, logger.NewStandardLogger(os.Stderr)) tx, err := db.BeginTx(ctx, true) assert.NoError(t, err) defer tx.Rollback() assert.NoError(t, schemar.CreateDatabase(tx, qdb)) node1 := &dax.Node{ Address: "n1", RoleTypes: []dax.RoleType{role}, } node2 := &dax.Node{ Address: "n2", RoleTypes: []dax.RoleType{role}, } node3 := &dax.Node{ Address: "n3", RoleTypes: []dax.RoleType{role}, } // Add two workers with some jobs evenly spread across them. _, err = bal.AddWorker(tx, node1) assert.NoError(t, err) _, err = bal.AddWorker(tx, node2) assert.NoError(t, err) for i := 0; i < 13; i++ { job := fmt.Sprintf("s%d", i) _, err = bal.AddJobs(tx, role, qtid, dax.Job(job)) assert.NoError(t, err) } exp := dax.WorkerInfo{ Address: "n1", Jobs: []dax.Job{"s0", "s10", "s12", "s2", "s4", "s6", "s8"}, } ws, err := bal.WorkerState(tx, role, "n1") assert.NoError(t, err) assert.Equal(t, exp, ws) exp = dax.WorkerInfo{ Address: "n2", Jobs: []dax.Job{"s1", "s11", "s3", "s5", "s7", "s9"}, } ws, err = bal.WorkerState(tx, role, "n2") assert.NoError(t, err) assert.Equal(t, exp, ws) // Update database options on schemar so min worker for db is 3. t.Run(fmt.Sprintf("test-%s", "set database options db min 3"), func(t *testing.T) { assert.NoError(t, schemar.SetDatabaseOption(tx, qdb.QualifiedID(), dax.DatabaseOptionWorkersMin, "3")) }) // Now, add a worker and confirm that it has received some jobs. _, err = bal.AddWorker(tx, node3) assert.NoError(t, err) exp = dax.WorkerInfo{ Address: "n3", Jobs: []dax.Job{"s6", "s7", "s8", "s9"}, } ws, err = bal.WorkerState(tx, role, "n3") assert.NoError(t, err) assert.Equal(t, exp, ws) // Finally, call Balance() and confirm that the appropriate jobs got are // as expected (actually, this is no longer needed here because we // automatically balance when we add workers, but calling it should // effectively be a no-op). _, err = bal.BalanceDatabase(tx, qdbid) assert.NoError(t, err) exp = dax.WorkerInfo{ Address: "n1", Jobs: []dax.Job{"s0", "s10", "s12", "s2", "s4"}, } ws, err = bal.WorkerState(tx, role, "n1") assert.NoError(t, err) assert.Equal(t, exp, ws) exp = dax.WorkerInfo{ Address: "n2", Jobs: []dax.Job{"s1", "s11", "s3", "s5"}, } ws, err = bal.WorkerState(tx, role, "n2") assert.NoError(t, err) assert.Equal(t, exp, ws) exp = dax.WorkerInfo{ Address: "n3", Jobs: []dax.Job{"s6", "s7", "s8", "s9"}, } ws, err = bal.WorkerState(tx, role, "n3") assert.NoError(t, err) assert.Equal(t, exp, ws) assert.NoError(t, tx.Commit()) }) }