Skip to content

Commit f8d2bdc

Browse files
vstinnerclaude
authored andcommitted
gh-150107: Fix asyncio sendfile fallback ignoring non-zero offset
The fallback paths in BaseEventLoop._sock_sendfile_fallback and _sendfile_fallback only seeked the file when offset was truthy, so an offset of 0 was respected but later non-zero offsets were dropped when the file lacked seek tracking. Seek whenever the file supports seek(). Also seek the CRT file pointer on Windows TransmitFile, which ignores OVERLAPPED.Offset for handles not opened with FILE_FLAG_OVERLAPPED. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c7cab73 commit f8d2bdc

9 files changed

Lines changed: 99 additions & 7 deletions

File tree

Lib/asyncio/base_events.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,7 @@ async def _sock_sendfile_native(self, sock, file, offset, count):
969969
f"and file {file!r} combination")
970970

971971
async def _sock_sendfile_fallback(self, sock, file, offset, count):
972-
if offset:
972+
if hasattr(file, 'seek'):
973973
file.seek(offset)
974974
blocksize = (
975975
min(count, constants.SENDFILE_FALLBACK_READBUFFER_SIZE)
@@ -1286,7 +1286,6 @@ async def sendfile(self, transport, file, offset=0, count=None,
12861286
raise RuntimeError(
12871287
f"fallback is disabled and native sendfile is not "
12881288
f"supported for transport {transport!r}")
1289-
12901289
return await self._sendfile_fallback(transport, file,
12911290
offset, count)
12921291

@@ -1295,7 +1294,7 @@ async def _sendfile_native(self, transp, file, offset, count):
12951294
"sendfile syscall is not supported")
12961295

12971296
async def _sendfile_fallback(self, transp, file, offset, count):
1298-
if offset:
1297+
if hasattr(file, 'seek'):
12991298
file.seek(offset)
13001299
blocksize = min(count, 16384) if count else 16384
13011300
buf = bytearray(blocksize)

Lib/asyncio/windows_events.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,9 @@ def sendfile(self, sock, file, offset, count):
610610
ov = _overlapped.Overlapped(NULL)
611611
offset_low = offset & 0xffff_ffff
612612
offset_high = (offset >> 32) & 0xffff_ffff
613+
# TransmitFile ignores OVERLAPPED.Offset for handles not opened with
614+
# FILE_FLAG_OVERLAPPED, so seek the CRT file pointer to match.
615+
file.seek(offset)
613616
ov.TransmitFile(sock.fileno(),
614617
msvcrt.get_osfhandle(file.fileno()),
615618
offset_low, offset_high,

Lib/test/libregrtest/logger.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import time
3+
from typing import Callable
34

45
from test.support import MS_WINDOWS
56
from .results import TestResults
@@ -19,16 +20,27 @@ def __init__(self, results: TestResults, quiet: bool, pgo: bool):
1920
self._results: TestResults = results
2021
self._quiet: bool = quiet
2122
self._pgo: bool = pgo
23+
self.get_mem_usage: Callable[[], int | None] | None = None
2224

2325
def log(self, line: str = '') -> None:
2426
empty = not line
2527

26-
# add the system load prefix: "load avg: 1.80 "
28+
# Add the memory usage: "mem: 1 GiB "
29+
if self.get_mem_usage is not None:
30+
mem = self.get_mem_usage()
31+
if mem:
32+
mib = mem / (1024*1024)
33+
if mib >= 1024:
34+
line = f"mem: {mib / 1024:.1f} GiB {line}"
35+
else:
36+
line = f"mem: {mib:.1f} MiB {line}"
37+
38+
# Add the system load prefix: "load avg: 1.80 "
2739
load_avg = self.get_load_avg()
2840
if load_avg is not None:
2941
line = f"load avg: {load_avg:.2f} {line}"
3042

31-
# add the timestamp prefix: "0:01:05 "
43+
# Add the timestamp prefix: "0:01:05 "
3244
log_time = time.perf_counter() - self.start_time
3345

3446
mins, secs = divmod(int(log_time), 60)

Lib/test/libregrtest/run_workers.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .single import PROGRESS_MIN_TIME
2323
from .utils import (
2424
StrPath, TestName,
25-
format_duration, print_warning, count, plural)
25+
format_duration, print_warning, count, plural, get_process_memory_usage)
2626
from .worker import create_worker_process, USE_PROCESS_GROUP
2727

2828
if MS_WINDOWS:
@@ -452,6 +452,12 @@ def wait_stopped(self, start_time: float) -> None:
452452
print_warning(f"Failed to join {self} in {format_duration(dt)}")
453453
break
454454

455+
def get_mem_usage(self):
456+
popen = self._popen
457+
if popen is None:
458+
return
459+
return get_process_memory_usage(popen.pid)
460+
455461

456462
def get_running(workers: list[WorkerThread]) -> str | None:
457463
running: list[str] = []
@@ -473,6 +479,7 @@ def __init__(self, num_workers: int, runtests: RunTests,
473479
logger: Logger, results: TestResults) -> None:
474480
self.num_workers = num_workers
475481
self.runtests = runtests
482+
self.logger = logger
476483
self.log = logger.log
477484
self.display_progress = logger.display_progress
478485
self.results: TestResults = results
@@ -598,9 +605,21 @@ def _process_result(self, item: QueueOutput) -> TestResult:
598605

599606
return result
600607

608+
def get_mem_usage(self):
609+
usage = 0
610+
main_mem = get_process_memory_usage(os.getpid())
611+
if main_mem:
612+
usage += main_mem
613+
for worker in self.workers:
614+
worker_mem = worker.get_mem_usage()
615+
if worker_mem:
616+
usage += worker_mem
617+
return usage
618+
601619
def run(self) -> None:
602620
fail_fast = self.runtests.fail_fast
603621
fail_env_changed = self.runtests.fail_env_changed
622+
self.logger.get_mem_usage = self.get_mem_usage
604623

605624
self.start_workers()
606625

@@ -625,3 +644,4 @@ def run(self) -> None:
625644
# worker when we exit this function
626645
self.pending.stop()
627646
self.stop_workers()
647+
self.logger.get_mem_usage = None

Lib/test/libregrtest/utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,3 +752,26 @@ def display_title(title):
752752
print(title)
753753
print("#" * len(title))
754754
print(flush=True)
755+
756+
757+
def get_process_memory_usage(pid: int) -> int | None:
758+
"""
759+
Read the private memory in bytes from /proc/pid/smaps.
760+
"""
761+
try:
762+
fp = open(f"/proc/{pid}/smaps", "rb")
763+
except OSError:
764+
return None
765+
766+
try:
767+
total = 0
768+
with fp:
769+
for line in fp:
770+
# Include both Private_Clean and Private_Dirty sections.
771+
line = line.rstrip()
772+
if line.startswith(b"Private_") and line.endswith(b'kB'):
773+
parts = line.split()
774+
total += int(parts[1]) * 1024
775+
return total
776+
except ProcessLookupError:
777+
return None

Lib/test/test_asyncio/test_sendfile.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,32 @@ def test_sock_sendfile_zero_size(self):
228228
self.assertEqual(ret, 0)
229229
self.assertEqual(self.file.tell(), 0)
230230

231+
def check_sock_sendfile_offset(self, data, offset, force_fallback=False):
232+
sock, proto = self.prepare_socksendfile()
233+
with tempfile.TemporaryFile() as f:
234+
f.write(data)
235+
f.flush()
236+
self.assertEqual(f.tell(), len(data))
237+
238+
if force_fallback:
239+
async def _sock_sendfile_fail(sock, file, offset, count):
240+
raise asyncio.exceptions.SendfileNotAvailableError()
241+
with support.swap_attr(self.loop, '_sock_sendfile_native', _sock_sendfile_fail):
242+
ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None))
243+
else:
244+
ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None))
245+
self.assertEqual(f.tell(), len(data))
246+
sock.close()
247+
self.run_loop(proto.wait_closed())
248+
self.assertEqual(ret, len(data) - offset)
249+
250+
def test_sock_sendfile_offset(self):
251+
data = b'abcdef'
252+
for offset in (0, len(data) // 2, len(data)):
253+
for force_fallback in (False, True):
254+
with self.subTest(offset=offset, force_fallback=force_fallback):
255+
self.check_sock_sendfile_offset(data, offset, force_fallback)
256+
231257
def test_sock_sendfile_mix_with_regular_send(self):
232258
buf = b"mix_regular_send" * (4 * 1024) # 64 KiB
233259
sock, proto = self.prepare_socksendfile()

Lib/test/test_regrtest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@
4141

4242
ROOT_DIR = os.path.join(os.path.dirname(__file__), '..', '..')
4343
ROOT_DIR = os.path.abspath(os.path.normpath(ROOT_DIR))
44-
LOG_PREFIX = r'[0-9]+:[0-9]+:[0-9]+ (?:load avg: [0-9]+\.[0-9]{2} )?'
44+
LOG_PREFIX = (
45+
r'[0-9]+:[0-9]+:[0-9]+ '
46+
r'(?:load avg: [0-9]+\.[0-9]{2} )?'
47+
r'(?:mem: [0-9]+\.[0-9] (?:MiB|GiB) )?'
48+
)
4549
RESULT_REGEX = (
4650
'passed',
4751
'failed',
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`asyncio`: ``sendfile()`` and ``sock_sendfile()`` event loop methods
2+
now call ``file.seek(offset)`` if *file* has a ``seek()`` method,
3+
even if *offset* is ``0`` (default value).
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
On Linux, regrtest now logs the total memory usage of all Python processes.
2+
Read the private memory in ``/proc/pid/smaps``. Patch by Victor Stinner.

0 commit comments

Comments
 (0)