Redis wrapper for KB things

v1.29.15 2025-02-20 07:12 UTC

README

Like Pdb, but for Redis. I guess.

This wraps existing Redis clients and normalises the API (between server versions and client implementations). It also introduces a few additional helpers:

  • Object serialization
  • Leaky bucket rate limiting
  • Locking
  • Export/import
  • Session handler

Also because I don't want a hard dependency on either predis or php-redis. They both have their problems (vague magical commands API, binary extension, etc). Or wouldn't it be wonderful if a 3rd option showed up /s. Also supports credis.

Install

Add as a dependency:

composer require karmabunny/rdb

Adapters

  • php-redis (binary extension)
  • predis (composer package)
  • credis (composer package)

This library doesn't implement a redis client itself, only wraps existing clients. After much effort trying to normalise the responses from all these client, it might seem like a good idea to just write our own that isn't so inconsistent.

But consider that we only know how inconsistent these libraries are because we've spent so much time trying to make them all behave the same. For example, client 'A' might do 'B' well but 'C' badly. Then client 'D' does 'B' badly but 'C' really well.

So as I sit here and scoff at their feeble attempts, I am reminded of a few things:

  1. I've already introduced so many of my own bugs during this journey.
  2. Unit testing is a gift from heaven.
  3. Normalising these inconsistencies has improved our own consistency, something probably not as achievable when writing a new client from scratch.
  4. also this: https://xkcd.com/927

Version support

This wrapper doesn't try to polyfill any missing features. It targets Redis server v3.0, as that's the common support among all the adapters.

This library wouldn't ever try to hide features behind target versions, but perhaps it could help smooth out any differences. Lua scripting could polyfill a lot of things tbh.

For example, BRPOPLPUSH is deprecated in v6.2 and might be removed in the distant future. In this case, the library would be able to dynamically replace (based on the server version) this with BLMOVE.

Plans for v2

TTL params

There is a preference for the millisecond version of a command, particularly TTL parameters. This is clearly misleading and already wildly inconsistent. Ideally this changes so that a 'float' is converted to the millisecond version and integer remains unchanged. Thus the input is always 'seconds'. The implementation should include the 'p' millisecond version of each command, similar to how we've implemented incr/decr.

Type errors

Type errors are currently (hopefully) always a null return. This can quite confusing at times, or helpful in others. Version 2 will likely permit both, defaulting to emitting exceptions.

Object methods

The expected type in the object method is currently optional, but most drivers required it. Version 2 will make them required.

Config

Notes:

  • The port number is default 6379 unless specified in the host option.
  • The protocol can be adjusted in the host option too: prefix tcp:// or udp://.
return [
    'host' => 'localhost',
    'prefix' => 'sitecode:',

    // Defaults
    'adapter' => 'predis',
    'object_driver' => JsonObjectDriver::class,
    'timeout' => 5,
    'lock_sleep' => 5,
    'chunk_size' => 50,
    'scan_size' => 1000,
    'scan_keys' => false,
    'options' => [],
];

Adapter options

Predis

The predis adapter accepts any options supported by the Predis client.

PhpRedis

Credis

Object drivers

The object driver is responsible for serialising and deserialising objects for the 'object methods'.

These are:

  • setObject
  • getObject
  • mSetObjects
  • mGetObjects

Available drivers:

  • PhpObjectDriver (default)
  • JsonObjectDriver
  • MsgPackObjectDriver
  • HashObjectDriver

Usage

Basic usage with a TTL. Great for caching.

use karmabunny\rdb\Rdb;

$config = require 'config.php';
$rdb = Rdb::create($config);

// Store 'blah' for 100 ms
$rdb->set('key', 'blah', 100);

$rdb->get('key');
// => blah

usleep(150 * 1000);

$rdb->get('key');
// => NULL

Object extensions will serialize in the PHP format. These have builtin assertions so things are always the correct shape.

$model = new MyModel('etc');
$rdb->setObject('objects:key', $model);

$rdb->getObject('objects:key', MyModel::class);
// => MyModel( etc )

$rdb->getObject('objects:key', OtherModel::class);
// => NULL

Locking provides a mechanism to restrict atomic access to a resource.

// Wait for a lock for up to 10 seconds.
$lock = $rdb->lock('locks:key', 10 * 1000);

if ($lock === null) {
    echo "Busy - too much contention\n";
}
else {
    // Do atomic things.
    $lock->release();
}

Leaky bucket is a rate-limiting algorithm. It's cute, easy to understand, and not too complex.

// A bucket with 60 drips per minute.
$bucket = $rdb->getBucket([
    'key' => 'key',

    // Defaults.
    'capacity' => 60,
    'drip_rate' => 1,

    // Optional.
    'prefix' => 'drips:',
    'costs' => [
        'GET' => 1,
        'POST' => 10,
    ],
]);

// One drip.
$full = $bucket->drip();

if ($full) {
    echo "We're full, please wait {$bucket->getWait()} ms\n";
}
else {
    // Do things.
}

// Big drip.
$bucket->drip(20);

// Named drip (10 drips).
$bucket->drip('POST');

// Write out the status to the headers for easy debugging.
$bucket->writeHeaders();

Contributing

Submit a PR if you like. But before you do, please do the following:

  1. Run composer analyse and fix any complaints there
  2. Run composer compat and fix those too
  3. Write some tests and run composer tests
  4. Document the methods here

Methods

Core Methods

  • keys
  • scan
  • del
  • exists
  • ttl
  • expire
  • expireat
  • type
  • rename
  • move
  • select
  • dump/restore

scalar:

  • get
  • set
  • mGet
  • mSet
  • append
  • substr
  • getRange
  • incrBy
  • decrBy
  • incrByFloat
  • decrByFloat

sets:

  • sMembers
  • sAdd
  • sRem
  • sCard
  • sIsMember
  • sMove
  • sScan
  • TODO sRandMember
  • TODO sDiff (+ store)
  • TODO sInter (+ store)
  • TODO sUnion (+ store)

lists:

  • lLen
  • lRange
  • lTrim
  • lSet
  • lRem
  • lIndex
  • lPush
  • lPop
  • blPop
  • rPush
  • rPop
  • brPop
  • brPoplPush

sorted sets:

  • zAdd
  • zIncrBy
  • zRange
  • zRem
  • zCard
  • zCount
  • zScore
  • zRank
  • zRevRank

hashes:

  • hSet
  • hGet
  • hExists
  • hDel
  • hmSet
  • hmGet
  • hGetAll
  • hIncrBy/hIncrByFloat
  • hKeys
  • hVals
  • hLen
  • hScan
  • hStrLen

Extended/Wrapper Methods

  • mScan
  • getObject
  • setObject
  • mGetObjects
  • mScanObjects
  • mSetObjects
  • setJson
  • getJson
  • setHash/getHash (nested arrays in hash)
  • pack/unpack (MessagePack format)
  • incr/decr
  • hIncr/hDecr
  • export/import

Builtin Utilities

  • 'Leaky bucket' rate limiting
  • Locking