Skip to content

Commit 7494df4

Browse files
fix(profiling): fix a race condition for parsing the newest profile
1 parent 548d46a commit 7494df4

File tree

2 files changed

+36
-36
lines changed

2 files changed

+36
-36
lines changed

tests/profiling/collector/pprof_utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,9 @@ def parse_newest_profile(filename_prefix: str) -> pprof_pb2.Profile:
142142
the newest profile that has given filename prefix.
143143
"""
144144
files = glob.glob(filename_prefix + ".*")
145-
# Sort files by creation timestamp (oldest first, newest last)
146-
files.sort(key=lambda f: os.path.getctime(f))
145+
# Sort files by logical timestamp (i.e. the sequence number, which is monotonically increasing);
146+
# this approach is more reliable than filesystem timestamps, especially when files are created rapidly.
147+
files.sort(key=lambda f: int(f.rsplit(".", 1)[-1]))
147148
filename = files[-1]
148149
with open(filename, "rb") as fp:
149150
dctx = zstd.ZstdDecompressor()

tests/profiling_v2/collector/test_threading.py

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import sys
77
import threading
8+
import time
89
from typing import Callable
910
from typing import List
1011
from typing import Optional
@@ -1022,47 +1023,45 @@ def test_upload_resets_profile(self) -> None:
10221023
with self.collector_class(capture_pct=100):
10231024
with self.lock_class(): # !CREATE! !ACQUIRE! !RELEASE! test_upload_resets_profile
10241025
pass
1026+
10251027
ddup.upload() # pyright: ignore[reportCallIssue]
10261028

10271029
linenos: LineNo = get_lock_linenos("test_upload_resets_profile", with_stmt=True)
10281030

1029-
try:
1030-
pprof: pprof_pb2.Profile = pprof_utils.parse_newest_profile(self.output_filename)
1031-
pprof_utils.assert_lock_events(
1032-
pprof,
1033-
expected_acquire_events=[
1034-
pprof_utils.LockAcquireEvent(
1035-
caller_name=self.test_name,
1036-
filename=os.path.basename(__file__),
1037-
linenos=linenos,
1038-
),
1039-
],
1040-
expected_release_events=[
1041-
pprof_utils.LockReleaseEvent(
1042-
caller_name=self.test_name,
1043-
filename=os.path.basename(__file__),
1044-
linenos=linenos,
1045-
),
1046-
],
1047-
)
1048-
except (AssertionError, KeyError) as e:
1049-
# This can be flaky due to timing or interference from other tests
1050-
pytest.skip(f"Profile validation failed (known flaky on some platforms): {e}")
1031+
pprof: pprof_pb2.Profile = pprof_utils.parse_newest_profile(self.output_filename)
1032+
pprof_utils.assert_lock_events(
1033+
pprof,
1034+
expected_acquire_events=[
1035+
pprof_utils.LockAcquireEvent(
1036+
caller_name=self.test_name,
1037+
filename=os.path.basename(__file__),
1038+
linenos=linenos,
1039+
),
1040+
],
1041+
expected_release_events=[
1042+
pprof_utils.LockReleaseEvent(
1043+
caller_name=self.test_name,
1044+
filename=os.path.basename(__file__),
1045+
linenos=linenos,
1046+
),
1047+
],
1048+
)
1049+
1050+
# Now we call upload() again, and we expect the profile to be empty
1051+
num_files_before_second_upload: int = len(glob.glob(self.output_filename + ".*"))
10511052

1052-
# Now we call upload() again without any new lock operations
1053-
# We expect the profile to be empty or contain no samples
10541053
ddup.upload() # pyright: ignore[reportCallIssue]
10551054

1056-
# Try to parse the newest profile - it should either not exist (no new file)
1057-
# or have no samples (which would raise AssertionError in parse_newest_profile)
1058-
try:
1059-
_ = pprof_utils.parse_newest_profile(self.output_filename)
1060-
# If we got here, a profile with samples exists
1061-
# This might be okay if other collectors are running
1062-
pytest.skip("Profile still has samples (possibly from other activity - known flaky)")
1063-
except (AssertionError, IndexError):
1064-
# Expected: no profile file or no samples
1065-
pass
1055+
num_files_after_second_upload: int = len(glob.glob(self.output_filename + ".*"))
1056+
1057+
# A new profile file should always be created (upload_seq increments)
1058+
assert (
1059+
num_files_after_second_upload - num_files_before_second_upload == 1
1060+
), f"Expected 1 new file, got {num_files_after_second_upload - num_files_before_second_upload}."
1061+
1062+
# The newest profile file should be empty (no samples), which causes an AssertionError
1063+
with pytest.raises(AssertionError, match="No samples found in profile"):
1064+
pprof_utils.parse_newest_profile(self.output_filename)
10661065

10671066
def test_lock_hash(self) -> None:
10681067
"""Test that __hash__ allows profiled locks to be used in sets and dicts."""

0 commit comments

Comments
 (0)