Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions go/test/endtoend/tabletmanager/tablet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,44 @@ import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"vitess.io/vitess/go/test/endtoend/cluster"
"vitess.io/vitess/go/vt/log"
)

// TestEnsureDB tests that vttablet creates the db as needed
func TestEnsureDB(t *testing.T) {
defer cluster.PanicHandler(t)

// Create new tablet
tablet := clusterInstance.NewVttabletInstance("replica", 0, "")
tablet.MysqlctlProcess = *cluster.MysqlCtlProcessInstance(tablet.TabletUID, tablet.MySQLPort, clusterInstance.TmpDirectory)
err := tablet.MysqlctlProcess.Start()
require.NoError(t, err)

log.Info(fmt.Sprintf("Started vttablet %v", tablet))
// Start vttablet process as replica. It won't be able to serve because there's no db.
err = clusterInstance.StartVttablet(tablet, "NOT_SERVING", false, cell, "dbtest", hostname, "0")
require.NoError(t, err)

// Make it the master.
err = clusterInstance.VtctlclientProcess.ExecuteCommand("TabletExternallyReparented", tablet.Alias)
require.NoError(t, err)

// It will still fail because the db is read-only.
assert.Equal(t, "NOT_SERVING", tablet.VttabletProcess.GetTabletStatus())
status := tablet.VttabletProcess.GetStatusDetails()
assert.Contains(t, status, "read-only")

// Switch to read-write and verify that that we go serving.
_ = clusterInstance.VtctlclientProcess.ExecuteCommand("SetReadWrite", tablet.Alias)
err = tablet.VttabletProcess.WaitForTabletType("SERVING")
require.NoError(t, err)
killTablets(t, tablet)
}

