Skip to content

Cache Patch

Cache Patch Module: Tenant-Aware Caching for Django

This module patches Django's cache framework to automatically isolate cache keys by tenant.

Problem

Django's default caching mechanism doesn't account for multi-tenancy: - Cached values from Tenant A are visible to Tenant B - Data leakage between tenants via shared cache - Cache collisions when multiple tenants use same key - Critical security issue in multi-tenant applications

Solution

This patch wraps Django's CacheHandler to automatically prefix all cache keys with the current tenant_id, ensuring complete cache isolation.

How It Works
  1. Intercepts all cache operations (get, set, delete, etc.)
  2. Extracts current tenant from TenantContext
  3. Prefixes all keys with "tenant_id:" pattern
  4. Routes to appropriate cache backend
  5. Returns prefixed values to application

Cache Isolation Example:

Default Django Cache (VULNERABLE):
```
Tenant A: cache.set('config', {'theme': 'dark'})
Tenant B: value = cache.get('config')  # Gets Tenant A's config!
```

With Patch (SAFE):
```
Tenant A: cache.set('config', {...})  # Stored as 'acme:config'
Tenant B: cache.get('config')          # Stored as 'globex:config'
# Each tenant's cache completely isolated
```
Key Prefixing Strategy

Pattern: "{tenant_id}:{original_key}"

Examples: - Original key: "user_prefs" → Prefixed key: "acme:user_prefs" - Original key: "session_data" → Prefixed key: "globex:session_data" - Original key: "cached_list" → Prefixed key: "dev:cached_list"

Prefixing Rules: 1. Only applied when using default cache config 2. Skipped for custom cache backends (if configured) 3. Uses TenantContext to get current tenant 4. Gracefully handles None tenant (no prefix applied)

Cache Alias Selection

The wrapper supports multiple cache aliases per tenant:

CACHES = {
    'default': {...},           # Main cache
    'acme': {...},              # Tenant-specific cache
    'globex': {...},            # Tenant-specific cache
    'sessions': {...},          # Shared sessions cache
}

Resolution order: 1. Check TenantContext.get_cache_alias() (tenant-specific) 2. Fall back to 'default' if no alias 3. Try to get cache handler[alias] 4. Fall back to handler['default'] if alias missing 5. Return cache backend

Thread Safety

The wrapper is thread-safe when: - TenantContext uses thread-local storage - Django cache backends are thread-safe - Tenant context properly set per thread

Performance Impact
  • Key prefixing: O(1) string concatenation
  • Cache lookup: Unaffected (index still works)
  • Memory: Slightly increased for prefixed keys
  • Negligible overhead (<1% in typical scenarios)
Compatibility
  • Works with all Django cache backends
  • Compatible with custom cache backends
  • Supports Redis, Memcached, Database, Locmem
  • Backwards compatible with Django cache API

Supported Cache Operations:

Dict-style access:
- cache[key] = value            # __setitem__
- value = cache[key]             # __getitem__
- del cache[key]                 # __delitem__
- key in cache                   # __contains__

Method-style access:
- cache.get(key)                 # Single-key
- cache.set(key, value)          # Single-key
- cache.add(key, value)          # Single-key
- cache.delete(key)              # Single-key
- cache.has_key(key)             # Single-key
- cache.incr(key)                # Single-key
- cache.decr(key)                # Single-key
- cache.touch(key)               # Single-key
- cache.get_or_set(key, default) # Single-key
- cache.keys(pattern)            # Pattern-based
Usage

The patch is automatically applied on module import:

# In Django settings:
OMNITENANT_CONFIG = {
    'PATCHES': {
        'cache': True,  # Enable cache patch
    }
}

# In application code - use cache normally:
from django.core.cache import cache

cache.set('user_data', {...})   # Automatically prefixed with tenant_id
data = cache.get('user_data')    # Automatically uses tenant prefix
Automatic Application

The patch_django_cache() function is called at module import time: - Replaces django.core.cache.cache - Replaces django.core.cache.caches - Intercepts all cache operations - No manual configuration needed

TenantAwareCacheWrapper

Wraps Django's CacheHandler to automatically isolate cache by tenant.

This wrapper intercepts all cache operations and prefixes cache keys with the current tenant_id, ensuring complete cache isolation between tenants.

