# Redis Locking and Caching System ## Overview The RSpade framework provides MANDATORY locking and optional caching: 1. **RsxLocks**: MANDATORY advisory locking with readers-writer pattern - Global APPLICATION lock acquired on EVERY request - Site-specific locks for multi-tenant isolation 2. **RsxCache**: LRU caching with automatic build key prefixing ## RsxLocks - MANDATORY Advisory Locking System ### Purpose MANDATORY readers-writer locks that coordinate ALL PHP processes: - **Global APPLICATION lock**: Every request acquires read lock immediately - **Site-specific locks**: Automatic for Rsx_Site_Model operations - **Exclusive operations**: Manifest/bundle builds upgrade to write ### System Lock Constants ```php // These are the ONLY locks used in the system: RsxLocks::LOCK_APPLICATION // Global app lock (acquired on EVERY request) RsxLocks::LOCK_MANIFEST_BUILD // Manifest rebuild operations RsxLocks::LOCK_BUNDLE_BUILD // Bundle compilation operations RsxLocks::LOCK_MIGRATION // Database migration operations RsxLocks::LOCK_SITE_PREFIX // Site-specific (e.g., SITE_1, SITE_2) ``` ### Key Features - **MANDATORY**: All requests acquire global read lock before manifest loads - **Multiple readers**: Any number of read locks can be held simultaneously - **Exclusive writers**: Write locks are exclusive (no other readers or writers) - **Deadlock prevention**: Upgrading readers don't block each other - **Automatic cleanup**: Locks released on process exit/crash ### How Locking Works 1. **Application Bootstrap** (`public/index.php` and `artisan`): ```php // This happens AUTOMATICALLY before anything else RsxBootstrap::initialize(); // Acquires global APPLICATION read lock ``` 2. **Site Lock Acquisition** (`Main::pre_dispatch`): ```php // This happens AUTOMATICALLY for web requests Rsx_Site_Model::acquire_site_read_lock(); // Site-specific lock ``` 3. **Upgrading for Exclusive Operations**: ```php // In Manifest rebuild or bundle compilation use App\RSpade\Core\Bootstrap\RsxBootstrap; // Upgrade global lock to write (all PHP processes must wait) $token = RsxBootstrap::upgrade_to_write_lock(); try { // Exclusive access - rebuild manifest, compile bundles, etc. } finally { // Lock automatically released on shutdown } ``` ### Common Lock Patterns #### Pattern 1: Prevent Concurrent Rebuilds ```php // In Manifest::rebuild() $needs_rebuild = check_if_rebuild_needed(); if ($needs_rebuild) { $lock = RsxLocks::get_lock( RsxLocks::SERVER_LOCK, RsxLocks::MANIFEST_BUILD_LOCK_ID, RsxLocks::WRITE_LOCK ); try { // Check again - another process may have rebuilt if (check_if_rebuild_needed()) { do_manifest_rebuild(); } } finally { RsxLocks::release_lock($lock); } } ``` #### Pattern 2: Read Lock During Request Handling ```php // In application bootstrap $lock = RsxLocks::get_lock( RsxLocks::SERVER_LOCK, RsxLocks::APPLICATION_REQUEST_LOCK_ID, RsxLocks::READ_LOCK ); // Register shutdown handler to release register_shutdown_function(function() use ($lock) { RsxLocks::release_lock($lock); }); // Now manifest won't rebuild during this request ``` #### Pattern 3: Exclusive Database Migration ```php // In migration command $lock = RsxLocks::get_lock( RsxLocks::DATABASE_LOCK, 'MIGRATION_LOCK', RsxLocks::WRITE_LOCK, 300 // 5 minute timeout for migrations ); try { run_database_migrations(); } finally { RsxLocks::release_lock($lock); } ``` ### Predefined Lock IDs ```php // Framework-provided lock identifiers RsxLocks::MANIFEST_BUILD_LOCK_ID = 'MANIFEST_BUILD'; RsxLocks::BUNDLE_BUILD_LOCK_ID = 'BUNDLE_BUILD'; RsxLocks::RSX_RUN_LOCK_ID = 'RSX_RUN'; RsxLocks::APPLICATION_REQUEST_LOCK_ID = 'APPLICATION_REQUEST'; // Use your own for custom locks $lock = RsxLocks::get_lock( RsxLocks::SERVER_LOCK, 'MY_CUSTOM_OPERATION', RsxLocks::WRITE_LOCK ); ``` ### Lock Monitoring ```php // Get lock statistics $stats = RsxLocks::get_lock_stats( RsxLocks::SERVER_LOCK, RsxLocks::MANIFEST_BUILD_LOCK_ID ); /* Returns: [ 'readers_active' => 3, 'writers_waiting' => 1, 'writer_active' => false, 'writer_token' => null ] */ // Emergency: Force clear all locks for a name RsxLocks::force_clear_lock( RsxLocks::SERVER_LOCK, 'STUCK_LOCK_NAME' ); ``` ## RsxCache - Caching System ### Purpose High-performance caching with automatic invalidation when code changes. ### Key Features - **Automatic prefixing**: Uses manifest build key to invalidate on code changes - **LRU eviction**: 128MB cache with automatic eviction of least-used items - **Type preservation**: Automatically serializes/deserializes complex types - **Batch operations**: Get/set multiple values atomically - **Remember pattern**: Cache-or-generate helper ### Initialization ```php use App\RSpade\Core\Cache\RsxCache; // Must be called after manifest loads // Usually done in bootstrap after Manifest::init() RsxCache::initialize(); ``` ### Basic Usage ```php // Set a value (never expires) RsxCache::set('user:123', $user); // Set with expiration RsxCache::set('api:response', $data, RsxCache::HOUR); RsxCache::set('temp:data', $value, 300); // 5 minutes // Get a value $user = RsxCache::get('user:123'); $data = RsxCache::get('missing:key', 'default value'); // Delete a key RsxCache::delete('user:123'); // Check existence if (RsxCache::exists('user:123')) { // Key exists } ``` ### Advanced Operations ```php // Remember pattern - cache or generate $users = RsxCache::remember('all_users', function() { return User::all(); // Expensive operation }, RsxCache::HOUR); // Batch operations $values = RsxCache::get_many(['key1', 'key2', 'key3']); RsxCache::set_many([ 'key1' => 'value1', 'key2' => 'value2' ], RsxCache::DAY); // Atomic counters $count = RsxCache::increment('page:views'); $count = RsxCache::increment('api:calls', 5); // +5 $count = RsxCache::decrement('stock:qty', 1); // Clear all cache (current build only) RsxCache::clear(); // Emergency: Clear everything RsxCache::clear_all(); ``` ### Cache Statistics ```php $stats = RsxCache::stats(); /* Returns: [ 'build_key' => 'abc123...', 'total_keys' => 1523, 'build_keys' => 1523, // Keys for current build 'used_memory' => '45.2MB', 'used_memory_peak' => '89.1MB', 'maxmemory' => '128MB', 'maxmemory_policy' => 'allkeys-lru' ] */ ``` ### Expiration Constants ```php RsxCache::NO_EXPIRATION = 0; // Never expire (default) RsxCache::HOUR = 3600; RsxCache::DAY = 86400; RsxCache::WEEK = 604800; ``` ## Configuration Options ### Locking Configuration ```php // config/rsx.php 'locking' => [ // Lock acquisition timeout in seconds 'timeout' => env('RSX_LOCK_TIMEOUT', 30), // Force write locks for ALL site requests (severe performance impact) 'site_always_write' => env('RSX_SITE_ALWAYS_WRITE', false), ], ``` **IMPORTANT**: Locking is MANDATORY and cannot be disabled. All requests acquire: 1. Global APPLICATION read lock (before manifest loads) 2. Site-specific lock (for Rsx_Site_Model operations) **WARNING**: The `site_always_write` option forces every site request to acquire an exclusive write lock immediately, completely serializing all requests for that site. This should only be used for: - Critical maintenance operations - Data migration or repair scripts - Situations where absolute consistency is required - Emergency debugging of race conditions When enabled in production CLI mode, a warning is displayed on stderr. ## Integration Examples ### Example 1: Manifest Building with Locks ```php class Manifest { public static function rebuild(): void { // Get exclusive lock for rebuilding $lock = RsxLocks::get_lock( RsxLocks::SERVER_LOCK, RsxLocks::MANIFEST_BUILD_LOCK_ID, RsxLocks::WRITE_LOCK ); try { // Do the rebuild static::do_rebuild(); // Clear cache after rebuild RsxCache::clear(); } finally { RsxLocks::release_lock($lock); } } } ``` ### Example 2: Cached API Responses ```php class Api_Controller { public static function get_users(Request $request) { $cache_key = 'api:users:' . md5($request->getQueryString()); return RsxCache::remember($cache_key, function() { // Expensive database query return User::with(['posts', 'comments']) ->where('active', true) ->get(); }, RsxCache::HOUR); } } ``` ### Example 3: Bundle Compilation ```php class Bundle_Compiler { public static function compile(string $bundle_name): void { // Prevent concurrent compilation $lock = RsxLocks::get_lock( RsxLocks::SERVER_LOCK, "BUNDLE_BUILD:{$bundle_name}", RsxLocks::WRITE_LOCK ); try { // Check if still needs compilation if (!static::needs_compilation($bundle_name)) { return; } // Compile the bundle $result = static::do_compile($bundle_name); // Cache the result RsxCache::set( "bundle:compiled:{$bundle_name}", $result, RsxCache::DAY ); } finally { RsxLocks::release_lock($lock); } } } ``` ## Best Practices ### Locking 1. **Always use try/finally** to ensure locks are released 2. **Keep lock duration minimal** - do only critical work while locked 3. **Use appropriate timeouts** - don't wait forever for locks 4. **Check condition twice** - once before lock, once after acquiring 5. **Use read locks when possible** - allows parallelism ### Caching 1. **Use descriptive keys** - include context (e.g., 'user:123:profile') 2. **Set appropriate expiration** - don't cache forever unless needed 3. **Clear selectively** - use delete() for specific keys vs clear() for all 4. **Monitor memory usage** - check stats() regularly 5. **Handle cache misses** - always provide defaults or regeneration logic ## Troubleshooting ### Lock Issues ```php // Check if locks are stuck $stats = RsxLocks::get_lock_stats(RsxLocks::SERVER_LOCK, 'LOCK_NAME'); if ($stats['writers_waiting'] > 5) { // Too many writers queued - investigate } // Force clear stuck lock (emergency only!) RsxLocks::force_clear_lock(RsxLocks::SERVER_LOCK, 'STUCK_LOCK'); ``` ### Cache Issues ```php // Check cache health $stats = RsxCache::stats(); if ($stats['used_memory'] === $stats['maxmemory']) { // Cache is full - may have high eviction rate } // Clear and rebuild cache RsxCache::clear_all(); // Cache will rebuild automatically on next requests ``` ### Redis Connection Issues Both systems will throw clear exceptions if Redis is unavailable: - `shouldnt_happen("Failed to connect to Redis for locking")` - `shouldnt_happen("Failed to connect to Redis for caching")` Ensure Redis is running and accessible via environment variables: - `REDIS_HOST` (default: 127.0.0.1) - `REDIS_PORT` (default: 6379) - `REDIS_SOCKET` (optional, preferred for performance) ## Performance Considerations ### Locking Performance - **Read locks are cheap** - Multiple readers cause no contention - **Write locks are expensive** - Block all other operations - **Use Unix socket** - Significantly faster than TCP - **Keep locks brief** - Sub-second lock duration ideal ### Caching Performance - **Unix socket**: ~150k ops/sec - **TCP localhost**: ~80-100k ops/sec - **Serialization overhead**: ~10-20% for complex objects - **LRU eviction**: Automatic, sub-millisecond ## Redis Database Assignment - **Database 0**: Cache (128MB, LRU eviction) - **Database 1**: Locks (no eviction, TTL-based expiry) - **Databases 2-15**: Available for custom use This separation ensures: - Lock keys never get evicted by cache pressure - Cache can use all available memory - Clear separation of concerns