|
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