Skip to content

Commit 1e6565d

Browse files
Merge branch 'main' into bigmemtest-rlimit
The memory watchdog now both reports the peak (GH-156024) and kills a test which uses more memory than it declares.
2 parents 874c66a + 21a6a8a commit 1e6565d

47 files changed

Lines changed: 1413 additions & 417 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Doc/library/asyncio-task.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -843,17 +843,13 @@ Timeouts
843843
Wait for the *fut* :ref:`awaitable <asyncio-awaitables>`
844844
to complete with a timeout.
845845

846-
If *fut* is a coroutine it is automatically scheduled as a Task.
847-
848846
*timeout* can either be ``None`` or a float or int number of seconds
849847
to wait for. If *timeout* is ``None``, block until the future
850848
completes.
851849

852-
If a timeout occurs, it cancels the task and raises
853-
:exc:`TimeoutError`.
850+
If a timeout occurs, it cancels *fut* and raises :exc:`TimeoutError`.
854851

855-
To avoid the task :meth:`cancellation <Task.cancel>`,
856-
wrap it in :func:`shield`.
852+
To prevent *fut* from being cancelled, wrap it in :func:`shield`.
857853

858854
The function will wait until the future is actually cancelled,
859855
so the total wait time may exceed the *timeout*. If an exception
@@ -894,6 +890,10 @@ Timeouts
894890
.. versionchanged:: 3.11
895891
Raises :exc:`TimeoutError` instead of :exc:`asyncio.TimeoutError`.
896892

893+
.. versionchanged:: 3.12
894+
Implemented using :func:`asyncio.timeout`, a coroutine passed as *fut*
895+
is no longer wrapped in a :class:`Task` when *timeout* is positive.
896+
897897

898898
Waiting primitives
899899
==================

Doc/library/tarfile.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,10 @@ reused in custom filters:
11121112
paths (in case the name is absolute
11131113
even after stripping slashes, e.g. ``C:/foo`` on Windows).
11141114
This raises :class:`~tarfile.AbsolutePathError`.
1115+
- Normalize filenames (:attr:`TarInfo.name`) that contain ``..`` components
1116+
using :func:`os.path.normpath`.
1117+
Note that this removes internal ``..`` components, which may change the
1118+
meaning of the name if it traverses symbolic links.
11151119
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files whose absolute
11161120
path (after following symlinks) would end up outside the destination.
11171121
This raises :class:`~tarfile.OutsideDestinationError`.
@@ -1120,6 +1124,10 @@ reused in custom filters:
11201124

11211125
Return the modified ``TarInfo`` member.
11221126

1127+
.. versionchanged:: next
1128+
1129+
Filenames containing ``..`` components are now normalized.
1130+
11231131
.. function:: data_filter(member, path)
11241132

11251133
Implements the ``'data'`` filter.

Doc/tutorial/stdlib.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ aids for working with large modules like :mod:`os`::
3636
<returns an extensive manual page created from the module's docstrings>
3737

3838
For daily file and directory management tasks, the :mod:`shutil` module provides
39-
a higher level interface that is easier to use::
39+
a higher-level interface that is easier to use::
4040

4141
>>> import shutil
4242
>>> shutil.copyfile('data.db', 'archive.db')
@@ -63,7 +63,7 @@ wildcard searches::
6363
Command-line arguments
6464
======================
6565

66-
Common utility scripts often need to process command line arguments. These
66+
Common utility scripts often need to process command-line arguments. These
6767
arguments are stored in the :mod:`sys` module's *argv* attribute as a list. For
6868
instance, let's take the following :file:`demo.py` file::
6969

@@ -77,7 +77,7 @@ line::
7777
['demo.py', 'one', 'two', 'three']
7878

7979
The :mod:`argparse` module provides a more sophisticated mechanism to process
80-
command line arguments. The following script extracts one or more filenames
80+
command-line arguments. The following script extracts one or more filenames
8181
and an optional number of lines to be displayed::
8282

8383
import argparse

