Skip to content

Commit 21c61b8

Browse files
committed
feat: Add Big Segment store support
Release-As: 2.0.0
1 parent 63c3815 commit 21c61b8

File tree

5 files changed

+197
-3
lines changed

5 files changed

+197
-3
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"require": {
2323
"php": ">=8.1",
2424
"predis/predis": ">=2.3.0 <3.0.0",
25-
"launchdarkly/server-sdk": ">=6.3.0 <7.0.0"
25+
"launchdarkly/server-sdk": ">=6.4.0 <7.0.0",
26+
"psr/log": "^3.0"
2627
},
2728
"require-dev": {
2829
"friendsofphp/php-cs-fixer": "^3.68",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaunchDarkly\Impl\Integrations;
6+
7+
use Exception;
8+
use LaunchDarkly\Integrations;
9+
use LaunchDarkly\Subsystems;
10+
use LaunchDarkly\Types;
11+
use Predis\ClientInterface;
12+
use Psr\Log\LoggerInterface;
13+
14+
/**
15+
* Internal implementation of the Predis BigSegmentsStore interface.
16+
*/
17+
class RedisBigSegmentsStore implements Subsystems\BigSegmentsStore
18+
{
19+
private const KEY_LAST_UP_TO_DATE = ':big_segments_synchronized_on';
20+
private const KEY_CONTEXT_INCLUDE = ':big_segment_include:';
21+
private const KEY_CONTEXT_EXCLUDE = ':big_segment_exclude:';
22+
23+
private readonly string $prefix;
24+
25+
/**
26+
* @param array<string,mixed> $options
27+
* - `prefix`: namespace prefix to add to all hash keys
28+
*/
29+
public function __construct(
30+
private readonly ClientInterface $connection,
31+
private readonly LoggerInterface $logger,
32+
readonly array $options = []
33+
) {
34+
$this->prefix = $options['prefix'] ?? Integrations\Redis::DEFAULT_PREFIX;
35+
}
36+
37+
public function getMetadata(): Types\BigSegmentsStoreMetadata
38+
{
39+
try {
40+
$lastUpToDate = $this->connection->get($this->prefix . self::KEY_LAST_UP_TO_DATE);
41+
} catch (Exception $e) {
42+
$this->logger->warning('Error getting last-up-to-date time from Redis', ['exception' => $e->getMessage()]);
43+
return new Types\BigSegmentsStoreMetadata(lastUpToDate: null);
44+
}
45+
46+
if ($lastUpToDate !== null) {
47+
$lastUpToDate = (int)$lastUpToDate;
48+
}
49+
50+
return new Types\BigSegmentsStoreMetadata(lastUpToDate: $lastUpToDate);
51+
}
52+
53+
public function getMembership(string $contextHash): ?array
54+
{
55+
try {
56+
$includeRefs = $this->connection->smembers($this->prefix . self::KEY_CONTEXT_INCLUDE . $contextHash);
57+
$excludeRefs = $this->connection->smembers($this->prefix . self::KEY_CONTEXT_EXCLUDE . $contextHash);
58+
} catch (Exception $e) {
59+
$this->logger->warning('Error getting big segment membership from Redis', ['exception' => $e->getMessage()]);
60+
return null;
61+
}
62+
63+
$membership = [];
64+
foreach ($excludeRefs as $ref) {
65+
$membership[$ref] = false;
66+
}
67+
68+
foreach ($includeRefs as $ref) {
69+
$membership[$ref] = true;
70+
}
71+
72+
return $membership;
73+
}
74+
}

src/LaunchDarkly/Integrations/Redis.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
namespace LaunchDarkly\Integrations;
44

5-
use \LaunchDarkly\Impl\Integrations\RedisFeatureRequester;
5+
use LaunchDarkly\Impl\Integrations\RedisFeatureRequester;
6+
use LaunchDarkly\Impl\Integrations\RedisBigSegmentsStore;
7+
use LaunchDarkly\Subsystems;
8+
use Predis\ClientInterface;
9+
use Psr\Log\LoggerInterface;
610

711
/**
812
* Integration with a Redis data store using the `predis` package.
@@ -45,4 +49,16 @@ public static function featureRequester(array $options = [])
4549
return new RedisFeatureRequester($baseUri, $sdkKey, array_merge($baseOptions, $options));
4650
};
4751
}
52+
53+
/**
54+
* @param array<string,mixed> $options
55+
* - `prefix`: namespace prefix to add to all hash keys
56+
* @return callable(LoggerInterface, array): Subsystems\BigSegmentsStore
57+
*/
58+
public static function bigSegmentsStore(ClientInterface $client, array $options = []): callable
59+
{
60+
return function (LoggerInterface $logger, array $baseOptions) use ($client, $options): Subsystems\BigSegmentsStore {
61+
return new RedisBigSegmentsStore($client, $logger, array_merge($baseOptions, $options));
62+
};
63+
}
4864
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
namespace LaunchDarkly\Impl\Integrations\Tests\Impl\Integrations;
4+
5+
use Exception;
6+
use LaunchDarkly\Impl\Integrations\RedisBigSegmentsStore;
7+
use PHPUnit\Framework;
8+
use Predis\ClientInterface;
9+
use Psr\Log;
10+
11+
class RedisBigSegmentsStoreTest extends Framework\TestCase
12+
{
13+
public function testGetMetadata(): void
14+
{
15+
$now = time();
16+
$logger = new Log\NullLogger();
17+
18+
$connection = $this->createMock(ClientInterface::class);
19+
$store = new RedisBigSegmentsStore($connection, $logger, []);
20+
21+
$connection->expects($this->once())
22+
->method('__call')
23+
->with('get', ['launchdarkly:big_segments_synchronized_on'])
24+
->willReturn("$now");
25+
26+
$metadata = $store->getMetadata();
27+
28+
$this->assertEquals($now, $metadata->getLastUpToDate());
29+
$this->assertFalse($metadata->isStale(10));
30+
}
31+
32+
public function testGetMetadataWithException(): void
33+
{
34+
$logger = new Log\NullLogger();
35+
36+
$connection = $this->createMock(ClientInterface::class);
37+
$store = new RedisBigSegmentsStore($connection, $logger, []);
38+
39+
$connection->expects($this->once())
40+
->method('__call')
41+
->with('get', ['launchdarkly:big_segments_synchronized_on'])
42+
->willThrowException(new \Exception('sorry'));
43+
44+
$metadata = $store->getMetadata();
45+
46+
$this->assertNull($metadata->getLastUpToDate());
47+
$this->assertTrue($metadata->isStale(10));
48+
}
49+
50+
public function testCanDetectInclusion(): void
51+
{
52+
$logger = new Log\NullLogger();
53+
54+
$connection = $this->createMock(ClientInterface::class);
55+
$store = new RedisBigSegmentsStore($connection, $logger, []);
56+
57+
$connection->expects($this->exactly(2))
58+
->method('__call')
59+
->willReturnCallback(function ($method, $args) {
60+
if ($method !== 'smembers') {
61+
return;
62+
}
63+
64+
return match ($args[0]) {
65+
'launchdarkly:big_segment_include:ctx' => ['key1', 'key2'],
66+
'launchdarkly:big_segment_exclude:ctx' => ['key1', 'key3'],
67+
default => [],
68+
};
69+
});
70+
71+
$membership = $store->getMembership('ctx');
72+
73+
$this->assertCount(3, $membership);
74+
$this->assertTrue($membership['key1']);
75+
$this->assertTrue($membership['key2']);
76+
$this->assertFalse($membership['key3']);
77+
}
78+
79+
public function testCanDetectInclusionWithException(): void
80+
{
81+
$logger = new Log\NullLogger();
82+
83+
$connection = $this->createMock(ClientInterface::class);
84+
$store = new RedisBigSegmentsStore($connection, $logger, []);
85+
86+
$connection->expects($this->exactly(2))
87+
->method('__call')
88+
->willReturnCallback(function ($method, $args) {
89+
if ($method !== 'smembers') {
90+
return;
91+
}
92+
93+
return match ($args[0]) {
94+
'launchdarkly:big_segment_include:ctx' => ['key1', 'key2'],
95+
'launchdarkly:big_segment_exclude:ctx' => throw new Exception('sorry'),
96+
default => [],
97+
};
98+
});
99+
100+
$membership = $store->getMembership('ctx');
101+
102+
$this->assertNull($membership);
103+
}
104+
}

tests/RedisFeatureRequesterWithClientTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace LaunchDarkly\Impl\Integrations\Tests;
44

5-
use LaunchDarkly\Impl\Integrations\RedisFeatureRequester;
65
use LaunchDarkly\Integrations\Redis;
76
use LaunchDarkly\SharedTest\DatabaseFeatureRequesterTestBase;
87
use Predis\Client;

0 commit comments

Comments
 (0)