@@ -14,16 +14,361 @@ metadata:
1414
1515# Frappe Cache & Locking
1616
17- > ** STATUS** : Stub - content pending V2.5 development phase.
18-
1917## Quick Reference
2018
21- _ Content to be developed._
19+ | Action | Method | Notes |
20+ | --------| --------| -------|
21+ | Set value | ` frappe.cache.set_value(key, val) ` | With optional TTL |
22+ | Get value | ` frappe.cache.get_value(key) ` | Returns ` None ` if missing |
23+ | Get or generate | ` frappe.cache.get_value(key, generator=fn) ` | Calls ` fn() ` on cache miss |
24+ | Delete value | ` frappe.cache.delete_value(key) ` | Single key or list of keys |
25+ | Delete by pattern | ` frappe.cache.delete_keys(pattern) ` | Wildcard ` * ` matching |
26+ | Hash set | ` frappe.cache.hset(name, key, val) ` | Redis hash field |
27+ | Hash get | ` frappe.cache.hget(name, key) ` | Single hash field |
28+ | Hash get all | ` frappe.cache.hgetall(name) ` | Full hash as dict |
29+ | Hash delete | ` frappe.cache.hdel(name, key) ` | Remove hash field |
30+ | Hash exists | ` frappe.cache.hexists(name, key) ` | Returns bool |
31+ | Cached document | ` frappe.get_cached_doc(dt, dn) ` | Full doc from cache |
32+ | Clear doc cache | ` frappe.clear_document_cache(dt, dn) ` | Invalidate cached doc |
33+ | Decorator cache | ` @redis_cache ` | Auto-cache function result |
34+ | Request cache | ` frappe.local.cache ` | Per-request dict (not Redis) |
35+
36+ ---
2237
2338## Decision Tree
2439
25- _ Content to be developed._
40+ ```
41+ What caching pattern do you need?
42+ │
43+ ├─ Cache a function result automatically?
44+ │ ├─ Pure function (same args → same result) → @redis_cache
45+ │ └─ Need custom key/TTL → manual get_value/set_value
46+ │
47+ ├─ Cache a document?
48+ │ ├─ Read-only access → frappe.get_cached_doc()
49+ │ └─ Need to invalidate → frappe.clear_document_cache()
50+ │
51+ ├─ Cache structured data (multiple fields)?
52+ │ └─ Redis hash → hset/hget/hgetall
53+ │
54+ ├─ Per-request cache (avoid repeated DB calls in one request)?
55+ │ └─ frappe.local.cache dict
56+ │
57+ ├─ Prevent concurrent execution?
58+ │ └─ Distributed lock → frappe.lock("resource_name")
59+ │
60+ └─ Invalidate cache?
61+ ├─ Single key → delete_value(key)
62+ ├─ Pattern → delete_keys("prefix*")
63+ └─ All site cache → frappe.clear_cache()
64+ ```
65+
66+ ---
67+
68+ ## String Operations
69+
70+ ### Set and Get
71+
72+ ``` python
73+ # Set a value (persists until evicted or deleted)
74+ frappe.cache.set_value(" exchange_rate_USD" , 1.08 )
75+
76+ # Set with TTL (expires after N seconds)
77+ frappe.cache.set_value(" exchange_rate_USD" , 1.08 , expires_in_sec = 3600 )
78+
79+ # Get value (returns None if missing)
80+ rate = frappe.cache.get_value(" exchange_rate_USD" )
81+
82+ # Get with generator (calls function on cache miss, stores result)
83+ rate = frappe.cache.get_value(
84+ " exchange_rate_USD" ,
85+ generator = lambda : fetch_exchange_rate(" USD" ),
86+ )
87+ ```
88+
89+ ### User-Scoped Values
90+
91+ ``` python
92+ # Store per-user preference
93+ frappe.cache.set_value(" dashboard_layout" , " compact" , user = " user@example.com" )
94+
95+ # Retrieve for specific user
96+ layout = frappe.cache.get_value(" dashboard_layout" , user = " user@example.com" )
97+ ```
98+
99+ ### Delete
100+
101+ ``` python
102+ # Single key
103+ frappe.cache.delete_value(" exchange_rate_USD" )
104+
105+ # Multiple keys
106+ frappe.cache.delete_value([" exchange_rate_USD" , " exchange_rate_EUR" ])
107+
108+ # Pattern-based deletion (wildcard)
109+ frappe.cache.delete_keys(" exchange_rate*" )
110+ ```
111+
112+ ---
113+
114+ ## Hash Operations
115+
116+ Use hashes to group related fields under a single key.
117+
118+ ``` python
119+ # Set hash fields
120+ frappe.cache.hset(" config|notifications" , " email_enabled" , True )
121+ frappe.cache.hset(" config|notifications" , " sms_enabled" , False )
122+ frappe.cache.hset(" config|notifications" , " max_retries" , 3 )
123+
124+ # Get single field
125+ email_on = frappe.cache.hget(" config|notifications" , " email_enabled" )
126+
127+ # Get all fields as dict
128+ config = frappe.cache.hgetall(" config|notifications" )
129+ # {"email_enabled": True, "sms_enabled": False, "max_retries": 3}
130+
131+ # Delete field
132+ frappe.cache.hdel(" config|notifications" , " sms_enabled" )
133+
134+ # Check existence
135+ exists = frappe.cache.hexists(" config|notifications" , " email_enabled" )
136+ ```
137+
138+ ### Hash with Generator
139+
140+ ``` python
141+ # hget with generator — calls function on miss
142+ value = frappe.cache.hget(
143+ " user|permissions" ,
144+ " user@example.com" ,
145+ generator = lambda : compute_permissions(" user@example.com" ),
146+ )
147+ ```
148+
149+ ---
150+
151+ ## @redis_cache Decorator
152+
153+ Automatically cache function return values based on arguments.
154+
155+ ``` python
156+ from frappe.utils.caching import redis_cache
157+
158+ @redis_cache
159+ def get_item_price (item_code , price_list ):
160+ """ Expensive query — cached automatically."""
161+ return frappe.db.get_value(" Item Price" ,
162+ {" item_code" : item_code, " price_list" : price_list},
163+ " price_list_rate" ,
164+ )
165+
166+ # First call — hits database, stores in Redis
167+ price = get_item_price(" ITEM-001" , " Standard Selling" )
168+
169+ # Second call — returns from cache
170+ price = get_item_price(" ITEM-001" , " Standard Selling" )
171+
172+ # Clear all cached results for this function
173+ get_item_price.clear_cache()
174+ ```
175+
176+ ### With TTL
177+
178+ ``` python
179+ @redis_cache (ttl = 300 ) # expires after 5 minutes
180+ def get_exchange_rate (from_currency , to_currency ):
181+ return fetch_rate_from_api(from_currency, to_currency)
182+ ```
183+
184+ ** Rules for @redis_cache:**
185+ - ALWAYS ensure arguments are hashable (strings, numbers, tuples). NEVER pass dicts or lists as arguments.
186+ - ALWAYS call ` .clear_cache() ` when underlying data changes.
187+ - NEVER use on functions with side effects — the function will NOT execute on cache hits.
188+
189+ ---
190+
191+ ## frappe.local.cache — Request-Scoped Cache
192+
193+ ` frappe.local.cache ` is a plain Python dict that lives for the duration of a single HTTP request. It is NOT stored in Redis.
194+
195+ ``` python
196+ def get_user_settings ():
197+ """ Avoid repeated DB calls within a single request."""
198+ if " user_settings" not in frappe.local.cache:
199+ frappe.local.cache[" user_settings" ] = frappe.get_doc(
200+ " User Settings" , frappe.session.user
201+ )
202+ return frappe.local.cache[" user_settings" ]
203+ ```
204+
205+ Use ` frappe.local.cache ` when:
206+ - The same data is needed multiple times in one request
207+ - The data does NOT need to persist across requests
208+ - You want zero Redis overhead
209+
210+ ---
211+
212+ ## Document Caching
213+
214+ ``` python
215+ # Get cached document (read-only, no permission check)
216+ settings = frappe.get_cached_doc(" System Settings" )
217+ item = frappe.get_cached_doc(" Item" , " ITEM-001" )
218+
219+ # Invalidate when document changes
220+ frappe.clear_document_cache(" Item" , " ITEM-001" )
221+
222+ # Cached single value
223+ val = frappe.db.get_value(" Item" , " ITEM-001" , " item_name" , cache = True )
224+ ```
225+
226+ NEVER modify a document returned by ` frappe.get_cached_doc() ` — it returns a shared reference. Modifications corrupt the cache for all subsequent reads.
227+
228+ ---
229+
230+ ## Distributed Locking
231+
232+ Prevent concurrent execution of critical sections using Redis-based locks.
233+
234+ ``` python
235+ # Context manager (recommended)
236+ with frappe.lock(" process_payroll" ):
237+ # Only one worker executes this block at a time
238+ process_all_salary_slips()
239+ # Lock auto-released on exit
240+
241+ # Manual lock/unlock
242+ frappe.lock(" inventory_sync" )
243+ try :
244+ sync_inventory()
245+ finally :
246+ frappe.unlock(" inventory_sync" ) # ALWAYS unlock in finally
247+ ```
248+
249+ ** Rules:**
250+ - ALWAYS use ` with frappe.lock() ` (context manager) to guarantee release.
251+ - NEVER hold locks for more than a few seconds — long locks cause worker starvation.
252+ - ALWAYS use descriptive lock names to avoid collisions.
253+
254+ ---
255+
256+ ## Cache Invalidation Patterns
257+
258+ ### Pattern 1: TTL-Based (Time-to-Live)
259+
260+ ``` python
261+ frappe.cache.set_value(" dashboard_stats" , compute_stats(), expires_in_sec = 300 )
262+ ```
263+
264+ Best for: Data that can be slightly stale (exchange rates, dashboard aggregates).
265+
266+ ### Pattern 2: Event-Based Invalidation
267+
268+ ``` python
269+ # In hooks.py
270+ doc_events = {
271+ " Item Price" : {
272+ " on_update" : " my_app.cache.invalidate_price_cache" ,
273+ " on_trash" : " my_app.cache.invalidate_price_cache" ,
274+ }
275+ }
276+
277+ # In my_app/cache.py
278+ def invalidate_price_cache (doc , method ):
279+ frappe.cache.delete_keys(" item_price*" )
280+ # Or clear specific function cache:
281+ # get_item_price.clear_cache()
282+ ```
283+
284+ Best for: Data that MUST be fresh immediately after changes.
285+
286+ ### Pattern 3: Hybrid (TTL + Event)
287+
288+ ``` python
289+ @redis_cache (ttl = 600 )
290+ def get_pricing_rules ():
291+ return frappe.get_all(" Pricing Rule" , fields = [" *" ])
292+
293+ # Event hook clears cache immediately on change
294+ def on_pricing_rule_update (doc , method ):
295+ get_pricing_rules.clear_cache()
296+ ```
297+
298+ Best for: Frequently read data with occasional updates.
299+
300+ ---
301+
302+ ## Common Cache Keys (Internal)
303+
304+ | Key Pattern | Content |
305+ | -------------| ---------|
306+ | ` doctype::meta::{dt} ` | DocType metadata |
307+ | ` user_permissions::{user} ` | User permission cache |
308+ | ` bootinfo::{user} ` | User boot info |
309+ | ` notifications::{user} ` | Notification counts |
310+ | ` document_cache::{dt}::{dn} ` | Cached document |
311+
312+ NEVER write to internal cache keys directly. ALWAYS use the documented API methods (` get_cached_doc ` , ` clear_document_cache ` , etc.).
313+
314+ ---
315+
316+ ## Performance Guidelines
317+
318+ 1 . ** ALWAYS set TTL** on cached values that derive from external data — without TTL, stale data persists until manual invalidation or Redis eviction.
319+ 2 . ** NEVER cache large objects** (>1 MB) — Redis uses pickle serialization, and large values increase serialization overhead and memory usage.
320+ 3 . ** ALWAYS use ` frappe.local.cache ` ** for data needed multiple times within a single request — it avoids Redis round-trips entirely.
321+ 4 . ** NEVER use ` frappe.clear_cache() ` ** as a routine invalidation strategy — it clears ALL cache keys for the site, causing a cold-cache performance hit.
322+ 5 . ** ALWAYS prefix custom cache keys** with your app name (e.g., ` myapp|exchange_rate ` ) to avoid collisions with Frappe internals.
323+
324+ ---
325+
326+ ## Redis Configuration
327+
328+ Default config: ` {bench}/config/redis_cache.conf `
329+
330+ | Setting | Default | Description |
331+ | ---------| ---------| -------------|
332+ | Port | 13000 | Redis cache port |
333+ | Bind | 127.0.0.1 | Listen address |
334+ | maxmemory-policy | allkeys-lru | Eviction policy |
335+ | maxmemory | 256mb | Max memory (adjustable) |
336+
337+ ---
338+
339+ ## Key Namespacing
340+
341+ All cache keys are automatically prefixed by Frappe with the site name:
342+
343+ ``` python
344+ # You write:
345+ frappe.cache.set_value(" my_key" , " value" )
346+
347+ # Redis stores:
348+ # "mysite.localhost|my_key"
349+ ```
350+
351+ ` frappe.cache.make_key(key, user, shared) ` handles prefixing. The ` shared=True ` parameter removes the site prefix for cross-site keys (rare use case).
352+
353+ ---
354+
355+ ## Version Differences
356+
357+ | Feature | v14 | v15 | v16 |
358+ | ---------| -----| -----| -----|
359+ | ` frappe.cache.set_value ` | Available | Available | Available |
360+ | ` @redis_cache ` | Not available | Available | Available |
361+ | ` @redis_cache(ttl=) ` | Not available | Available | Available |
362+ | ` frappe.lock ` context mgr | Available | Available | Available |
363+ | ` frappe.local.cache ` | Available | Available | Available |
364+ | ` hget ` with generator | Available | Available | Available |
365+
366+ ---
26367
27368## See Also
28369
29- _ Cross-references to be added._
370+ - [ references/examples.md] ( references/examples.md ) — Cache implementation patterns
371+ - [ references/anti-patterns.md] ( references/anti-patterns.md ) — Common cache mistakes
372+ - [ references/api-reference.md] ( references/api-reference.md ) — Complete API signatures
373+ - ` frappe-core-database ` — Database queries that benefit from caching
374+ - ` frappe-core-permissions ` — User permission caching
0 commit comments