Skip to content

Clear featured-list caches when featured learning paths are edited - #3757

Draft
mbertrand wants to merge 8 commits into
mainfrom
featured-list-cache-clearing
Draft

Clear featured-list caches when featured learning paths are edited#3757
mbertrand wants to merge 8 commits into
mainfrom
featured-list-cache-clearing

Conversation

@mbertrand

@mbertrand mbertrand commented Aug 11, 2026

Copy link
Copy Markdown
Member

What are the relevant tickets?

Closes https://github.com/mitodl/hq/issues/11979

Description (What does it do?)

Channel managers couldn't see featured-list edits for up to 24h (Redis view cache) plus 30min (Fastly page cache). Now:

  • Editing a unit channel's featured learning path through the API (path update/delete, item add/reorder/remove, or the bulk add-to-list action) enqueues a post-commit celery task that clears the featured_resources Redis cache, hard-purges /c/unit/<name> in Fastly, and soft-purges the homepage.
  • Nothing runs on the editor's request path; a failed enqueue (broker down) just logs.
  • Frontend learning-path mutations also invalidate the featured React Query cache, so editors see the change without a full reload.
  • call_fastly_purge_api gains a soft=True kwarg (sends Fastly-Soft-Purge: 1).

How can this be tested?

Run the full stack (COMPOSE_PROFILES=backend,frontend,keycloak,apisix) with the anonymous view cache enabled: set REDIS_VIEW_CACHE_DURATION=600 in env/backend.local.env (the shared default is 0, which disables it) and restart web + celery.

  1. Seed featured lists, log in once at http://open.odl.local:8062 as the admin user (admin@odl.local / admin), then make that user a learning-path editor:

    docker compose run --rm web python manage.py populate_featured_lists
    docker compose exec -T web python manage.py shell -c "
    from django.contrib.auth import get_user_model
    from learning_resources.utils import update_editor_group
    update_editor_group(get_user_model().objects.get(email='admin@odl.local'), True)"
  2. In a private/incognito window (anonymous), load a unit channel page — e.g. http://open.odl.local:8062/c/unit/mitx — and note the courses in the featured carousel. Load the homepage too. This primes the anonymous Redis cache for /api/v1/featured/.

  3. In the window where you're logged in as the admin/editor user, go to http://open.odl.local:8062/learningpaths, open "MITx Featured Resources", and remove a course from the list (or add one via a course card's add-to-list dialog). You must be logged in as an admin or learning-path editor — anonymous or regular users can't see or edit learning paths. Watch the worker pick it up:

    docker compose logs -f celery | grep -iE "clear_featured|Skipping Fastly"

    Within a second or two of the edit you should see:

    Task learning_resources.tasks.clear_featured_caches[...] received
    Skipping Fastly purge for /c/unit/mitx (dev environment)
    Skipping Fastly purge for / (dev environment)
    Task learning_resources.tasks.clear_featured_caches[...] succeeded in 0.009s
    

    The Fastly purges are skipped locally (no FASTLY_API_KEY); on RC/prod they become real PURGE requests against /c/unit/<name> (hard) and / (soft).

  4. Reload the incognito channel page and homepage: the featured carousel reflects the edit. Without this PR, the anonymous featured API response stays cached for REDIS_VIEW_CACHE_DURATION (24h default in deployed envs) and the pages keep rendering the old list.

I validated the backend chain locally by driving the same endpoints from the API side (editor DELETE on a featured list item → 204 → celery task ran in 9ms → anonymous /api/v1/featured/ fresh within seconds, where it stayed stale before). The React Query invalidation is covered by the jest tests (yarn test frontends/api/src/hooks/learningPaths frontends/api/src/hooks/learningResources).

Additional Context

  • hq#11979 says "synchronously (best-effort, non-blocking on failure)"; this is asynchronous-after-commit instead. The task fires within a beat of the save, so the editor still sees changes within seconds, without in-request Redis/Fastly calls that could block or 504 the save.
  • Fastly URL purges are exact-match, so query-string variants of the channel page (?topic=...) stay cached up to s-maxage (30 min). The base URL the editor refreshes is hard-purged. Per-channel surrogate keys are the upgrade path if that ever matters.
  • Edits via Django admin/shell don't trigger invalidation — the hooks are on the viewsets deliberately, since model signals would fire on every ETL run.

mbertrand and others added 8 commits August 11, 2026 13:58
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The browser query client's 30-minute staleTime (matched to the CDN TTL)
means the editor's own session never refetches the featured list after
they edit a learning path — the backend purge only helps on a full page
reload. Invalidate the featured query key in the mutations that map to
the backend's cache-clearing write hooks so the editor sees their change
immediately on client-side navigation too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hoist the tasks import to module level (tasks was already imported at
module scope, so the function-local import bought nothing), shrink the
bulk-action id set (featured_list_id__in coerces strings), and delete
the tautological retry-config test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rize

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

OpenAPI Changes

No changes detected

View full changelog

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant