Skip to content

Commit e07a1bd

Browse files
Use timestamp instead of datetime to achieve faster cookie expiration… (#7837)
(cherry picked from commit 8ae650b) Co-authored-by: Rongrong <[email protected]>
1 parent 53476df commit e07a1bd

File tree

4 files changed

+46
-49
lines changed

4 files changed

+46
-49
lines changed

CHANGES/7824.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use timestamp instead of ``datetime`` to achieve faster cookie expiration in ``CookieJar``.

aiohttp/cookiejar.py

+33-34
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import asyncio
2+
import calendar
23
import contextlib
34
import datetime
45
import os # noqa
56
import pathlib
67
import pickle
78
import re
9+
import time
810
from collections import defaultdict
911
from http.cookies import BaseCookie, Morsel, SimpleCookie
12+
from math import ceil
1013
from typing import ( # noqa
1114
DefaultDict,
1215
Dict,
@@ -24,7 +27,7 @@
2427
from yarl import URL
2528

2629
from .abc import AbstractCookieJar, ClearCookiePredicate
27-
from .helpers import is_ip_address, next_whole_second
30+
from .helpers import is_ip_address
2831
from .typedefs import LooseCookies, PathLike, StrOrURL
2932

3033
__all__ = ("CookieJar", "DummyCookieJar")
@@ -52,9 +55,22 @@ class CookieJar(AbstractCookieJar):
5255

5356
DATE_YEAR_RE = re.compile(r"(\d{2,4})")
5457

55-
MAX_TIME = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
56-
57-
MAX_32BIT_TIME = datetime.datetime.fromtimestamp(2**31 - 1, datetime.timezone.utc)
58+
# calendar.timegm() fails for timestamps after datetime.datetime.max
59+
# Minus one as a loss of precision occurs when timestamp() is called.
60+
MAX_TIME = (
61+
int(datetime.datetime.max.replace(tzinfo=datetime.timezone.utc).timestamp()) - 1
62+
)
63+
try:
64+
calendar.timegm(time.gmtime(MAX_TIME))
65+
except OSError:
66+
# Hit the maximum representable time on Windows
67+
# https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-localtime32-localtime64
68+
MAX_TIME = calendar.timegm((3000, 12, 31, 23, 59, 59, -1, -1, -1))
69+
except OverflowError:
70+
# #4515: datetime.max may not be representable on 32-bit platforms
71+
MAX_TIME = 2**31 - 1
72+
# Avoid minuses in the future, 3x faster
73+
SUB_MAX_TIME = MAX_TIME - 1
5874

5975
def __init__(
6076
self,
@@ -83,14 +99,8 @@ def __init__(
8399
for url in treat_as_secure_origin
84100
]
85101
self._treat_as_secure_origin = treat_as_secure_origin
86-
self._next_expiration = next_whole_second()
87-
self._expirations: Dict[Tuple[str, str, str], datetime.datetime] = {}
88-
# #4515: datetime.max may not be representable on 32-bit platforms
89-
self._max_time = self.MAX_TIME
90-
try:
91-
self._max_time.timestamp()
92-
except OverflowError:
93-
self._max_time = self.MAX_32BIT_TIME
102+
self._next_expiration: float = ceil(time.time())
103+
self._expirations: Dict[Tuple[str, str, str], float] = {}
94104

95105
def save(self, file_path: PathLike) -> None:
96106
file_path = pathlib.Path(file_path)
@@ -104,14 +114,14 @@ def load(self, file_path: PathLike) -> None:
104114

105115
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
106116
if predicate is None:
107-
self._next_expiration = next_whole_second()
117+
self._next_expiration = ceil(time.time())
108118
self._cookies.clear()
109119
self._host_only_cookies.clear()
110120
self._expirations.clear()
111121
return
112122

113123
to_del = []
114-
now = datetime.datetime.now(datetime.timezone.utc)
124+
now = time.time()
115125
for (domain, path), cookie in self._cookies.items():
116126
for name, morsel in cookie.items():
117127
key = (domain, path, name)
@@ -127,13 +137,11 @@ def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
127137
del self._expirations[(domain, path, name)]
128138
self._cookies[(domain, path)].pop(name, None)
129139

130-
next_expiration = min(self._expirations.values(), default=self._max_time)
131-
try:
132-
self._next_expiration = next_expiration.replace(
133-
microsecond=0
134-
) + datetime.timedelta(seconds=1)
135-
except OverflowError:
136-
self._next_expiration = self._max_time
140+
self._next_expiration = (
141+
min(*self._expirations.values(), self.SUB_MAX_TIME) + 1
142+
if self._expirations
143+
else self.MAX_TIME
144+
)
137145

138146
def clear_domain(self, domain: str) -> None:
139147
self.clear(lambda x: self._is_domain_match(domain, x["domain"]))
@@ -149,9 +157,7 @@ def __len__(self) -> int:
149157
def _do_expiration(self) -> None:
150158
self.clear(lambda x: False)
151159

152-
def _expire_cookie(
153-
self, when: datetime.datetime, domain: str, path: str, name: str
154-
) -> None:
160+
def _expire_cookie(self, when: float, domain: str, path: str, name: str) -> None:
155161
self._next_expiration = min(self._next_expiration, when)
156162
self._expirations[(domain, path, name)] = when
157163

@@ -209,12 +215,7 @@ def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> No
209215
if max_age:
210216
try:
211217
delta_seconds = int(max_age)
212-
try:
213-
max_age_expiration = datetime.datetime.now(
214-
datetime.timezone.utc
215-
) + datetime.timedelta(seconds=delta_seconds)
216-
except OverflowError:
217-
max_age_expiration = self._max_time
218+
max_age_expiration = min(time.time() + delta_seconds, self.MAX_TIME)
218219
self._expire_cookie(max_age_expiration, domain, path, name)
219220
except ValueError:
220221
cookie["max-age"] = ""
@@ -323,7 +324,7 @@ def _is_path_match(req_path: str, cookie_path: str) -> bool:
323324
return non_matching.startswith("/")
324325

325326
@classmethod
326-
def _parse_date(cls, date_str: str) -> Optional[datetime.datetime]:
327+
def _parse_date(cls, date_str: str) -> Optional[int]:
327328
"""Implements date string parsing adhering to RFC 6265."""
328329
if not date_str:
329330
return None
@@ -384,9 +385,7 @@ def _parse_date(cls, date_str: str) -> Optional[datetime.datetime]:
384385
if year < 1601 or hour > 23 or minute > 59 or second > 59:
385386
return None
386387

387-
return datetime.datetime(
388-
year, month, day, hour, minute, second, tzinfo=datetime.timezone.utc
389-
)
388+
return calendar.timegm((year, month, day, hour, minute, second, -1, -1, -1))
390389

391390

392391
class DummyCookieJar(AbstractCookieJar):

aiohttp/helpers.py

-7
Original file line numberDiff line numberDiff line change
@@ -545,13 +545,6 @@ def is_ip_address(host: Optional[Union[str, bytes, bytearray, memoryview]]) -> b
545545
return is_ipv4_address(host) or is_ipv6_address(host)
546546

547547

548-
def next_whole_second() -> datetime.datetime:
549-
"""Return current time rounded up to the next whole second."""
550-
return datetime.datetime.now(datetime.timezone.utc).replace(
551-
microsecond=0
552-
) + datetime.timedelta(seconds=0)
553-
554-
555548
_cached_current_datetime: Optional[int] = None
556549
_cached_formatted_datetime = ""
557550

tests/test_cookiejar.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -101,23 +101,27 @@ def test_date_parsing() -> None:
101101
assert parse_func("") is None
102102

103103
# 70 -> 1970
104-
assert parse_func("Tue, 1 Jan 70 00:00:00 GMT") == datetime.datetime(
105-
1970, 1, 1, tzinfo=utc
104+
assert (
105+
parse_func("Tue, 1 Jan 70 00:00:00 GMT")
106+
== datetime.datetime(1970, 1, 1, tzinfo=utc).timestamp()
106107
)
107108

108109
# 10 -> 2010
109-
assert parse_func("Tue, 1 Jan 10 00:00:00 GMT") == datetime.datetime(
110-
2010, 1, 1, tzinfo=utc
110+
assert (
111+
parse_func("Tue, 1 Jan 10 00:00:00 GMT")
112+
== datetime.datetime(2010, 1, 1, tzinfo=utc).timestamp()
111113
)
112114

113115
# No day of week string
114-
assert parse_func("1 Jan 1970 00:00:00 GMT") == datetime.datetime(
115-
1970, 1, 1, tzinfo=utc
116+
assert (
117+
parse_func("1 Jan 1970 00:00:00 GMT")
118+
== datetime.datetime(1970, 1, 1, tzinfo=utc).timestamp()
116119
)
117120

118121
# No timezone string
119-
assert parse_func("Tue, 1 Jan 1970 00:00:00") == datetime.datetime(
120-
1970, 1, 1, tzinfo=utc
122+
assert (
123+
parse_func("Tue, 1 Jan 1970 00:00:00")
124+
== datetime.datetime(1970, 1, 1, tzinfo=utc).timestamp()
121125
)
122126

123127
# No year

0 commit comments

Comments
 (0)