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

sql: make internal executor streaming #59330

Merged
merged 1 commit into from
Feb 10, 2021

Conversation

yuzefovich
Copy link
Member

@yuzefovich yuzefovich commented Jan 23, 2021

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 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

@yuzefovich yuzefovich added the do-not-merge bors won't merge a PR with this label. label Jan 23, 2021
@cockroach-teamcity
Copy link
Member

This change is Reviewable

@yuzefovich yuzefovich force-pushed the internal-executor branch 7 times, most recently from 2c1ec41 to 1f6cf97 Compare January 23, 2021 05:30
@yuzefovich yuzefovich requested a review from a team as a code owner January 23, 2021 05:30
@yuzefovich yuzefovich removed the request for review from a team January 23, 2021 05:31
@yuzefovich yuzefovich force-pushed the internal-executor branch 6 times, most recently from 9220e4c to c65dd5d Compare January 23, 2021 07:12
@yuzefovich yuzefovich requested review from a team and miretskiy and removed request for a team January 23, 2021 07:15
@yuzefovich yuzefovich removed the request for review from miretskiy January 23, 2021 07:20
@yuzefovich yuzefovich force-pushed the internal-executor branch 8 times, most recently from d541992 to 2d5b4e6 Compare January 23, 2021 19:58
@yuzefovich
Copy link
Member Author

bors r-

I think we can improve the semantics around closing the channel.

@craig
Copy link
Contributor

craig bot commented Feb 8, 2021

Canceled.

@yuzefovich yuzefovich force-pushed the internal-executor branch 2 times, most recently from 47d6628 to 2a1124a Compare February 8, 2021 01:28
@yuzefovich
Copy link
Member Author

@jordanlewis could please take another quick look at the change on when the channel is closed?

@yuzefovich yuzefovich force-pushed the internal-executor branch 2 times, most recently from d3c2f48 to edb89cf Compare February 8, 2021 02:37
@yuzefovich
Copy link
Member Author

Added another commit that improves the short-circuiting behavior. PTAL.

Copy link
Member Author

@yuzefovich yuzefovich left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: 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
Copy link
Contributor

@andreimatei andreimatei left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: 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.

Copy link
Member

@jordanlewis jordanlewis left a comment

Choose a reason for hiding this comment

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

Reviewable status: :shipit: 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.

Copy link
Member Author

@yuzefovich yuzefovich left a 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: :shipit: 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.

@craig
Copy link
Contributor

craig bot commented Feb 10, 2021

This PR was included in a batch that was canceled, it will be automatically retried

@craig
Copy link
Contributor

craig bot commented Feb 10, 2021

Build succeeded:

@craig craig bot merged commit aaa8c54 into cockroachdb:master Feb 10, 2021
@yuzefovich yuzefovich deleted the internal-executor branch February 10, 2021 14:59
craig bot pushed a commit that referenced this pull request Apr 19, 2023
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants