Skip to content

Commit

Permalink
feat: Add Big Segment store support
Browse files Browse the repository at this point in the history
Release-As: 2.0.0
  • Loading branch information
keelerm84 committed Jan 17, 2025
1 parent e2a0a73 commit d11c37a
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 3 deletions.
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",
"predis/predis": ">=2.3.0 <3.0.0",
"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
74 changes: 74 additions & 0 deletions src/LaunchDarkly/Impl/Integrations/RedisBigSegmentsStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace LaunchDarkly\Impl\Integrations;

use Exception;
use LaunchDarkly\Integrations;
use LaunchDarkly\Subsystems;
use LaunchDarkly\Types;
use Predis\ClientInterface;
use Psr\Log\LoggerInterface;

/**
* Internal implementation of the Predis BigSegmentsStore interface.
*/
class RedisBigSegmentsStore 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 ClientInterface $connection,
private readonly LoggerInterface $logger,
readonly array $options = []
) {
$this->prefix = $options['prefix'] ?? Integrations\Redis::DEFAULT_PREFIX;
}

public function getMetadata(): Types\BigSegmentsStoreMetadata
{
try {
$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 !== null) {
$lastUpToDate = (int)$lastUpToDate;
}

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

public function getMembership(string $contextHash): ?array
{
try {
$includeRefs = $this->connection->smembers($this->prefix . self::KEY_CONTEXT_INCLUDE . $contextHash);
$excludeRefs = $this->connection->smembers($this->prefix . self::KEY_CONTEXT_EXCLUDE . $contextHash);
} catch (Exception $e) {
$this->logger->warning('Error getting big segment 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;
}
}
18 changes: 17 additions & 1 deletion src/LaunchDarkly/Integrations/Redis.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

namespace LaunchDarkly\Integrations;

use \LaunchDarkly\Impl\Integrations\RedisFeatureRequester;
use LaunchDarkly\Impl\Integrations\RedisFeatureRequester;
use LaunchDarkly\Impl\Integrations\RedisBigSegmentsStore;
use LaunchDarkly\Subsystems;
use Predis\ClientInterface;
use Psr\Log\LoggerInterface;

/**
* Integration with a Redis data store using the `predis` package.
Expand Down Expand Up @@ -45,4 +49,16 @@ public static function featureRequester(array $options = [])
return new RedisFeatureRequester($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(ClientInterface $client, array $options = []): callable
{
return function (LoggerInterface $logger, array $baseOptions) use ($client, $options): Subsystems\BigSegmentsStore {
return new RedisBigSegmentsStore($client, $logger, array_merge($baseOptions, $options));
};
}
}
104 changes: 104 additions & 0 deletions tests/Impl/Integrations/RedisBigSegmentStoreTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

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

use Exception;
use LaunchDarkly\Impl\Integrations\RedisBigSegmentsStore;
use PHPUnit\Framework;
use Predis\ClientInterface;
use Psr\Log;

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

$connection = $this->createMock(ClientInterface::class);
$store = new RedisBigSegmentsStore($connection, $logger, []);

$connection->expects($this->once())
->method('__call')
->with('get', ['launchdarkly:big_segments_synchronized_on'])
->willReturn("$now");

$metadata = $store->getMetadata();

$this->assertEquals($now, $metadata->getLastUpToDate());
$this->assertFalse($metadata->isStale(10));
}

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

$connection = $this->createMock(ClientInterface::class);
$store = new RedisBigSegmentsStore($connection, $logger, []);

$connection->expects($this->once())
->method('__call')
->with('get', ['launchdarkly:big_segments_synchronized_on'])
->willThrowException(new \Exception('sorry'));

$metadata = $store->getMetadata();

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

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

$connection = $this->createMock(ClientInterface::class);
$store = new RedisBigSegmentsStore($connection, $logger, []);

$connection->expects($this->exactly(2))
->method('__call')
->willReturnCallback(function ($method, $args) {
if ($method !== 'smembers') {
return;
}

return match ($args[0]) {
'launchdarkly:big_segment_include:ctx' => ['key1', 'key2'],
'launchdarkly:big_segment_exclude:ctx' => ['key1', 'key3'],
default => [],
};
});

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

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

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

$connection = $this->createMock(ClientInterface::class);
$store = new RedisBigSegmentsStore($connection, $logger, []);

$connection->expects($this->exactly(2))
->method('__call')
->willReturnCallback(function ($method, $args) {
if ($method !== 'smembers') {
return;
}

return match ($args[0]) {
'launchdarkly:big_segment_include:ctx' => ['key1', 'key2'],
'launchdarkly:big_segment_exclude:ctx' => throw new Exception('sorry'),
default => [],
};
});

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

$this->assertNull($membership);
}
}
1 change: 0 additions & 1 deletion tests/RedisFeatureRequesterWithClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace LaunchDarkly\Impl\Integrations\Tests;

use LaunchDarkly\Impl\Integrations\RedisFeatureRequester;
use LaunchDarkly\Integrations\Redis;
use LaunchDarkly\SharedTest\DatabaseFeatureRequesterTestBase;
use Predis\Client;
Expand Down

0 comments on commit d11c37a

Please sign in to comment.