Files
rspade_system/app/RSpade/Core/REDIS_USAGE.md
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
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>
2025-10-21 02:08:33 +00:00

12 KiB
Executable File

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

// 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):

    // This happens AUTOMATICALLY before anything else
    RsxBootstrap::initialize();  // Acquires global APPLICATION read lock
    
  2. Site Lock Acquisition (Main::pre_dispatch):

    // This happens AUTOMATICALLY for web requests
    Rsx_Site_Model::acquire_site_read_lock();  // Site-specific lock
    
  3. 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:

  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

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

  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

// 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