Include/internal/pycore_interpframe_structs.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ struct _PyInterpreterFrame {
6666
PyObject *prefix##_qualname; \
6767
_PyErr_StackItem prefix##_exc_state; \
6868
PyObject *prefix##_origin_or_finalizer; \
69-
char prefix##_hooks_inited; \
70-
char prefix##_closed; \
71-
char prefix##_running_async; \
69+
int8_t prefix##_hooks_inited; \
70+
int8_t prefix##_closed; \
71+
int8_t prefix##_running_async; \
7272
/* The frame */ \
7373
int8_t prefix##_frame_state; \
7474
_PyInterpreterFrame prefix##_iframe; \

Lib/asyncio/streams.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,17 @@ def connection_made(self, transport):
239239
self._over_ssl = transport.get_extra_info('sslcontext') is not None
240240
if self._client_connected_cb is not None:
241241
writer = StreamWriter(transport, self, reader, self._loop)
242-
res = self._client_connected_cb(reader, writer)
242+
try:
243+
res = self._client_connected_cb(reader, writer)
244+
except Exception as exc:
245+
self._loop.call_exception_handler({
246+
'message': 'Unhandled exception in client_connected_cb',
247+
'exception': exc,
248+
'transport': transport,
249+
})
250+
transport.close()
251+
self._strong_reader = None
252+
return
243253
if coroutines.iscoroutine(res):
244254
def callback(task):
245255
if task.cancelled():

Lib/asyncio/tasks.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -440,15 +440,13 @@ def _release_waiter(waiter, *args):
440440
async def wait_for(fut, timeout):
441441
"""Wait for the single Future or coroutine to complete, with timeout.
442442
443-
Coroutine will be wrapped in Task.
444-
445443
Returns result of the Future or coroutine. When a timeout occurs,
446-
it cancels the task and raises TimeoutError. To avoid the task
447-
cancellation, wrap it in shield().
444+
it cancels fut and raises TimeoutError. To prevent fut from being
445+
cancelled, wrap it in shield().
448446
449-
If the wait is cancelled, the task is also cancelled.
447+
If the wait is cancelled, fut is also cancelled.
450448
451-
If the task suppresses the cancellation and returns a value instead,
449+
If fut suppresses the cancellation and returns a value instead,
452450
that value is returned.
453451
454452
This function is a coroutine.

Lib/platform.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,11 +427,16 @@ def _win32_ver(version, csd, ptype):
427427

428428
winver = getwindowsversion()
429429
is_client = (getattr(winver, 'product_type', 1) == 1)
430-
try:
431-
version = _syscmd_ver()[2]
432-
major, minor, build = map(int, version.split('.'))
433-
except ValueError:
434-
major, minor, build = winver.platform_version or winver[:3]
430+
431+
if winver.device_family == "Desktop":
432+
try:
433+
version = _syscmd_ver()[2]
434+
major, minor, build = map(int, version.split('.'))
435+
except ValueError:
436+
major, minor, build = winver.platform_version or winver[:3]
437+
version = '{0}.{1}.{2}'.format(major, minor, build)
438+
else:
439+
major, minor, build = winver[:3]
435440
version = '{0}.{1}.{2}'.format(major, minor, build)
436441

437442
# getwindowsversion() reflect the compatibility mode Python is

Lib/tarfile.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,13 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
833833
# For example, 'C:/foo' on Windows.
834834
raise AbsolutePathError(member)
835835
# Ensure we stay in the destination
836+
if '..' in name.replace(os.sep, '/').split('/'):
837+
# Directories are created from the name as given, so a name that
838+
# leaves the destination part-way through would create them
839+
# outside it even if the resolved path stays inside.
840+
normalized = os.path.normpath(name)
841+
if normalized != name:
842+
name = new_attrs['name'] = normalized
836843
target_path = os.path.realpath(os.path.join(dest_path, name),
837844
strict=os.path.ALLOW_MISSING)
838845
if os.path.commonpath([target_path, dest_path]) != dest_path:

Lib/test/support/__init__.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,9 +1307,9 @@ def _limit_address_space(nbytes):
13071307
def _memory_watchdog(proc, limit):
13081308
"""Return a function watching the memory used by the test in *proc*.
13091309
1310-
It reports the usage in verbose mode, and kills the test if it uses more
1311-
than *limit* bytes. This is the only limit where the address space cannot
1312-
be limited.
1310+
It reports the usage in verbose mode, keeps the largest value it saw in
1311+
its ``peak`` attribute, and kills the test if it uses more than *limit*
1312+
bytes. This is the only limit where the address space cannot be limited.
13131313
"""
13141314
# Imported here: test.support does not depend on test.libregrtest.
13151315
from test.libregrtest.utils import get_process_memory_usage
@@ -1318,12 +1318,14 @@ def watch():
13181318
mem = get_process_memory_usage(proc.pid)
13191319
if mem is None:
13201320
return
1321+
watch.peak = max(watch.peak, mem)
13211322
if verbose:
13221323
print(f" ... process data size: {mem / (1024 ** 3):.1f} GiB",
13231324
flush=True)
13241325
if limit is not None and mem > limit:
13251326
watch.exceeded = mem
13261327
proc.kill()
1328+
watch.peak = 0
13271329
watch.exceeded = None
13281330
return watch
13291331

@@ -1393,6 +1395,22 @@ def wrapper(self):
13931395
f'the test used {watchdog.exceeded / _1G:.1f} GiB, '
13941396
f'more than the {size * memuse / _1G:.1f} GiB '
13951397
f'it declares')
1398+
if verbose:
1399+
# The subprocess measures its own peak exactly. What the
1400+
# parent sampled is only a lower bound.
1401+
maxrss = payload and payload.get('maxrss')
1402+
peak = maxrss or watchdog.peak
1403+
if peak:
1404+
print(f" ... peak memory use: "
1405+
f"{peak / (1024 ** 3):.1f} GiB"
1406+
f"{'' if maxrss else ' or more'}",
1407+
flush=True)
1408+
majflt = payload and payload.get('majflt')
1409+
if majflt:
1410+
# The test did not fit in memory, so its
1411+
# timing means little.
1412+
print(f" ... {majflt} major page faults: the test "
1413+
f"waited for the disk", flush=True)
13961414
isolation._replay_test(self, payload, output, returncode)
13971415
return
13981416

Lib/test/support/subprocess_runner.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,45 @@ def _outcome(kind, test, detail):
7272
for t, tb in result.expectedFailures]
7373
outcomes += [_outcome('skipped', t, reason) for t, reason in result.skipped]
7474

75-
payload = {'outcomes': outcomes, 'durations': result.id_durations}
75+
def _usage():
76+
"""What this process used: peak resident set size in bytes, and the
77+
number of major page faults it took, either of which can be None.
78+
79+
A major page fault is served from disk, so a non-zero count means swapping.
80+
81+
The modules are imported here, after the test has run, so that the test
82+
does not see them.
83+
"""
84+
try:
85+
import resource
86+
except ImportError:
87+
pass
88+
else:
89+
usage = resource.getrusage(resource.RUSAGE_SELF)
90+
# Solaris and illumos leave these fields at 0, which no live process
91+
# has, so treat it as "not supported".
92+
if not usage.ru_maxrss:
93+
return {'maxrss': None, 'majflt': None}
94+
# ru_maxrss is in bytes on macOS, in kilobytes on Linux and the BSDs.
95+
maxrss = usage.ru_maxrss
96+
return {'maxrss': maxrss if sys.platform == 'darwin' else maxrss * 1024,
97+
'majflt': usage.ru_majflt}
98+
try:
99+
import os
100+
import _winapi
101+
handle = _winapi.OpenProcess(
102+
_winapi.PROCESS_QUERY_LIMITED_INFORMATION, False, os.getpid())
103+
except (ImportError, OSError):
104+
return {'maxrss': None, 'majflt': None}
105+
try:
106+
info = _winapi.GetProcessMemoryInfo(handle)
107+
finally:
108+
_winapi.CloseHandle(handle)
109+
# PageFaultCount counts all faults, not only the ones served from disk.
110+
return {'maxrss': info['PeakWorkingSetSize'], 'majflt': None}
111+
112+
113+
payload = {'outcomes': outcomes, 'durations': result.id_durations, **_usage()}
76114
with open(outfile, 'wb') as f:
77115
marshal.dump(payload, f)
78116

0 commit comments

Comments
 (0)