-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(server): fix memory leak on lua error #4236
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
Conversation
Signed-off-by: adi_holden <[email protected]>
Good catch, Adi! What do you mean by scoping? how this change makes sure that |
src/core/interpreter.cc
Outdated
@@ -467,7 +469,7 @@ int RedisLogCommand(lua_State* lua) { | |||
int argc = lua_gettop(lua); | |||
if (argc < 2) { | |||
PushError(lua, "redis.log() requires two arguments or more."); | |||
return RaiseError(lua); | |||
RaiseError(lua); |
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 think that keeping the return
part here makes sense, like the lua API docs mention (this way readers can't be mislead to think that the flow will continue)
Unless you rename it like I suggested above, then I think it's ok as is
And indeed, very nice catch! |
Signed-off-by: adi_holden <[email protected]>
src/core/interpreter.cc
Outdated
@@ -993,7 +969,11 @@ int Interpreter::RedisGenericCommand(bool raise_error, bool async, ObjectExplore | |||
/* Pop all arguments from the stack, we do not need them anymore | |||
* and this way we guaranty we will have room on the stack for the result. */ | |||
lua_pop(lua_, argc); | |||
return std::make_optional(args); |
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.
args reference name_buffer
, which is on stack 🤷🏼
src/core/interpreter.cc
Outdated
|
||
std::optional<int> Interpreter::CallRedisFunction(bool* raise_error, bool async, |
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 think the interface here is too much. It has optional that wraps int that based on the code can return only 0 and 1, and we have raise_error that is another output argument?
- CallRedisFunction should return number of arguments it put on stack. Does CallRedisFunction ever return 0? Is lua stack really on 0 when it happens ?
- raise_error seems to be relevant only if CallRedisFunction returns nullopt.
Maybe introduce CallResult = variant<int, bool>
so if the first argument is defined then it's number of results and the second is raise_error?
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.
simplified the flow
src/core/interpreter.cc
Outdated
@@ -1017,9 +997,9 @@ int Interpreter::RedisGenericCommand(bool raise_error, bool async, ObjectExplore | |||
return 0; | |||
|
|||
// Raise error for regular 'call' command if needed. | |||
if (raise_error && translator->HasError()) { | |||
if (*raise_error && translator->HasError()) { |
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.
what does it mean
if (!translator)
return 0;
above ?
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 actually dont know.. this is not code that I changed
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 see we use it inside DragonflyHashCommand, were we define a custom explorer StringCollectorTranslator
but why do we return here 0 I dont know
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 changed it to return 1 and it looks like everything works
Signed-off-by: adi_holden <[email protected]>
Signed-off-by: adi_holden <[email protected]>
Signed-off-by: adi_holden <[email protected]>
src/core/interpreter.cc
Outdated
|
||
// Calls redis function | ||
// return true if error needs to be raised in case api returns error. |
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's kinda backwards to return true
if there's an error, and false
on success, don't you 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.
I flipped it though I do find it convenient to return true if we need to raise an error
src/core/interpreter.cc
Outdated
return 1; | ||
} | ||
|
||
// IMPORTANT! all allocations withing this funciton must be freed |
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.
// IMPORTANT! all allocations withing this funciton must be freed | |
// IMPORTANT! all allocations within this funciton must be freed |
std::optional<absl::FixedArray<std::string_view, 4>> args = PrepareArgs(); | ||
if (args.has_value()) { | ||
raise_error = CallRedisFunction(raise_error, async, explorer, SliceSpan{*args}); | ||
} |
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.
do we need an else
here to force raise_error
to be true
?
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 the idea is that raise error is a param to function RedisGenericCommand
and if we need to raise error in case of an error it will be true when this function is called
the call to CallRedisFunction will overide this param if the invocation of the command was successful
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 guess I don't have the full context here..
What's the purpose of PushError()
without calling RaiseErrorAndAbort()
? Where is this error later read?
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.
when calling RaiseErrorAndAbort the script execution will abort
when pushing and error and not calling RaiseErrorAndAbort the script will not abort, so the error will be returned for the executed command and the script writer can decide how to handle this error
Signed-off-by: adi_holden <[email protected]>
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
The bug:
calling lua_error does not return, instead it unwinds the Lua call stack until an error handler is found or the
script exits. This lead to memory leak on object that should release memory in destructor.
Specific example is the absl::FixedArray<string_view, 4> args(argc); which allocates on heap if argc > 4. The free was not called leading to memory leak.
The fix:
Add scoping to RedisGenericCommand and call RaiseError which calls lua_error after goto after destructors are called