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>
12 KiB
Executable File
Redis Locking and Caching System
Overview
The RSpade framework provides MANDATORY locking and optional caching:
- RsxLocks: MANDATORY advisory locking with readers-writer pattern
- Global APPLICATION lock acquired on EVERY request
- Site-specific locks for multi-tenant isolation
- 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
// 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
-
Application Bootstrap (
public/index.phpandartisan):// This happens AUTOMATICALLY before anything else RsxBootstrap::initialize(); // Acquires global APPLICATION read lock -
Site Lock Acquisition (
Main::pre_dispatch):// This happens AUTOMATICALLY for web requests Rsx_Site_Model::acquire_site_read_lock(); // Site-specific lock -
Upgrading for Exclusive Operations:
// 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
// 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
// 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
// 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
// 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
// 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
use App\RSpade\Core\Cache\RsxCache;
// Must be called after manifest loads
// Usually done in bootstrap after Manifest::init()
RsxCache::initialize();
Basic Usage
// 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
// 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
$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
RsxCache::NO_EXPIRATION = 0; // Never expire (default)
RsxCache::HOUR = 3600;
RsxCache::DAY = 86400;
RsxCache::WEEK = 604800;
Configuration Options
Locking Configuration
// 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:
- Global APPLICATION read lock (before manifest loads)
- 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
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
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
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
- Always use try/finally to ensure locks are released
- Keep lock duration minimal - do only critical work while locked
- Use appropriate timeouts - don't wait forever for locks
- Check condition twice - once before lock, once after acquiring
- Use read locks when possible - allows parallelism
Caching
- Use descriptive keys - include context (e.g., 'user:123:profile')
- Set appropriate expiration - don't cache forever unless needed
- Clear selectively - use delete() for specific keys vs clear() for all
- Monitor memory usage - check stats() regularly
- Handle cache misses - always provide defaults or regeneration logic
Troubleshooting
Lock Issues
// 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
// 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