Skip to content

Commit 4ccaf62

Browse files
committed
Add APC-enabled LDD requester, integ tests
1 parent 9988908 commit 4ccaf62

11 files changed

+292
-15
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
/doc/
33
*.iml
44
composer.phar
5+
.vagrant
6+
integration-tests/vendor
7+
integration-tests/composer.lock
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
namespace LaunchDarkly\Tests;
3+
4+
require_once 'vendor/autoload.php';
5+
6+
use LaunchDarkly\LDClient;
7+
use LaunchDarkly\LDUserBuilder;
8+
9+
class LDDFeatureRetrieverTest extends \PHPUnit_Framework_TestCase {
10+
11+
public function testGet() {
12+
$redis = new \Predis\Client(array(
13+
"scheme" => "tcp",
14+
"host" => 'localhost',
15+
"port" => 6379));
16+
$client = new LDClient("BOGUS_API_KEY", array('feature_requester_class' => '\\LaunchDarkly\\LDDFeatureRequester'));
17+
$builder = new LDUserBuilder(3);
18+
$user = $builder->build();
19+
20+
$redis->del("launchdarkly:features");
21+
$this->assertEquals("jim", $client->toggle('foo', $user, 'jim'));
22+
$redis->hset("launchdarkly:features", 'foo', $this->gen_feature("foo", "bar"));
23+
$this->assertEquals("bar", $client->toggle('foo', $user, 'jim'));
24+
}
25+
26+
public function testGetApc() {
27+
$redis = new \Predis\Client(array(
28+
"scheme" => "tcp",
29+
"host" => 'localhost',
30+
"port" => 6379));
31+
$client = new LDClient("BOGUS_API_KEY", array('feature_requester_class' => '\\LaunchDarkly\\ApcLDDFeatureRequester',
32+
'apc_expiration' => 1));
33+
$builder = new LDUserBuilder(3);
34+
$user = $builder->build();
35+
36+
$redis->del("launchdarkly:features");
37+
$this->assertEquals("jim", $client->toggle('foo', $user, 'jim'));
38+
$redis->hset("launchdarkly:features", 'foo', $this->gen_feature("foo", "bar"));
39+
$this->assertEquals("bar", $client->toggle('foo', $user, 'jim'));
40+
41+
# cached value so not updated
42+
$redis->hset("launchdarkly:features", 'foo', $this->gen_feature("foo", "baz"));
43+
$this->assertEquals("bar", $client->toggle('foo', $user, 'jim'));
44+
45+
apc_delete("launchdarkly:features.foo");
46+
$this->assertEquals("baz", $client->toggle('foo', $user, 'jim'));
47+
}
48+
49+
private function gen_feature($key, $val) {
50+
$data = <<<EOF
51+
{"name": "Feature $key", "key": "$key", "kind": "flag", "salt": "Zm9v", "on": true,
52+
"variations": [{"value": "$val", "weight": 100,
53+
"targets": [{"attribute": "key", "op": "in", "values": []}],
54+
"userTarget": {"attribute": "key", "op": "in", "values": []}},
55+
{"value": false, "weight": 0,
56+
"targets": [{"attribute": "key", "op": "in", "values": []}],
57+
"userTarget": {"attribute": "key", "op": "in", "values": []}}],
58+
"commitDate": "2015-09-08T21:24:16.712Z",
59+
"creationDate": "2015-09-08T21:06:16.527Z",
60+
"version": 4}
61+
EOF;
62+
return $data;
63+
}
64+
65+
}
66+

integration-tests/README.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
To run the tests, run:
2+
3+
vagrant up
4+
vagrant ssh
5+
cd project/integration-tests
6+
vendor/phpunit/phpunit/phpunit LDDFeatureRequesterTest.php
7+

integration-tests/Vagrantfile

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# -*- mode: ruby -*-
2+
# vi: set ft=ruby :
3+
4+
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
5+
VAGRANTFILE_API_VERSION = "2"
6+
7+
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
8+
# All Vagrant configuration is done here. The most common configuration
9+
# options are documented and commented below. For a complete reference,
10+
# please see the online documentation at vagrantup.com.
11+
12+
# Every Vagrant virtual environment requires a box to build off of.
13+
config.vm.box = "ubuntu/trusty64"
14+
15+
# The url from where the 'config.vm.box' box will be fetched if it
16+
# doesn't already exist on the user's system.
17+
config.vm.box_url = "https://vagrantcloud.com/ubuntu/boxes/trusty64"
18+
19+
config.vm.provision :shell, path: "bootstrap.sh"
20+
21+
# Create a forwarded port mapping which allows access to a specific port
22+
# within the machine from a port on the host machine. In the example below,
23+
# accessing "localhost:8080" will access port 80 on the guest machine.
24+
# config.vm.network :forwarded_port, guest: 80, host: 8080
25+
26+
# Create a private network, which allows host-only access to the machine
27+
# using a specific IP.
28+
# config.vm.network :private_network, ip: "192.168.33.10"
29+
30+
# Create a public network, which generally matched to bridged network.
31+
# Bridged networks make the machine appear as another physical device on
32+
# your network.
33+
# config.vm.network :public_network
34+
35+
# If true, then any SSH connections made will enable agent forwarding.
36+
# Default value: false
37+
# config.ssh.forward_agent = true
38+
39+
# Share an additional folder to the guest VM. The first argument is
40+
# the path on the host to the actual folder. The second argument is
41+
# the path on the guest to mount the folder. And the optional third
42+
# argument is a set of non-required options.
43+
config.vm.synced_folder "..", "/home/vagrant/project"
44+
45+
config.vm.provider :virtualbox do |vb|
46+
vb.auto_nat_dns_proxy = false
47+
vb.customize ["modifyvm", :id, "--natdnsproxy1", "off" ]
48+
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "off" ]
49+
vb.customize ["modifyvm", :id, "--memory", "2048"]
50+
end
51+
end

