#27890 closed Bug (fixed)
runtests.py cleanup exception on Python 3.6
Reported by: | Vytis Banaitis | Owned by: | nobody |
---|---|---|---|
Component: | Core (Other) | Version: | 1.11 |
Severity: | Release blocker | Keywords: | |
Cc: | Triage Stage: | Accepted | |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
Per Tim's suggestion, I've looked into a cleanup exception that happens when running tests on Python 3.6:
$ ./tests/runtests.py basic Testing against Django installed in '/home/tim/code/django/django' with up to 3 processes .... Destroying test database for alias 'other'... Traceback (most recent call last): File "/opt/python3.6.0/lib/python3.6/multiprocessing/util.py", line 254, in _run_finalizers finalizer() File "/opt/python3.6.0/lib/python3.6/multiprocessing/util.py", line 186, in __call__ res = self._callback(*self._args, **self._kwargs) File "/home/tim/.virtualenvs/django36/lib/python3.6/shutil.py", line 465, in rmtree onerror(os.lstat, path, sys.exc_info()) File "/home/tim/.virtualenvs/django36/lib/python3.6/shutil.py", line 463, in rmtree orig_st = os.lstat(path) FileNotFoundError: [Errno 2] No such file or directory: '/tmp/django_k0xziymh/pymp-i4s112bj'
What I found out:
multiprocessing
is involved. Indeed, running with--parallel=1
does not trigger this error.- Jinja2 is involved. Running tests without Jinja2 installed does not trigger this error.
What happens:
multiprocessing
registersatexit
handler.runtests.py
creates a temp dir (e.g./tmp/django_k0xziymh
) and registersatexit
handler to remove it.multiprocessing
creates a temp dir (e.g./tmp/django_k0xziymh/pymp-i4s112bj
) which will be deleted in the handler it registered earlier.- Tests happen.
runtests.py
exit handler deletes the temp dir.multiprocessing
exit handler tries to delete the inner temp dir but it is already gone.
On earlier Python versions 1 and 2 are swapped and 5 and 6 are swapped, so the error does not happen.
Jinja2 is imported by a chain of imports starting with from django.test import TestCase, TransactionTestCase
(full chain below).
On Python 3.6 Jinja2 patches async support which eventually imports multiprocessing.util
which registers the exit handler.
Possible solutions:
- Create the temp dir before importing
django
modules. - Move some imports in
runtests.py
into functions, thereby delaying the indirect import of Jinja2. - Move some imports somewhere else into functions to break the import chain.
- ...
The import chain leading up to the import of multiprocessing.util
(with uninteresting import machinery stack frames removed):
File "./runtests.py", line 18, in <module> from django.test import TestCase, TransactionTestCase File "/home/vytis/src/django/django/test/__init__.py", line 5, in <module> from django.test.client import Client, RequestFactory File "/home/vytis/src/django/django/test/client.py", line 12, in <module> from django.core.handlers.base import BaseHandler File "/home/vytis/src/django/django/core/handlers/base.py", line 7, in <module> from django.urls import get_resolver, set_urlconf File "/home/vytis/src/django/django/urls/__init__.py", line 1, in <module> from .base import ( File "/home/vytis/src/django/django/urls/base.py", line 8, in <module> from .exceptions import NoReverseMatch, Resolver404 File "/home/vytis/src/django/django/urls/exceptions.py", line 1, in <module> from django.http import Http404 File "/home/vytis/src/django/django/http/__init__.py", line 5, in <module> from django.http.response import ( File "/home/vytis/src/django/django/http/response.py", line 13, in <module> from django.core.serializers.json import DjangoJSONEncoder File "/home/vytis/src/django/django/core/serializers/__init__.py", line 23, in <module> from django.core.serializers.base import SerializerDoesNotExist File "/home/vytis/src/django/django/core/serializers/base.py", line 6, in <module> from django.db import models File "/home/vytis/src/django/django/db/models/__init__.py", line 3, in <module> from django.db.models.aggregates import * # NOQA File "/home/vytis/src/django/django/db/models/aggregates.py", line 5, in <module> from django.db.models.expressions import Func, Star File "/home/vytis/src/django/django/db/models/expressions.py", line 6, in <module> from django.db.models import fields File "/home/vytis/src/django/django/db/models/fields/__init__.py", line 11, in <module> from django import forms File "/home/vytis/src/django/django/forms/__init__.py", line 6, in <module> from django.forms.boundfield import * # NOQA File "/home/vytis/src/django/django/forms/boundfield.py", line 5, in <module> from django.forms.widgets import Textarea, TextInput File "/home/vytis/src/django/django/forms/widgets.py", line 21, in <module> from .renderers import get_default_renderer File "/home/vytis/src/django/django/forms/renderers.py", line 11, in <module> from django.template.backends.jinja2 import Jinja2 File "/home/vytis/src/django/django/template/backends/jinja2.py", line 1, in <module> import jinja2 File "/home/vytis/src/env/django-py36/lib/python3.6/site-packages/jinja2/__init__.py", line 81, in <module> _patch_async() File "/home/vytis/src/env/django-py36/lib/python3.6/site-packages/jinja2/__init__.py", line 77, in _patch_async from jinja2.asyncsupport import patch_all File "/home/vytis/src/env/django-py36/lib/python3.6/site-packages/jinja2/asyncsupport.py", line 13, in <module> import asyncio File "/opt/python/lib/python3.6/asyncio/__init__.py", line 21, in <module> from .base_events import * File "/opt/python/lib/python3.6/asyncio/base_events.py", line 17, in <module> import concurrent.futures File "/opt/python/lib/python3.6/concurrent/futures/__init__.py", line 17, in <module> from concurrent.futures.process import ProcessPoolExecutor File "/opt/python/lib/python3.6/concurrent/futures/process.py", line 55, in <module> from multiprocessing.connection import wait File "/opt/python/lib/python3.6/multiprocessing/connection.py", line 23, in <module> from . import util
Change History (11)
comment:1 by , 8 years ago
Component: | Testing framework → Core (Other) |
---|---|
Triage Stage: | Unreviewed → Accepted |
follow-up: 3 comment:2 by , 8 years ago
follow-up: 4 comment:3 by , 8 years ago
Replying to Claude Paroz:
Another option might be to register a custom function that wraps
shutil.rmtree
and catches the exception.
It's possible, but would require monkey-patching multiprocessing
.
follow-up: 5 comment:4 by , 8 years ago
Replying to Vytis Banaitis:
Replying to Claude Paroz:
Another option might be to register a custom function that wraps
shutil.rmtree
and catches the exception.
It's possible, but would require monkey-patching
multiprocessing
.
Really? My idea was something like:
def custom_delete(tmpdir): try: shutil.rmtree(tmpdir) except FileNotFoundError: pass atexit.register(custom_delete, TMPDIR)
comment:5 by , 8 years ago
Replying to Claude Paroz:
Really? My idea was something like:
def custom_delete(tmpdir): try: shutil.rmtree(tmpdir) except FileNotFoundError: pass atexit.register(custom_delete, TMPDIR)
Removal of TMPDIR happens without error.
The error is raised by multiprocessing
exit handler which tries to delete a directory inside of TMPDIR.
comment:7 by , 8 years ago
Severity: | Normal → Release blocker |
---|---|
Version: | master → 1.11 |
I looked a little and didn't see an obvious solution besides some import rearranging as suggested in the ticket description.
comment:9 by , 8 years ago
Rather than the rearranging imports approach of the first PR, an alternative PR deletes multiprocessing's temporary directory removal handler to avoid the error.
Going forward, patching cpython to ignore the error might be an option:
-
Lib/multiprocessing/util.py
diff --git a/Lib/multiprocessing/util.py b/Lib/multiprocessing/util.py index 1a2c0db..6843d09 100644
a b def get_temp_dir(): 113 113 import shutil, tempfile 114 114 tempdir = tempfile.mkdtemp(prefix='pymp-') 115 115 info('created temp directory %s', tempdir) 116 Finalize(None, shutil.rmtree, args=[tempdir], exitpriority=-100)116 Finalize(None, shutil.rmtree, args=[tempdir], kwargs={'ignore_errors': True}, exitpriority=-100) 117 117 process.current_process()._config['tempdir'] = tempdir 118 118 return tempdir 119 119
Another option might be to register a custom function that wraps
shutil.rmtree
and catches the exception.