// TestLocalMetadata tests the contents of local_metadata table after vttablet startup
func TestLocalMetadata(t *testing.T) {
defer cluster.PanicHandler(t)
Expand Down
5 changes: 5 additions & 0 deletions go/vt/dbconfigs/dbconfigs.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ func (dbcfgs *DBConfigs) AppDebugWithDB() Connector {
return dbcfgs.makeParams(&dbcfgs.appdebugParams, true)
}

// AllPrivsConnector returns connection parameters for appdebug with no dbname set.
func (dbcfgs *DBConfigs) AllPrivsConnector() Connector {
return dbcfgs.makeParams(&dbcfgs.allprivsParams, false)
}

// AllPrivsWithDB returns connection parameters for appdebug with dbname set.
func (dbcfgs *DBConfigs) AllPrivsWithDB() Connector {
return dbcfgs.makeParams(&dbcfgs.allprivsParams, true)
Expand Down
3 changes: 3 additions & 0 deletions go/vt/dbconfigs/dbconfigs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ func TestAccessors(t *testing.T) {
if got, want := dbc.AppWithDB().connParams.DbName, "db"; got != want {
t.Errorf("dbc.AppWithDB().DbName: %v, want %v", got, want)
}
if got, want := dbc.AllPrivsConnector().connParams.DbName, ""; got != want {
t.Errorf("dbc.AllPrivsWithDB().DbName: %v, want %v", got, want)
}
if got, want := dbc.AllPrivsWithDB().connParams.DbName, "db"; got != want {
t.Errorf("dbc.AllPrivsWithDB().DbName: %v, want %v", got, want)
}
Expand Down
12 changes: 7 additions & 5 deletions go/vt/vttablet/tabletmanager/rpc_replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,6 @@ func (tm *TabletManager) InitMaster(ctx context.Context) (string, error) {
return "", err
}

// If using semi-sync, we need to enable it before going read-write.
if err := tm.fixSemiSync(topodatapb.TabletType_MASTER); err != nil {
return "", err
}

// Set the server read-write, from now on we can accept real
// client writes. Note that if semi-sync replication is enabled,
// we'll still need some replicas to be able to commit transactions.
Expand All @@ -256,6 +251,13 @@ func (tm *TabletManager) InitMaster(ctx context.Context) (string, error) {
if err := tm.changeTypeLocked(ctx, topodatapb.TabletType_MASTER); err != nil {
return "", err
}

// Enforce semi-sync after changing the type to master. Otherwise, the
// master will hang while trying to create the database.
if err := tm.fixSemiSync(topodatapb.TabletType_MASTER); err != nil {
return "", err
}

return mysql.EncodePosition(pos), nil
}

Expand Down
48 changes: 48 additions & 0 deletions go/vt/vttablet/tabletserver/schema/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"sync"
"time"

"vitess.io/vitess/go/vt/dbconnpool"
"vitess.io/vitess/go/vt/vtgate/evalengine"

"golang.org/x/net/context"
Expand All @@ -40,6 +41,7 @@ import (
"vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv"

binlogdatapb "vitess.io/vitess/go/vt/proto/binlogdata"
topodatapb "vitess.io/vitess/go/vt/proto/topodata"
vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
)

Expand Down Expand Up @@ -71,6 +73,9 @@ type Engine struct {

conns *connpool.Pool
ticks *timer.Timer

// dbCreationFailed is for preventing log spam.
dbCreationFailed bool
}

// NewEngine creates a new Engine.
Expand Down Expand Up @@ -110,6 +115,49 @@ func (se *Engine) InitDBConfig(cp dbconfigs.Connector) {
se.cp = cp
}

// EnsureConnectionAndDB ensures that we can connect to mysql.
// If tablet type is master and there is no db, then the database is created.
// This function can be called before opening the Engine.
func (se *Engine) EnsureConnectionAndDB(tabletType topodatapb.TabletType) error {
ctx := tabletenv.LocalContext()
conn, err := dbconnpool.NewDBConnection(ctx, se.env.Config().DB.AppWithDB())
if err == nil {
conn.Close()
se.dbCreationFailed = false
return nil
}
if tabletType != topodatapb.TabletType_MASTER {
return err
}
if merr, isSQLErr := err.(*mysql.SQLError); !isSQLErr || merr.Num != mysql.ERBadDb {
return err
}

// We are master and db is not found. Let's create it.
// We use allprivs instead of DBA because we want db create to fail if we're read-only.
conn, err = dbconnpool.NewDBConnection(ctx, se.env.Config().DB.AllPrivsConnector())
if err != nil {
return vterrors.Wrap(err, "allprivs connection failed")
}
defer conn.Close()

dbname := se.env.Config().DB.DBName
_, err = conn.ExecuteFetch(fmt.Sprintf("create database if not exists `%s`", dbname), 1, false)
if err != nil {
if !se.dbCreationFailed {
// This is the first failure.
log.Errorf("db creation failed for %v: %v, will keep retrying", dbname, err)
se.dbCreationFailed = true
}
return err
}

se.dbCreationFailed = false
log.Infof("db %v created", dbname)
se.dbCreationFailed = false
return nil
}

// Open initializes the Engine. Calling Open on an already
// open engine is a no-op.
func (se *Engine) Open() error {
Expand Down
13 changes: 7 additions & 6 deletions go/vt/vttablet/tabletserver/state_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type stateManager struct {

type (
schemaEngine interface {
EnsureConnectionAndDB(topodatapb.TabletType) error
Open() error
MakeNonMaster()
Close()
Expand Down Expand Up @@ -395,7 +396,7 @@ func (sm *stateManager) VerifyTarget(ctx context.Context, target *querypb.Target
func (sm *stateManager) serveMaster() error {
sm.watcher.Close()

if err := sm.connect(); err != nil {
if err := sm.connect(topodatapb.TabletType_MASTER); err != nil {
return err
}

Expand All @@ -414,7 +415,7 @@ func (sm *stateManager) unserveMaster() error {

sm.watcher.Close()

if err := sm.connect(); err != nil {
if err := sm.connect(topodatapb.TabletType_MASTER); err != nil {
return err
}

Expand All @@ -428,7 +429,7 @@ func (sm *stateManager) serveNonMaster(wantTabletType topodatapb.TabletType) err
sm.tracker.Close()
sm.se.MakeNonMaster()

if err := sm.connect(); err != nil {
if err := sm.connect(wantTabletType); err != nil {
return err
}

Expand All @@ -446,7 +447,7 @@ func (sm *stateManager) unserveNonMaster(wantTabletType topodatapb.TabletType) e

sm.se.MakeNonMaster()

if err := sm.connect(); err != nil {
if err := sm.connect(wantTabletType); err != nil {
return err
}

Expand All @@ -456,8 +457,8 @@ func (sm *stateManager) unserveNonMaster(wantTabletType topodatapb.TabletType) e
return nil
}

func (sm *stateManager) connect() error {
if err := sm.qe.IsMySQLReachable(); err != nil {
func (sm *stateManager) connect(tabletType topodatapb.TabletType) error {
if err := sm.se.EnsureConnectionAndDB(tabletType); err != nil {
return err
}
if err := sm.se.Open(); err != nil {
Expand Down
20 changes: 15 additions & 5 deletions go/vt/vttablet/tabletserver/state_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func TestStateManagerServeMaster(t *testing.T) {
verifySubcomponent(t, 9, sm.messager, testStateOpen)

assert.False(t, sm.se.(*testSchemaEngine).nonMaster)
assert.True(t, sm.qe.(*testQueryEngine).isReachable)
assert.True(t, sm.se.(*testSchemaEngine).ensureCalled)
assert.False(t, sm.qe.(*testQueryEngine).stopServing)

assert.Equal(t, topodatapb.TabletType_MASTER, sm.target.TabletType)
Expand Down Expand Up @@ -286,7 +286,7 @@ func TestStateManagerTransitionFailRetry(t *testing.T) {
transitionRetryInterval = 10 * time.Millisecond

sm := newTestStateManager(t)
sm.qe.(*testQueryEngine).failMySQL = true
sm.se.(*testSchemaEngine).failMySQL = true

err := sm.SetServingType(topodatapb.TabletType_MASTER, testNow, StateServing, "")
require.Error(t, err)
Expand Down Expand Up @@ -613,7 +613,19 @@ func (tos testOrderState) State() testState {

type testSchemaEngine struct {
testOrderState
nonMaster bool
ensureCalled bool
nonMaster bool

failMySQL bool
}

func (te *testSchemaEngine) EnsureConnectionAndDB(tabletType topodatapb.TabletType) error {
if te.failMySQL {
te.failMySQL = false
return errors.New("intentional error")
}
te.ensureCalled = true
return nil
}

func (te *testSchemaEngine) Open() error {
Expand Down Expand Up @@ -658,7 +670,6 @@ func (te *testReplTracker) Status() (time.Duration, error) {

type testQueryEngine struct {
testOrderState
isReachable bool
stopServing bool

failMySQL bool
Expand All @@ -675,7 +686,6 @@ func (te *testQueryEngine) IsMySQLReachable() error {
te.failMySQL = false
return errors.New("intentional error")
}
te.isReachable = true
return nil
}

Expand Down