integration-tests/bootstrap.sh

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/bin/bash
2+
3+
# init
4+
apt-get update 2> /dev/null
5+
6+
# redis
7+
apt-get install -y redis-server 2> /dev/null
8+
9+
# ntp
10+
apt-get install ntp -y 2> /dev/null
11+
service ntp restart
12+
13+
# install dependencies and services
14+
apt-get install unzip -y 2> /dev/null
15+
apt-get install -y vim curl 2> /dev/null
16+
apt-get install git -y 2> /dev/null
17+
18+
# PHP things
19+
echo "Install PHP things"
20+
apt-get install -y php-apc 2> /dev/null
21+
apt-get install -y phpunit 2> /dev/null
22+
23+
# phpbrew stuff for 5.4
24+
apt-get build-dep php5 2> /dev/null
25+
apt-get install -y php5 php5-dev php-pear autoconf automake curl build-essential libxslt1-dev re2c libxml2 libxml2-dev php5-cli bison libbz2-dev libreadline-dev 2> /dev/null
26+
apt-get install -y libfreetype6 libfreetype6-dev libpng12-0 libpng12-dev libjpeg-dev libjpeg8-dev libjpeg8 libgd-dev libgd3 libxpm4 libltdl7 libltdl-dev 2> /dev/null
27+
apt-get install -y libssl-dev openssl 2> /dev/null
28+
apt-get install -y gettext libgettextpo-dev libgettextpo0 2> /dev/null
29+
apt-get install -y php5-cli 2> /dev/null
30+
apt-get install -y libmcrypt-dev 2> /dev/null
31+
apt-get install -y libreadline-dev 2> /dev/null
32+
33+
# set vim tabs
34+
cat <<EOF > /home/vagrant/.vimrc
35+
set tabstop=4
36+
EOF
37+
chown vagrant.vagrant /home/vagrant/.vimrc
38+
39+
su - vagrant
40+
cd ~vagrant
41+
pwd
42+
curl -s -L -O https://github.com/phpbrew/phpbrew/raw/master/phpbrew
43+
chmod +x phpbrew
44+
sudo mv phpbrew /usr/bin/phpbrew
45+
phpbrew init
46+
phpbrew known --update
47+
phpbrew update
48+
phpbrew install 5.4.34 +default
49+
50+
echo "source $HOME/.phpbrew/bashrc" >> /home/vagrant/.bashrc
51+
source $HOME/.bashrc
52+
phpbrew switch php-5.4.34
53+
phpbrew ext install apc
54+
echo "apc.enable_cli = 1" >> ~/.phpbrew/php/php-5.4.34/etc/php.ini
55+
56+
57+
cd /home/vagrant/project/integration-tests
58+
curl -sS https://getcomposer.org/installer | php
59+
php composer.phar install

integration-tests/composer.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "launchdarkly/launchdarkly-php-integration-tests",
3+
"description": "Integration tests",
4+
"homepage": "https://github.com/launchdarkly/php-client",
5+
"license": "Apache-2.0",
6+
"authors": [
7+
{
8+
"name": "LaunchDarkly <team@launchdarkly.com>",
9+
"homepage": "http://launchdarkly.com/"
10+
}
11+
],
12+
"require": {
13+
"php": ">=5.3",
14+
"predis/predis": "1.0.*"
15+
},
16+
"require-dev": {
17+
"phpunit/phpunit": "4.3.*"
18+
},
19+
"autoload": {
20+
"psr-4": {
21+
"": "../src/"
22+
}
23+
},
24+
"autoload-dev": {
25+
"psr-4": {
26+
"": "."
27+
}
28+
}
29+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
namespace LaunchDarkly;
3+
4+
5+
/**
6+
* Feature requester from an LDD-populated redis, with APC caching
7+
*
8+
* @package LaunchDarkly
9+
*/
10+
class ApcLDDFeatureRequester extends LDDFeatureRequester {
11+
protected $_expiration = 30;
12+
13+
function __construct($baseUri, $apiKey, $options) {
14+
parent::__construct($baseUri, $apiKey, $options);
15+
16+
if (isset($options['apc_expiration'])) {
17+
$this->_expiration = (int)$options['apc_expiration'];
18+
}
19+
}
20+
21+
22+
protected function get_from_cache($key) {
23+
$key = self::make_cache_key($key);
24+
$enabled = apc_fetch($key);
25+
if ($enabled === false) {
26+
return null;
27+
}
28+
else {
29+
return $enabled;
30+
}
31+
}
32+
33+
protected function store_in_cache($key, $val) {
34+
apc_add($this->make_cache_key($key), $val, $this->_expiration);
35+
}
36+
37+
private function make_cache_key($name) {
38+
return $this->_features_key.'.'.$name;
39+
}
40+
}

