Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[no-release-notes] go: sqle: dprocedures: dolt_gc: Implement a session-aware GC safepoint controller. #8798

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

reltuk
Copy link
Contributor

@reltuk reltuk commented Jan 29, 2025

Currently, if you run GC against a running sql-server, as part of the GC process the server would cancel all inflight queries and close all existing SQL connections. It would also leave the inflight connection which made the call dolt_gc() request in an invalidated state where it would fail all queries going forward. The end result is that calling dolt_gc on a running server is disruptive and requires careful handling by existing users.

This PR introduces a new session-aware safepoint controller. In order to establish a safepoint, it starts tracking all inflight sessions shortly after the GC process begins. It adds lifecycle hooks so that those sessions get a chance to give their GC roots to the GC process once they are quiesced and before the GC process completes. It allows the GC process to block on these rendezvous so that it can be certain it has seen all inflight work before it finalizes the GC.

To turn this behavior on, run dolt sql-server with DOLT_GC_SAFEPOINT_CONTROLLER_CHOICE=session_aware.

This PR builds on a number of preceding PRs, and introduces two major subtleties:

  1. Session lifecycle callbacks need to be made from everywhere that may be running mutations against the database. For example, PRs leading up to this one needed to change GMS server/handler, GMS eventscheduler, Dolt remotesrv and Dolt sqle/cluster. It is easy to forget these in a particular instance and everything will seemingly work, but GC will no longer be safe.

  2. (*DoltSession).VisitGCRoots needs to know how to find all reachable GC roots from the session. It's easy to forget to update it if you add things to session state, or anywhere else. If a developer omits a GC root, everything will seemingly work, but GC will no longer be safe.

That being said, auto GC is a critical requirement for Dolt and that means making it so that GC is correct and not disruptive to ongoing workloads. As a result, we are pursuing this solution as is, and will continue iterating on developer ergonomics and safety under change going forward.

Allows dolt_gc implementation to carry state, such as a session
manager. This prepares for it to implement more robust GC safepoints.
…le to control dolt_gc safepoint behavior.

This is a short-term setting which will allow choosing the session-aware
gc safepoint behavior, instead of the legacy behavior which kills all
in-flight connections when performing a GC.
@reltuk reltuk requested review from max-hoffman and zachmu January 29, 2025 00:03
@coffeegoddd
Copy link
Contributor

@reltuk DOLT

comparing_percentages
100.000000 to 100.000000
version result total
fc3217e ok 5937457
version total_tests
fc3217e 5937457
correctness_percentage
100.0

…ave more principled lifecycle. Starting replication never uses the replcation execution context.
@coffeegoddd
Copy link
Contributor

@reltuk DOLT

comparing_percentages
100.000000 to 100.000000
version result total
13c9ddf ok 5937457
version total_tests
13c9ddf 5937457
correctness_percentage
100.0

@coffeegoddd
Copy link
Contributor

@reltuk DOLT

comparing_percentages
100.000000 to 100.000000
version result total
819136d ok 5937457
version total_tests
819136d 5937457
correctness_percentage
100.0

Copy link
Member

@zachmu zachmu left a comment

Choose a reason for hiding this comment

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

The thing I don't understand about this behavior: new sessions starting after the call to BeginGC are not subject to the restrictions to not begin new work until GC finalization finishes. This means they can write chunks that the GC finalizer won't know about, since they aren't in the set of sessions visited during finalization.

The only way this could work is if the chunks they are writing aren't subject to collection, maybe because they are after a high water mark in the journal or something similar? I missed the first several PRs in this chain of work so I'm catching up here, but I couldn't easily find whether that assumption is true. Even then it's not obvious to me how we would prevent a new session from having a chunk it needs be collected in all cases (it could need a chunk that it thinks is already present in the value store, but is getting marked for collection).

Some high-level and in-method comments could clear this up, maybe

