Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
23f1ffe
Clear socket file in use error after logging warning
fulghum Mar 7, 2023
242ae97
Ensuring we use the InMem dbFactoryUrl when the specified Filesystem …
fulghum Mar 8, 2023
4c63da2
Moving function that was in the wrong file
fulghum Mar 11, 2023
f39524e
Adding tests to repro drop database session data invalidation bug.
fulghum Mar 11, 2023
00af2c5
First, rough pass on drop database session state invalidation
fulghum Mar 11, 2023
cc45562
First pass at synchronization for Dolt session state
fulghum Mar 13, 2023
37c5ca6
Merge branch 'main' into fulghum/drop-database
fulghum Mar 13, 2023
ed4e3c3
Upgrading GMS dep to latest commit on fulghum/drop-database dev branch
fulghum Mar 13, 2023
224d258
Note about test framework
fulghum Mar 13, 2023
f191745
Cleaning up
fulghum Mar 13, 2023
fe1a824
Bumping to latest GMS build from fulghum/drop-database dev branch
fulghum Mar 13, 2023
5e416eb
[ga-format-pr] Run go/utils/repofmt/format_repo.sh and go/Godeps/upda…
fulghum Mar 13, 2023
471f755
Bumping GMS to latest build from fulghum/drop-database dev branch
fulghum Mar 13, 2023
e4155d9
Merge branch 'fulghum/drop-database' of https://github.com/dolthub/do…
fulghum Mar 13, 2023
21ce51f
[ga-format-pr] Run go/utils/repofmt/format_repo.sh and go/Godeps/upda…
fulghum Mar 13, 2023
d8c3187
Merge branch 'main' into fulghum/drop-database
fulghum Mar 13, 2023
8e6cde6
Updating to latest GMS build at tip of fulghum/drop-database dev branch.
fulghum Mar 14, 2023
f4b265e
[ga-format-pr] Run go/utils/repofmt/format_repo.sh and go/Godeps/upda…
fulghum Mar 14, 2023
3463860
Merge branch 'main' into fulghum/drop-database
fulghum Mar 15, 2023
b0fc1aa
Merge branch 'main' into fulghum/drop-database
fulghum Mar 15, 2023
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
1 change: 1 addition & 0 deletions go/cmd/dolt/commands/sqlserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ func Serve(

if errors.Is(startError, server.UnixSocketInUseError) {
lgr.Warn("unix socket set up failed: file already in use: ", serverConf.Socket)
startError = nil
} else if startError != nil {
cli.PrintErr(startError)
return
Expand Down
39 changes: 38 additions & 1 deletion go/libraries/doltcore/sqle/database_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dfunctions"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dprocedures"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
"github.com/dolthub/dolt/go/libraries/doltcore/sqlserver"
"github.com/dolthub/dolt/go/libraries/doltcore/table/editor"
"github.com/dolthub/dolt/go/libraries/utils/filesys"
"github.com/dolthub/dolt/go/store/types"
Expand Down Expand Up @@ -109,6 +110,13 @@ func NewDoltDatabaseProviderWithDatabases(defaultBranch string, fs filesys.Files
externalProcedures.Register(esp)
}

// If the specified |fs| is an in mem file system, default to using the InMemDoltDB dbFactoryUrl so that all
// databases are created with the same file system type.
dbFactoryUrl := doltdb.LocalDirDoltDB
if _, ok := fs.(*filesys.InMemFS); ok {
dbFactoryUrl = doltdb.InMemDoltDB
}

return DoltDatabaseProvider{
dbLocations: dbLocations,
databases: dbs,
Expand All @@ -117,7 +125,7 @@ func NewDoltDatabaseProviderWithDatabases(defaultBranch string, fs filesys.Files
mu: &sync.RWMutex{},
fs: fs,
defaultBranch: defaultBranch,
dbFactoryUrl: doltdb.LocalDirDoltDB,
dbFactoryUrl: dbFactoryUrl,
InitDatabaseHook: ConfigureReplicationDatabaseHook,
isStandby: new(bool),
}, nil
Expand Down Expand Up @@ -698,6 +706,35 @@ func (p DoltDatabaseProvider) DropDatabase(ctx *sql.Context, name string) error
}

delete(p.databases, dbKey)

return p.invalidateDbStateInAllSessions(ctx, name)
}

// invalidateDbStateInAllSessions removes the db state for this database from every session. This is necessary when a
// database is dropped, so that other sessions don't use stale db state.
func (p DoltDatabaseProvider) invalidateDbStateInAllSessions(ctx *sql.Context, name string) error {
runningServer := sqlserver.GetRunningServer()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This global is super spooky! Can we thread the server through when we construct the database provider for the engine? Definitely not related to this PR per se, but seeing this global makes me want to immediately go try to remove it...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look! Yeah... looks like we could change sqlengine.go and server.go a little bit to set the MySQL server directly into DoltDatabaseProvider so that it doesn't have to reach into that global var. I'll push a commit with that and we can see how it looks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well... this turned out to be a little messier than I was expecting... It looks like we're always using DoltDatabaseProvider as a struct for some reason, and not as a pointer to a struct. The DoltDatabaseProvider struct gets created when we create the engine and then passed (as a copy) into GMS. After that, the sqlEngine is instantiated and we can start the sql-server at which point we get the server reference that we want to set in the provider, but we can't change the provider registered in GMS, because a copy was passed in, and we don't have a reference to that memory.

I'm happy to change DoltDatabaseProvider to be used as a pointer (seems like a good change to make for several reasons), but it feels like that might be cleaner in a separate PR since it'll be more than just a couple real quick changes like I thought this was going to be.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation and investigating this. I agree this doesn't need to be addressed here :).

if runningServer != nil {
sessionManager := runningServer.SessionManager()
err := sessionManager.Iter(func(session sql.Session) (bool, error) {
dsess, ok := session.(*dsess.DoltSession)
if !ok {
return false, fmt.Errorf("unexpected session type: %T", session)
}

// We need to invalidate this database state for EVERY session, even if other sessions aren't actively
// using this database, since they could still reference it with a db-qualified table name.
err := dsess.RemoveDbState(ctx, name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all a bit crunchy. Probably fine to check in for now, but it seems like this is a sign of missing abstractions on the GMS side.

Historically the Session interface was really underdefined for how much it was responsible for, and this kind of thing is a similar situation. We really want to engine to orchestrate dropping the database, including things like 1) preventing new connections to that DB once the drop operation begins, 2) cleanly invalidating any sessions / transactions that have that DB in scope. Might need to define a SessionProvider / SessionManager interface for this to work cleanly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree. I briefly thought about pushing some of this down to SessionManager (seems appropriate), but the state is at the Dolt layer of course, so it needed a callback hook and felt like the scope was increasing. You're right that it definitely smells like the abstraction is not ideal here.

if err != nil {
return true, err
}
return false, nil
})
if err != nil {
return err
}
}

return nil
}

