Александр Кошелев написал подробную статью про инвалидацию кешей. Самое интересное (для меня, по крайней мере) в ней то, что там сделана попытка придумать декларативный синтаксис описания процесса инвалидации. И хотя в общем случае такая задача не решается, было бы очень полезно найти подход, который бы работал в пресловутых 80% случаев.

Пишу "бы", потому что для нас в "Офлайне" он, кажется, не сработает. Хочу, таким образом, покритиковать некоторые особенности этого решения. Может быть кто-нибудь придумает, как его улучшить.

Синтаксис

Первая придирка довольно простая — мне жутко не нравится синтаксис с многоэтажными декораторами. Я придерживаюсь той точки зрения, что в декоратор нужно выносить все то, что неважно для понимания логики работы функции. Кеширование — как раз тот случай. Но сама по себе конструкция из 4-10 строк настолько лезет в глаза, что заставить себя абстрагироваться от нее довольно трудно.

У нас в проекте вся логика инвалидации чего бы то ни было снесена в отдельный файл, где на сигналы вешаются функции инвалидации. То же самое можно сделать и в этом случае: завести "invalidation.py" в котором описать один большой список из dict'ов, которые иначе размазаны по коду. Очень похоже на urlconf будет по идее :-).

Завязка на модельные сигналы

Возможно я ошибаюсь, но мне кажется, что весь дизайн этой декларативной инвалидации ориентирован на то, чтобы вешаться в первую очередь на модельные сигналы (post_save, post_delete и т.п.) Я совершенно убежден, что использовать именно их для инвалидации кеша — неверная идея, потому что они работают на более низком уровне абстракции, чем тот, который должен знать про кеширование.

Пример. Вы храните в модели поста денормализованное количество комментариев, которое обновляется при появлении и удалении комментариев:

class Post(models.Model):
    comment_count = models.IntegerField()

    def _update_comment_count(self):
        self.comment_count = # могучий запрос
        self.save()

class Comment(models.Model):
    post = models.ForiegnKey(Post)

    def save(self):
        super(Comment, self).save()
        self.post._update_comment_count()

    def delete(self):
        # аналогично save

Если завязать, как у Александра, инвалидацию блока "последние 5 постов" на сохранение модели Post, то он будет инвалидироваться с каждым комментарием.

Это, понятное дело, только пример. Я хочу показать, что кеширование и инвалидация в Джанго — операции уровня view. Именно там, где программа понимает, "шо канкретна" имел в виду пользователь, она может послать нужные сигналы, по которым можно будет делать и более вменяемую инвалидацию.

Вот насколько декларативный синтаксис будет хорошо подходить к ситуации, когда большинство инвалидирующих сигналов — кастомные, сказать не берусь. Надо, наверное, попользоваться и понять.

Сложные случаи

Но больше всего мне беспокоит другое. А именно то, что сложные случаи, для которых у Александра предусмотрен fallback из декларативной модели в императивную — функция checker — будут составлять не такую уж и маленькую часть. У нас в проекте именно так и происходит. Вот, например, как описывается инвалидация блоков с аватарками людей идущих на событие в разных местах системы:

def invalidate_faces(sender):
    event = sender.instance.event
    city = get_user_city().text_code
    keys = [
        '%s-%s-%s-%s-%s-%s' % ('faces', event_id, city, category and category.id, tag and tag.id, user)
        for user in [None, sender.user.yandex_uid]
        for event_id in [event.id, None]
        for category in [None, event.category]
        for tag in [None] + list(event.tags.all())
    ]
    clear_keys(keys)

Это четверной вложенный for, который генерирует целую пачку ключей, каждый из которых отвечает за вид этого блока в разных условиях (индексная страница, страница выборки событий, страница одного события и т.д.) И надо сказать, остальные инвалидации у нас тоже происходят именно над пачками ключей. Как описать это декларативно, я не представляю...

Впрочем, возможно у нас слишком усложнены правила показа всего этого добра. А возможно, во всем виновата моя идея свалить всю инвалидацию в один файл... Не знаю. Время покажет, а пока это просто мысль "на подумать"...

Комментарии: 3

  1. Александр Соловьёв

    То же самое можно сделать и в этом случае: завести “invalidation.py” в котором описать один большой список из dict’ов, которые иначе размазаны по коду.

    Я об этом же думал. :-) Имхо, довольно здраво должно выйти. Но пока ничего реализовать не пытался...

  2. Давид Мзареулян

    Вот насколько декларативный синтаксис будет хорошо подходить к ситуации, когда большинство инвалидирующих сигналов — кастомные, сказать не берусь.

    А какие тут могут быть проблемы? Вроде как сигналы — и в Африке сигналы.

    Это четверной вложенный for, который генерирует целую пачку ключей, каждый из которых отвечает за вид этого блока в разных условиях…

    Внушаетъ. А не логичнее ли поставить кэш в зависимость от ЧЕТЫРЁХ ключей, и инвалидировать его по изменении любого?

    p. s. Тутошний OpenID не смог принять мой адрес ЖЖ.

  3. Иван Сагалаев

    А какие тут могут быть проблемы? Вроде как сигналы — и в Африке сигналы.

    Проблема в данных, которые идут вместе с сигналом. У них свободный формат и семантика, поэтому мне кажется, что всегда придется писать код, который знает, что означают эти данные.

    Этой проблемы не возникает с pre_save, pre_delete, потому что формат их параметра известен — это instance модели.

Добавить комментарий