Skip to content

feat: Add Big Segment store support #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"require": {
"php": ">=8.1",
"ext-redis": "*",
"launchdarkly/server-sdk": ">=6.3.0 <7.0.0"
"launchdarkly/server-sdk": ">=6.4.0 <7.0.0",
"psr/log": "^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.68",
Expand Down
80 changes: 80 additions & 0 deletions src/LaunchDarkly/Impl/Integrations/PHPRedisBigSegmentsStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

namespace LaunchDarkly\Impl\Integrations;

use Exception;
use LaunchDarkly\Integrations;
use LaunchDarkly\Subsystems;
use LaunchDarkly\Types;
use Psr\Log\LoggerInterface;
use Redis;

/**
* Internal implementation of the php-redis BigSegmentsStore interface.
*/
class PHPRedisBigSegmentsStore implements Subsystems\BigSegmentsStore
{
private const KEY_LAST_UP_TO_DATE = ':big_segments_synchronized_on';
private const KEY_CONTEXT_INCLUDE = ':big_segment_include:';
private const KEY_CONTEXT_EXCLUDE = ':big_segment_exclude:';

private readonly string $prefix;

/**
* @param array<string,mixed> $options
* - `prefix`: namespace prefix to add to all hash keys
*/
public function __construct(
private readonly Redis $connection,
private readonly LoggerInterface $logger,
readonly array $options = []
) {
/** @var string */
$this->prefix = $options['prefix'] ?? Integrations\PHPRedis::DEFAULT_PREFIX;
}

public function getMetadata(): Types\BigSegmentsStoreMetadata
{
try {
/** @var string|false */
$lastUpToDate = $this->connection->get($this->prefix . self::KEY_LAST_UP_TO_DATE);
} catch (Exception $e) {
$this->logger->warning('Error getting last-up-to-date time from Redis', ['exception' => $e->getMessage()]);
return new Types\BigSegmentsStoreMetadata(lastUpToDate: null);
}

if ($lastUpToDate === false) {
$lastUpToDate = null;
} else {
$lastUpToDate = (int)$lastUpToDate;
}

return new Types\BigSegmentsStoreMetadata(lastUpToDate: $lastUpToDate);
}

public function getMembership(string $contextHash): ?array
{
try {
/** @var array<string> */
$includeRefs = $this->connection->sMembers($this->prefix . self::KEY_CONTEXT_INCLUDE . $contextHash);
/** @var array<string> */
$excludeRefs = $this->connection->sMembers($this->prefix . self::KEY_CONTEXT_EXCLUDE . $contextHash);
} catch (Exception $e) {
$this->logger->warning('Error getting big segments membership from Redis', ['exception' => $e->getMessage()]);
return null;
}

$membership = [];
foreach ($excludeRefs as $ref) {
$membership[$ref] = false;
}

foreach ($includeRefs as $ref) {
$membership[$ref] = true;
}

return $membership;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace LaunchDarkly\Impl\Integrations;

use LaunchDarkly\Integrations;
use Redis;

/**
Expand All @@ -20,7 +21,7 @@ public function __construct(string $baseUri, string $sdkKey, array $options)
/** @var ?string **/
$this->prefix = $options['redis_prefix'] ?? null;
if ($this->prefix === null || $this->prefix === '') {
$this->prefix = 'launchdarkly';
$this->prefix = Integrations\PHPRedis::DEFAULT_PREFIX;
}

/** @var ?Redis */
Expand Down
25 changes: 23 additions & 2 deletions src/LaunchDarkly/Integrations/PHPRedis.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

namespace LaunchDarkly\Integrations;

use LaunchDarkly\Impl\Integrations\PHPRedisFeatureRequester;
use LaunchDarkly\Impl\Integrations;
use LaunchDarkly\Subsystems;
use Psr\Log\LoggerInterface;
use Redis;

/**
* Integration with a Redis data store using the `phpredis` extension.
*/
class PHPRedis
{
const DEFAULT_PREFIX = 'launchdarkly';

/**
* Configures an adapter for reading feature flag data from Redis using persistent connections.
*
Expand Down Expand Up @@ -40,7 +45,23 @@ public static function featureRequester($options = [])
}

return function (string $baseUri, string $sdkKey, array $baseOptions) use ($options) {
return new PHPRedisFeatureRequester($baseUri, $sdkKey, array_merge($baseOptions, $options));
return new Integrations\PHPRedisFeatureRequester($baseUri, $sdkKey, array_merge($baseOptions, $options));
};
}

/**
* @param array<string,mixed> $options
* - `prefix`: namespace prefix to add to all hash keys
* @return callable(LoggerInterface, array): Subsystems\BigSegmentsStore
*/
public static function bigSegmentsStore(Redis $client, array $options = []): callable
{
if (!extension_loaded('redis')) {
throw new \RuntimeException("phpredis extension is required to use Integrations\\PHPRedis");
}

return function (LoggerInterface $logger, array $baseOptions) use ($client, $options): Subsystems\BigSegmentsStore {
return new Integrations\PHPRedisBigSegmentsStore($client, $logger, array_merge($baseOptions, $options));
};
}
}
88 changes: 88 additions & 0 deletions tests/Impl/Integrations/PHPRedisBigSegmentsStoreTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace LaunchDarkly\Impl\Integrations\Tests\Impl\Integrations;

use LaunchDarkly\Impl\Integrations\PHPRedisBigSegmentsStore;
use PHPUnit\Framework;
use Psr\Log;
use Redis;

class PHPRedisBigSegmentsStoreTest extends Framework\TestCase
{
public function testGetMetadata(): void
{
$now = time();
$logger = new Log\NullLogger();

$connection = new Redis();
$connection->flushAll();
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);

$metadata = $store->getMetadata();
$this->assertNull($metadata->getLastUpToDate());
$this->assertTrue($metadata->isStale(10));

$connection->set('launchdarkly:big_segments_synchronized_on', $now);
$metadata = $store->getMetadata();
$this->assertEquals($now, $metadata->getLastUpToDate());
$this->assertFalse($metadata->isStale(10));
}

public function testGetMetadataWithInvalidConfiguration(): void
{
$logger = new Log\NullLogger();

$connection = new Redis(['port' => 33_333, 'connectTimeout' => 1]);
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);

$metadata = $store->getMetadata();

$this->assertNull($metadata->getLastUpToDate());
$this->assertTrue($metadata->isStale(10));
}

public function testCanDetectInclusion(): void
{
$logger = new Log\NullLogger();

$connection = new Redis();
$connection->flushAll();
$connection->sAdd('launchdarkly:big_segment_include:ctx', 'key1', 'key2');
$connection->sAdd('launchdarkly:big_segment_exclude:ctx', 'key1', 'key3');

$store = new PHPRedisBigSegmentsStore($connection, $logger, []);

$membership = $store->getMembership('ctx') ?? [];

$this->assertCount(3, $membership);
$this->assertTrue($membership['key1']);
$this->assertTrue($membership['key2']);
$this->assertFalse($membership['key3']);
}

public function testCanDetectInclusionWithEmptyData(): void
{
$logger = new Log\NullLogger();

$connection = new Redis();
$connection->flushAll();

$store = new PHPRedisBigSegmentsStore($connection, $logger, []);

$membership = $store->getMembership('ctx');

$this->assertNotNull($membership);
$this->assertCount(0, $membership);
}

public function testCanDetectInclusionWithInvalidConfiguration(): void
{
$logger = new Log\NullLogger();

$connection = new Redis(['port' => 33_333, 'connectTimeout' => 1]);
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
$membership = $store->getMembership('ctx');

$this->assertNull($membership);
}
}
Loading