Skip to content

Instantly share code, notes, and snippets.

@tgvashworth
Created December 23, 2025 18:14
Show Gist options
  • Select an option

  • Save tgvashworth/009a926286654e4cec537007d6313b9e to your computer and use it in GitHub Desktop.

Select an option

Save tgvashworth/009a926286654e4cec537007d6313b9e to your computer and use it in GitHub Desktop.
Analysis: Remove in-memory list cost cache from Gyrinx

Plan: Remove In-Memory List Cost Cache

Background

The codebase has TWO cost caching mechanisms:

  1. Database fields (rating_current, stash_current, dirty) - KEEP - central to facts system
  2. In-memory Django cache (caches["core_list_cache"]) - REMOVE - no longer needed

The in-memory cache is updated by signals on every List/ListFighter/Assignment change. These signals are expensive and redundant now that the facts system uses DB fields.

How The Systems Work

Facts System (DB fields):

  • facts() returns cached DB values if dirty=False
  • facts_from_db(update=True) recalculates and updates DB fields
  • facts_with_fallback() tries facts() first, falls back to calculation
  • Display methods (cost_display, rating_display) use facts when can_use_facts() is True

In-Memory Cache (to be removed):

  • update_cost_cache() writes to caches["core_list_cache"]
  • cost_int_cached property reads from cache
  • Signals fire on every List/ListFighter/Assignment change
  • Falls back to expensive cost_int() recalculation if cache miss

Why Remove It

Once all lists have ListAction tracking and views prefetch latest_actions:

  • can_use_facts() always returns True
  • Display methods use facts() which reads DB fields
  • In-memory cache is never read
  • Signal overhead becomes pure waste

Prerequisites Before Signal Removal

1. Fix Views Missing with_related_data() Prefetch

Without latest_actions prefetch, can_use_facts() returns False and code falls back to the in-memory cache.

can_use_facts() implementation:

def can_use_facts(self) -> bool:
    if hasattr(self, "latest_actions"):
        return bool(self.latest_actions)
    return False

High Priority Gaps:

File Line Issue
gyrinx/core/views/__init__.py 66-68 Dashboard user lists - missing prefetch
gyrinx/core/views/__init__.py 85-93 Dashboard campaign gangs - missing prefetch
gyrinx/core/views/list.py 171-173 ListsListView - missing prefetch
gyrinx/core/views/campaign.py 473-477 Available lists for campaign
gyrinx/core/views/__init__.py 251-253 User profile public lists

Fix Pattern:

# Before
List.objects.filter(...).select_related("content_house")

# After
List.objects.filter(...).with_related_data(with_latest_action=True)

2. Run Backfill in Production

All lists need at least one ListAction so can_use_facts() returns True.

  • Enable FEATURE_LIST_ACTION_CREATE_INITIAL=True (already done)
  • Use admin filter "Missing action tracking" to find lists without actions
  • Run "Initialize action tracking" admin action on all lists

Phase 1: Fix Prefetch Gaps

Files to modify:

  • gyrinx/core/views/__init__.py - Dashboard and profile views
  • gyrinx/core/views/list.py - ListsListView
  • gyrinx/core/views/campaign.py - Campaign list selection

Phase 2: Remove In-Memory Cache Signals

After backfill is complete, remove these signals from gyrinx/core/models/list.py:

Signal Line Trigger
update_list_cost_cache_from_list_change ~1165 List post_save, m2m_changed
update_list_cost_cache ~3350 ListFighter pre_delete, post_save, m2m_changed
update_list_cache_for_assignment ~4244 ListFighterEquipmentAssignment post_delete, post_save
update_list_cache_for_weapon_profiles ~4262 M2M weapon_profiles_field
update_list_cache_for_weapon_accessories ~4273 M2M weapon_accessories_field
update_list_cache_for_upgrades ~4284 M2M upgrades_field

Phase 3: Remove Cache Infrastructure

Methods to delete from gyrinx/core/models/list.py:

  • List.cost_cache_key() (~770)
  • List.update_cost_cache() (~774)
  • List.cost_int_cached property (~335) - replace usages with facts_with_fallback().wealth
  • List.cost_int() method - verify no remaining callers first

Views to update:

  • gyrinx/core/views/list.py - refresh_list_cost view (~847) - remove or update to use facts

Settings to potentially remove:

  • CACHE_LIST_TTL if only used by this cache
  • core_list_cache from CACHES config (if dedicated)

Tests to update:

  • gyrinx/core/tests/test_models_core.py - remove cache assertions
  • gyrinx/core/tests/test_list_refresh_cost.py - update or remove

Testing Strategy

  1. After Phase 1: Verify can_use_facts() returns True for all list displays

    • Add temporary logging if needed
    • Check that cost_int_cached fallback is never hit
  2. After Phase 2: Verify no errors, list costs still display correctly

    • All display methods should use facts() path
    • No performance regressions
  3. After Phase 3: Verify all cost_display calls work without cache

    • Run full test suite
    • Manual smoke test on key pages

Execution Order

  1. Phase 1: Fix prefetch gaps (can do now)
  2. Run backfill in production
  3. Verify all lists have actions via admin filter
  4. Phase 2: Remove signals
  5. Phase 3: Remove cache infrastructure

Notes

  • credits_current is NOT a cache - it's the authoritative balance. Keep it.
  • rating_current, stash_current, dirty are DB cache fields - KEEP them, they're central to facts system
  • Only the in-memory Django cache (caches["core_list_cache"]) is being removed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment