#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:
multiprocessingis involved. Indeed, running with--parallel=1does not trigger this error.- Jinja2 is involved. Running tests without Jinja2 installed does not trigger this error.
What happens:
multiprocessingregistersatexithandler.runtests.pycreates a temp dir (e.g./tmp/django_k0xziymh) and registersatexithandler to remove it.multiprocessingcreates a temp dir (e.g./tmp/django_k0xziymh/pymp-i4s112bj) which will be deleted in the handler it registered earlier.- Tests happen.
runtests.pyexit handler deletes the temp dir.multiprocessingexit 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
djangomodules. - Move some imports in
runtests.pyinto 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 , 9 years ago
| Component: | Testing framework → Core (Other) |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
follow-up: 3 comment:2 by , 9 years ago
follow-up: 4 comment:3 by , 9 years ago
Replying to Claude Paroz:
Another option might be to register a custom function that wraps
shutil.rmtreeand catches the exception.
It's possible, but would require monkey-patching multiprocessing.
follow-up: 5 comment:4 by , 9 years ago
Replying to Vytis Banaitis:
Replying to Claude Paroz:
Another option might be to register a custom function that wraps
shutil.rmtreeand 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 , 9 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 , 9 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 , 9 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.rmtreeand catches the exception.