Skip to content

Commit 8ae650b

Browse files
authored
Use timestamp instead of datetime to achieve faster cookie expiration in CookieJar (#7824)
#7583 #7819 (comment)
1 parent 1e86b77 commit 8ae650b

File tree

4 files changed

+46
-49
lines changed

4 files changed

+46
-49
lines changed

CHANGES/7824.feature

Lines changed: 1 addition & 0 deletions
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

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import calendar
12
import contextlib
23
import datetime
34
import os # noqa
45
import pathlib
56
import pickle
67
import re
8+
import time
79
import warnings
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,
@@ -81,14 +97,8 @@ def __init__(
8197
for url in treat_as_secure_origin
8298
]
8399
self._treat_as_secure_origin = treat_as_secure_origin
84-
self._next_expiration = next_whole_second()
85-
self._expirations: Dict[Tuple[str, str, str], datetime.datetime] = {}
86-
# #4515: datetime.max may not be representable on 32-bit platforms
87-
self._max_time = self.MAX_TIME
88-
try:
89-
self._max_time.timestamp()
90-
except OverflowError:
91-
self._max_time = self.MAX_32BIT_TIME
100+
self._next_expiration: float = ceil(time.time())
101+
self._expirations: Dict[Tuple[str, str, str], float] = {}
92102

93103
def save(self, file_path: PathLike) -> None:
94104
file_path = pathlib.Path(file_path)
@@ -102,14 +112,14 @@ def load(self, file_path: PathLike) -> None:
102112

103113
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
104114
if predicate is None:
105-
self._next_expiration = next_whole_second()
115+
self._next_expiration = ceil(time.time())
106116
self._cookies.clear()
107117
self._host_only_cookies.clear()
108118
self._expirations.clear()
109119
return
110120

111121
to_del = []
112-
now = datetime.datetime.now(datetime.timezone.utc)
122+
now = time.time()
113123
for (domain, path), cookie in self._cookies.items():
114124
for name, morsel in cookie.items():
115125
key = (domain, path, name)
@@ -125,13 +135,11 @@ def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
125135
del self._expirations[(domain, path, name)]
126136
self._cookies[(domain, path)].pop(name, None)
127137

128-
next_expiration = min(self._expirations.values(), default=self._max_time)
129-
try:
130-
self._next_expiration = next_expiration.replace(
131-
microsecond=0
132-
) + datetime.timedelta(seconds=1)
133-
except OverflowError:
134-
self._next_expiration = self._max_time
138+
self._next_expiration = (
139+
min(*self._expirations.values(), self.SUB_MAX_TIME) + 1
140+
if self._expirations
141+
else self.MAX_TIME
142+
)
135143

136144
def clear_domain(self, domain: str) -> None:
137145
self.clear(lambda x: self._is_domain_match(domain, x["domain"]))
@@ -147,9 +155,7 @@ def __len__(self) -> int:
147155
def _do_expiration(self) -> None:
148156
self.clear(lambda x: False)
149157

150-
def _expire_cookie(
151-
self, when: datetime.datetime, domain: str, path: str, name: str
152-
) -> None:
158+
def _expire_cookie(self, when: float, domain: str, path: str, name: str) -> None:
153159
self._next_expiration = min(self._next_expiration, when)
154160
self._expirations[(domain, path, name)] = when
155161

@@ -207,12 +213,7 @@ def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> No
207213
if max_age:
208214
try:
209215
delta_seconds = int(max_age)
210-
try:
211-
max_age_expiration = datetime.datetime.now(
212-
datetime.timezone.utc
213-
) + datetime.timedelta(seconds=delta_seconds)
214-
except OverflowError:
215-
max_age_expiration = self._max_time
216+
max_age_expiration = min(time.time() + delta_seconds, self.MAX_TIME)
216217
self._expire_cookie(max_age_expiration, domain, path, name)
217218
except ValueError:
218219
cookie["max-age"] = ""
@@ -328,7 +329,7 @@ def _is_path_match(req_path: str, cookie_path: str) -> bool:
328329
return non_matching.startswith("/")
329330

330331
@classmethod
331-
def _parse_date(cls, date_str: str) -> Optional[datetime.datetime]:
332+
def _parse_date(cls, date_str: str) -> Optional[int]:
332333
"""Implements date string parsing adhering to RFC 6265."""
333334
if not date_str:
334335
return None
@@ -388,9 +389,7 @@ def _parse_date(cls, date_str: str) -> Optional[datetime.datetime]:
388389
if year < 1601 or hour > 23 or minute > 59 or second > 59:
389390
return None
390391

391-
return datetime.datetime(
392-
year, month, day, hour, minute, second, tzinfo=datetime.timezone.utc
393-
)
392+
return calendar.timegm((year, month, day, hour, minute, second, -1, -1, -1))
394393

395394

396395
class DummyCookieJar(AbstractCookieJar):

aiohttp/helpers.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -530,13 +530,6 @@ def is_ip_address(host: Optional[Union[str, bytes, bytearray, memoryview]]) -> b
530530
return is_ipv4_address(host) or is_ipv6_address(host)
531531

532532

533-
def next_whole_second() -> datetime.datetime:
534-
"""Return current time rounded up to the next whole second."""
535-
return datetime.datetime.now(datetime.timezone.utc).replace(
536-
microsecond=0
537-
) + datetime.timedelta(seconds=0)
538-
539-
540533
_cached_current_datetime: Optional[int] = None
541534
_cached_formatted_datetime = ""
542535

tests/test_cookiejar.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,23 +102,27 @@ def test_date_parsing() -> None:
102102
assert parse_func("") is None
103103

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

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

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

119122
# No timezone string
120-
assert parse_func("Tue, 1 Jan 1970 00:00:00") == datetime.datetime(
121-
1970, 1, 1, tzinfo=utc
123+
assert (
124+
parse_func("Tue, 1 Jan 1970 00:00:00")
125+
== datetime.datetime(1970, 1, 1, tzinfo=utc).timestamp()
122126
)
123127

124128
# No year

0 commit comments

Comments
 (0)