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

Do not allow exceptions to propagate from backend #40

Merged
merged 3 commits into from
Jan 3, 2023

Conversation

stephenfin
Copy link
Contributor

@stephenfin stephenfin commented Dec 26, 2022

The pyproject_api project provides an implementation of a PEP-517 build frontend. Consider the architecture described in the proposal [1]:

+-----------+          +---------------+           +----------------+
| frontend  | -spawn-> | child cmdline | -Python-> |    backend     |
|           |          |   interface   |           | implementation |
+-----------+          +---------------+           +----------------+

We can map the these components to 'pyproject_api' components like so:

  • frontend ->
    pyproject_api._frontend.Frontend (or rather, a concrete implementation of same)

  • child cmdline implementation ->
    pyproject_api._backend

  • backend implementation ->
    (the backend defined by the [build-system] build-backend key in pyproject.yaml, typically as parsed by the pyproject_api._frontend.Frontend.create_args_from_folder method)

In both the pyproject_api._via_fresh_subprocess.SubprocessFrontend and tox.tox_env.python.virtual_env.package.pyproject.Pep517VirtualEnvFrontend concrete implementations of pyproject_api._frontend.Frontend, the child cmdline implementation provided by pyproject_api._backend is called via subprocess. However, there is a crucial difference: while the former will start a new subprocess for each command invoked, the latter will reuse the backend. This can be problematic. Communication between the frontends and the child cmdline implementation is done by issuing commands in serialized JSON format to the pyproject_api._backend executable via stdin, then waiting for a special Backend: Wrote response foo to $file message to appear on stdout, before finally reading the contents of the (JSON-formatted) file at $file.

In the success case, the file at $file will contain a single key, result, while in the error case the file will contain three keys, code, exc_type and exc_msg. The contents of the latter keys are generated by catching any exception raised by the build backend and processing it. However - and here is the crux of the issue - if these exceptions are not derived from Exception then we allow them to bubble up, killing the backend process in the process. A comment inline suggests this is intended to allow things like SystemExit or KeyboardInterrupt, but setuptools' (or rather, distutils') exceptions do not derive from Exception either.

This is an issue for frontends that re-use backends like the Pep517VirtualEnvFrontend provided by tox. These backends expect to politely cleanup when the backend fails (as indicated by the presence of code, exc_type and exc_msg keys in the result file) by calling a special _exit command on the backend. However, if the backend process has died, there's nothing left to answer this request. This causes tox at least to hang forever.

There are a two solutions to this. Firstly, we can remove the special case for these two exceptions thus allowing the frontend to manage the lifecycle of the _backend executable process regardless. Alternatively, we could add a new key to the result file indicating that the backend process has died and the frontend should not expect to be able to do any cleanup. The latter option would require changing the "interface" defined by this result file and updating all frontend implementations to support this. As a result, we opt for the former as the simpler choice. A well behaved frontend should ensure a backend process is never allowed to run forever so the end-user experience should be identical.

[*] Note that this is different to errors the occur while initially starting the backend executable or processing a command. If the backend fails to start, a separate special message is issued (failed to start backend), which the frontend is expected to handle, and the exception is re-raised. Invalid requests, meanwhile, are simply ignored and a message is logged which the frontend could choose to handle.

Closes: #39

The 'pyproject_api' project provides an implementation of a PEP-517
build frontend. Consider the architecture described in the proposal [1]:

  +-----------+          +---------------+           +----------------+
  | frontend  | -spawn-> | child cmdline | -Python-> |    backend     |
  |           |          |   interface   |           | implementation |
  +-----------+          +---------------+           +----------------+

We can map the these components to 'pyproject_api' components like so:

  frontend ->
    pyproject_api._frontend.Frontend (or rather, a concrete
    implementation of same)

  child cmdline implementation ->
    pyproject_api._backend

  backend implementation ->
    (the backend defined by the '[build-system] build-backend' key in
    'pyproject.yaml', typically as parsed by the
    'pyproject_api._frontend.Frontend.create_args_from_folder' method)

