Skip to content

Commit 087be86

Browse files
committed
Use DB for clean tracking
1 parent 0aaaaac commit 087be86

File tree

7 files changed

+63
-33
lines changed

7 files changed

+63
-33
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ Using the following categories, list your changes in this order:
3434

3535
## [Unreleased]
3636

37-
- Nothing yet!
37+
### Changed
38+
39+
- A thread-safe cache is no longer required.
3840

3941
## [3.2.1] - 2023-06-29
4042

docs/python/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ReactPy requires a multiprocessing-safe and thread-safe cache.
1+
# ReactPy benefits from a fast, well indexed cache.
22
REACTPY_CACHE = "default"
33

44
# ReactPy requires a multiprocessing-safe and thread-safe database.

src/reactpy_django/components.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,6 @@ def _cached_static_contents(static_path: str):
210210
)
211211

212212
# Fetch the file from cache, if available
213-
# Cache is preferrable to `use_memo` due to multiprocessing capabilities
214213
last_modified_time = os.stat(abs_path).st_mtime
215214
cache_key = f"reactpy_django:static_contents:{static_path}"
216215
file_contents = caches[REACTPY_CACHE].get(
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.3 on 2023-08-04 05:49
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("reactpy_django", "0003_componentsession_delete_componentparams"),
9+
]
10+
11+
operations = [
12+
migrations.CreateModel(
13+
name="Config",
14+
fields=[
15+
(
16+
"id",
17+
models.BigAutoField(
18+
auto_created=True,
19+
primary_key=True,
20+
serialize=False,
21+
verbose_name="ID",
22+
),
23+
),
24+
("cleaned_at", models.DateTimeField(auto_now_add=True)),
25+
],
26+
),
27+
]

src/reactpy_django/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,19 @@ class ComponentSession(models.Model):
99
uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore
1010
params = models.BinaryField(editable=False) # type: ignore
1111
last_accessed = models.DateTimeField(auto_now_add=True) # type: ignore
12+
13+
14+
class Config(models.Model):
15+
"""A singleton model for storing ReactPy configuration."""
16+
17+
cleaned_at = models.DateTimeField(auto_now_add=True) # type: ignore
18+
19+
def save(self, *args, **kwargs):
20+
"""Singleton save method."""
21+
self.pk = 1
22+
super().save(*args, **kwargs)
23+
24+
@classmethod
25+
def load(cls):
26+
obj, created = cls.objects.get_or_create(pk=1)
27+
return obj

src/reactpy_django/utils.py

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from typing import Any, Callable, Sequence
1313

1414
from channels.db import database_sync_to_async
15-
from django.core.cache import caches
1615
from django.db.models import ManyToManyField, ManyToOneRel, prefetch_related_objects
1716
from django.db.models.base import Model
1817
from django.db.models.query import QuerySet
@@ -24,7 +23,6 @@
2423

2524
from reactpy_django.exceptions import ComponentDoesNotExistError, ComponentParamError
2625

27-
2826
_logger = logging.getLogger(__name__)
2927
_component_tag = r"(?P<tag>component)"
3028
_component_path = r"(?P<path>\"[^\"'\s]+\"|'[^\"'\s]+')"
@@ -332,39 +330,22 @@ def create_cache_key(*args):
332330
def db_cleanup(immediate: bool = False):
333331
"""Deletes expired component sessions from the database.
334332
This function may be expanded in the future to include additional cleanup tasks."""
335-
from .config import (
336-
REACTPY_CACHE,
337-
REACTPY_DATABASE,
338-
REACTPY_DEBUG_MODE,
339-
REACTPY_RECONNECT_MAX,
340-
)
341-
from .models import ComponentSession
333+
from .config import REACTPY_DATABASE, REACTPY_DEBUG_MODE, REACTPY_RECONNECT_MAX
334+
from .models import ComponentSession, Config
342335

343336
clean_started_at = datetime.now()
344-
cache_key: str = create_cache_key("last_cleaned")
345-
now_str: str = datetime.strftime(timezone.now(), DATE_FORMAT)
346-
cleaned_at_str: str = caches[REACTPY_CACHE].get(cache_key)
347-
cleaned_at: datetime = timezone.make_aware(
348-
datetime.strptime(cleaned_at_str or now_str, DATE_FORMAT)
349-
)
337+
config = Config.load()
338+
cleaned_at = config.cleaned_at
350339
clean_needed_by = cleaned_at + timedelta(seconds=REACTPY_RECONNECT_MAX)
351-
expires_by: datetime = timezone.now() - timedelta(seconds=REACTPY_RECONNECT_MAX)
352-
353-
# Component params exist in the DB, but we don't know when they were last cleaned
354-
if not cleaned_at_str and ComponentSession.objects.using(REACTPY_DATABASE).all():
355-
_logger.warning(
356-
"ReactPy has detected component sessions in the database, "
357-
"but no timestamp was found in cache. This may indicate that "
358-
"the cache has been cleared."
359-
)
340+
expires_by = timezone.now() - timedelta(seconds=REACTPY_RECONNECT_MAX)
360341

361342
# Delete expired component parameters
362-
# Use timestamps in cache (`cleaned_at_str`) as a no-dependency rate limiter
363-
if immediate or not cleaned_at_str or timezone.now() >= clean_needed_by:
343+
if immediate or timezone.now() >= clean_needed_by:
364344
ComponentSession.objects.using(REACTPY_DATABASE).filter(
365345
last_accessed__lte=expires_by
366346
).delete()
367-
caches[REACTPY_CACHE].set(cache_key, now_str, timeout=None)
347+
config.cleaned_at = timezone.now()
348+
config.save()
368349

369350
# Check if cleaning took abnormally long
370351
clean_duration = datetime.now() - clean_started_at

tests/test_app/admin.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from django.contrib import admin
2+
from reactpy_django.models import ComponentSession, Config
3+
24
from test_app.models import (
35
AsyncForiegnChild,
46
AsyncRelationalChild,
@@ -10,8 +12,6 @@
1012
TodoItem,
1113
)
1214

13-
from reactpy_django.models import ComponentSession
14-
1515

1616
@admin.register(TodoItem)
1717
class TodoItemAdmin(admin.ModelAdmin):
@@ -55,4 +55,9 @@ class AsyncForiegnChildAdmin(admin.ModelAdmin):
5555

5656
@admin.register(ComponentSession)
5757
class ComponentSessionAdmin(admin.ModelAdmin):
58-
list_display = ("uuid", "last_accessed")
58+
list_display = ["uuid", "last_accessed"]
59+
60+
61+
@admin.register(Config)
62+
class ConfigAdmin(admin.ModelAdmin):
63+
list_display = ["pk", "cleaned_at"]

0 commit comments

Comments
 (0)