Skip to content

Commit a7d69a3

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

File tree

5 files changed

+195
-4
lines changed

5 files changed

+195
-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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace LaunchDarkly\Impl\Integrations\Tests\Impl\Integrations;
4+
5+
use LaunchDarkly\Impl\Integrations\PHPRedisBigSegmentsStore;
6+
use PHPUnit\Framework;
7+
use Psr\Log;
8+
use Redis;
9+
10+
class PHPRedisBigSegmentsStoreTest extends Framework\TestCase
11+
{
12+
public function testGetMetadata(): void
13+
{
14+
$now = time();
15+
$logger = new Log\NullLogger();
16+
17+
$connection = new Redis();
18+
$connection->flushAll();
19+
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
20+
21+
$metadata = $store->getMetadata();
22+
$this->assertNull($metadata->getLastUpToDate());
23+
$this->assertTrue($metadata->isStale(10));
24+
25+
$connection->set('launchdarkly:big_segments_synchronized_on', $now);
26+
$metadata = $store->getMetadata();
27+
$this->assertEquals($now, $metadata->getLastUpToDate());
28+
$this->assertFalse($metadata->isStale(10));
29+
}
30+
31+
public function testGetMetadataWithInvalidConfiguration(): void
32+
{
33+
$logger = new Log\NullLogger();
34+
35+
$connection = new Redis(['port' => 33_333, 'connectTimeout' => 1]);
36+
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
37+
38+
$metadata = $store->getMetadata();
39+
40+
$this->assertNull($metadata->getLastUpToDate());
41+
$this->assertTrue($metadata->isStale(10));
42+
}
43+
44+
public function testCanDetectInclusion(): void
45+
{
46+
$logger = new Log\NullLogger();
47+
48+
$connection = new Redis();
49+
$connection->flushAll();
50+
$connection->sAdd('launchdarkly:big_segment_include:ctx', 'key1', 'key2');
51+
$connection->sAdd('launchdarkly:big_segment_exclude:ctx', 'key1', 'key3');
52+
53+
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
54+
55+
$membership = $store->getMembership('ctx') ?? [];
56+
57+
$this->assertCount(3, $membership);
58+
$this->assertTrue($membership['key1']);
59+
$this->assertTrue($membership['key2']);
60+
$this->assertFalse($membership['key3']);
61+
}
62+
63+
public function testCanDetectInclusionWithEmptyData(): void
64+
{
65+
$logger = new Log\NullLogger();
66+
67+
$connection = new Redis();
68+
$connection->flushAll();
69+
70+
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
71+
72+
$membership = $store->getMembership('ctx');
73+
74+
$this->assertNotNull($membership);
75+
$this->assertCount(0, $membership);
76+
}
77+
78+
public function testCanDetectInclusionWithInvalidConfiguration(): void
79+
{
80+
$logger = new Log\NullLogger();
81+
82+
$connection = new Redis(['port' => 33_333, 'connectTimeout' => 1]);
83+
$store = new PHPRedisBigSegmentsStore($connection, $logger, []);
84+
$membership = $store->getMembership('ctx');
85+
86+
$this->assertNull($membership);
87+
}
88+
}

0 commit comments

Comments
 (0)