-
Notifications
You must be signed in to change notification settings - Fork 3.2k
mp.input: avoiding race conditions arising from multiple input requests #17256
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
base: master
Are you sure you want to change the base?
mp.input: avoiding race conditions arising from multiple input requests #17256
Conversation
b0b1bb6 to
4de6117
Compare
This is exactly what If you just don't set The proposed auto-closing after 20 seconds example is also trivially fixed by killing the timer in I also think |
player/lua/console.lua
Outdated
| mp.register_script_message("get-input", function (script_name, args) | ||
| if open and script_name ~= input_caller then | ||
| mp.commandv("script-message-to", input_caller, "input-event", | ||
| mp.register_script_message("terminate-input", function(message) |
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.
Why not just add this to disable and set not active if message is nil? Or just replace disable if you don't like that name, it was never documented anyway,
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 created a new script-message because I wasn't sure if disable was being used anywhere else and I didn't want to mess with the interface. If its not an issue I will just add a new argument for disable.
|
Also if we do want this why not just save the last id within |
Scripts written before
The fact that the current solution involves removing the Now we could just document this, something like:
However, I think we can do better than this and provide an API that allows using
You're right, I overstated the guardrails required for the timer, not sure what I was thinking there. However, this suggestion is still vulnerable to race conditions; the timeout can go off after a new request has replaced the current one, but before the
This would indeed fix race conditions between scripts, but it does not fix them within the same script. There is a period of time, after a new I think there's a decent middle-ground here. We could keep the explicit ids and allow them to be passed to
Personally, I think this definition would give me much more confidence as a script author than the other definition I wrote above. (Technically the above proposal might break any scripts that were using |
ae0243f to
3d69795
Compare
|
I have added two new commits to plug a couple of other race conditions.
|
Well I was aking about the advantage over leaving
Yeah sure though that's extremely unlikely. By the way, this is what stats.lua already does.
I still think this should be solveable without making developers pass around ids since input.lua has all the needed information. Something like mp.register_script_message(handler_id, function (type, args) {
local latest_id_backup = latest_id
latest_id = handler_id
t[type](args)
latest_id = latest_id_backupEDIT: also avoid restoring the id if it was changed within the callback |
Hmm, I see what you mean, we could do something like this: local function register_event_handler(t)
local handler_id = "input-event/"..handle_counter
handle_counter = handle_counter + 1
latest_handler_id = handler_id
mp.register_script_message(handler_id, function (type, args)
-- do not process events (other than closed) for an input that has been overwritten
if latest_handler_id ~= handler_id and type ~= 'closed' then
return
end
if t[type] then
local completions, completion_pos, completion_append =
t[type](unpack(utils.parse_json(args or "") or {}))
if type == "complete" and completions then
mp.commandv("script-message-to", "console", "complete",
utils.format_json(completions), completion_pos,
completion_append or "")
end
end
if type == "closed" then
mp.unregister_script_message(handler_id)
end
end)
return handler_id
endThis would avoid data races from occurring as a result of global_timer = mp.add_timeout(20, input.terminate, true)
...
global_timer:kill()
input.get({
prompt = "Open File:\n> ",
history_path = "~~state/open-file-history",
opened = function()
global_timer:resume()
end,
edited = function()
global_timer:kill()
global_timer:resume()
end,
submit = function(path)
mp.commandv("loadfile", path)
end,
closed = function()
global_timer:kill()
end
})And of course there may be a couple of events that end up getting dropped like a last millisecond submit request, but the timing would be so close that that shouldn't be an issue. If you think this is the better option, then I can change the PR. This should also address the main cause of in-service log race conditions, meaning that the |
|
Well I personally think it's nicer to not add |
b4fc459 to
35b1cfd
Compare
|
Okay, I have force pushed a new set of commits that automatically pass console.lua log and handler ids. I believe this means that all the inherent race conditions in |
35b1cfd to
bec968c
Compare
2e549ce to
60fa827
Compare
This makes changes to mp.input and console.lua so that every input.get request uses a unique script-message to handle input events. Previously, making new input.get requests shortly after the termination of a previous request made by the same script could cause a race condition where the input handler was closed but the new request was still being drawn in the UI. This was caused by the `closed` event for the previous request being received only after the new request was registered, hence closing the event handler for the new request instead. In addition, this commit makes the behaviour of calling input.get while another request is active more consistent. When a new request is received it overwrites the in-progress request, sending a `closed` event. However, previously, the `closed` event could not be sent if both requests came from the same script, as the new request would have overwritten the event handler. Now, the `closed` event is called regardless of where the new request comes from.
This commit ensures that `input.terminate()` can only close input requests made by the same script, and prevents any in-transit events for old input requests from being processed. Previously, there was no way to guarantee that the input request being terminated was the one intended; the asynchronous nature of the API meant that it was always possible (though unlikely) that another client may have activated its own input request while the termination request was in transit. This commit removes the race condition between different scripts calling `input.terminate()` by sending the script name alongside the termination message. In addition, when a script overwrites one of its own input requests, there may be incoming events still in transit. Some of these events may have a decent chance of calling `input.terminate()` if they are processed (e.g., `submit`). This commit avoids this issue by only processing `closed` requests once a new `input.get()` request is made.
It was previously possible for completion messages to be received by a new input request, if one was created while the message was in transit. Since it is trivial to avoid this by passing the script name and existing handle_id value, we may as well do so and guarantee that there will not be any data races.
This commit modifies the log methods in mp.input to always send the id of the latest `input.get()` request with log entries. Previously, the log methods applied to whichever input request happened to be open when the log message was received. Even when scripts used these methods correctly, there was the risk of sending a log to the wrong log buffer if the active input request changed while the log message was in transit; a race condition. Now the id of the latest `input.get()` request is sent alongside the log messages, preventing data races between scripts while also preventing those logs from being discarded.
60fa827 to
9cb1c5d
Compare
|
I believe I have addressed all of the review comments 👍. |
This PR builds on the changes proposed in #17251, I am submitting this now for comments.
The
mp.inpututilities are extremely useful, however the library currently does not provide the tools to safely use the various methods without creating code that is vulnerable to race conditions. Most of these race conditions are relatively uncommon, requiring multiple input requests to be made in close succession to each other. However, as the library becomes more popular and developers start (ab)using it more, these conditions become more likely to occur. This PR is just to point out where these data races currently occur and to present and discuss some possible solutions. @guidocella keen to hear your thoughts, especially.I have identified three parts of the code where race conditions can occur:
input.terminate().Edit:
4. When responding to autocomplete events.
input.terminate()Problem
According to the discussions I've had and the examples I have seen, the primary suggested usecase for
input.terminate()is to close the input request that a client is using once it no longer needs any further input. However,input.terminate()does not specify which input it closes, it just closes whatever input is open. This means that if a second client creates a new input request at approximately the same time that the first client calls terminate on their input, the terminate request from the first client may arrive after the second client has opened their input prompt, immediately terminating it. This can be rather easily tested with the following two scripts:In the above examples, whether the second prompt ends up being displayed to the user depends on the length of the while loop, a race condition.
Obviously this is a very contrived example, but there are scenarios where this is viable. For example, the first script could easily be a user configured
input.selectmenu of arbitrary commands (e.g., a more modern version of this), which a user has configured to trigger another input dialog.Solution
An easy way to mitigate this issue is to take some queues from
mp.command_native_async()andmp.abort_async_command()and use a uniqueidvalue for terminations. Take the below example:We use an opaque
idvalue returned byinput.getorinput.selectand pass it intoinput.terminate(). The above example uses this to implement a timeout on the request. Normally, this would require significantly more guard rails to preventinput.terminate()from being called after this input has been closed, but with ids it's trivial.I propose we add these return values for
input.getandinput.selectand optionally allow them to be passed intoinput.terminate(), and update the documentation to propose the above approach as best practice. That way, no existing scripts break (a nil argument toinput.terminate()still clears everything), but future scripts that follow best practice avoid causing race conditions. It's also fully backwards compatible; if a script using these ids were run on an older version of mpv, it would be akin to passing a nil value intoinput.terminate(), which means things will just work as they do now.This PR already does these things.
Logs
Problem
The problem with the logs is basically the same as with terminate; the logs operate on whatever input happens to be open even if it's not what the client might expect. This is trivially easy to test:
Launch mpv, open
commands.luato see the spam in the console output, then use Ctrl+Home to replacecommands.luawith our second prompt. You should see that several of thehey!!!!!spam makes it into the logs of the new request.Solution
I imagine that the way to avoid this issue would be to explicitly specify the log id of logs rather than rely on the correct logger being active in
console.lua. I haven't made any changes to this currently, as the log format seems a little more strictly defined and I wasn't sure about the best way to add the id information (Hence the RFC).