-
-
Notifications
You must be signed in to change notification settings - Fork 530
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
base: main
Are you sure you want to change the base?
Conversation
Allows dolt_gc implementation to carry state, such as a session manager. This prepares for it to implement more robust GC safepoints.
…afepointController which can work with it.
…t_gc safepoint controller.
…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.
…ave more principled lifecycle. Starting replication never uses the replcation execution context.
There was a problem hiding this 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) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
There was a problem hiding this 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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Brilliant!
// 3) It sets |OutstandingCommand| for the Session to true. Only | ||
// one command can be outstanding at a time, and whether a command |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 DOLT
|
… session. Fixes special-case lifecycle for dolt_gc procedure.
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 callingdolt_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
withDOLT_GC_SAFEPOINT_CONTROLLER_CHOICE=session_aware
.This PR builds on a number of preceding PRs, and introduces two major subtleties:
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.
(*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.