agateblue / django-dynamic-preferences

Dynamic global and instance settings for your django project
https://django-dynamic-preferences.readthedocs.org/
BSD 3-Clause "New" or "Revised" License
348 stars 86 forks source link

Caching is not invalidated #303

Closed lekhnath closed 9 months ago

lekhnath commented 9 months ago

I've made a management script that can turn on and off the maintenance mode and set up tools to handle it. The value is always read from cache, though, and cache is never changed. What am I supposed to do? What should I do to make the information read? It would also be nicer to update the cache when the number changes. Samples of code:

dynamic_preferences_registry.py

from dynamic_preferences.types import BooleanPreference
from dynamic_preferences.registries import global_preferences_registry

@global_preferences_registry.register
class MaintenanceMode(BooleanPreference):
    name = 'maintenance_mode'
    default = False

middleware.py

from dynamic_preferences.registries import global_preferences_registry
from django.http import HttpResponse
from rest_framework import status

# We instantiate a manager for our global preferences
global_preferences = global_preferences_registry.manager()

def maintenance_middleware(get_response):
    def middleware(request):
        if global_preferences['maintenance_mode'] == True:
            return HttpResponse(
                'Maintenance', status=status.HTTP_503_SERVICE_UNAVAILABLE
            )

        response = get_response(request)

        return response

    return middleware

*management/commands/site_maintenance.py

from dynamic_preferences.registries import global_preferences_registry
from django.core.management import BaseCommand

# We instantiate a manager for our global preferences
global_preferences = global_preferences_registry.manager()

class Command(BaseCommand):
    help = 'Enable/Disable maintenance mode'

    def add_arguments(self, parser):
        parser.add_argument('mode',
                            help='provide on or off value')

    def handle(self, *args, **options) -> str | None:

        if options['mode'] == 'on':
            global_preferences['maintenance_mode'] = True
            print('Maintenace is now activated')
        elif options['mode'] == 'off':
            global_preferences['maintenance_mode'] = False
            print('Maintenace is now deactivated')
        else:
            raise Exception('Invalid mode %s' % options['mode'])

By running these commands, it can turn on and off the "maintenance_mode" in the database, but this tool will always read the value that it was at server start-up.

agateblue commented 9 months ago

In middleware.py, can you try moving global_preferences = global_preferences_registry.manager() inside the middleware function, like this:


def maintenance_middleware(get_response):
    def middleware(request):
        # We instantiate a manager for our global preferences
        global_preferences = global_preferences_registry.manager()
        if global_preferences['maintenance_mode'] == True:
            return HttpResponse(
                'Maintenance', status=status.HTTP_503_SERVICE_UNAVAILABLE
            )

        response = get_response(request)

        return response

    return middleware

I suspect some caching is occuring there, and because the manager is instanciated at the top level of the module, the value is cached on first access and never updated.

It would also be nicer to update the cache when the number changes

I'm not sure what you mean by that, could you clarify?

lekhnath commented 9 months ago

Eventually, the changed value was read. It took a while—maybe because it was after cache ttl or because I wasn't patient enough—but it was read. I tried moving the call to the manager inside a middleware method, like you suggested, but it didn't affect anything. It shows up after a while, even if I change it straight from the database.

PS: Sorry for the typo there It would also be nicer to update the cache when the number changes. I meant when the value changes in database.

agateblue commented 9 months ago

We do invalidate the cache in case on an update. This happens through a post save signal here:

https://github.com/agateblue/django-dynamic-preferences/blob/6712ace490f6ec3f892beb87aa8fc0ead9d4d16b/dynamic_preferences/models.py#L132

Which triggers https://github.com/agateblue/django-dynamic-preferences/blob/6712ace490f6ec3f892beb87aa8fc0ead9d4d16b/dynamic_preferences/managers.py#L107

Can you share your settings.CACHES configuration? I'm wondering what type of cache you are using.

lekhnath commented 9 months ago

Hi @agateblue

I haven't setup any particular "CACHES" settings in Django. The usual Django cache is being used for now.

lekhnath commented 9 months ago

This is my current settings for dyanamic_preferences :


# available settings with their default values
DYNAMIC_PREFERENCES = {

    # a python attribute that will be added to model instances with preferences
    # override this if the default collide with one of your models attributes/fields
    'MANAGER_ATTRIBUTE': 'preferences',

    # The python module in which registered preferences will be searched within each app
    'REGISTRY_MODULE': 'preferences_registry',

    # Allow quick editing of preferences directly in admin list view
    # WARNING: enabling this feature can cause data corruption if multiple users
    # use the same list view at the same time, see https://code.djangoproject.com/ticket/11313
    'ADMIN_ENABLE_CHANGELIST_FORM': False,

    # Customize how you can access preferences from managers. The default is to
    # separate sections and keys with two underscores. This is probably not a settings you'll
    # want to change, but it's here just in case
    'SECTION_KEY_SEPARATOR': '__',

    # Use this to disable auto registration of the GlobalPreferenceModel.
    # This can be useful to register your own model in the global_preferences_registry.
    'ENABLE_GLOBAL_MODEL_AUTO_REGISTRATION': True,

    # Use this to disable caching of preference. This can be useful to debug things
    'ENABLE_CACHE': True,

    # Use this to select which chache should be used to cache preferences. Defaults to default.
    'CACHE_NAME': 'default',

    # Use this to disable checking preferences names. This can be useful to debug things
    'VALIDATE_NAMES': True,
}
agateblue commented 9 months ago

Hi @agateblue

I haven't setup any particular "CACHES" settings in Django. The usual Django cache is being used for now.

This is the source of your issue then. The default cache is using local process memory (https://docs.djangoproject.com/en/5.0/ref/settings/#caches).

As each process get a distinct and indépendant cache, your management command does not update the cache of your server process.

You need to switch to another cache backend that is shared between processes (such as redis, memcached or file, though performances could be worse than no caching with this last one) or disable caching for dynamic preferences entirely.

lekhnath commented 9 months ago

I sincerely appreciate your advice, @agateblue. Much obliged. I am planning to use the redis cache. I hope it resolves my problem. I'm going to close now. In case this problem persists after the use of alternative caching mechanisms, I will open another issue.

agateblue commented 9 months ago

You're welcome, I'm glad it helped :)