feat(php): implement BGSAVE, BGSAVE SCHEDULE, and BGSAVE CANCEL commands#237
feat(php): implement BGSAVE, BGSAVE SCHEDULE, and BGSAVE CANCEL commands#237prateek-kumar-improving wants to merge 31 commits into
Conversation
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: prateek-kumar-improving <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
| CommandResponse* element = &response->array_value[0]; | ||
| CommandResponse* first_value = element->map_value; | ||
| if (first_value && first_value->response_type == String && |
There was a problem hiding this comment.
Does this match the behaviour of PHPRedis – does it also only take the first value (even though it is entirely possible that different nodes may return different values)?
There was a problem hiding this comment.
Yes, this matches PHPRedis behavior. PHPRedis's bgSave() returns a single bool or string — it doesn't return an array of per-node results.
There was a problem hiding this comment.
Did you confirm (either by running it, from the documentation, or from the code) that the value returned by PHPRedis is simply the value from one of the nodes? Can you explain how you verified this?
There was a problem hiding this comment.
Verified from PHPRedis documentation (cluster.md). BGSAVE is listed as a "Directed node command" — in RedisCluster, it's sent to a single node (specified by the caller) and returns that node's scalar response.
PHPRedis does not aggregate multi-node results. Our allPrimaries route is a GLIDE-specific extension; taking the first value maintains the scalar return type that PHPRedis users expect.
When a user passes a key or host:port, they get a single-node response (just like PHPRedis). The "take first value from Map" logic only exists for multi-node routes (allPrimaries, allNodes).
PHPRedis doesn't have an equivalent to allPrimaries — it's a GLIDE addition.
There was a problem hiding this comment.
In that case, should GLIDE return return a dictionary of strings (map from address to response string) for multi-node routes? If multi-node routes are not supported by PHPRedis at all, then this would be an extension of the PHPRedis API, rather than a change to it (i.e. the result would still be compatible for PHPRedis-support commands), and it would be consistent with the other GLIDE clients (which return a dictionary/ClusterValue for multi-node routes). If we do not return dictionary for multi-node commands, than we hiding responses from the user and mis-representing the actual responses from the server.
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
…rsively Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
| } else if (response->response_type == Map && response->array_value && | ||
| response->array_value_len > 0) { | ||
| /* Multi-node response (cluster routing) - process first node's value recursively */ | ||
| CommandResponse* element = &response->array_value[0]; | ||
| CommandResponse* first_value = element->map_value; | ||
| if (first_value) { | ||
| return process_core_string_result(first_value, output, return_value); | ||
| } |
There was a problem hiding this comment.
Why is this only looking at the first array_value? Should we also process the other responses as well?
There was a problem hiding this comment.
PHPRedis bgSave() returns a single scalar — it doesn't aggregate multi-node results. In PHPRedis cluster, BGSAVE is a "directed node command" sent to one specific node. Our allPrimaries route is a GLIDE-specific extension; we take the first value to maintain the scalar return type.
There was a problem hiding this comment.
Would this correctly handles bgSave("allPrimaries")?
There was a problem hiding this comment.
I think that we may need to return a dictionary/ClusterValue for multi-node routes, as suggested in this comment: #237 (comment). This is what other GLIDE clients do, and I believe that it is still compatible with PHPRedis behaviour.
There was a problem hiding this comment.
Thanks! I have reviewed these changes and added a few more suggestions, but generally looks good – I think this is a much improved interface that more closely matches other GLIDE clients.
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
| public function flushDB(mixed $route, bool $async = false): ValkeyGlideCluster|bool; | ||
|
|
||
| /** | ||
| * @see ValkeyGlide::bgSave |
There was a problem hiding this comment.
The differences in return type and behaviour between standalone and cluster are not documented here.
| // When no save is in progress, CANCEL returns false (error response) | ||
| $result = $this->valkey_glide->bgSave('allPrimaries', 'CANCEL'); | ||
| $this->assertFalse($result); |
There was a problem hiding this comment.
Why does BGSAVE CANCEL not return an array like the other methods? This command could still succeed on some nodes and fail on others, right?
For other clients, failure causes an exception to be thrown, so we are not losing information if we return "OK"; but in this case, where – as I understand it – failure would NOT cause an exception, we are losing information if we don't return a bool. Moreover, it is unclear what this bool actually means. Does true mean that ALL nodes returned true? Or just one? Or most of them?
There was a problem hiding this comment.
Added a comment for this: d2d1991.
It's a limitation of the glide-core's response aggregation, not a PHP-level design choice. For BGSAVE (which has no explicit ResponsePolicy in redis-rs), multi-node routing uses the None policy path. Before building the Value::Map of per-node results, the core iterates all resolved values and returns the first ServerError as a single Err if any node errors. When CANCEL is called with no save in progress, ALL nodes return errors, so the core collapses them into a single command_error — we never receive a Map response on the PHP side.
This means:
- Success case (save was in progress): at least one node returns OK → no error short-circuit → we get the Map → PHP returns array<string, bool>
- All-failure case (no save in progress): all nodes error → core returns single Err → PHP returns scalar false
This matches PHPRedis semantics (returns false on failure) and is consistent with how the Python GLIDE client handles this — Python would throw a RequestError in the same scenario rather than returning a dict.
Changing this would require modifying the core's ResponsePolicy for BGSAVE (e.g., to Special) so per-node errors are preserved in the Map, which would be a separate core-level change. I've added an explanatory comment in the test.
| } | ||
|
|
||
| /* Select processor based on OPT_REPLY_LITERAL: | ||
| * - With OPT_REPLY_LITERAL: return raw string (process_core_string_result) |
There was a problem hiding this comment.
Should this be process_core_bgsave_string_result instead?
| * - With OPT_REPLY_LITERAL: return raw string (process_core_string_result) | |
| * - With OPT_REPLY_LITERAL: return raw string (process_core_bgsave_string_result) |
| if (execute_core_command(valkey_glide, &core_args, NULL, processor, return_value)) { | ||
| if (valkey_glide->is_in_batch_mode) { | ||
| /* In batch mode, return $this for method chaining */ | ||
| ZVAL_COPY(return_value, object); | ||
| return 1; | ||
| } | ||
|
|
||
| return 1; | ||
| } else { | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
Nit. Can simplify this. Reduces return paths/branching and nesting:
| if (execute_core_command(valkey_glide, &core_args, NULL, processor, return_value)) { | |
| if (valkey_glide->is_in_batch_mode) { | |
| /* In batch mode, return $this for method chaining */ | |
| ZVAL_COPY(return_value, object); | |
| return 1; | |
| } | |
| return 1; | |
| } else { | |
| return 0; | |
| } | |
| if (!execute_core_command(valkey_glide, &core_args, NULL, processor, return_value)) { | |
| return 0; | |
| } | |
| if (valkey_glide->is_in_batch_mode) { | |
| /* In batch mode, return $this for method chaining */ | |
| ZVAL_COPY(return_value, object); | |
| } | |
| return 1; |
| * Converts String/Ok responses to true, Null to false. | ||
| * For multi-node routes, returns an associative array of node => bool. | ||
| */ | ||
| int process_core_bgsave_bool_result(CommandResponse* response, void* output, zval* return_value) { |
There was a problem hiding this comment.
I think we can significantly simplify this and reduce duplication by implementing something like process_core_cluster_result, which would take a converter function and apply it to a single- or multi-node cluster result. This would be similar to logic that exists in other clients, and would avoid duplication for new commands.
Here is an AI generated example of what this could look like:
int process_core_cluster_result(
CommandResponse* response,
void* output,
zval* return_value,
z_result_processor_t single_node_processor
) {
if (!response) {
ZVAL_FALSE(return_value);
return 0;
}
if (response->response_type == Map && response->array_value &&
response->array_value_len > 0) {
array_init(return_value);
for (long i = 0; i < response->array_value_len; i++) {
CommandResponse* element = &response->array_value[i];
CommandResponse* value = element->map_value;
if (element->map_key && element->map_key->string_value && value) {
zval node_result;
single_node_processor(value, output, &node_result);
add_assoc_zval_ex(return_value,
element->map_key->string_value,
element->map_key->string_value_len,
&node_result);
}
}
return 1;
}
// Single-node: delegate directly
return single_node_processor(response, output, return_value);
}
int process_core_cluster_bool_result(CommandResponse* r, void* o, zval* rv) {
return process_core_cluster_result(r, o, rv, process_core_bool_result);
}
int process_core_cluster_string_result(CommandResponse* r, void* o, zval* rv) {
return process_core_cluster_result(r, o, rv, process_core_string_result);This is much simpler, and has the advantage of being more generic and reusable.
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Signed-off-by: Prateek Kumar <prateek.kumar@improving.com>
Summary
Implement BGSAVE, BGSAVE SCHEDULE, and BGSAVE CANCEL commands for both standalone and cluster clients, matching PHPRedis API and the implementation in other GLIDE clients (Go, Python,
Node, C#).
Issue link
This Pull Request is linked to issue (URL): #236
Related issues and pull requests:
BGSAVEcommands valkey-glide-csharp#436Features / Behaviour Changes
Implementation
Testing
Checklist
Before submitting the PR make sure the following are checked: