Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
71355e4
feat(php): implement BGSAVE, BGSAVE SCHEDULE, and BGSAVE CANCEL commands
prateek-kumar-improving Jun 30, 2026
47d82d6
PHP: Fix lint and update changelog
prateek-kumar-improving Jun 30, 2026
0c53959
Merge branch 'main' into php/implement-bgsave-command
prateek-kumar-improving Jun 30, 2026
2c6c9b5
PHP: fix tests
prateek-kumar-improving Jul 2, 2026
7c8bc2d
PHP: Fix tests
prateek-kumar-improving Jul 2, 2026
df873ed
PHP: Fix tests
prateek-kumar-improving Jul 2, 2026
d73aee2
PHP: Fix cluster tests
prateek-kumar-improving Jul 2, 2026
605f77f
PHP: Fix CI errors
prateek-kumar-improving Jul 2, 2026
8b49e3f
PHP: Update command response types
prateek-kumar-improving Jul 2, 2026
215e710
PHP: Fix CI error
prateek-kumar-improving Jul 2, 2026
457659d
Add generic waitFor helper and simplify test retry logic
prateek-kumar-improving Jul 3, 2026
87383a1
Remove is_array checks
prateek-kumar-improving Jul 3, 2026
5c8bee2
PHP: Add isSaveInProgress helper
prateek-kumar-improving Jul 3, 2026
7339acd
Remove waitForSaveNotInProgress from the end of tests
prateek-kumar-improving Jul 3, 2026
6124361
Add OPT_REPLY_LITERAL tests
prateek-kumar-improving Jul 3, 2026
a4a3484
PHP: Add OPT_REPLY_LITERAL cancel tests
prateek-kumar-improving Jul 6, 2026
fd083ba
PHP: Remove waitForSaveNotInProgress override from cluster test
prateek-kumar-improving Jul 6, 2026
9f9d976
PHP: Refactor process_core_string_result to handle Map responses recu…
prateek-kumar-improving Jul 6, 2026
258411d
PHP: add batch tests
prateek-kumar-improving Jul 6, 2026
4302cf1
Fix batch tests
prateek-kumar-improving Jul 6, 2026
7732a98
Update test for randomNode testing
prateek-kumar-improving Jul 7, 2026
646893a
Add multi-node array return types
prateek-kumar-improving Jul 7, 2026
5d20fcb
Fix tests
prateek-kumar-improving Jul 7, 2026
eb16ac8
PHP: Fix bgSave common code
prateek-kumar-improving Jul 7, 2026
bdec78d
PHP: fix tests
prateek-kumar-improving Jul 7, 2026
a657785
PHP: Fix tests
prateek-kumar-improving Jul 7, 2026
7a122c9
PHP: Fix cluster documentation
prateek-kumar-improving Jul 9, 2026
d2d1991
PHP: Add documentation comment for bgSave Cancel case
prateek-kumar-improving Jul 9, 2026
83da041
Fix process_core_cluster_result method
prateek-kumar-improving Jul 9, 2026
d370bcd
PHP: create generic process_core_cluster_result helper
prateek-kumar-improving Jul 9, 2026
84db088
PHP: Fix lint errors
prateek-kumar-improving Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

### Changes

