Skip to content

Commit 61cae59

Browse files
committed
feat: Add Big Segment store support
Release-As: 2.0.0
1 parent ccb15fe commit 61cae59

File tree

5 files changed

+203
-4
lines changed

5 files changed

+203
-4
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
"ext-redis": "*",
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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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 Psr\Log\LoggerInterface;
12+
use Redis;
13+
14+
/**
15+
* Internal implementation of the php-redis BigSegmentsStore interface.
16+
*/
17+
class PHPRedisBigSegmentsStore 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 Redis $connection,
31+
private readonly LoggerInterface $logger,
32+
readonly array $options = []
33+
) {
34+
/** @var string */
35+
$this->prefix = $options['prefix'] ?? Integrations\PHPRedis::DEFAULT_PREFIX;
36+
}
37+
38+
public function getMetadata(): Types\BigSegmentsStoreMetadata
39+
{
40+
try {
41+
/** @var string|false */
42+
$lastUpToDate = $this->connection->get($this->prefix . self::KEY_LAST_UP_TO_DATE);
43+
} catch (Exception $e) {
44+
$this->logger->warning('Error getting last-up-to-date time from Redis', ['exception' => $e->getMessage()]);
45+
return new Types\BigSegmentsStoreMetadata(lastUpToDate: null);
46+
}
47+
48+
if ($lastUpToDate === false) {
49+
$lastUpToDate = null;
50+
} else {
51+
$lastUpToDate = (int)$lastUpToDate;
52+
}
53+
54+
return new Types\BigSegmentsStoreMetadata(lastUpToDate: $lastUpToDate);
55+
}
56+
57+
public function getMembership(string $contextHash): ?array
58+
{
59+
try {
60+
/** @var array<string> */
61+
$includeRefs = $this->connection->sMembers($this->prefix . self::KEY_CONTEXT_INCLUDE . $contextHash);
62+
/** @var array<string> */
63+
$excludeRefs = $this->connection->sMembers($this->prefix . self::KEY_CONTEXT_EXCLUDE . $contextHash);
64+
} catch (Exception $e) {
65+
$this->logger->warning('Error getting big segments membership from Redis', ['exception' => $e->getMessage()]);
66+
return null;
67+
}
68+
69+
$membership = [];
70+
foreach ($excludeRefs as $ref) {
71+
$membership[$ref] = false;
72+
}
73+
74+
foreach ($includeRefs as $ref) {
75+
$membership[$ref] = true;
76+
}
77+
78+
return $membership;
79+
}
80+
}

src/LaunchDarkly/Impl/Integrations/PHPRedisFeatureRequester.php

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

33
namespace LaunchDarkly\Impl\Integrations;
44

5+
use LaunchDarkly\Integrations;
56
use Redis;
67

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

2627
/** @var ?Redis */

src/LaunchDarkly/Integrations/PHPRedis.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
namespace LaunchDarkly\Integrations;
44

5-
use LaunchDarkly\Impl\Integrations\PHPRedisFeatureRequester;
5+
use LaunchDarkly\Impl\Integrations;
6+
use LaunchDarkly\Subsystems;
7+
use Psr\Log\LoggerInterface;
8+
use Redis;
69

710
/**
811
* Integration with a Redis data store using the `phpredis` extension.
912
*/
1013
class PHPRedis
1114
{
15+
const DEFAULT_PREFIX = 'launchdarkly';
16+
1217
/**
1318
* Configures an adapter for reading feature flag data from Redis using persistent connections.
1419
*
@@ -40,7 +45,23 @@ public static function featureRequester($options = [])
4045
}
4146

4247
return function (string $baseUri, string $sdkKey, array $baseOptions) use ($options) {
43-
return new PHPRedisFeatureRequester($baseUri, $sdkKey, array_merge($baseOptions, $options));
48+
return new Integrations\PHPRedisFeatureRequester($baseUri, $sdkKey, array_merge($baseOptions, $options));
49+
};
50+
}
51+
52+
/**
53+
* @param array<string,mixed> $options
54+
* - `prefix`: namespace prefix to add to all hash keys
55+
* @return callable(LoggerInterface, array): Subsystems\BigSegmentsStore
56+
*/
57+
public static function bigSegmentsStore(Redis $client, array $options = []): callable
58+
{
59+
if (!extension_loaded('redis')) {
60+
throw new \RuntimeException("phpredis extension is required to use Integrations\\PHPRedis");
61+
}
62+
63+
return function (LoggerInterface $logger, array $baseOptions) use ($client, $options): Subsystems\BigSegmentsStore {
64+
return new Integrations\PHPRedisBigSegmentsStore($client, $logger, array_merge($baseOptions, $options));
4465
};
4566
}
4667
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace LaunchDarkly\Impl\Integrations\Tests\Impl\Integrations;
4+
5+
use Exception;
6+
use LaunchDarkly\Impl\Integrations\PHPRedisBigSegmentsStore;
7+
use PHPUnit\Framework;
8+
use Psr\Log;
9+
use Redis;
10+
11+
class PHPRedisBigSegmentsStoreTest extends Framework\TestCase
12+
{
13+
public function testGetMetadata(): void
14+
{
15+
$now = time();
16+
$logger = new Log\NullLogger();
17+
18+
$connection = $this->createMock(Redis::class);
19+
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
20+
21+
$connection->expects($this->once())
22+
->method('get')
23+
->with('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(Redis::class);
37+
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
38+
39+
$connection->expects($this->once())
40+
->method('get')
41+
->with('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(Redis::class);
55+
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
56+
57+
$connection->expects($this->exactly(2))
58+
->method('sMembers')
59+
->willReturnCallback(function (string $key) {
60+
return match ($key) {
61+
'launchdarkly:big_segment_include:ctx' => ['key1', 'key2'],
62+
'launchdarkly:big_segment_exclude:ctx' => ['key1', 'key3'],
63+
default => [],
64+
};
65+
});
66+
67+
$membership = $store->getMembership('ctx') ?? [];
68+
69+
$this->assertCount(3, $membership);
70+
$this->assertTrue($membership['key1']);
71+
$this->assertTrue($membership['key2']);
72+
$this->assertFalse($membership['key3']);
73+
}
74+
75+
public function testCanDetectInclusionWithException(): void
76+
{
77+
$logger = new Log\NullLogger();
78+
79+
$connection = $this->createMock(Redis::class);
80+
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
81+
82+
$connection->expects($this->exactly(2))
83+
->method('sMembers')
84+
->willReturnCallback(function (string $key) {
85+
return match ($key) {
86+
'launchdarkly:big_segment_include:ctx' => ['key1', 'key2'],
87+
'launchdarkly:big_segment_exclude:ctx' => throw new Exception('sorry'),
88+
default => [],
89+
};
90+
});
91+
92+
$membership = $store->getMembership('ctx');
93+
94+
$this->assertNull($membership);
95+
}
96+
}

0 commit comments

Comments
 (0)