Design
  • Wraps DjangoCacheHandler (Django's cache manager)
  • Delegates actual caching to underlying cache backends
  • Adds transparent tenant-key prefixing layer
  • Supports both dict-style and method-style access
Features
  • Automatic tenant key prefixing
  • Multiple cache alias support
  • Fallback handling for missing aliases
  • Thread-safe operation
  • Compatible with all Django cache backends
  • Zero configuration needed
Cache Alias Routing

Routes to appropriate cache based on current tenant:

  1. Check TenantContext.get_cache_alias() for tenant-specific alias
  2. Fall back to 'default' if no alias configured
  3. Attempt to get handler[alias]
  4. Fall back to handler['default'] if alias missing
  5. Return selected cache backend
Key Prefixing

Automatically prefixes all single-key operations:

Original: cache.get('user_prefs') Prefixed: cache.get('acme:user_prefs') # When in 'acme' tenant

Prefixing is smart: - Only prefixes when using default config - Skips for custom cache backends - Handles None tenant gracefully - Applied transparently to caller

Supported Access Patterns

Dict-style: cache[key] = value, value = cache[key] Method-style: cache.get(key), cache.set(key, value)

Attributes:

Name Type Description
_handler CacheHandler

Wrapped Django cache handler

Source code in django_omnitenant/patches/cache.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
class TenantAwareCacheWrapper:
    """
    Wraps Django's CacheHandler to automatically isolate cache by tenant.

    This wrapper intercepts all cache operations and prefixes cache keys with
    the current tenant_id, ensuring complete cache isolation between tenants.

    Design:
        - Wraps DjangoCacheHandler (Django's cache manager)
        - Delegates actual caching to underlying cache backends
        - Adds transparent tenant-key prefixing layer
        - Supports both dict-style and method-style access

    Features:
        - Automatic tenant key prefixing
        - Multiple cache alias support
        - Fallback handling for missing aliases
        - Thread-safe operation
        - Compatible with all Django cache backends
        - Zero configuration needed

    Cache Alias Routing:
        Routes to appropriate cache based on current tenant:

        1. Check TenantContext.get_cache_alias() for tenant-specific alias
        2. Fall back to 'default' if no alias configured
        3. Attempt to get handler[alias]
        4. Fall back to handler['default'] if alias missing
        5. Return selected cache backend

    Key Prefixing:
        Automatically prefixes all single-key operations:

        Original: cache.get('user_prefs')
        Prefixed: cache.get('acme:user_prefs')  # When in 'acme' tenant

        Prefixing is smart:
        - Only prefixes when using default config
        - Skips for custom cache backends
        - Handles None tenant gracefully
        - Applied transparently to caller

    Supported Access Patterns:
        Dict-style: cache[key] = value, value = cache[key]
        Method-style: cache.get(key), cache.set(key, value)

    Attributes:
        _handler (DjangoCacheHandler): Wrapped Django cache handler
    """

    def __init__(self, handler: DjangoCacheHandler):
        """
        Initialize wrapper with Django's CacheHandler.

        Args:
            handler (DjangoCacheHandler): The original Django cache handler
                                        to be wrapped for tenant awareness

        Stores handler for delegation of actual cache operations.
        """
        self._handler = handler

    def _get_cache(self):
        """
        Get appropriate cache backend for current tenant.

        Resolution Process:
            1. Get cache alias from TenantContext (tenant-specific)
            2. Fall back to 'default' if no alias configured
            3. Attempt to get cache from handler[alias]
            4. Fall back to handler['default'] if alias missing
            5. Return cache backend

        Cache Alias:
            The cache alias determines which Django CACHES config is used:

            ```python
            # settings.py
            CACHES = {
                'default': {...},   # Used if no tenant alias
                'redis': {...},     # Could be tenant alias
                'acme': {...},      # Could be tenant-specific alias
            }
            ```

            TenantContext.get_cache_alias() returns tenant's configured alias,
            or None if tenant should use default cache.

        Returns:
            django.core.cache.backends.*: The cache backend for current context

        Examples:
            ```python
            # No tenant set, no alias configured
            # Returns: handler['default']

            # Tenant 'acme' set, alias 'redis'
            # Returns: handler['redis']

            # Tenant 'globex' set, but 'globex' alias missing
            # Returns: handler['default'] (fallback)
            ```
        """
        # Get configured cache alias for current tenant
        # Falls back to 'default' if no alias configured
        alias = TenantContext.get_cache_alias() or "default"

        try:
            # Attempt to get cache backend for this alias
            return self._handler[alias]
        except KeyError:
            # Alias doesn't exist, fall back to default cache
            return self._handler["default"]

    def _apply_prefix(self, key):
        """
        Apply tenant prefix to cache key if using default config.

        Prefixing Logic:
            1. Get cache alias for current tenant
            2. Check if using default cache config
            3. Get current tenant from TenantContext
            4. If tenant exists, prepend tenant_id
            5. Otherwise return key unchanged

        Prefix Pattern:
            "{tenant_id}:{original_key}"

            Examples:
            - Tenant 'acme', key 'user_data' → 'acme:user_data'
            - Tenant 'globex', key 'config' → 'globex:config'
            - No tenant, key 'shared' → 'shared' (no prefix)

        Smart Prefixing:
            Only applies prefix when:
            - Using default cache configuration
            - Tenant exists in current context

            Doesn't prefix when:
            - Using custom cache backend (IS_USING_DEFAULT_CONFIG = False)
            - No tenant set (None)
            - Tenant context not initialized

        Args:
            key (str): Original cache key to prefix

        Returns:
            str: Prefixed key (if conditions met) or original key

        Examples:
            ```python
            # With tenant 'acme' and default config
            _apply_prefix('user_prefs') → 'acme:user_prefs'

            # With no tenant set
            _apply_prefix('user_prefs') → 'user_prefs'

            # With custom cache config
            _apply_prefix('user_prefs') → 'user_prefs' (not prefixed)
            ```
        """
        # Get cache alias (default if not configured)
        alias = TenantContext.get_cache_alias() or "default"

        # Check if using default cache configuration
        # IS_USING_DEFAULT_CONFIG flag indicates whether to apply prefixing
        if settings.CACHES[alias].get("IS_USING_DEFAULT_CONFIG", True):
            # Get current tenant from context
            tenant = TenantContext.get_tenant()

            # Only apply prefix if tenant is set
            if tenant is not None:
                # Prepend tenant_id with colon separator
                return f"{tenant.tenant_id}:{key}"

        # Return key unchanged (no prefix applied)
        return key

    # --- Dict-style access ---
    def __getitem__(self, key):
        """
        Support dict-style get: value = cache[key]

        Internally calls get() method on underlying cache with prefixed key.

        Args:
            key (str): Cache key to retrieve

        Returns:
            object: Cached value or None if key not found
        """
        # Apply tenant prefix and get from cache
        return self._get_cache().get(self._apply_prefix(key))

    def __setitem__(self, key, value):
        """
        Support dict-style set: cache[key] = value

        Internally calls set() method on underlying cache with prefixed key.

        Args:
            key (str): Cache key to set
            value (object): Value to cache
        """
        # Apply tenant prefix and set in cache
        self._get_cache().set(self._apply_prefix(key), value)

    def __delitem__(self, key):
        """
        Support dict-style delete: del cache[key]

        Internally calls delete() method on underlying cache with prefixed key.

        Args:
            key (str): Cache key to delete

        Raises:
            KeyError: If key doesn't exist in cache
        """
        # Apply tenant prefix and delete from cache
        deleted = self._get_cache().delete(self._apply_prefix(key))

        # Raise KeyError if deletion failed
        if not deleted:
            raise KeyError(key)

    def __contains__(self, key):
        """
        Support dict-style contains: key in cache

        Checks if key exists in cache by attempting to retrieve it.

        Args:
            key (str): Cache key to check

        Returns:
            bool: True if key exists and has value, False otherwise
        """
        # Apply tenant prefix and check if key exists
        # Key exists if get() returns non-None value
        return self._get_cache().get(self._apply_prefix(key)) is not None

    # --- Attribute access (methods like get, set, delete, etc.) ---
    def __getattr__(self, name):
        """
        Support method-style access: cache.get(key), cache.set(key, value)

        Intercepts method calls and wraps them to apply tenant prefixing to keys.

        Method Interception:
            1. Get requested method from underlying cache
            2. If callable, create wrapper function
            3. Wrapper applies prefix to first argument (key)
            4. Wrapper delegates to original method
            5. Return wrapped method

        Supported Single-Key Methods (prefix applied):
            - get, set, add, delete
            - has_key, incr, decr, touch
            - get_or_set

            Pattern: All methods where first positional arg is cache key

        Supported Pattern Methods (prefix applied):
            - keys: Pattern-based key search

        Non-Key Methods (no prefix applied):
            - clear, close, etc.

        Args:
            name (str): Name of method being accessed

        Returns:
            callable or object: Wrapped method or delegated attribute

        Examples:
            ```python
            cache.get('user_id')           # Calls wrapper, applies prefix
            cache.set('config', {...})     # Calls wrapper, applies prefix
            cache.keys('user_*')           # Calls wrapper, applies prefix to pattern
            cache.clear()                  # Direct delegation, no prefix
            ```
        """
        # Get the underlying cache backend
        cache = self._get_cache()

        # Get the requested attribute from cache
        attr = getattr(cache, name)

        # If attribute is callable (method), wrap it
        if callable(attr):

            def wrapper(*args, **kwargs):
                # If no arguments, call method as-is
                if not args:
                    return attr(*args, **kwargs)

                # Single-key methods: apply prefix to first argument (the key)
                # These methods take a key as first positional argument
                if name in {
                    "get",
                    "set",
                    "add",
                    "delete",
                    "has_key",
                    "incr",
                    "decr",
                    "touch",
                    "get_or_set",
                }:
                    # Apply prefix to first argument (key)
                    # Keep other arguments unchanged
                    args = (self._apply_prefix(args[0]), *args[1:])

                # Pattern-based methods: apply prefix to pattern argument
                # keys() method takes pattern as first argument
                elif name == "keys":
                    # Apply prefix to first argument (pattern)
                    # Pattern matching still works with prefixed keys
                    args = (self._apply_prefix(args[0]), *args[1:])

                # Call original method with prefixed arguments
                return attr(*args, **kwargs)

            return wrapper

        # For non-callable attributes, return directly
        return attr

    # --- Django expects this ---
    def close_all(self):
        """
        Close all cache connections.

        Django cache framework calls this to clean up resources.
        This implementation iterates through all cache aliases and
        calls close() on each backend that supports it.

        Implementation:
            1. Get internal cache storage (_caches)
            2. Iterate through each alias
            3. Get cache backend for each alias
            4. Call close() if backend supports it
            5. Clean up connections

        Thread Safety:
            Safe to call from any thread. Each cache backend
            handles its own close() implementation.
        """
        # Iterate through all configured cache aliases
        for alias in getattr(self._handler, "_caches", {}):
            # Get cache backend for this alias
            backend = self._handler[alias]

            # Call close() if backend supports it
            if hasattr(backend, "close"):
                backend.close()

__init__(handler)

Initialize wrapper with Django's CacheHandler.

Parameters:

Name Type Description Default

handler

CacheHandler

The original Django cache handler to be wrapped for tenant awareness

required

Stores handler for delegation of actual cache operations.

Source code in django_omnitenant/patches/cache.py
199
200
201
202
203
204
205
206
207
208
209
def __init__(self, handler: DjangoCacheHandler):
    """
    Initialize wrapper with Django's CacheHandler.

    Args:
        handler (DjangoCacheHandler): The original Django cache handler
                                    to be wrapped for tenant awareness

    Stores handler for delegation of actual cache operations.
    """
    self._handler = handler

__getitem__(key)

Support dict-style get: value = cache[key]

Internally calls get() method on underlying cache with prefixed key.

Parameters:

Name Type Description Default

key

str

Cache key to retrieve

required

Returns:

Name Type Description
object

Cached value or None if key not found

Source code in django_omnitenant/patches/cache.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def __getitem__(self, key):
    """
    Support dict-style get: value = cache[key]

    Internally calls get() method on underlying cache with prefixed key.

    Args:
        key (str): Cache key to retrieve

    Returns:
        object: Cached value or None if key not found
    """
    # Apply tenant prefix and get from cache
    return self._get_cache().get(self._apply_prefix(key))

__setitem__(key, value)

Support dict-style set: cache[key] = value

Internally calls set() method on underlying cache with prefixed key.

Parameters:

Name Type Description Default

key

str

Cache key to set

required

value

object

Value to cache

required
Source code in django_omnitenant/patches/cache.py
343
344
345
346
347
348
349
350
351
352
353
354
def __setitem__(self, key, value):
    """
    Support dict-style set: cache[key] = value

    Internally calls set() method on underlying cache with prefixed key.

    Args:
        key (str): Cache key to set
        value (object): Value to cache
    """
    # Apply tenant prefix and set in cache
    self._get_cache().set(self._apply_prefix(key), value)

__delitem__(key)

Support dict-style delete: del cache[key]

Internally calls delete() method on underlying cache with prefixed key.

Parameters:

Name Type Description Default

key

str

Cache key to delete

required

Raises:

Type Description
KeyError

If key doesn't exist in cache

Source code in django_omnitenant/patches/cache.py
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
def __delitem__(self, key):
    """
    Support dict-style delete: del cache[key]

    Internally calls delete() method on underlying cache with prefixed key.

    Args:
        key (str): Cache key to delete

    Raises:
        KeyError: If key doesn't exist in cache
    """
    # Apply tenant prefix and delete from cache
    deleted = self._get_cache().delete(self._apply_prefix(key))

    # Raise KeyError if deletion failed
    if not deleted:
        raise KeyError(key)

__contains__(key)

Support dict-style contains: key in cache

Checks if key exists in cache by attempting to retrieve it.

Parameters:

Name Type Description Default

key

str

Cache key to check

required

Returns:

Name Type Description
bool

True if key exists and has value, False otherwise

Source code in django_omnitenant/patches/cache.py
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
def __contains__(self, key):
    """
    Support dict-style contains: key in cache

    Checks if key exists in cache by attempting to retrieve it.

    Args:
        key (str): Cache key to check

    Returns:
        bool: True if key exists and has value, False otherwise
    """
    # Apply tenant prefix and check if key exists
    # Key exists if get() returns non-None value
    return self._get_cache().get(self._apply_prefix(key)) is not None

__getattr__(name)

Support method-style access: cache.get(key), cache.set(key, value)

Intercepts method calls and wraps them to apply tenant prefixing to keys.

Method Interception
  1. Get requested method from underlying cache
  2. If callable, create wrapper function
  3. Wrapper applies prefix to first argument (key)
  4. Wrapper delegates to original method
  5. Return wrapped method

Supported Single-Key Methods (prefix applied): - get, set, add, delete - has_key, incr, decr, touch - get_or_set

Pattern: All methods where first positional arg is cache key

Supported Pattern Methods (prefix applied): - keys: Pattern-based key search

Non-Key Methods (no prefix applied): - clear, close, etc.

Parameters:

Name Type Description Default

name

str

Name of method being accessed

required

Returns:

Type Description

callable or object: Wrapped method or delegated attribute

Examples:

cache.get('user_id')           # Calls wrapper, applies prefix
cache.set('config', {...})     # Calls wrapper, applies prefix
cache.keys('user_*')           # Calls wrapper, applies prefix to pattern
cache.clear()                  # Direct delegation, no prefix
Source code in django_omnitenant/patches/cache.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
def __getattr__(self, name):
    """
    Support method-style access: cache.get(key), cache.set(key, value)

    Intercepts method calls and wraps them to apply tenant prefixing to keys.

    Method Interception:
        1. Get requested method from underlying cache
        2. If callable, create wrapper function
        3. Wrapper applies prefix to first argument (key)
        4. Wrapper delegates to original method
        5. Return wrapped method

    Supported Single-Key Methods (prefix applied):
        - get, set, add, delete
        - has_key, incr, decr, touch
        - get_or_set

        Pattern: All methods where first positional arg is cache key

    Supported Pattern Methods (prefix applied):
        - keys: Pattern-based key search

    Non-Key Methods (no prefix applied):
        - clear, close, etc.

    Args:
        name (str): Name of method being accessed

    Returns:
        callable or object: Wrapped method or delegated attribute

    Examples:
        ```python
        cache.get('user_id')           # Calls wrapper, applies prefix
        cache.set('config', {...})     # Calls wrapper, applies prefix
        cache.keys('user_*')           # Calls wrapper, applies prefix to pattern
        cache.clear()                  # Direct delegation, no prefix
        ```
    """
    # Get the underlying cache backend
    cache = self._get_cache()

    # Get the requested attribute from cache
    attr = getattr(cache, name)

    # If attribute is callable (method), wrap it
    if callable(attr):

        def wrapper(*args, **kwargs):
            # If no arguments, call method as-is
            if not args:
                return attr(*args, **kwargs)

            # Single-key methods: apply prefix to first argument (the key)
            # These methods take a key as first positional argument
            if name in {
                "get",
                "set",
                "add",
                "delete",
                "has_key",
                "incr",
                "decr",
                "touch",
                "get_or_set",
            }:
                # Apply prefix to first argument (key)
                # Keep other arguments unchanged
                args = (self._apply_prefix(args[0]), *args[1:])

            # Pattern-based methods: apply prefix to pattern argument
            # keys() method takes pattern as first argument
            elif name == "keys":
                # Apply prefix to first argument (pattern)
                # Pattern matching still works with prefixed keys
                args = (self._apply_prefix(args[0]), *args[1:])

            # Call original method with prefixed arguments
            return attr(*args, **kwargs)

        return wrapper

    # For non-callable attributes, return directly
    return attr

close_all()

Close all cache connections.

Django cache framework calls this to clean up resources. This implementation iterates through all cache aliases and calls close() on each backend that supports it.

Implementation
  1. Get internal cache storage (_caches)
  2. Iterate through each alias
  3. Get cache backend for each alias
  4. Call close() if backend supports it
  5. Clean up connections
Thread Safety

Safe to call from any thread. Each cache backend handles its own close() implementation.

Source code in django_omnitenant/patches/cache.py
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
def close_all(self):
    """
    Close all cache connections.

    Django cache framework calls this to clean up resources.
    This implementation iterates through all cache aliases and
    calls close() on each backend that supports it.

    Implementation:
        1. Get internal cache storage (_caches)
        2. Iterate through each alias
        3. Get cache backend for each alias
        4. Call close() if backend supports it
        5. Clean up connections

    Thread Safety:
        Safe to call from any thread. Each cache backend
        handles its own close() implementation.
    """
    # Iterate through all configured cache aliases
    for alias in getattr(self._handler, "_caches", {}):
        # Get cache backend for this alias
        backend = self._handler[alias]

        # Call close() if backend supports it
        if hasattr(backend, "close"):
            backend.close()

patch_django_cache()

Patch Django cache framework to use tenant-aware caching.

This function replaces Django's cache references with our tenant-aware wrapper, ensuring all cache operations are tenant-isolated.

Patching Strategy
  1. Create original Django CacheHandler
  2. Wrap it with TenantAwareCacheWrapper
  3. Replace module-level references
  4. Update lazy connection proxies
  5. All cache operations now go through wrapper
Module-Level Replacements
  • django.core.cache.cache: Single cache instance
  • django.core.cache.caches: Multi-cache manager
Lazy Proxy Updates

Django uses connection proxies for lazy cache initialization. Updates _connections dict to ensure all paths use wrapper.

Automatic Application

This function is called at module import time: - No manual setup required - Patches applied before application code runs - Django cache API works transparently

Side Effects

After this patch: - All cache.get() calls automatically prefix keys - All cache.set() calls automatically prefix keys - All cache operations are tenant-isolated - No code changes needed in application

Example
# Application code (automatically uses patched cache):
from django.core.cache import cache

cache.set('config', {...})  # Automatically prefixed
data = cache.get('config')   # Automatically uses prefix
Source code in django_omnitenant/patches/cache.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
def patch_django_cache():
    """
    Patch Django cache framework to use tenant-aware caching.

    This function replaces Django's cache references with our tenant-aware
    wrapper, ensuring all cache operations are tenant-isolated.

    Patching Strategy:
        1. Create original Django CacheHandler
        2. Wrap it with TenantAwareCacheWrapper
        3. Replace module-level references
        4. Update lazy connection proxies
        5. All cache operations now go through wrapper

    Module-Level Replacements:
        - django.core.cache.cache: Single cache instance
        - django.core.cache.caches: Multi-cache manager

    Lazy Proxy Updates:
        Django uses connection proxies for lazy cache initialization.
        Updates _connections dict to ensure all paths use wrapper.

    Automatic Application:
        This function is called at module import time:
        - No manual setup required
        - Patches applied before application code runs
        - Django cache API works transparently

    Side Effects:
        After this patch:
        - All cache.get() calls automatically prefix keys
        - All cache.set() calls automatically prefix keys
        - All cache operations are tenant-isolated
        - No code changes needed in application

    Example:
        ```python
        # Application code (automatically uses patched cache):
        from django.core.cache import cache

        cache.set('config', {...})  # Automatically prefixed
        data = cache.get('config')   # Automatically uses prefix
        ```
    """
    # Create original Django cache handler
    # This is the unpatched handler that does actual caching
    original_handler = DjangoCacheHandler()

    # Wrap handler with tenant-aware wrapper
    # All operations now go through wrapper for prefix application
    tenant_aware_wrapper = TenantAwareCacheWrapper(original_handler)

    # Replace module-level cache references
    # Both cache (single instance) and caches (multi-instance)
    django.core.cache.caches = tenant_aware_wrapper
    django.core.cache.cache = tenant_aware_wrapper

    # Update lazy connection proxies
    # Django uses proxies for deferred cache initialization
    # Update _connections to ensure all paths use wrapper
    if hasattr(django.core.cache, "_connections"):
        for name in ("cache", "caches"):
            if name in django.core.cache._connections:
                # Replace proxy target with wrapper
                django.core.cache._connections[name] = tenant_aware_wrapper