Fix bin/publish: use correct .env path for rspade_system Fix bin/publish script: prevent grep exit code 1 from terminating script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
348 lines
10 KiB
Plaintext
Executable File
348 lines
10 KiB
Plaintext
Executable File
CACHING IN RSPADE
|
|
==================
|
|
|
|
RSpade provides two caching strategies for different use cases:
|
|
|
|
1. **Request-scoped caching** - RsxCache::once()
|
|
2. **Build-scoped caching** - RsxCache::remember()
|
|
|
|
Both use closures for cache-or-generate patterns, providing clean syntax
|
|
for wrapping expensive operations.
|
|
|
|
|
|
REQUEST-SCOPED CACHING (RsxCache::once)
|
|
========================================
|
|
|
|
Simplest caching strategy - stores values in static property for request duration.
|
|
|
|
WHEN TO USE:
|
|
- Expensive calculations called multiple times per request
|
|
- Data that doesn't need persistence across requests
|
|
- No Redis overhead needed
|
|
- No locking needed (single process, single request)
|
|
|
|
CHARACTERISTICS:
|
|
- Lives in PHP memory (static array)
|
|
- No serialization overhead
|
|
- Zero lock contention
|
|
- Cleared automatically at end of request
|
|
- No expiration (request lifetime only)
|
|
|
|
SYNTAX:
|
|
$data = RsxCache::once($key, function() {
|
|
return expensive_calculation();
|
|
});
|
|
|
|
EXAMPLES:
|
|
|
|
// Avoid re-calculating same value multiple times in request
|
|
function get_user_permissions($user_id) {
|
|
return RsxCache::once("user_permissions:{$user_id}", function() use ($user_id) {
|
|
// This expensive query only runs once per request
|
|
return User_Model::find($user_id)
|
|
->permissions()
|
|
->with('roles')
|
|
->get();
|
|
});
|
|
}
|
|
|
|
// Called multiple times, but calculation only happens once
|
|
$perms1 = get_user_permissions(123); // Runs query
|
|
$perms2 = get_user_permissions(123); // Returns cached
|
|
$perms3 = get_user_permissions(123); // Returns cached
|
|
|
|
// Safe to call from different parts of codebase
|
|
class Dashboard_Controller {
|
|
public static function index(Request $request, array $params = []) {
|
|
$perms = get_user_permissions($user_id); // May use cache
|
|
// ...
|
|
}
|
|
}
|
|
|
|
class Sidebar_Component extends Jqhtml_Component {
|
|
public function on_load() {
|
|
$perms = get_user_permissions($this->args->user_id); // May use cache
|
|
}
|
|
}
|
|
|
|
|
|
BUILD-SCOPED CACHING (RsxCache::remember)
|
|
==========================================
|
|
|
|
Redis-based caching with advisory locking and automatic build key prefixing.
|
|
|
|
WHEN TO USE:
|
|
- Expensive operations that don't change often
|
|
- Data shared across multiple requests/processes
|
|
- Need to prevent cache stampede
|
|
- Want automatic invalidation on code deployment
|
|
|
|
CHARACTERISTICS:
|
|
- Stored in Redis with build key prefix
|
|
- Survives across requests until manifest rebuild
|
|
- Advisory write lock prevents stampede
|
|
- Automatic double-check after lock acquisition
|
|
- Optional expiration time
|
|
- Serialization/deserialization automatic
|
|
|
|
SYNTAX:
|
|
// Never expires (default)
|
|
$data = RsxCache::remember($key, function() {
|
|
return expensive_operation();
|
|
});
|
|
|
|
// Expires after specified seconds
|
|
$data = RsxCache::remember($key, function() {
|
|
return expensive_operation();
|
|
}, $seconds);
|
|
|
|
LOCKING BEHAVIOR:
|
|
|
|
When cache miss occurs:
|
|
1. Fast check (no lock) - returns if cached
|
|
2. Acquire write lock on cache key
|
|
3. Check again (another process may have built it)
|
|
4. Execute callback if still missing
|
|
5. Store result in Redis
|
|
6. Release lock
|
|
|
|
This prevents stampede: if 100 requests hit at once, only one builds cache
|
|
while others wait for lock, then use the built cache.
|
|
|
|
EXAMPLES:
|
|
|
|
// Cache expensive API response forever (until deployment)
|
|
function get_product_catalog() {
|
|
return RsxCache::remember('product_catalog', function() {
|
|
return External_Api::fetch_all_products();
|
|
});
|
|
}
|
|
|
|
// Cache with 1 hour expiration
|
|
function get_trending_products() {
|
|
return RsxCache::remember('trending_products', function() {
|
|
return Product_Model::where('views', '>', 1000)
|
|
->orderBy('views', 'desc')
|
|
->limit(10)
|
|
->get();
|
|
}, RsxCache::HOUR);
|
|
}
|
|
|
|
// Complex calculation cached per user
|
|
function get_user_dashboard_data($user_id) {
|
|
return RsxCache::remember("dashboard_data:{$user_id}", function() use ($user_id) {
|
|
return [
|
|
'stats' => User_Stats::calculate($user_id),
|
|
'recent_activity' => Activity::for_user($user_id)->limit(20)->get(),
|
|
'recommendations' => Recommendation_Engine::generate($user_id),
|
|
];
|
|
}, RsxCache::DAY);
|
|
}
|
|
|
|
// Expensive join query
|
|
function get_all_users_with_permissions() {
|
|
return RsxCache::remember('users_with_permissions', function() {
|
|
// This might take 500ms, but only runs once per deployment
|
|
return DB::select("
|
|
SELECT u.*, GROUP_CONCAT(p.name) as permissions
|
|
FROM users u
|
|
LEFT JOIN user_permissions up ON u.id = up.user_id
|
|
LEFT JOIN permissions p ON up.permission_id = p.id
|
|
GROUP BY u.id
|
|
");
|
|
});
|
|
}
|
|
|
|
|
|
EXPIRATION CONSTANTS
|
|
====================
|
|
|
|
RsxCache provides constants for common expiration times:
|
|
|
|
RsxCache::NO_EXPIRATION // 0 - never expire (default)
|
|
RsxCache::HOUR // 3600 seconds
|
|
RsxCache::DAY // 86400 seconds
|
|
RsxCache::WEEK // 604800 seconds
|
|
|
|
EXAMPLES:
|
|
|
|
RsxCache::remember($key, $callback); // Never expires
|
|
RsxCache::remember($key, $callback, null); // Never expires
|
|
RsxCache::remember($key, $callback, RsxCache::HOUR); // 1 hour
|
|
RsxCache::remember($key, $callback, RsxCache::DAY); // 1 day
|
|
RsxCache::remember($key, $callback, 300); // 5 minutes
|
|
RsxCache::remember($key, $callback, 86400 * 30); // 30 days
|
|
|
|
|
|
CACHE KEY NAMING
|
|
================
|
|
|
|
Use descriptive keys with context:
|
|
|
|
GOOD:
|
|
user:123:permissions
|
|
product_catalog:category_5
|
|
dashboard_stats:2024-10
|
|
api_response:github_user:hansonw
|
|
|
|
BAD:
|
|
permissions
|
|
cache1
|
|
temp
|
|
data
|
|
|
|
|
|
CACHE INVALIDATION
|
|
==================
|
|
|
|
BUILD-SCOPED CACHE:
|
|
- Automatically cleared on manifest rebuild (code deployment)
|
|
- Build key prefix ensures old cache invisible after deployment
|
|
- Manual clear: RsxCache::delete($key)
|
|
- Clear all: RsxCache::clear()
|
|
|
|
REQUEST-SCOPED CACHE:
|
|
- Automatically cleared at end of request
|
|
- No manual clearing needed/possible
|
|
|
|
|
|
WHEN TO USE WHICH
|
|
=================
|
|
|
|
USE RsxCache::once() FOR:
|
|
✓ Expensive calculations called multiple times in one request
|
|
✓ Avoiding duplicate work within single request
|
|
✓ Simple in-memory caching
|
|
✓ Getter functions that might be called from multiple places
|
|
✓ Component data loading that might trigger multiple times
|
|
|
|
USE RsxCache::remember() FOR:
|
|
✓ Database queries that don't change often
|
|
✓ External API calls
|
|
✓ Expensive computations shared across requests
|
|
✓ Product catalogs, configuration data
|
|
✓ Analytics/reporting data
|
|
✓ Anything where cache stampede is a concern
|
|
|
|
|
|
ANTI-PATTERNS
|
|
=============
|
|
|
|
DON'T cache user-specific sensitive data without user ID in key:
|
|
❌ RsxCache::remember('user_data', fn() => get_current_user_data());
|
|
✅ RsxCache::remember("user_data:{$user_id}", fn() => get_user_data($user_id));
|
|
|
|
DON'T use request-scoped cache for data that should persist:
|
|
❌ RsxCache::once('product_catalog', fn() => fetch_all_products());
|
|
✅ RsxCache::remember('product_catalog', fn() => fetch_all_products());
|
|
|
|
DON'T use short expiration when you want build-scoped:
|
|
❌ RsxCache::remember($key, $callback, 60); // Why expire if it's build data?
|
|
✅ RsxCache::remember($key, $callback); // Let manifest rebuild clear it
|
|
|
|
DON'T wrap fast operations:
|
|
❌ RsxCache::once('user_id', fn() => RsxAuth::id());
|
|
✅ $user_id = RsxAuth::id(); // Already fast, no caching needed
|
|
|
|
DON'T use external side effects in callback:
|
|
❌ RsxCache::remember($key, function() {
|
|
Log::info('Building cache'); // Don't do this
|
|
send_email_notification(); // Definitely don't do this
|
|
return calculate_data();
|
|
});
|
|
|
|
|
|
ADVANCED PATTERNS
|
|
=================
|
|
|
|
LAYERED CACHING:
|
|
|
|
Combine once() and remember() for two-level caching:
|
|
|
|
function get_user_permissions($user_id) {
|
|
// Request-scoped cache (fastest)
|
|
return RsxCache::once("user_permissions:{$user_id}", function() use ($user_id) {
|
|
// Build-scoped cache (fast)
|
|
return RsxCache::remember("user_permissions:{$user_id}", function() use ($user_id) {
|
|
// Database query (slow)
|
|
return User_Model::find($user_id)->permissions()->get();
|
|
}, RsxCache::HOUR);
|
|
});
|
|
}
|
|
|
|
First call: Database query → Store in Redis → Store in memory → Return
|
|
Second call (same request): Memory → Return
|
|
Third call (different request): Redis → Store in memory → Return
|
|
|
|
CONDITIONAL CACHING:
|
|
|
|
function get_data($use_cache = true) {
|
|
if (!$use_cache) {
|
|
return fetch_fresh_data();
|
|
}
|
|
|
|
return RsxCache::remember('data', function() {
|
|
return fetch_fresh_data();
|
|
});
|
|
}
|
|
|
|
CACHE WARMING:
|
|
|
|
// In deployment script or scheduled job
|
|
function warm_critical_caches() {
|
|
RsxCache::remember('product_catalog', fn() => Product::all());
|
|
RsxCache::remember('site_config', fn() => Config::load_all());
|
|
RsxCache::remember('menu_structure', fn() => Menu::build_tree());
|
|
}
|
|
|
|
|
|
DEBUGGING
|
|
=========
|
|
|
|
Check if data is cached:
|
|
|
|
if (RsxCache::exists($key)) {
|
|
// Cached
|
|
}
|
|
|
|
View cache statistics:
|
|
|
|
$stats = RsxCache::stats();
|
|
// Returns: total_keys, used_memory, maxmemory, etc.
|
|
|
|
Manually clear cache:
|
|
|
|
RsxCache::delete($key); // Clear specific key
|
|
RsxCache::clear(); // Clear all build-scoped cache
|
|
|
|
Note: Request-scoped cache (once) is not inspectable - it's just a static array
|
|
that lives for the request duration.
|
|
|
|
|
|
PERFORMANCE CHARACTERISTICS
|
|
============================
|
|
|
|
RsxCache::once():
|
|
- Speed: Microseconds (array lookup)
|
|
- Overhead: None (simple array)
|
|
- Scalability: Single process only
|
|
|
|
RsxCache::remember():
|
|
- Speed: 1-2ms (Redis, Unix socket)
|
|
- Overhead: Serialization + lock acquisition on miss
|
|
- Scalability: Shared across all processes
|
|
|
|
Cache build with lock:
|
|
- First miss: Lock wait + callback execution + storage
|
|
- Concurrent misses: All wait for lock, first builds, rest use result
|
|
- Hit after build: 1-2ms (no lock needed)
|
|
|
|
|
|
RELATED SYSTEMS
|
|
===============
|
|
|
|
See also:
|
|
- php artisan rsx:man locking - RsxLocks advisory locking system
|
|
- php artisan rsx:man manifest - Manifest build key system
|
|
- /system/app/RSpade/Core/REDIS_USAGE.md - Redis database allocation
|