// from the working set. This is used in GC, for example, where all dependencies of the in-memory working
// set value need to be accounted for.
func (ddb *DoltDB) WorkingSetHashes(ctx context.Context, ws *WorkingSet) ([]hash.Hash, error) {
spec, err := ws.writeValues(ctx, ddb, nil)
Copy link
Member

Choose a reason for hiding this comment

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

The logic in this method seems like it would be more naturally contained by the WorkingSet type

panic("SesisonBeginCommand called on a session that already had an outstanding command.")
}
toWait := state.QuiesceCallbackDone.Load().(chan struct{})
select {
Copy link
Member

Choose a reason for hiding this comment

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

A comment might be helpful for this latch logic

Is the idea that we immediately unblock on a closed channel, but otherwise we actually do block until channel close? Not obvious why the unlocking then relocking is required in the latter case.

Copy link
Member

Choose a reason for hiding this comment

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

So reading the tests, it seems the purpose of this logic is that existing sessions must wait to begin new commands until an existing call to Wait() has completed, but new sessions aren't subject to this constraint?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a bunch of comments and some helper methods and stuff. Maybe it's more clear...if you want to take a look...

go/libraries/doltcore/sqle/dsess/session.go Outdated Show resolved Hide resolved
go/libraries/doltcore/sqle/dsess/session.go Show resolved Hide resolved
Copy link
Contributor

@max-hoffman max-hoffman left a comment

Choose a reason for hiding this comment

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

LGTM, just some naming comments for session controller. Same comments about testing from before, I'm running into the same issues in stats where you have to dig a bit to find weird concurrency bugs.

func (d *doltBinlogReplicaController) AutoStart(_ context.Context) error {
runningState, err := loadReplicationRunningState(d.ctx)
func (d *doltBinlogReplicaController) AutoStart(ctx *sql.Context) error {
sql.SessionCommandBegin(ctx.Session)
Copy link
Contributor

Choose a reason for hiding this comment

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

I noted this on the other PR, I don't think we are generally very disciplined about sql session lifecycle management. Maybe it only matters in a few key places for GC

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, but we will have to get better where it matters...

@@ -484,9 +485,11 @@ func (d *DoltHarness) NewReadOnlyEngine(provider sql.DatabaseProvider) (enginete
if err != nil {
return nil, err
}
gcSafepointController := dsess.NewGCSafepointController()
readOnlyProvider.RegisterProcedure(dprocedures.NewDoltGCProcedure(gcSafepointController))
Copy link
Contributor

Choose a reason for hiding this comment

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

does this need to be structured different from other procs? the controller is accessible from the session, and it seems like the controller holds all of the state

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Brilliant!

Comment on lines 226 to 227
// 3) It sets |OutstandingCommand| for the Session to true. Only
// one command can be outstanding at a time, and whether a command
Copy link
Contributor

Choose a reason for hiding this comment

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

wording maybe confused me a bit, you mean that each session can only have one controller callback at a time?

Copy link
Contributor

Choose a reason for hiding this comment

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

also seems like you use OutstandingVisitCall and OutstandingCommand interchangeably?

Copy link
Contributor

Choose a reason for hiding this comment

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

the optionality and use of this in waiter seems like it's basically like "do callback", which is always going to be a session finalize outside of unit tests. I guess just it might be possible to simplify "SessionCommand"'s naming, there are so many session prefixed names moving around

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I could definitely find better names.

OutstandingVisitCall and OutstandingCommand are different concepts here. Basically, OutstandingCommand just means "the application layer is currently touching this session." OutstandingCommand is used to control when we visit the session if we make a Waiter – we can visit sessions without commands immediately, but for sessions currently doing work, we need to wait until their commandend call comes in.

OutstandingVisitCall means we made a waiter and we are currently calling the callback on this session. In the case, the rest of the application layer should not be touching the session, and SessionCommandBegin will block.

@coffeegoddd
Copy link
Contributor

@reltuk DOLT

comparing_percentages
100.000000 to 100.000000
version result total
7c1cff7 ok 5937457
version total_tests
7c1cff7 5937457
correctness_percentage
100.0

@coffeegoddd
Copy link
Contributor

@coffeegoddd DOLT

comparing_percentages
100.000000 to 100.000000
version result total
f79db4a ok 5937457
version total_tests
f79db4a 5937457
correctness_percentage
100.0

… session. Fixes special-case lifecycle for dolt_gc procedure.
@coffeegoddd
Copy link
Contributor

@reltuk DOLT

comparing_percentages
100.000000 to 100.000000
version result total
b9f6a56 ok 5937457
version total_tests
b9f6a56 5937457
correctness_percentage
100.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants