-
Notifications
You must be signed in to change notification settings - Fork 696
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
Don't copy utilityStmt into the incorrect memory context #2128
Conversation
Codecov Report
@@ Coverage Diff @@
## master #2128 +/- ##
==========================================
+ Coverage 93.72% 93.79% +0.06%
==========================================
Files 100 100
Lines 25895 25907 +12
==========================================
+ Hits 24271 24300 +29
+ Misses 1624 1607 -17 |
You could use I guess we do various kinds of trickery with We should maybe also reconsider some of the ways in which we (ab)use the COPY utility hook. For example, it seems we have some special logic in the utility hook around |
@lithp could you update the title |
plpgsql
func fails after first invocation
Nice catch with
I think we're safe to
I've pushed another commit which implements (2). For now I'm copying into the original memory context and leaving the old parse tree alone. The only other danger I can think of is that some other part of postgres holds onto a reference to the old |
|
||
parsetree = copyObject(parsetree); |
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'm pretty hesitant to say we should perform all of ProcessCopyStmt
under the parsetree
's planner context, it seems like a good way to get a memory leak (I don't mean a long-lived one, I just mean running the same function over and over in the same connection could keep adding things to the cached plan's context, right?)
I have two alternatives:
- Copy the object into your own context, pass it to
ProcessCopyStmt
, then copy the return value into the plan context. This way we're only copying what we end up needing for the cached plan (i.e. the parsetree) - Keep the code as-is, except switch back to the previous context before calling
ProcessCopyStmt
. To do this, you'll need to go intoProcessCopyStmt
and find anywhere you write to the parsetree. So far as I can see, this is itsrelation->schemaname
(already handled on line 826 in the past, apparently by me), the filename (on line 913), and the entirety of lines 845 to 870 (which build an entirely new copy statement and select query, which would need to be in the right context)
Performing two copies seems silly but I suppose it's the easier solution?
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.
Yeh, this definitely leaks a bunch of memory into CachedPlan
, which has session lifetime.
|
||
parsetree = copyObject(parsetree); | ||
parsetree = ProcessCopyStmt((CopyStmt *) parsetree, completionTag, |
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.
Alternately, you could refactor all of ProcessCopyStmt
to ensure it never mutates the parsetree
it is handed. Instead, you'd declare variables like newSchemaName
or newFileName
or newQuery
and use them to build a new CopyStatement
at the very bottom of the method (in the right memory context), and return that. This is probably both cleanest and most performant in the long run, but I wouldn't mind if you go with the double-copy + a TODO
to get unblocked.
OK, so I don't really like the whole I think you can If you want to go with another approach, see the two alternatives I suggest (getting rid of mutations to the input I'll leave it up to you as to which you choose; I think the double-copy is fine to unblock you, though. Long-term we should refactor the |
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 gave a couple of impressions on this approach and some alternatives. Pick one (and maybe @marcocitus can give this a second look) and .
|
||
parsetree = copyObject(parsetree); |
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.
Yeh, this definitely leaks a bunch of memory into CachedPlan
, which has session lifetime.
Since this is blocking the code-freeze I decided to go with the quicker option. It does leak an extra copy of This also has the advantage of being fool-proof, future coders of Also, I'm pretty sure that the first copy is unnecessary, I don't think it matters whether we scribble on it, but just to be safe I left the first copy in, utility statements are usually (always?) small so this shouldn't add a noticeable overhead. |
cfce59b
to
274a434
Compare
----------------------- | ||
|
||
(1 row) | ||
|
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.
It'd be nice to also add a test with some Citus-specific COPY logic that scribbles on the parse tree (e.g. COPY distributed_table TO ...
)
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've added it but it can't really show correctness, just that we don't crash.
COPY […] TO STDOUT
isn't permitted in PL/pgSQL, and writing to a file isn't something we do in tests, so that leaves sending it to a program, which works but doesn't really print any output.
I considered writing out to some sort of csv
file in the results directory and then adding a correct csv
file in the expected directory (they'd probably have to end in out
) and adding a blank test with that name to trick pg_regress
/diff
into comparing the output and expectation of the COPY TO
for us, but that seemed way too hacky.
So I've just added a test that verifies we don't crash.
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.
Hm, I've actually removed the test because I'm seeing weird behavior in Travis with COPY TO PROGRAM (namely a broken pipe). It no longer crashes, at any rate.
parsetree = ProcessCopyStmt((CopyStmt *) parsetree, completionTag, | ||
&commandMustRunAsOwner); | ||
|
||
previousContext = MemoryContextSwitchTo(planContext); |
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.
This warrants a comment and maybe even point out the memory leak.
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.
Both done.
|
||
parsetree = copyObject(parsetree); |
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.
Could we use an intermediate variable here? It gets a bit confusing that we're dealing with 3 different values of parsetree
.
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.
Fixed.
I'll give this a quick look after the team meeting and we can get it merged. |
@marcocitus I talked to @lithp and I'm gonna clean this up according to your latest feedback and merge. Can't we add a |
Assuming we'd recursively free the tree, then that may be what we should be doing, but we'd have to make very sure that the original parse tree won't get used after the call to the process utility hook. |
Fine with me. |
I wrongly assumed there'd be some sort of recursive free in PostgreSQL, akin to copy and equality functions. I suppose it's a testament to the memory functions of PostgreSQL that I've never really needed it. |
934fe18
to
37c4828
Compare
utilityStmt sometimes (such as when it's inside of a plpgsql function) comes from a cached plan, which is kept in a child of the CacheMemoryContext. When we naively call copyObject we're copying it into a statement-local context, which corrupts the cached plan when it's thrown away.
While working on #2119 I discovered #2127,
plpgsql
functions which callCOPY
fail on the second invocation. There's a full explanation in #2127, but the important part is that wecopyObject
into a memory context which gets thrown away. This PR removes the copy.I don't think this is the right solution. I wonder if you have any feedback which could point me to the correct solution. Some ideas I've come up with:
pstmt
is in, andcopyObject
into that context.CacheMemoryContext
.(2) seems like the best solution, but I'm not sure it's possible.