Expand Down
23 changes: 21 additions & 2 deletions go/libraries/doltcore/sqle/dsess/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ func DSessFromSess(sess sql.Session) *DoltSession {
// LookupDbState returns the session state for the database named
func (d *DoltSession) lookupDbState(ctx *sql.Context, dbName string) (*DatabaseSessionState, bool, error) {
dbName = strings.ToLower(dbName)
d.mu.Lock()
dbState, ok := d.dbStates[dbName]
d.mu.Unlock()
if ok {
return dbState, ok, nil
}
Expand Down Expand Up @@ -165,12 +167,14 @@ func (d *DoltSession) lookupDbState(ctx *sql.Context, dbName string) (*DatabaseS
}
}

// If we got this far, we have a valid inital database state, so add it to the session for future reuse
// If we got this far, we have a valid initial database state, so add it to the session for future reuse
if err = d.AddDB(ctx, init); err != nil {
return nil, ok, err
}

d.mu.Lock()
dbState, ok = d.dbStates[dbName]
d.mu.Unlock()
if !ok {
return nil, false, sql.ErrDatabaseNotFound.New(dbName)
}
Expand All @@ -190,6 +194,14 @@ func (d *DoltSession) LookupDbState(ctx *sql.Context, dbName string) (*DatabaseS
return s, ok, nil
}

// RemoveDbState invalidates any cached db state in this session, for example, if a database is dropped.
func (d *DoltSession) RemoveDbState(_ *sql.Context, dbName string) error {
d.mu.Lock()
defer d.mu.Unlock()
delete(d.dbStates, strings.ToLower(dbName))
return nil
}

// Flush flushes all changes sitting in edit sessions to the session root for the database named. This normally
// happens automatically as part of statement execution, and is only necessary when the session is manually batched (as
// for bulk SQL import)
Expand Down Expand Up @@ -1063,6 +1075,9 @@ func (d *DoltSession) setHeadRefSessionVar(ctx *sql.Context, db, value string) e
}

func (d *DoltSession) setForeignKeyChecksSessionVar(ctx *sql.Context, key string, value interface{}) error {
d.mu.Lock()
defer d.mu.Unlock()

convertedVal, err := sqltypes.Int64.Convert(value)
if err != nil {
return err
Expand Down Expand Up @@ -1091,7 +1106,9 @@ func (d *DoltSession) setForeignKeyChecksSessionVar(ctx *sql.Context, key string
}

// HasDB returns true if |sess| is tracking state for this database.
func (d *DoltSession) HasDB(ctx *sql.Context, dbName string) bool {
func (d *DoltSession) HasDB(_ *sql.Context, dbName string) bool {
d.mu.Lock()
defer d.mu.Unlock()
_, ok := d.dbStates[strings.ToLower(dbName)]
return ok
}
Expand All @@ -1106,7 +1123,9 @@ func (d *DoltSession) AddDB(ctx *sql.Context, dbState InitialDbState) error {
DefineSystemVariablesForDB(db.Name())

sessionState := NewEmptyDatabaseSessionState()
d.mu.Lock()
d.dbStates[strings.ToLower(db.Name())] = sessionState
d.mu.Unlock()
sessionState.dbName = db.Name()
sessionState.db = db

Expand Down
121 changes: 121 additions & 0 deletions go/libraries/doltcore/sqle/enginetest/dolt_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,133 @@ var DoltBranchMultiSessionScriptTests = []queries.ScriptTest{
},
}

// DropDatabaseMultiSessionScriptTests test that when dropping a database, other sessions are properly updated
// and don't get left with old state that causes incorrect results.
// Note: this test needs to be run against a real Dolt sql-server, and not just with our transaction test scripts,
// because the transaction tests currently have a different behavior for session management and don't emulate prod.
var DropDatabaseMultiSessionScriptTests = []queries.ScriptTest{
{
Name: "Test multi-session behavior for dropping databases",
SetUpScript: []string{
"create database db01;",
"create table db01.t01 (pk int primary key);",
"insert into db01.t01 values (101), (202), (303);",
},
Assertions: []queries.ScriptTestAssertion{
{
Query: "/* client a */ use db01;",
Expected: []sql.Row{},
},
{
Query: "/* client b */ use db01;",
Expected: []sql.Row{},
},
{
Query: "/* client a */ show tables;",
Expected: []sql.Row{{"t01"}},
},
{
Query: "/* client b */ show tables;",
Expected: []sql.Row{{"t01"}},
},
{
Query: "/* client a */ drop database db01;",
Expected: []sql.Row{},
},
{
// TODO: This test runner doesn't currently support asserting against null values
Query: "/* client a */ select database() is NULL;",
Expected: []sql.Row{{1}},
},
{
Query: "/* client a */ show databases like 'db01';",
Expected: []sql.Row{},
},
{
Query: "/* client a */ create database db01;",
Expected: []sql.Row{},
},
{
Query: "/* client b */ select database();",
Expected: []sql.Row{{"db01"}},
},
{
Query: "/* client b */ show tables;",
Expected: []sql.Row{},
},
},
},
{
Name: "Test multi-session behavior for dropping databases with a revision db",
SetUpScript: []string{
"create database db01;",
"use db01;",
"create table db01.t01 (pk int primary key);",
"insert into db01.t01 values (101), (202), (303);",
"call dolt_commit('-Am', 'commit on main');",
"call dolt_checkout('-b', 'branch1');",
"insert into db01.t01 values (1001), (2002), (3003);",
"call dolt_commit('-Am', 'commit on branch1');",
},
Assertions: []queries.ScriptTestAssertion{
{
Query: "/* client a */ use db01;",
Expected: []sql.Row{},
},
{
Query: "/* client b */ use `db01/branch1`;",
Expected: []sql.Row{},
},
{
Query: "/* client a */ show tables;",
Expected: []sql.Row{{"t01"}},
},
{
Query: "/* client b */ show tables;",
Expected: []sql.Row{{"t01"}},
},
{
Query: "/* client a */ drop database db01;",
Expected: []sql.Row{},
},
{
// TODO: This test runner doesn't currently support asserting against null values
Query: "/* client a */ select database() is NULL;",
Expected: []sql.Row{{1}},
},
{
Query: "/* client a */ show databases like 'db01';",
Expected: []sql.Row{},
},
{
Query: "/* client a */ create database db01;",
Expected: []sql.Row{},
},
{
// At this point, this is an invalid revision database, and any queries against it will fail.
Query: "/* client b */ select database();",
Expected: []sql.Row{{"db01/branch1"}},
},
{
Query: "/* client b */ show tables;",
ExpectedErrStr: "Error 1105: database not found: db01/branch1",
},
},
},
}

// TestDoltMultiSessionBehavior runs tests that exercise multi-session logic on a running SQL server. Statements
// are sent through the server, from out of process, instead of directly to the in-process engine API.
func TestDoltMultiSessionBehavior(t *testing.T) {
testMultiSessionScriptTests(t, DoltBranchMultiSessionScriptTests)
}

// TestDropDatabaseMultiSessionBehavior tests that dropping a database from one session correctly updates state
// in other sessions.
func TestDropDatabaseMultiSessionBehavior(t *testing.T) {
testMultiSessionScriptTests(t, DropDatabaseMultiSessionScriptTests)
}

func testMultiSessionScriptTests(t *testing.T, tests []queries.ScriptTest) {
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
Expand Down