* PHP: Add `BGSAVE`, `BGSAVE SCHEDULE`, and `BGSAVE CANCEL` commands for standalone and cluster clients ([#236](https://github.com/valkey-io/valkey-glide-php/issues/236))
* PHP: Fix pie install from Packagist using PIE pre-packaged-source download method ([#233](https://github.com/valkey-io/valkey-glide-php/pull/233))
* PHP: Fix pie install from Packagist by setting preferred-install to source for submodule resolution ([#232](https://github.com/valkey-io/valkey-glide-php/pull/232))
* PHP: Fix pie install ([#229](https://github.com/valkey-io/valkey-glide-php/pull/229))
* PHP: Set CLIENT SETINFO lib-name=GlidePHP and lib-ver to extension version for proper client identification
* PHP: Add address resolver support for standalone and cluster clients, allowing custom host/port remapping at connection time ([#196](https://github.com/valkey-io/valkey-glide-php/pull/196))
Expand Down
19 changes: 19 additions & 0 deletions tests/ValkeyGlideBaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,23 @@ protected function skipIfDnsNotEnabled(): void
$this->markTestSkipped('DNS tests are disabled. Set VALKEY_GLIDE_DNS_TESTS_ENABLED=1 to enable.');
}
}

/**
* Waits until the given condition callback returns true, polling every 100ms.
*
* @param callable $condition A callback that returns true when the condition is met.
* @param int $timeoutSeconds Maximum time to wait in seconds.
* @param string $message Failure message if timeout is reached.
*/
protected function waitFor(callable $condition, int $timeoutSeconds = 10, string $message = 'waitFor timed out'): void
{
$start = time();
while (time() - $start < $timeoutSeconds) {
if ($condition()) {
return;
}
usleep(100000); // 100ms
}
$this->fail($message);
}
}
172 changes: 172 additions & 0 deletions tests/ValkeyGlideClusterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,178 @@ public function testFlushAll()
}
}

/**
* Override for cluster - info() requires a route parameter and checks all primaries.
*/
protected function isSaveInProgress(): bool
{
$info = $this->valkey_glide->info('allPrimaries', 'persistence');
foreach ($info as $nodeInfo) {
if (
$nodeInfo['rdb_bgsave_in_progress'] == '1'
|| $nodeInfo['aof_rewrite_in_progress'] == '1'
) {
return true;
}
}
return false;
}

public function testBgSave()
Comment thread
currantw marked this conversation as resolved.
{
$this->waitForSaveNotInProgress();

// Test with allPrimaries route - returns array of node => bool
$result = $this->valkey_glide->bgSave('allPrimaries');
$this->assertIsArray($result);
$this->assertGT(0, count($result));
foreach ($result as $nodeAddress => $nodeResult) {
$this->assertIsString($nodeAddress);
$this->assertStringContains(':', $nodeAddress);
$this->assertTrue($nodeResult);
}

$this->waitForSaveNotInProgress();

// Test with randomNode route - returns scalar bool
$result = $this->valkey_glide->bgSave('randomNode');
$this->assertTrue($result);
}

public function testBgSaveSchedule()
{
$this->waitForSaveNotInProgress();

// Test with allPrimaries route - returns array of node => bool
$result = $this->valkey_glide->bgSave('allPrimaries', 'SCHEDULE');
$this->assertIsArray($result);
$this->assertGT(0, count($result));
foreach ($result as $nodeAddress => $nodeResult) {
$this->assertIsString($nodeAddress);
$this->assertStringContains(':', $nodeAddress);
$this->assertTrue($nodeResult);
}

$this->waitForSaveNotInProgress();

// Test with randomNode route - returns scalar bool
$result = $this->valkey_glide->bgSave('randomNode', 'SCHEDULE');
$this->assertTrue($result);
}

public function testBgSaveCancel()
{
if (!$this->minVersionCheck('8.1.0')) {
$this->markTestSkipped('BGSAVE CANCEL requires Valkey 8.1.0+');
return;
}

$this->waitForSaveNotInProgress();

// When no save is in progress, CANCEL returns false (not an array) even for
// multi-node routes. This is because the glide-core's response aggregation
// checks all node responses for errors before building the Map result. When
// ALL nodes return a ServerError (no save in progress), the core collapses
// them into a single command_error rather than returning a per-node Map.
// This matches PHPRedis semantics where bgSave() returns false on failure.
// A successful CANCEL (when a save IS in progress) would return an array
// for multi-node routes since at least one node returns a success response.
$result = $this->valkey_glide->bgSave('allPrimaries', 'CANCEL');
$this->assertFalse($result);

$result = $this->valkey_glide->bgSave('randomNode', 'CANCEL');
$this->assertFalse($result);
}

public function testBgSaveWithReplyLiteral()
{
$this->waitForSaveNotInProgress();

$this->valkey_glide->setOption(ValkeyGlide::OPT_REPLY_LITERAL, true);

// Test with allPrimaries route - returns array of node => string
$result = $this->valkey_glide->bgSave('allPrimaries');
$this->assertIsArray($result);
foreach ($result as $nodeResult) {
$this->assertIsString($nodeResult);
$this->assertContains($nodeResult, $this->bgsaveResponses());
}

$this->waitForSaveNotInProgress();

// Test with randomNode route - returns scalar string
$result = $this->valkey_glide->bgSave('randomNode');
$this->assertIsString($result);
$this->assertContains($result, $this->bgsaveResponses());

$this->waitForSaveNotInProgress();

// Test SCHEDULE with allPrimaries route - returns array of node => string
$result = $this->valkey_glide->bgSave('allPrimaries', 'SCHEDULE');
$this->assertIsArray($result);
foreach ($result as $nodeResult) {
$this->assertIsString($nodeResult);
$this->assertContains($nodeResult, $this->bgsaveResponses());
}

$this->waitForSaveNotInProgress();

// Test SCHEDULE with randomNode route - returns scalar string
$result = $this->valkey_glide->bgSave('randomNode', 'SCHEDULE');
$this->assertIsString($result);
$this->assertContains($result, $this->bgsaveResponses());

if ($this->minVersionCheck('8.1.0')) {
$this->waitForSaveNotInProgress();

// CANCEL with no save in progress returns false (see testBgSaveCancel comment)
$result = $this->valkey_glide->bgSave('allPrimaries', 'CANCEL');
$this->assertFalse($result);

$result = $this->valkey_glide->bgSave('randomNode', 'CANCEL');
$this->assertFalse($result);
}

$this->valkey_glide->setOption(ValkeyGlide::OPT_REPLY_LITERAL, false);
}

public function testBgSaveBatch()
{
if (!$this->havePipeline()) {
$this->markTestSkipped('Pipeline not supported');
return;
}

$this->waitForSaveNotInProgress();

$this->valkey_glide->pipeline();
$this->valkey_glide->bgSave('allPrimaries');
$result = $this->valkey_glide->exec();
$this->assertIsArray($result[0]);
foreach ($result[0] as $nodeResult) {
$this->assertTrue($nodeResult);
}

$this->waitForSaveNotInProgress();

$this->valkey_glide->pipeline();
$this->valkey_glide->bgSave('allPrimaries', 'SCHEDULE');
$result = $this->valkey_glide->exec();
$this->assertIsArray($result[0]);
foreach ($result[0] as $nodeResult) {
$this->assertTrue($nodeResult);
}

if ($this->minVersionCheck('8.1.0')) {
$this->waitForSaveNotInProgress();

$this->valkey_glide->pipeline();
$this->valkey_glide->bgSave('allPrimaries', 'CANCEL');
$result = $this->valkey_glide->exec();
$this->assertFalse($result[0]);
}
}

public function testInfo()
{
$fields = [
Expand Down
119 changes: 119 additions & 0 deletions tests/ValkeyGlideTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2585,6 +2585,125 @@ public function testFlushAll()
$this->assertEquals(0, $this->valkey_glide->dbSize());
}

public function testBgSave()
{
$this->waitForSaveNotInProgress();

$result = $this->valkey_glide->bgSave();
$this->assertTrue($result);
}

public function testBgSaveSchedule()
{
$this->waitForSaveNotInProgress();

$result = $this->valkey_glide->bgSave('SCHEDULE');
$this->assertTrue($result);
}

public function testBgSaveCancel()
{
if (!$this->minVersionCheck('8.1.0')) {
$this->markTestSkipped('BGSAVE CANCEL requires Valkey 8.1.0+');
return;
}

$this->waitForSaveNotInProgress();

$result = $this->valkey_glide->bgSave('CANCEL');
$this->assertFalse($result);
}

public function testBgSaveWithReplyLiteral()
{
$this->waitForSaveNotInProgress();

$this->valkey_glide->setOption(ValkeyGlide::OPT_REPLY_LITERAL, true);

$result = $this->valkey_glide->bgSave();
$this->assertIsString($result);
$this->assertContains($result, $this->bgsaveResponses());

$this->waitForSaveNotInProgress();

$result = $this->valkey_glide->bgSave('SCHEDULE');
$this->assertIsString($result);
$this->assertContains($result, $this->bgsaveResponses());
Comment thread
currantw marked this conversation as resolved.

if ($this->minVersionCheck('8.1.0')) {
$this->waitForSaveNotInProgress();

$result = $this->valkey_glide->bgSave('CANCEL');
$this->assertFalse($result);
}

$this->valkey_glide->setOption(ValkeyGlide::OPT_REPLY_LITERAL, false);
}

public function testBgSaveBatch()
{
if (!$this->havePipeline()) {
$this->markTestSkipped('Pipeline not supported');
return;
}

$this->waitForSaveNotInProgress();

$this->valkey_glide->pipeline();
$this->valkey_glide->bgSave();
$result = $this->valkey_glide->exec();
$this->assertTrue($result[0]);

$this->waitForSaveNotInProgress();

$this->valkey_glide->pipeline();
$this->valkey_glide->bgSave('SCHEDULE');
$result = $this->valkey_glide->exec();
$this->assertTrue($result[0]);

if ($this->minVersionCheck('8.1.0')) {
$this->waitForSaveNotInProgress();

$this->valkey_glide->pipeline();
$this->valkey_glide->bgSave('CANCEL');
$result = $this->valkey_glide->exec();
$this->assertFalse($result[0]);
}
}

/**
* Valid BGSAVE response strings (used when OPT_REPLY_LITERAL is enabled).
*/
protected function bgsaveResponses(): array
{
return [
'Background saving started',
'Background saving scheduled',
];
}

/**
* Returns true if a background save (RDB or AOF) is currently in progress.
*/
protected function isSaveInProgress(): bool
{
$info = $this->valkey_glide->info('persistence');
return $info['rdb_bgsave_in_progress'] == '1'
|| $info['aof_rewrite_in_progress'] == '1';
}

/**
* Helper method to wait for any background save operations to complete.
*/
protected function waitForSaveNotInProgress()
{
$this->waitFor(
fn() => !$this->isSaveInProgress(),
10,
'Timed out waiting for background save to complete'
);
}
Comment thread
currantw marked this conversation as resolved.

public function testTTL()
{
$this->valkey_glide->set('x', 'y');
Expand Down
16 changes: 16 additions & 0 deletions valkey_glide.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,22 @@ public function flushAll(?bool $sync = null): ValkeyGlide|bool;
*/
public function flushDB(?bool $sync = null): ValkeyGlide|bool;

/**
* Asynchronously saves the dataset to disk in the background.
*
* When called without a mode argument, initiates a background save immediately.
* When called with "SCHEDULE", schedules a background save to run when possible.
* When called with "CANCEL", aborts all in-progress and scheduled background saves (Valkey 8.1+).
*
* @param string|null $mode Optional mode: null for default, "SCHEDULE" to schedule,
* or "CANCEL" to abort (requires Valkey 8.1+).
* @return ValkeyGlide|bool|string Returns true on success, false on failure.
* With OPT_REPLY_LITERAL enabled, returns the status string instead.
*
* @see https://valkey.io/commands/bgsave
*/
public function bgSave(?string $mode = null): ValkeyGlide|bool|string;
Comment thread
prateek-kumar-improving marked this conversation as resolved.

/**
* Functions is an API for managing code to be executed on the server.
*
Expand Down
5 changes: 5 additions & 0 deletions valkey_glide_cluster.c
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,11 @@ FLUSHDB_METHOD_IMPL(ValkeyGlideCluster)
FLUSHALL_METHOD_IMPL(ValkeyGlideCluster)
/* }}} */

/* {{{ proto ValkeyGlideCluster::bgSave(mixed route, [string mode])
* Asynchronously saves the dataset to disk in the background. */
BGSAVE_METHOD_IMPL(ValkeyGlideCluster)
/* }}} */

/* {{{ proto ValkeyGlideCluster::dbsize(string key)
* proto ValkeyGlideCluster::dbsize(array host_port) */
DBSIZE_METHOD_IMPL(ValkeyGlideCluster)
Expand Down
Loading
Loading