-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
sql: make internal executor streaming #59330
Conversation
2c1ec41
to
1f6cf97
Compare
9220e4c
to
c65dd5d
Compare
6c8980f
to
c6d9b34
Compare
d541992
to
2d5b4e6
Compare
bors r- I think we can improve the semantics around closing the channel. |
Canceled. |
47d6628
to
2a1124a
Compare
@jordanlewis could please take another quick look at the change on when the channel is closed? |
d3c2f48
to
edb89cf
Compare
Added another commit that improves the short-circuiting behavior. PTAL. |
edb89cf
to
921166a
Compare
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.
Reviewable status: complete! 0 of 0 LGTMs obtained (and 1 stale) (waiting on @andreimatei and @jordanlewis)
pkg/sql/internal.go, line 343 at r3 (raw file):
// We also need to exhaust the channel since the connExecutor goroutine // might be blocked on sending the row in AddRow(). // TODO(yuzefovich): at the moment, the connExecutor goroutine will not stop
Had to remove the second commit that tried to address it via deriving a child context before instantiating the connExecutor and canceling it here, in Close()
. The problem is that the context cancellation error might be sent on the channel, and I don't think it is possible to distinguish the error between "external" (the user of the iterator API cancels the context) which should be returned and "internal" (caused by us canceling the context). Any ideas on how to address this TODO?
This commit updates the internal executor to operate in a streaming fashion by refactoring its internal logic to implement an iterator pattern. A new method `QueryInternalEx` (and its counterpart `QueryInternal`) is introduced (both not used currently) while all existing methods of `InternalExecutor` interface are implemented using the new iterator logic. The communication between the iterator goroutine (the receiver) and the connExecutor goroutine (the sender) is done via a buffered (of 32 size in non-test setting) channel. The channel is closed when the connExecutor goroutine exits its run() loop. Care needs to be taken when closing the iterator - we need to make sure to close the stmtBuf (so that there are no more commands for the connExecutor goroutine to execute) and then we need to drain the channel (since the connExecutor goroutine might be blocked on adding a row to the channel). After that we have to wait for the connExecutor goroutine to exit so that we can finish the tracing span. For convenience purposes, if the iterator is fully exhausted, it will get closed automatically. Release note: None
921166a
to
178239e
Compare
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.
Reviewable status: complete! 0 of 0 LGTMs obtained (and 1 stale) (waiting on @andreimatei, @jordanlewis, and @yuzefovich)
pkg/sql/internal.go, line 766 at r1 (raw file):
But I think it's strictly worse than what we have here in terms of potential for true streaming usage, isn't it?
It's about the latency-to-first-result vs automatic retries tradeoff. Letting the caller specify the buffer size would put it in control of this tradeoff.
Unless we want to expose some kind of contract that forces the users of the internal executor to choose their portal limit, which feels wrong (too complex).
The interface we should be shooting for, in my opinion, should be either the go sql package driver interface, or pgwire. Implementing the streaming in the form of portals would keep the door open to using this internal executor through libpq or go sql. I think this channel-based results communication moves us away from that, but I'm not sure.
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.
Reviewable status: complete! 0 of 0 LGTMs obtained (and 1 stale) (waiting on @andreimatei and @yuzefovich)
pkg/sql/internal.go, line 766 at r1 (raw file):
Previously, andreimatei (Andrei Matei) wrote…
But I think it's strictly worse than what we have here in terms of potential for true streaming usage, isn't it?
It's about the latency-to-first-result vs automatic retries tradeoff. Letting the caller specify the buffer size would put it in control of this tradeoff.
Unless we want to expose some kind of contract that forces the users of the internal executor to choose their portal limit, which feels wrong (too complex).
The interface we should be shooting for, in my opinion, should be either the go sql package driver interface, or pgwire. Implementing the streaming in the form of portals would keep the door open to using this internal executor through libpq or go sql. I think this channel-based results communication moves us away from that, but I'm not sure.
I think your opinion is reasonable. But the burden to do this again is quite high. The proximal goal, now solved, of correcting the unbounded memory usage is by far the worst problem that we've had with the internal executor domain, IMO. I think from a strict priority POV, these other reasonable suggestions can come later.
pkg/sql/internal.go, line 343 at r3 (raw file):
Previously, yuzefovich (Yahor Yuzefovich) wrote…
Had to remove the second commit that tried to address it via deriving a child context before instantiating the connExecutor and canceling it here, in
Close()
. The problem is that the context cancellation error might be sent on the channel, and I don't think it is possible to distinguish the error between "external" (the user of the iterator API cancels the context) which should be returned and "internal" (caused by us canceling the context). Any ideas on how to address this TODO?
Hmm... I guess you could try to wrap the internal version and detect it specially, or the other way around? I don't know. What are the consequences of this TODO? Leaked goroutines are quite bad, will we leak goroutines? Or will execution just take a while to finish in some cases?
I think in general it's not great to have operations that don't cancel when top level contexts are canceled, because users expect that their cancellations trickle all the way down.
I'm okay merging this for now to get this work done. But you should make an issue and plan to do it soon, I think.
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.
TFTRs!
bors r+
Reviewable status: complete! 0 of 0 LGTMs obtained (and 1 stale) (waiting on @andreimatei and @jordanlewis)
pkg/sql/internal.go, line 343 at r3 (raw file):
Previously, jordanlewis (Jordan Lewis) wrote…
Hmm... I guess you could try to wrap the internal version and detect it specially, or the other way around? I don't know. What are the consequences of this TODO? Leaked goroutines are quite bad, will we leak goroutines? Or will execution just take a while to finish in some cases?
I think in general it's not great to have operations that don't cancel when top level contexts are canceled, because users expect that their cancellations trickle all the way down.
I'm okay merging this for now to get this work done. But you should make an issue and plan to do it soon, I think.
The consequences are only performance-related.
Imagine that we have a long-running query like SELECT * FROM t
executed via the iterator API, but we are only interested in the first few rows. Once the caller satisfies its limit, it'll call iterator.Close()
to finish early. However, the query execution (i.e. of ExecStmt
command currently being executed by the connExecutor) will not stop right away, still all of the remaining rows will be pushed onto the channel, and the iterator will drop them on the floor here, in Close()
. This TODO is about improving this situation so that the connExecutor short-circuited the execution of the current command.
To be clear, this is not about leaked goroutines; and if the caller cancels the context, that will stop the connExecutor goroutine too, so we are listening for top level context cancelation.
I'll file an issue to track addressing this, but I currently don't see an easy way to address this TODO.
This PR was included in a batch that was canceled, it will be automatically retried |
Build succeeded: |
101477: sql: fix internal executor when it encounters a retry error r=yuzefovich a=yuzefovich This PR contains several commits that fix a long-standing bug in the internal executor that could make it double count some things (rows or metadata) if an internal retry error occurs. In particular, since at least 21.1 (when we introduced the "streaming" internal executor in #59330) if the IE encounters a retry error _after_ it has communicated some results to the client, it would proceed to retry the corresponding command as if the incomplete execution and the retry error never happened. In other words, it was possible for some rows to be double "counted" (either to be directly included multiple times into the result set or indirectly into the "rows affected" number). This PR fixes the problem by returning the retry error to the client in cases when some actual rows have already been communicated and by resetting the number of "rows affected" when "rewinding the stmt buffer" in order to retry the error transparently. Fixes: #98558. Co-authored-by: Yahor Yuzefovich <yahor@cockroachlabs.com>
This commit updates the internal executor to operate in a streaming
fashion by refactoring its internal logic to implement an iterator
pattern. A new method
QueryInternalEx
(and its counterpartQueryInternal
) is introduced (both not used currently) while allexisting methods of
InternalExecutor
interface are implementedusing the new iterator logic.
The communication between the iterator goroutine (the receiver) and the
connExecutor goroutine (the sender) is done via a buffered (of 32 size
in non-test setting) channel. The channel is closed when the
connExecutor goroutine exits its run() loop.
Care needs to be taken when closing the iterator - we need to make sure
to close the stmtBuf (so that there are no more commands for the
connExecutor goroutine to execute) and then we need to unblockingly
drain the channel (since the connExecutor goroutine might be blocked on
adding a row to the channel). After that we have to wait for the
connExecutor goroutine to exit so that we can finish the tracing span.
For convenience purposes, if the iterator is fully exhausted, it will
get closed automatically.
Addresses: #48595.
Release note: None