|
5 | 5 | import logging |
6 | 6 | import os |
7 | 7 | import re |
8 | | -from datetime import datetime, timedelta |
| 8 | +from datetime import timedelta |
9 | 9 | from fnmatch import fnmatch |
10 | 10 | from importlib import import_module |
11 | 11 | from inspect import iscoroutinefunction |
12 | 12 | from typing import Any, Callable, Sequence |
13 | 13 |
|
14 | 14 | from channels.db import database_sync_to_async |
15 | | -from django.core.cache import caches |
16 | 15 | from django.db.models import ManyToManyField, ManyToOneRel, prefetch_related_objects |
17 | 16 | from django.db.models.base import Model |
18 | 17 | from django.db.models.query import QuerySet |
|
24 | 23 |
|
25 | 24 | from reactpy_django.exceptions import ComponentDoesNotExistError, ComponentParamError |
26 | 25 |
|
27 | | - |
28 | 26 | _logger = logging.getLogger(__name__) |
29 | 27 | _component_tag = r"(?P<tag>component)" |
30 | 28 | _component_path = r"(?P<path>\"[^\"'\s]+\"|'[^\"'\s]+')" |
@@ -88,14 +86,18 @@ def _register_component(dotted_path: str) -> Callable: |
88 | 86 | """Adds a component to the mapping of registered components. |
89 | 87 | This should only be called on startup to maintain synchronization during mulitprocessing. |
90 | 88 | """ |
91 | | - from reactpy_django.config import REACTPY_REGISTERED_COMPONENTS |
| 89 | + from reactpy_django.config import ( |
| 90 | + REACTPY_FAILED_COMPONENTS, |
| 91 | + REACTPY_REGISTERED_COMPONENTS, |
| 92 | + ) |
92 | 93 |
|
93 | 94 | if dotted_path in REACTPY_REGISTERED_COMPONENTS: |
94 | 95 | return REACTPY_REGISTERED_COMPONENTS[dotted_path] |
95 | 96 |
|
96 | 97 | try: |
97 | 98 | REACTPY_REGISTERED_COMPONENTS[dotted_path] = import_dotted_path(dotted_path) |
98 | 99 | except AttributeError as e: |
| 100 | + REACTPY_FAILED_COMPONENTS.add(dotted_path) |
99 | 101 | raise ComponentDoesNotExistError( |
100 | 102 | f"Error while fetching '{dotted_path}'. {(str(e).capitalize())}." |
101 | 103 | ) from e |
@@ -266,7 +268,7 @@ def django_query_postprocessor( |
266 | 268 | # Force the query to execute |
267 | 269 | getattr(data, field.name, None) |
268 | 270 |
|
269 | | - if many_to_one and type(field) == ManyToOneRel: |
| 271 | + if many_to_one and type(field) == ManyToOneRel: # noqa: #E721 |
270 | 272 | prefetch_fields.append(field.related_name or f"{field.name}_set") |
271 | 273 |
|
272 | 274 | elif many_to_many and isinstance(field, ManyToManyField): |
@@ -332,35 +334,23 @@ def create_cache_key(*args): |
332 | 334 | def db_cleanup(immediate: bool = False): |
333 | 335 | """Deletes expired component sessions from the database. |
334 | 336 | This function may be expanded in the future to include additional cleanup tasks.""" |
335 | | - from .config import REACTPY_CACHE, REACTPY_DEBUG_MODE, REACTPY_RECONNECT_MAX |
336 | | - from .models import ComponentSession |
337 | | - |
338 | | - clean_started_at = datetime.now() |
339 | | - cache_key: str = create_cache_key("last_cleaned") |
340 | | - now_str: str = datetime.strftime(timezone.now(), DATE_FORMAT) |
341 | | - cleaned_at_str: str = caches[REACTPY_CACHE].get(cache_key) |
342 | | - cleaned_at: datetime = timezone.make_aware( |
343 | | - datetime.strptime(cleaned_at_str or now_str, DATE_FORMAT) |
344 | | - ) |
345 | | - clean_needed_by = cleaned_at + timedelta(seconds=REACTPY_RECONNECT_MAX) |
346 | | - expires_by: datetime = timezone.now() - timedelta(seconds=REACTPY_RECONNECT_MAX) |
| 337 | + from .config import REACTPY_DEBUG_MODE, REACTPY_RECONNECT_MAX |
| 338 | + from .models import ComponentSession, Config |
347 | 339 |
|
348 | | - # Component params exist in the DB, but we don't know when they were last cleaned |
349 | | - if not cleaned_at_str and ComponentSession.objects.all(): |
350 | | - _logger.warning( |
351 | | - "ReactPy has detected component sessions in the database, " |
352 | | - "but no timestamp was found in cache. This may indicate that " |
353 | | - "the cache has been cleared." |
354 | | - ) |
| 340 | + config = Config.load() |
| 341 | + start_time = timezone.now() |
| 342 | + cleaned_at = config.cleaned_at |
| 343 | + clean_needed_by = cleaned_at + timedelta(seconds=REACTPY_RECONNECT_MAX) |
355 | 344 |
|
356 | 345 | # Delete expired component parameters |
357 | | - # Use timestamps in cache (`cleaned_at_str`) as a no-dependency rate limiter |
358 | | - if immediate or not cleaned_at_str or timezone.now() >= clean_needed_by: |
359 | | - ComponentSession.objects.filter(last_accessed__lte=expires_by).delete() |
360 | | - caches[REACTPY_CACHE].set(cache_key, now_str, timeout=None) |
| 346 | + if immediate or timezone.now() >= clean_needed_by: |
| 347 | + expiration_date = timezone.now() - timedelta(seconds=REACTPY_RECONNECT_MAX) |
| 348 | + ComponentSession.objects.filter(last_accessed__lte=expiration_date).delete() |
| 349 | + config.cleaned_at = timezone.now() |
| 350 | + config.save() |
361 | 351 |
|
362 | 352 | # Check if cleaning took abnormally long |
363 | | - clean_duration = datetime.now() - clean_started_at |
| 353 | + clean_duration = timezone.now() - start_time |
364 | 354 | if REACTPY_DEBUG_MODE and clean_duration.total_seconds() > 1: |
365 | 355 | _logger.warning( |
366 | 356 | "ReactPy has taken %s seconds to clean up expired component sessions. " |
|
0 commit comments