src/LaunchDarkly/FeatureRequester.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface FeatureRequester {
77
* Gets feature data from a likely cached store
88
*
99
* @param $key string feature key
10-
* @return mixed|null The decoded JSON feature data, or null if missing
10+
* @return array|null The decoded JSON feature data, or null if missing
1111
*/
1212
public function get($key);
1313
}

src/LaunchDarkly/GuzzleFeatureRequester.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function __construct($baseUri, $apiKey, $options) {
3636
* Gets feature data from a likely cached store
3737
*
3838
* @param $key string feature key
39-
* @return mixed The decoded JSON feature data, or null if missing
39+
* @return array|null The decoded JSON feature data, or null if missing
4040
*/
4141
public function get($key) {
4242
try {

src/LaunchDarkly/LDClient.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class LDClient {
1717
protected $_offline;
1818

1919
/** @var FeatureRequester */
20-
protected $_featureRetriever;
20+
protected $_featureRequester;
2121

2222
/**
2323
* Creates a new client instance that connects to LaunchDarkly.
@@ -50,12 +50,12 @@ public function __construct($apiKey, $options = array()) {
5050

5151
$this->_eventProcessor = new EventProcessor($apiKey, $options);
5252

53-
if (isset($options['feature_retriever_class'])) {
54-
$featureRetrieverClass = $options['feature_retriever_class'];
53+
if (isset($options['feature_requester_class'])) {
54+
$featureRequesterClass = $options['feature_requester_class'];
5555
} else {
56-
$featureRetrieverClass = '\\LaunchDarkly\\GuzzleFeatureRequester';
56+
$featureRequesterClass = '\\LaunchDarkly\\GuzzleFeatureRequester';
5757
}
58-
$this->_featureRetriever = new $featureRetrieverClass($this->_baseUri, $apiKey, $options);
58+
$this->_featureRequester = new $featureRequesterClass($this->_baseUri, $apiKey, $options);
5959
}
6060

6161
public function getFlag($key, $user, $default = false) {
@@ -184,7 +184,7 @@ protected function _sendFlagRequestEvent($key, $user, $value) {
184184

185185
protected function _toggle($key, $user, $default) {
186186
try {
187-
$data = $this->_featureRetriever->get($key);
187+
$data = $this->_featureRequester->get($key);
188188
if ($data == null) {
189189
error_log("LDClient::_toggle received null from retriever, using default");
190190
return $default;

src/LaunchDarkly/LDDFeatureRetriever.php renamed to src/LaunchDarkly/LDDFeatureRequester.php

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ class LDDFeatureRequester implements FeatureRequester {
1111
function __construct($baseUri, $apiKey, $options) {
1212
$this->_baseUri = $baseUri;
1313
$this->_apiKey = $apiKey;
14-
$this->_options = $options;
1514
if (!isset($options['redis_host'])) {
1615
$options['redis_host'] = 'localhost';
1716
}
1817
if (!isset($options['redis_port'])) {
1918
$options['redis_port'] = 6379;
2019
}
2120

21+
$this->_options = $options;
22+
2223
$prefix = "launchdarkly";
2324
if (isset($options['redis_prefix'])) {
2425
$prefix = $options['redis_prefix'];
@@ -39,16 +40,37 @@ protected function get_connection() {
3940
* Gets feature data from a likely cached store
4041
*
4142
* @param $key string feature key
42-
* @return mixed The decoded JSON feature data, or null if missing
43+
* @return array|null The decoded JSON feature data, or null if missing
4344
*/
4445
public function get($key) {
45-
$redis = $this->get_connection();
46-
$raw = $redis->hget($this->_features_key, $key);
47-
if ($raw) {
48-
return json_decode($raw);
46+
$raw = $this->get_from_cache($key);
47+
if ($raw === null) {
48+
$redis = $this->get_connection();
49+
$raw = $redis->hget($this->_features_key, $key);
50+
if ($raw) {
51+
$this->store_in_cache($key, $raw);
52+
}
4953
}
50-
else {
54+
if ($raw) {
55+
return json_decode($raw, True);
56+
} else {
5157
return null;
5258
}
5359
}
60+
61+
/**
62+
* Gets the value from local cache. No-op by default.
63+
* @param $key string The feature key
64+
* @return null|array The feature data or null if missing
65+
*/
66+
protected function get_from_cache($key) {
67+
return null;
68+
}
69+
70+
/**
71+
* Stores the feature data into the local cache. No-op by default.
72+
* @param $key string The feature key
73+
* @param $val array The feature data
74+
*/
75+
protected function store_in_cache($key, $val) {}
5476
}

0 commit comments

Comments
 (0)