In both the 'pyproject_api._via_fresh_subprocess.SubprocessFrontend' and
'tox.tox_env.python.virtual_env.package.pyproject.Pep517VirtualEnvFrontend'
concrete implementations of 'pyproject_api._frontend.Frontend', the
child cmdline implementation provided by 'pyproject_api._backend' is
called via subprocess. However, there is a crucial difference: while the
former will start a new subprocess for each command invoked, the latter
will reuse the backend. This can be problematic. Communication between
the frontends and the child cmdline implementation is done by issuing
commands in serialized JSON format to the 'pyproject_api._backend'
executable via stdin, then waiting for a special "Backend: Wrote
response foo to $file" message to appear on stdout, before finally
reading the contents of the (JSON-formatted) file at '$file'.

In the success case, the file at '$file' will contain a single key,
'result', while in the error case the file will contain three keys,
'code', 'exc_type' and 'exc_msg'. The contents of the latter keys are
generated by catching any exception raised by the build backend and
processing it. However - and here is the crux of the issue - if these
exceptions are not derived from 'Exception' then we allow them to bubble
up, killing the backend process in the process. A comment inline
suggests this is intended to allow things like 'SystemExit' or
'KeyboardInterrupt', but setuptools' (or rather, distutils') exceptions
do not derive from 'Exception' either.

This is an issue for frontends that re-use backends like the
'Pep517VirtualEnvFrontend' provided by tox. These backends expect to
politely cleanup when the backend fails (as indicated by the presence of
'code', 'exc_type', and 'exc_msg' keys in the result file) by calling a
special '_exit' command on the backend. However, if the backend process
has died, there's nothing left to answer this request. This causes tox
at least to hang forever.

There are a two solutions to this. Firstly, we can remove the special
case for these two exceptions thus allowing the frontend to manage the
lifecycle of the '_backend' executable process regardless. An
alternative option would be to add a new key to the result file
indicating the backend process has died and the frontend should not
expect to be able to do any cleanup, but this would require changing the
"interface" defined by this result file and updating all frontend
implementations to support this. In this change, we opt for the former
since it's simpler and a well behaved frontend should ensure a backend
process is never allowed to run forever.

[*] Note that this is different to errors the occur while initially
starting the backend executable or processing a command. If the backend
fails to start, a separate special message is issued ("failed to start
backend"), which the frontend is expected to handle, and the exception
is re-raised. Invalid requests, meanwhile, are simply ignored and a
message is logged which the frontend could choose to handle.

[1] https://peps.python.org/pep-0517/#appendix-a-comparison-to-pep-516

Signed-off-by: Stephen Finucane <stephen@that.guru>
Closes: tox-dev#39
@gaborbernat
Copy link
Member

Can you add a test?

Copy link
Member

@gaborbernat gaborbernat left a comment

Choose a reason for hiding this comment

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

Can we add a test for this please, otherwise looks good.

Add some simple-ish tests to prevent regressions. While we're here, we
correct a typo in a test filename.

Signed-off-by: Stephen Finucane <stephen@that.guru>
Related: tox-dev#39
@stephenfin
Copy link
Contributor Author

Can we add a test for this please, otherwise looks good.

Done 👍

@gaborbernat
Copy link
Member

You'll need to please the CI gods 😅 and add a changelog entry.

Signed-off-by: Stephen Finucane <stephen@that.guru>
@stephenfin
Copy link
Contributor Author

I added the missing types to (hopefully) fix the CI. I held off updating the changelog since I don't know what the version number will be.

(As an aside, it would be nice to use something like reno or towncrier here.

@gaborbernat
Copy link
Member

1.3.0 would be a fine version.

@gaborbernat gaborbernat merged commit 0ddcee1 into tox-dev:main Jan 3, 2023
@stephenfin stephenfin deleted the issue-39 branch January 3, 2023 16:38
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.

No way to determine if a backend error was fatal
2 participants