Skip to content

Add session locking support to RedisCluster - #2836

Open
rveznaver wants to merge 1 commit into
phpredis:developfrom
rveznaver:cluster_session_lock
Open

Add session locking support to RedisCluster#2836
rveznaver wants to merge 1 commit into
phpredis:developfrom
rveznaver:cluster_session_lock

Conversation

@rveznaver

@rveznaver rveznaver commented May 9, 2026

Copy link
Copy Markdown

July 2026 update: I realised my former pull request description read more like a stream of consciousness novel than technical writing, so this should be a better version.

Description

This pull request adds PHP session locking support to the RedisCluster session save handler.

The implementation collocates the session lock on the same slot as the session itself, ensures session lock acquisition is atomic, and any operations on the session are made on the slot owner (replicas cannot guarantee data freshness). These three guarantees enable the cluster session locks to behave like those already set up for standalone Redis.

The implementation purposely avoids using any general distributed lock mechanism (i.e. Redlock) given that sessions themselves are already sharded (though both the session and its lock may be replicated). It adds a LUA script for lock acquisition, but defaults to the already existing LUA conditional delete (both Valkey and Redis conditional delete commands are supported as opt-in).

Implementation

Most of the code is a mirror copy of the standalone Redis flow, adapted to work on RedisCluster. The real work lies in a few notable changes that require attention.

added the redis_cluster_session struct

The cluster handle is bundled with the lock status, mirroring the redis_pool struct

typedef struct {
    redisCluster *cluster;
    redis_session_lock_status lock_status;
} redis_cluster_session;

added generate_cluster_lock_key and select_cluster_lock_key_form functions

The cluster lock key uses Redis Cluster Hash tags which basically wraps the session key around braces (thus creating a hash tag) and adds a _LOCK suffix, like so:

SESSIONKEY -> {SESSIONKEY}_LOCK

The code also autodetects the case in which there is already a tag inside the session key itself, like so:

{TAG}SESSIONKEY -> {TAG}SESSIONKEY_LOCK

The latter use case is strongly discouraged in the README.md, but support is there as one may imagine someone might want to force all sessions to go to a specific slot.

The select_cluster_lock_key_form function was adapted from the Redis source code keyHashSlot(). It is O(n) so impact is negligible.

added LOCK_RW_LUA_STR Lua script

The Lua script is the basis of the aforementioned atomic operation. It acquires the lock and reads the session data in a single call. The actual ordering of the operations is in reverse to prevent Redis from doing a SET on the lock if the session data GET fails (Redis does not rollback all operations within a LUA script).

The script will use GETEX to refresh the session TTL if set, fallback to regular GET. It supports setting the lock with a TTL and without, but cluster_lock_acquire_and_read sets the lock TTL to max_execution_time if redis.session.lock_expire is set to 0. To note: the lock TTL is defined in seconds, but written in milliseconds.

local d
if tonumber(ARGV[3]) > 0 then
  d = redis.call('GETEX', KEYS[1], 'EX', ARGV[3])
else
  d = redis.call('GET', KEYS[1])
end

local l
if tonumber(ARGV[2]) > 0 then
  l = redis.call('SET', KEYS[2], ARGV[1], 'NX', 'PX', ARGV[2])
else
  l = redis.call('SET', KEYS[2], ARGV[1], 'NX')
end

return { l and 1 or 0, d or '' }

The reason the cluster lock key uses hash tags is so this script does not trip CROSSSLOT and fail.

operations sent to master slot

Due to possible stale data, the code ensures operations are sent to the master slot by setting:

void foo(redisCluster *c) {
    c->readonly = 0;
}

That being said, any failover should be automatically handled by following Redis MOVED errors (unless the lock was lost in the failover). This should also work on AWS ElastiCache Serverless (though I would personally not set up sessions on such a cluster).

support for multiple lock release commands

Depending on the configured options, cluster_lock_release may call:

  • cluster_lock_release_delex (Redis 8.4+)
  • cluster_lock_release_delifeq (Valkey 9.0+)
  • cluster_lock_release_lua (Lua using existing script)

table of function equivalence

Most of the locking functions are a mirror of the standalone session handling code. This is on purpose as I copied them over before adapting them to work on the cluster, so if you are used to reading the session handling code, this should be functionally identical.

RedisCluster function Standalone Redis equivalent
generate_cluster_lock_key generate_lock_key
cluster_set_session_lock_key set_session_lock_key
cluster_lock_acquire lock_acquire
cluster_write_allowed write_allowed
cluster_get_del_result get_del_result
cluster_lock_release lock_release
cluster_lock_release_delex lock_release_delex
cluster_lock_release_delifeq lock_release_delifeq
cluster_lock_release_lua lock_release_lua

Note: cluster_send_release_cmd, cluster_read_session_data, and cluster_lock_acquire_and_read are all implemented inline in the standalone flow.

Tests

All the tests for session locking on the cluster have been uncommented and helpers have been slightly adapted where necessary.

I have also tested this against both Redis and Valkey clusters in multiple Docker environments, but those tests have not been included as I do not see the repo managing any Docker tests.

Backport on 6.3.0

This feature has also been backported on top of 6.3.0, code is in the 6.3.0_cluster_session_lock branch

@michael-grunder michael-grunder self-assigned this May 12, 2026
@michael-grunder

Copy link
Copy Markdown
Member

Thanks. I will try to make time to play around with this.

Session cluster locking would be nice.

@rveznaver

Copy link
Copy Markdown
Author

Welcome! Let me know if you require any changes, but I do believe the general idea should be safe from a distributed locking standpoint (provided no keys get evicted).

@rveznaver

Copy link
Copy Markdown
Author

Hi @michael-grunder, did you manage to play with this a bit?

@michael-grunder

Copy link
Copy Markdown
Member

Sorry, I've been swamped. I will give it a test though, it's a useful feature for sure.

@rveznaver
rveznaver force-pushed the cluster_session_lock branch from 39db5de to ee7be30 Compare June 22, 2026 21:15
@rveznaver

Copy link
Copy Markdown
Author

I have rebased this on the latest develop:

  • moved commands to the new redis_cmd_fmt (which removed the REDIS_CLUSTER_SPPRINTF in library.h)
  • cluster_session_key now returns a zend_string
  • I hit an edge case in my tests (Debian bookworm / gcc12) when redis_cmd_fmt passes an int instead of size_t so I made sure I use ZEND_STRL in all those calls (otherwise it blows up in a very bad way). Might be worth making a CI check for that last bit.

@rveznaver
rveznaver force-pushed the cluster_session_lock branch from ee7be30 to a0d09af Compare July 2, 2026 21:30
@rveznaver

rveznaver commented Jul 21, 2026

Copy link
Copy Markdown
Author

Updated the pull request description, the code should now be easier to follow :)

@rveznaver
rveznaver force-pushed the cluster_session_lock branch 2 times, most recently from 55997f4 to fb3a3de Compare July 24, 2026 16:13
@rveznaver

Copy link
Copy Markdown
Author

Rebased on latest develop, now using cluster_send_rcmd_ex(c, slot, cmd)

Adds PHP session locking support to the RedisCluster session save handler.

The implementation collocates the session lock on the same slot as the
session itself, ensures session lock acquisition is atomic, and any
operations on the session are made on the slot owner (replicas cannot
guarantee data freshness). These three guarantees enable the cluster
session locks to behave like those already set up for standalone Redis.
@rveznaver
rveznaver force-pushed the cluster_session_lock branch from fb3a3de to daad26a Compare July 26, 2026 11:55
@rveznaver

Copy link
Copy Markdown
Author

Rebased on latest develop, added the fix for session handlers for php 8.6.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants