From f4b6918778862ce301b061365d17833dade4fafc Mon Sep 17 00:00:00 2001 From: Cy Rossignol Date: Tue, 22 Aug 2017 11:48:21 +0000 Subject: [PATCH] Add ability to configure package using environment variables Signed-off-by: Cy Rossignol --- README.md | 989 +++++++++++++++------ config/redis-sentinel.php | 283 ++++++ src/Configuration/HostNormalizer.php | 172 ++++ src/Configuration/Loader.php | 352 ++++++++ src/RedisSentinelServiceProvider.php | 61 +- tests/Configuration/LoaderTest.php | 490 ++++++++++ tests/RedisSentinelServiceProviderTest.php | 25 +- tests/stubs/config.php | 6 +- 8 files changed, 2071 insertions(+), 307 deletions(-) create mode 100644 config/redis-sentinel.php create mode 100644 src/Configuration/HostNormalizer.php create mode 100644 src/Configuration/Loader.php create mode 100644 tests/Configuration/LoaderTest.php diff --git a/README.md b/README.md index 237114f..f50410e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ Laravel Drivers for Redis Sentinel ================================== -[![Build Status](https://travis-ci.org/monospice/laravel-redis-sentinel-drivers.svg?branch=2.x)](https://travis-ci.org/monospice/laravel-redis-sentinel-drivers) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/monospice/laravel-redis-sentinel-drivers/badges/quality-score.png?b=2.x)](https://scrutinizer-ci.com/g/monospice/laravel-redis-sentinel-drivers/?branch=2.x) +[![Build Status][travis-badge]][travis] +[![Scrutinizer Code Quality][scrutinizer-badge]][scrutinizer] -**Laravel configuration wrapper for highly-available Redis Sentinel -replication.** +#### Redis Sentinel integration for Laravel and Lumen. [Redis Sentinel][sentinel] provides high-availability, monitoring, and load-balancing for Redis servers configured for master-slave replication. @@ -13,48 +12,67 @@ load-balancing for Redis servers configured for master-slave replication. Sentinel setups flexibly out-of-the-box. This limits configuration of Sentinel to a single service. -For example, if we wish to use Redis behind Sentinel for both caching and -session handling through Laravel's API, we cannot use separate Redis databases -for cache and session entries like we can in a standard single-server Redis -setup without Sentinel. This causes issues when we need to clear the cache, -because Laravel erases stored session information as well. - -This package wraps the configuration of Laravel's Redis caching, session, and -queue APIs for Sentinel with the ability to set options for our Redis services -independently. The package configuration exists separately from Laravel's -default `redis` configuration so we can choose to use the Sentinel connection -as needed by the environment. A developer may use a standalone Redis server in -their local environment, while production environments operate a Redis Sentinel -set of servers. +For instance, if we wish to use Redis behind Sentinel for both the cache and +session in Laravel's API, we cannot set separate Redis databases for for both +types of data like we can in a standard, single-server Redis setup without +Sentinel. This causes issues when we need to clear the cache, because Laravel +erases stored session information as well. + +This package wraps the configuration of Laravel's caching, session, and queue +APIs for Sentinel with the ability to set options for our Redis services +independently. We configure the package separately from Laravel's standard +Redis configuration so we can choose to use the Sentinel connections as needed +by the environment. A developer may use a standalone Redis server in their +local environment, while production environments operate a Redis Sentinel set +of servers. + +Contents +-------- + + - [Quickstart **(TL;DR)**][s-quickstart] + - [Requirements](#requirements) + - [Installation](#installation) + - [Configuration Options](#configuration) + - [Environment-Based Configuration][s-env-config] + - [Standard Laravel Configuration Files][s-standard-config] + - [Package Configuration File][s-package-config] + - [Hybrid Configuration][s-hybrid-config] + - [Override the Standard Redis API][s-override-redis-api] + - [Executing Redis Commands (RedisSentinel Facade)][s-facade] + - [Testing](#testing) + - [License](#license) + - [Appendix: Environment Variables][s-appx-env-vars] + - [Appendix: Configuration Examples][s-appx-examples] Requirements ------------ -- PHP 5.4 or greater -- [Redis][redis] 2.8 or greater (for Sentinel support) -- [Predis][predis] 1.1 or greater (for Sentinel client support) -- [Laravel][laravel] 5.0 or greater (Laravel 4.x doesn't support the required - Predis version) + - PHP 5.4 or greater + - [Redis][redis] 2.8 or greater (for Sentinel support) + - [Predis][predis] 1.1 or greater (for Sentinel client support) + - [Laravel][laravel] or [Lumen][lumen] 5.0 or greater (4.x doesn't support the + required Predis version) **Note:** Laravel 5.4 introduced the ability to use the [PhpRedis][php-redis] extension as a Redis client for the framework. This package does not yet support the PhpRedis option. This Readme assumes prior knowledge of configuring [Redis][redis] for [Redis -Sentinel][sentinel] and [using Redis with Laravel][laravel-redis-docs]. +Sentinel][sentinel] and [using Redis with Laravel][laravel-redis-docs] or +[Lumen][lumen-redis-docs]. Installation ------------ We're using Laravel, so we'll install through composer, of course! -**For Laravel 5.4 and above:** +#### For Laravel/Lumen 5.4 and above: ``` composer require monospice/laravel-redis-sentinel-drivers ``` -**For Laravel 5.3 and below:** +#### For Laravel/Lumen 5.3 and below: ``` composer require monospice/laravel-redis-sentinel-drivers:^1.0 @@ -63,60 +81,224 @@ composer require monospice/laravel-redis-sentinel-drivers:^1.0 If the project does not already use Redis with Laravel, this will install the [Predis][predis] package as well. -To use the drivers, add the package's service provider to `config/app.php`: +#### Register the Service Provider + +To use the drivers in Laravel, add the package's service provider to +*config/app.php*: ```php -... 'providers' => [ ... Monospice\LaravelRedisSentinel\RedisSentinelServiceProvider::class, ... ], -... ``` -Usage ------ +In Lumen, register the service provider in *bootstrap/app.php*: + +```php +$app->register(Monospice\LaravelRedisSentinel\RedisSentinelServiceProvider::class); +``` + +Quickstart (TL;DR) +------------------ + +After [installing](#installation) the package, set the following in *.env*: + +```shell +CACHE_DRIVER=redis-sentinel +SESSION_DRIVER=redis-sentinel +QUEUE_DRIVER=redis-sentinel +REDIS_DRIVER=redis-sentinel + +REDIS_HOST=sentinel1.example.com, sentinel2.example.com, 10.0.0.1, etc. +REDIS_PORT=26379 +REDIS_SENTINEL_SERVICE=mymaster # or your Redis master group name + +REDIS_CACHE_DATABASE=1 +REDIS_SESSION_DATABASE=2 +REDIS_QUEUE_DATABASE=3 +``` + +The following should now use Redis Sentinel connections: + +```php +Redis::get('key'); +Cache::get('key'); +Session::get('key'); +Queue::push(new Job()); +``` + +This example configures the package [through the environment][s-env-config]. It +[overrides Laravel's standard Redis API][s-override-redis-api] by setting +`REDIS_DRIVER` to `redis-sentinel`. See [appendix][s-appx-env-vars] for all of +the configurable environment variables. Optionally, enable the [`RedisSentinel` +facade][s-facade]. + +Configuration +------------- + +We can configure the package three ways depending on the needs of the +application: + + - [Environment-Based Configuration][s-env-config] + - [Standard Laravel Configuration Files][s-standard-config] + - [Package Configuration File][s-package-config] + +A [hybrid configuration][s-hybrid-config] uses two or more of these methods. + +With the release of version 2.2.0, the package supports simple configuration +through environment variables with a default configuration structure suitable +for many applications. This especially relieves Lumen users of the need to +create several config files that may not already exist with an initial Lumen +installation. + +The package continues to support advanced configuration through standard config +files without requiring changes for existing projects. + +Environment-Based Configuration +------------------------------- + +For suitable applications, the package's ability to configure itself from the +environment eliminates the need to create or modify configuration files in many +scenarios. The package automatically configures Redis Sentinel connections and +the application cache, session, and queue services for these connections using +environment variables. + +Developers may still configure the package [through standard Laravel +configuration files][s-standard-config] when the application requirements +exceed the package's automatic configuration capacity. + +Typically, we assign the application environment variables in the project's +[*.env* file][laravel-env-docs] during development. The configuration for a +basic application may be as simple as setting the following values in this +file: + +```shell +REDIS_HOST=sentinel.example.com +REDIS_PORT=26379 +REDIS_SENTINEL_SERVICE=mymaster +``` + +This sets up the *default* Redis Sentinel connection for the package's services +that we can access through the [`RedisSentinel` facade][s-facade] (or by +resolving `app('redis-sentinel')` from the container) like we would for +Laravel's [standard Redis API][laravel-redis-api-docs]. To use this Sentinel +connection for Laravel's cache, session, or queue services, change the +following values as well: + +```shell +CACHE_DRIVER=redis-sentinel +SESSION_DRIVER=redis-sentinel +QUEUE_DRIVER=redis-sentinel +``` + +#### Connection-Specific Configuration + +In many cases, we'd set different connection parameters for the application +cache, session, and queue. We may configure different Redis databases for our +cache and session (so that clearing the cache doesn't erase our user session +information), and the Redis servers that contain the application queue may +reside behind a different Sentinel service (master group name): + +```shell +REDIS_CACHE_DATABASE=1 +REDIS_SESSION_DATABASE=2 +REDIS_QUEUE_SERVICE=queue-service +``` + +#### Specifying Multiple Hosts + +To supply multiple hosts for a connection through environment variables, set +the value of any `*_HOST` variable to a comma-seperated string of hostnames or +IP addresses: + +```shell +REDIS_HOST=sentinel1.example.com, sentinel2.example.com +REDIS_CACHE_HOST=10.0.0.1, 10.0.0.2, 10.0.0.3 +REDIS_QUEUE_HOST=tcp://10.0.0.4:26379, tcp://10.0.0.4:26380 +``` + +Hosts share the port set for the connection unless we explicitly include the +port number after the hostname as shown. + +#### Mixed Applications + +The previous examples set the `REDIS_HOST` and `REDIS_PORT` variables, which +Laravel also reads to configure standard Redis connections. This enables +developers to [use the same variables][s-dev-vs-prod-example] in development, +with a single Redis server, and in production, with a full set of Sentinel +servers. However, if an application contains code that sends requests to both +Redis and Sentinel connections in the same environment, we must assign one or +more of the Sentinel-specific variables instead: + +```shell +REDIS_SENTINEL_HOST=sentinel.example.com +REDIS_SENTINEL_PORT=26379 +REDIS_SENTINEL_PASSWORD=secret +REDIS_SENTINEL_DATABASE=0 +``` + +#### Other Environment Configuration Options + +We can change the value of `REDIS_DRIVER` to `redis-sentinel` to [override the +standard Laravel Redis API][s-override-redis-api]. + +For a full list of the environment variables this package consumes, see the +[appendix][s-appx-env-vars]. Check out the package's [internal configuration +file](config/redis-sentinel.php) or the [environment-based configuration +examples][s-env-config-examples] to better understand how the package uses +environment variables. + +Using Standard Configuration Files +---------------------------------- -For the most part, we won't need to interact with the classes in this package -directly. This package is implemented through Laravel configuration files. +In addition to [environment-based configuration][s-env-config], the package +allows developers to configure Redis Sentinel integration through Laravel's +standard configuration files. This option exists for cases when applications +require more advanced or specialized Sentinel configuration than the package's +default environment-based configuration can provide. -- [Configuring the Redis Sentinel Database - Connection](#database-connection-configuration) -- [Configuring Cache, Session, and Queue - drivers](#cache-session-and-queue-drivers) -- [Using Sentinel Connections for Standalone Redis - Commands](#using-sentinel-connections-for-standalone-redis-commands) -- [Connecting to Sentinel Directly](#connecting-to-sentinel-directly) +For this configuration method, we'll modify the following config files: + - *config/database.php* - to define the Redis Sentinel connections + - *config/cache.php* - to define a Redis Sentinel cache store + - *config/session.php* - to set the Redis Sentinel connection for sessions + - *config/queue.php* - to define a Redis Sentinel queue connection -Database Connection Configuration ---------------------------------- +**Note:** Lumen users may [create a package config file][s-package-config] +instead to avoid the need to create all of the above files if they don't exist. + +When [environment-based configuration][s-env-config] satisfies the needs of the +application, we do not need to modify any config files. The code illustrated +in the following sections [overrides][s-hybrid-config] the package's automatic +configuration. + +### Redis Sentinel Connection Configuration We'll configure the Redis Sentinel database connections separately from -Laravel's default Redis database connection. This enables us to use the +Laravel's default Redis database connection. This enables us to use Laravel's standard Redis functionality side-by-side if needed, such as if a developer -uses a single Redis server in their local environment, while the production +runs a single Redis server in their local environment, while the production environment operates a full set of Redis and Sentinel servers. We don't need to remove the `'redis'` driver config block that ships with Laravel by default. -**Note:** Laravel passes these configuration options to the [Predis][predis] client -library, so we can include advanced configuration options here if needed. See -the [Predis Documentation][predis-docs] for more information. +**Note:** Laravel passes these configuration options to the [Predis][predis] +client library, so we can include advanced configuration options here if +needed. See the [Predis Documentation][predis-docs] for more information. -### Basic Configuration +#### Basic Configuration For a simple setup with a single Sentinel server, add the following block to -`config/database.php` for the `'redis-sentinel'` database driver. +*config/database.php* for the `'redis-sentinel'` database driver. ```php -... 'redis-sentinel' => [ 'default' => [ [ - 'host' => env('REDIS_SENTINEL_HOST', 'localhost'), - 'port' => env('REDIS_SENTINEL_PORT', 26379), + 'host' => env('REDIS_HOST', 'localhost'), + 'port' => env('REDIS_PORT', 26379), ], ], @@ -129,7 +311,6 @@ For a simple setup with a single Sentinel server, add the following block to ], ], -... ``` As you can see, our `'default'` connection includes the address or hostname of @@ -146,141 +327,24 @@ configuration, we'll wrap the definition of each host in another array, like we can see in the following section. Of course, be sure to add the environment configuration variables from the -example above to `.env`. - -The configuration block above is actually almost a drop-in replacement for -Laravel's built-in `'redis'` connection configuration to use Sentinel without -this package. However, Laravel's Redis configuration offers limited flexibility -for anything more complex than this basic Sentinel setup. A single Sentinel -server or a single connection is typically insufficient for highly-available or -complex applications. We'll take a look at more advanced configuration below. - -### Multi-Sentinel Configuration - -In a true highly-available Redis setup, we'll run more than one Sentinel server -in a quorum. This adds redundancy for a failure event during which one or more -Sentinel servers become unresponsive. We can add multiple Sentinel server -definitions to our `'default'` connection from the example above: - -```php -... -'redis-sentinel' => [ - - 'default' => [ - [ - 'host' => sentinel1.example.com - 'port' => 26379 - ], - [ - 'host' => sentinel2.example.com - 'port' => 26379 - ], - [ - 'host' => sentinel3.example.com - 'port' => 26379 - ], - ], - - 'options' => [ - 'service' => env('REDIS_SENTINEL_SERVICE', 'mymaster'), - 'parameters' => [ - 'password' => env('REDIS_PASSWORD', null), - 'database' => 0, - ], - ], - -], -... -``` - -With this configuration, we declare three Sentinel servers that can handle -requests for our Redis service, `mymaster`. If one of the Sentinel servers -fails, the Predis client will select a different Sentinel server to send -requests to. - -Typically, in a clustered environment, we don't want to hard-code each server -into our configuration like above. We may use some form of load balancing or -service discovery to route requests to a Sentinel server through an aggregate -hostname like `sentinels.example.com`, for example, for flexible deployment and -arbritrary scaling. This discussion is outside the scope of this document. - -### Multi-service Configuration - -As we mentioned previously, we likely want to separate the Redis connections -Laravel uses for each of our services. For example, we'd use separate databases -on a Redis server for our cache and session storage. In this example, we may -also want to create a database on a different set of Redis servers managed by -Sentinel for something like a feed. For this setup, we'll configure multiple -`'redis-sentinel'` connections: - -```php -... -'redis-sentinel' => [ - - 'cache' => [ - [ - 'host' => env('REDIS_SENTINEL_HOST', 'localhost'), - 'port' => env('REDIS_SENTINEL_PORT', 26379), - ], - 'options' => [ - 'service' => env('REDIS_SENTINEL_SERVICE', 'mymaster'), - 'parameters' => [ - 'password' => env('REDIS_PASSWORD', null), - 'database' => 0, - ], - ], - ], +example above to *.env*. - 'session' => [ - [ - 'host' => env('REDIS_SENTINEL_HOST', 'localhost'), - 'port' => env('REDIS_SENTINEL_PORT', 26379), - ], - 'options' => [ - 'service' => env('REDIS_SENTINEL_SERVICE', 'mymaster'), - 'parameters' => [ - 'password' => env('REDIS_PASSWORD', null), - 'database' => 1, - ], - ], - ], +#### Advanced Configuration - 'feed' => [ - [ - 'host' => env('REDIS_SENTINEL_HOST', 'localhost'), - 'port' => env('REDIS_SENTINEL_PORT', 26379), - ], - 'options' => [ - 'service' => env('REDIS_SENTINEL_FEED_SERVICE', 'feed-service'), - 'parameters' => [ - 'password' => env('REDIS_PASSWORD', null), - 'database' => 0, - ], - ], - ], - -], -... -``` - -Notice that we removed the global `'options'` array and created a local -`'options'` array for each connection. In this example setup, we store -the application cache and sessions on one Redis server set, and feed data -in another set. On the first set, we set the Redis database for storing cache -data to database `0`, and the database for session data to `1`. This enables -us to clear our application cache without erasing user sessions. +The configuration block above is almost a drop-in replacement for Laravel's +built-in `'redis'` connection configuration that we could use to configure an +application for Sentinel without this package. However, we cannot configure +Laravel's standard Redis connections for anything more complex than this basic +Sentinel setup because of limitations in the way Laravel parses its Redis +configuration. A single Sentinel server or connection is typically insufficient +for highly-available or complex applications. Read the [examples in the +appendix][s-appx-examples] for more robust configuration instructions: -Our example setup includes a second set of Redis servers for storing feed data. -The example Sentinel servers contain configuration for the first set with the -service name, `mymaster`, and for the secord set with the service name, -`feed-service`. The local connection options allow us to specify which service -the connection makes requests for. As you can see, we set the service name of -the `'feed'` connection to `'feed-service'`. + - [Connections with Multiple Sentinel Hosts][s-multi-sentinel-example] + - [Multiple Service-Specific Connections][s-multi-service-example] -For more information about Sentinel service configuration, see the [Redis -Sentinel Documentation][sentinel]. -### Other Sentinel Connection Options +#### Other Sentinel Connection Options The Predis client supports some additional configuration options that determine how it handles connections to Sentinel servers. We can add these to the global @@ -288,7 +352,6 @@ how it handles connections to Sentinel servers. We can add these to the global for a single connection. The default values are shown below: ```php -... 'options' => [ ... @@ -313,118 +376,168 @@ for a single connection. The default values are shown below: // connection with a Redis master or slave server 'update_sentinels' => false, ], -... ``` -Cache, Session, and Queue Drivers ---------------------------------- +### Cache, Session, and Queue Drivers After configuring the Sentinel database connections, we can instruct Laravel to use these connections for the application's cache, session, and queue services. -Remember that we don't need to use Sentinel for all of these services. We could -use a standard Redis connection for one and a Sentinel connection for another, -if desired, but we likely want to take advantage of Sentinel for all of our -Redis connections if we use it. +Remember that we don't need to set Sentinel connections for all of these +services. We could select a standard Redis connection for one and a Sentinel +connection for another, if desired, but we likely want to take advantage of +Sentinel for all of our Redis connections if it's available. -### Cache +#### Cache -Add the following store definition to `config/cache.php` in the `'stores'` +Add the following store definition to *config/cache.php* in the `'stores'` array: ```php -... 'stores' => [ ... 'redis-sentinel' => [ 'driver' => 'redis-sentinel', 'connection' => 'default', ], - ... ], -... ``` -...and set the `CACHE_DRIVER` environment variable to `redis-sentinel` in -`.env`. +...and change the `CACHE_DRIVER` environment variable to `redis-sentinel` in +*.env*. -If you created a specific connection in the `'redis-sentinel'` database -configuration for the cache, replace `'default'` with the name of the -connection. +If the application contains a specific connection in the `'redis-sentinel'` +database configuration for the cache, replace `'default'` with its name. -### Session +#### Session -Set the `SESSION_DRIVER` environment variable to `redis-sentinel` in `.env` and -set the `'connection'` directive to `'default'`, or the name of the specific -connection you created for storing sessions in the `'redis-sentinel'` database -configuration, in `config/session.php`. +Change the `SESSION_DRIVER` environment variable to `redis-sentinel` in *.env*. +Then, in *config/session.php*, set the `'connection'` directive to `'default'`, +or the name of the specific connection created for storing sessions from the +`'redis-sentinel'` database configuration. -### Queue +#### Queue -Add the following connection definition to `config/queue.php` in the +Add the following connection definition to *config/queue.php* in the `'connections'` array: ```php -... 'connections' => [ ... 'redis-sentinel' => [ 'driver' => 'redis-sentinel', 'connection' => 'default', 'queue' => 'default', - 'expire' => 60, + 'retry_after' => 90, // Laravel >= 5.4.30 + 'expire' => 90, // Laravel < 5.4.30 ], - ... ], -... ``` -...and set the `QUEUE_DRIVER` environment variable to `redis-sentinel` in -`.env`. +...and change the `QUEUE_DRIVER` environment variable to `redis-sentinel` in +*.env*. + +If the application contains a specific connection in the `'redis-sentinel'` +database configuration for the queue, replace `'connection' => 'default'` with +its name. -If you created a specific connection in the `'redis-sentinel'` database -configuration for the queue, replace `'connection' => 'default'` with the name -of the connection. If desired, replace the value of `'queue'` with the name of -the queue you'd like to use. +Using a Package Configuration File +---------------------------------- +Lumen projects don't include configuration files by default. Instead, by +convention, Lumen reads configuration information from the environment. If we +wish to configure this package through config files as described in the +[previous section][s-standard-config], rather than using the [environment-based +configuration][s-env-config], we can add a single package configuration file: +*config/redis-sentinel.php*. This alleviates the need to create several +standard config files in Lumen. -Using Sentinel Connections for Standalone Redis Commands --------------------------------------------------------- +The package configuration file contains elements that the package merges back +into the main configuration locations at runtime. To illustrate, when the +custom *redis-sentinel.php* file contains: + +```php +return [ + 'database' => + 'redis-sentinel' => [ /* ...Redis Sentinel connections... */ ] + ] +]; +``` + +...the package will set the `database.redis-sentinel` configuration value from +the value of `redis-sentinel.database.redis-sentinel` when the application +boots [unless the key already exists][s-hybrid-config]. + +We can customize the package's [internal config file](config/redis-sentinel.php) +by copying it into our project's *config/* directory and changing the values +as needed. Lumen users may need to create this directory if it doesn't exist. + +A custom package config file need only contain the top-level elements that +developer wishes to customize: in the code shown above, the custom config file +only overrides the package's default configuration for Redis Sentinel +connections, so the package will still automatically configure the cache, +session, and queue services using environment variables. + +Hybrid Configuration +-------------------- + +Although unnecessary in most cases, developers may combine two or more of the +configuration methods provided by this package. For example, an application may +contain a [standard][s-standard-config] or [package][s-package-config] config +file that defines the Redis Sentinel connections, but rely on the package's +automatic [environment-based configuration][s-env-config] to set up the cache, +session, and queue services for Sentinel. + +The package uses configuration data in this order of precedence: + + 1. Standard Laravel configuration files + 2. A custom package configuration file + 3. Automatic environment-based configuration + +This means that package-specific values in the standard config files override +values in a custom package config file, which, in turn, override the package's +default automatic configuration through environment variables. In other words, +a custom package config file inherits the values from the package's default +configuration that it does not explicitly declare, and the main application +configuration receives the values from both of these that it does not provide +in a standard config file. + +Override the Standard Redis API +------------------------------- This package adds Redis Sentinel drivers for Laravel's caching, session, and -queue APIs, and the developer may select which of these to use Sentinel -connections for. However, Laravel also provides an API for interacting with -Redis directly through the `Redis` facade, or through +queue APIs, and the developer may select which of these to utilize Sentinel +connections for. However, Laravel also provides [an API][laravel-redis-api-docs] +for interacting with Redis directly through the `Redis` facade, or through `Illuminate\Redis\Database` which we can resolve through the application container (`app('redis')`, dependency injection, etc.). When installed, this package does not impose the use of Sentinel for all Redis -requests. In fact, the developer may choose to use Sentinel connections for -some features and continue to use Laravel's standard Redis connections for -others. By default, this package does not replace Laravel's built-in Redis API. +requests. In fact, we can choose to use Sentinel connections for some features +and continue to use Laravel's standard Redis connections for others. By +default, this package does not replace Laravel's built-in Redis API. -For example, a developer may decide to use Sentinel connections for the -application's cache and sessions, but directly interact with a single Redis -server using Laravel's standard Redis connections. +As an example, we may decide to use Sentinel connections for the application's +cache and sessions, but directly interact with a single Redis server using +Laravel's standard Redis connections. That said, this package provides the option to override Laravel's Redis API so that any Redis commands use the Sentinel connection configuration defined by the `'redis-sentinel'` database driver. To use this feature, add the following configuration directive to the root of -the `'redis'` connection definition in `config/database.php`: +the `'redis'` connection definition in *config/database.php* (if not using +[environment-based configuration][s-env-config]): ```php -... 'redis' => [ ... 'driver' => env('REDIS_DRIVER', 'default'), ... ], -... ``` -...and add the environment variable `REDIS_DRIVER` with the value `sentinel` to -`.env`. +...and add the environment variable `REDIS_DRIVER` to *.env* with the value +`redis-sentinel`. When enabled, Redis commands executed through the `Redis` facade or the `redis` service (`app('redis')`, etc) will operate using the Sentinel connections. @@ -432,52 +545,55 @@ service (`app('redis')`, etc) will operate using the Sentinel connections. This makes it easier for developers to use a standalone Redis server in their local environments and switch to a full Sentinel set of servers in production. -Connecting to Sentinel Directly -------------------------------- +Executing Redis Commands (RedisSentinel Facade) +------------------------------------------------- -If a developer wishes to send Redis commands to Redis instances behind a -Sentinel server directly, like, for example, through the `Redis` facade, but -doesn't want to override Laravel's Redis API as above, he or she can use the -`RedisSentinel` facade provided by this package or resolve the database driver -from the application container: +If a we need to send Redis commands to Redis instances behind a Sentinel server +directly, such as we can through the `Redis` facade, but we don't want to +[override Laravel's Redis API][s-override-redis-api] as above, we can use the +`RedisSentinel` facade provided by this package or resolve the `redis-sentinel` +service from the application container: ```php -// Uses the 'some-connection' connection defined in the 'redis-sentinel' -// database driver configuration: -RedisSentinel::connection('some-connection')->get('some-key'); -app('redis-sentinel')->connection('some-connection')->get('some-key'); - -// Uses the 'some-connection' connection defined in the standard 'redis' -// database driver configuration: -Redis::connection('some-connection')->get('some-key'); -app('redis')->connection('some-connection')->get('some-key'); +// Uses the 'default' connection defined in the 'redis-sentinel' config block: +RedisSentinel::get('some-key'); +app('redis-sentinel')->get('some-key'); + +// Uses the 'default' connection defined in the standard 'redis' config block: +Redis::get('some-key'); +app('redis')->get('some-key'); ``` This provides support for uncommon use cases for which an application may need -to connect to both standard Redis servers and Sentinel clusters. We recommend -the approach described in the previous section to uniformly use Sentinel for -the entire application when possible. +to connect to both standard Redis servers and Sentinel clusters in the same +environment. When possible, follow the approach described in the previous +section to uniformly connect to Sentinel throughout application to decouple the +code from the Redis implementation. -To use the facade, add the following alias to the `'aliases'` array in -`config/app.php`: +To enable the facade in Laravel, add the following alias to the `'aliases'` +array in *config/app.php*: ```php -... 'aliases' => [ ... 'RedisSentinel' => Monospice\LaravelRedisSentinel\RedisSentinel::class, ... ], -... +``` + +In Lumen, add the alias to *bootstrap/app.php*: + +```php +class_alias('Monospice\LaravelRedisSentinel\RedisSentinel', 'RedisSentinel'); ``` Testing ------- This package includes a PHPUnit test suite with unit tests for the package's -classes. Because Predis and Laravel both contain full test suites, and because -our code simply wraps these libraries, this package does not perform -functional/integration tests against running Redis or Sentinel servers. We may +classes. This package does not perform functional/integration tests against +running Redis or Sentinel servers because Predis and Laravel both contain full +test suites, and because the package code simply wraps these libraries. We may add these types of tests in the future if needed. ``` @@ -490,10 +606,355 @@ License The MIT License (MIT). Please see the [LICENSE File](LICENSE) for more information. +------------------------------------------------------------------------------- + +Appendix: Environment Variables +------------------------------- + +The package consumes the following environment variables when using the default +[environment-based configuration][s-env-config]. Developers only need to supply +values for the variables that apply to their particular application and Redis +setup. The default values are sufficient in most cases. + +### `REDIS_{HOST,PORT,PASSWORD,DATABASE}` + +The basic connection parameters used by default for all Sentinel connections. + +To simplify environment configuration, this package attempts to read both the +`REDIS_*` and the `REDIS_SENTINEL_*` environment variables that specify values +shared by multiple Sentinel connections. If an application does not execute +commands through both Redis Sentinel and standard Redis connections [at the same +time][s-facade], this feature allows developers to use the same environment +variale names in development (with a single Redis server) and in production +(with a full set of Sentinel servers). + + - `REDIS_HOST` - One [or more][s-multiple-hosts] hostnames or IP + addresses. Defaults to `localhost` when unset. + - `REDIS_PORT` - The listening port of the Sentinel servers. Defaults to + `26379` for Sentinel connections when unset. + - `REDIS_PASSWORD` - The password, if any, used to authenticate with the Redis + servers *behind* Sentinel (Sentinels don't support password auth themselves). + - `REDIS_DATABASE` - The number of the database to select when issuing commands + to the Redis servers behind Sentinel (`0` to `15` in a normal Redis config). + Defaults to `0`. + +### `REDIS_SENTINEL_{HOST,PORT,PASSWORD,DATABASE}` + +Set these variables instead of the above when the application uses both standard +Redis and Redis Sentinel connections at the same time. + + - `REDIS_SENTINEL_HOST` - See `REDIS_HOST`. + - `REDIS_SENTINEL_PORT` - See `REDIS_PORT`. + - `REDIS_SENTINEL_PASSWORD` - See `REDIS_PASSWORD`. + - `REDIS_SENTINEL_DATABASE` - See `REDIS_DATABASE`. + +### `REDIS_SENTINEL_SERVICE` + +The Redis master group name (as specified in the Sentinel server configuration +file) that identifies the default Sentinel service used by all Sentinel +connections. Defaults to `mymaster`. + +Set `REDIS_CACHE_SERVICE`, `REDIS_SESSION_SERVICE`, or `REDIS_QUEUE_SERVICE` to +override this value for a service-specific connection. + +### `REDIS_SENTINEL_{TIMEOUT,RETRY_LIMIT,RETRY_WAIT,DISCOVERY}` + +The Predis client supports some additional configuration options that determine +how it handles connections to Sentinel servers. + + - `REDIS_SENTINEL_TIMEOUT` - The amount of time (in seconds) the client waits + before determining that a connection attempt to a Sentinel server failed. + Defaults to `0.100`. + - `REDIS_SENTINEL_RETRY_LIMIT` - The number of attempts the client tries to + contact a Sentinel server before it determines that it cannot reach all + Sentinel servers in a quorum. A value of `0` instructs the client to throw + an exception after the first failed attempt, while a value of `-1` causes + the client to continue to retry connections to Sentinel indefinitely. + Defaults to `20`. + - `REDIS_SENTINEL_RETRY_WAIT` - The amount of time (in milliseconds) the + client waits before attempting to contact another Sentinel server if the + previous server did not respond. Defaults to `1000`. + - `REDIS_SENTINEL_DISCOVERY` - Instructs the client to query the first + reachable Sentinel server for an updated set of Sentinels each time the + client needs to establish a connection with a Redis master or slave server. + Defaults to `false`. + +### `REDIS_DRIVER` + +Set the value of this variable to `redis-sentinel` to [override Laravel's +standard Redis API][s-override-redis-api]. + +### `CACHE_DRIVER`, `SESSION_DRIVER`, `QUEUE_DRIVER` + +Laravel uses these to select the backends for the application cache, session, +and queue services. Set the value to `redis-sentinel` for each service that the +application should use Sentinel connections for. + +### `REDIS_{CACHE,SESSION,QUEUE}_{HOST,PORT,PASSWORD,DATABASE,SERVICE}` + +These variables configure service-specific connection parameters when they +differ from the default Sentinel connection parameters for the cache, session, +and queue connections. + + - `REDIS_CACHE_HOST` - Overrides `REDIS_HOST` or `REDIS_SENTINEL_HOST` for + the default *cache* connection. + - `REDIS_CACHE_PORT` - Overrides `REDIS_PORT` or `REDIS_SENTINEL_PORT` for + the default *cache* connection. + - `REDIS_CACHE_PASSWORD` - Overrides `REDIS_PASSWORD` or + `REDIS_SENTINEL_PASSWORD` for the default *cache* connection. + - `REDIS_CACHE_DATABASE` - Overrides `REDIS_DATABASE` or + `REDIS_SENTINEL_DATABASE` for the default *cache* connection. + - `REDIS_CACHE_SERVICE` - Overrides `REDIS_SENTINEL_SERVICE` for the default + *cache* connection. + + + - `REDIS_SESSION_HOST` - Overrides `REDIS_HOST` or `REDIS_SENTINEL_HOST` for + the default *session* connection. + - `REDIS_SESSION_PORT` - Overrides `REDIS_PORT` or `REDIS_SENTINEL_PORT` for + the default *session* connection. + - `REDIS_SESSION_PASSWORD` - Overrides `REDIS_PASSWORD` or + `REDIS_SENTINEL_PASSWORD` for the default *session* connection. + - `REDIS_SESSION_DATABASE` - Overrides `REDIS_DATABASE` or + `REDIS_SENTINEL_DATABASE` for the default *session* connection. + - `REDIS_SESSION_SERVICE` - Overrides `REDIS_SENTINEL_SERVICE` for the default + *session* connection. + + + - `REDIS_QUEUE_HOST` - Overrides `REDIS_HOST` or `REDIS_SENTINEL_HOST` for + the default *queue* connection. + - `REDIS_QUEUE_PORT` - Overrides `REDIS_PORT` or `REDIS_SENTINEL_PORT` for + the default *queue* connection. + - `REDIS_QUEUE_PASSWORD` - Overrides `REDIS_PASSWORD` or + `REDIS_SENTINEL_PASSWORD` for the default *queue* connection. + - `REDIS_QUEUE_DATABASE` - Overrides `REDIS_DATABASE` or + `REDIS_SENTINEL_DATABASE` for the default *queue* connection. + - `REDIS_QUEUE_SERVICE` - Overrides `REDIS_SENTINEL_SERVICE` for the default + *queue* connection. + + +### `CACHE_REDIS_CONNECTION`, `CACHE_REDIS_SENTINEL_CONNECTION` + +The name of the Sentinel connection to set for the application cache when +`CACHE_DRIVER` equals `redis-sentinel`. It defaults to the package's internal, +auto-configured *cache* connection when unset. + +### `QUEUE_REDIS_CONNECTION`, `QUEUE_REDIS_SENTINEL_CONNECTION` + +The name of the Sentinel connection to set for the application queue when +`QUEUE_DRIVER` equals `redis-sentinel`. It defaults to the package's internal, +auto-configured *queue* connection when unset. + +### `SESSION_CONNECTION` + +The name of the Sentinel connection to set for storing application sessions +when `SESSION_DRIVER` equals `redis-sentinel`. It defaults to the package's +internal, auto-configured *session* connection when unset. + +Appendix: Configuration Examples +-------------------------------- + + - [Environment-based Configuration Examples][s-env-config-examples] + - [Development vs. Production][s-dev-vs-prod-example] + - [Configuration File Examples][s-standard-config-examples] + - [Multi-Sentinel Configuration][s-multi-sentinel-example] + - [Multi-Service Configuration][s-multi-service-example] + +### Environment-based Configuration Examples + +Supplemental examples for [environment-based-configuration][s-env-config]. + +#### Development vs. Production + +This example shows how we might change the values of environment variables +between environments when we run a single Redis server in development and a +full set of Sentinel servers in production. + +```shell +# Development: # Production: + +CACHE_DRIVER=redis CACHE_DRIVER=redis-sentinel +SESSION_DRIVER=redis SESSION_DRIVER=redis-sentinel +QUEUE_DRIVER=redis QUEUE_DRIVER=redis-sentinel + +REDIS_HOST=localhost REDIS_HOST=sentinel1, sentinel2, sentinel3 +REDIS_PORT=6379 REDIS_PORT=26379 +REDIS_SENTINEL_SERVICE=null REDIS_SENTINEL_SERVICE=mymaster +``` + +Don't forget to run the `artisan config:cache` command in production when +possible. This dramatically improves the configuration loading time for the +application and this package. + +[Best practice][phpdotenv-usage-notes] suggests that we avoid using the +development *.env* file in production environments. Consider other means to set +environment variables: + +> "phpdotenv is made for development environments, and generally should not be +> used in production. In production, the actual environment variables should be +> set so that there is no overhead of loading the .env file on each request. +> This can be achieved via an automated deployment process with tools like +> Vagrant, chef, or Puppet, or can be set manually with cloud hosts..." + +### Configuration File Examples + +These examples demonstrate how to setup Laravel's standard configuration files +to configure the package for more-advanced setups. For an introduction to using +configuration files, read the [config file documentation][s-standard-config]. + +#### Multi-Sentinel Connection Configuration + +In a true highly-available Redis setup, we'll run more than one Sentinel server +in a quorum. This adds redundancy for a failure event during which one or more +Sentinel servers become unresponsive. We can add multiple Sentinel server +definitions to our `'default'` connection in *config/database.php*: + +```php +... +'redis-sentinel' => [ + + 'default' => [ + [ + 'host' => 'sentinel1.example.com', + 'port' => 26379, + ], + [ + 'host' => 'sentinel2.example.com', + 'port' => 26379, + ], + [ + 'host' => 'sentinel3.example.com' + 'port' => 26379, + ], + ], + + 'options' => [ + 'service' => env('REDIS_SENTINEL_SERVICE', 'mymaster'), + 'parameters' => [ + 'password' => env('REDIS_PASSWORD', null), + 'database' => 0, + ], + ], + +], +``` + +With this configuration, we declare three Sentinel servers that can handle +requests for our Redis service, `mymaster` (the master group name as specified +in the Sentinel server configuration file). If one of the Sentinel servers +fails, the Predis client will select a different Sentinel server to send +requests to. + +Typically, in a clustered environment, we don't want to hard-code each server +into our configuration like above. We may install some form of load balancing +or service discovery to route requests to a Sentinel server through an +aggregate hostname, such as `sentinels.example.com`, for flexible deployment +and arbritrary scaling. + +#### Multi-service Connection Configuration + +As we mentioned previously, we likely want to separate the Redis connections +Laravel uses for each of our services. For instance, we'd use separate databases +on a Redis server for our cache and session storage. In this example, we may +also want to create a database on a different set of Redis servers managed by +Sentinel for something like a feed. For this setup, we'll configure multiple +`'redis-sentinel'` connections: + +```php +... +'redis-sentinel' => [ + + 'cache' => [ + [ + 'host' => env('REDIS_HOST', 'localhost'), + 'port' => env('REDIS_PORT', 26379), + ], + 'options' => [ + 'service' => env('REDIS_SENTINEL_SERVICE', 'mymaster'), + 'parameters' => [ + 'password' => env('REDIS_PASSWORD', null), + 'database' => 0, + ], + ], + ], + + 'session' => [ + [ + 'host' => env('REDIS_HOST', 'localhost'), + 'port' => env('REDIS_PORT', 26379), + ], + 'options' => [ + 'service' => env('REDIS_SENTINEL_SERVICE', 'mymaster'), + 'parameters' => [ + 'password' => env('REDIS_PASSWORD', null), + 'database' => 1, + ], + ], + ], + + 'feed' => [ + [ + 'host' => env('REDIS_HOST', 'localhost'), + 'port' => env('REDIS_PORT', 26379), + ], + 'options' => [ + 'service' => env('REDIS_SENTINEL_FEED_SERVICE', 'feed-service'), + 'parameters' => [ + 'password' => env('REDIS_PASSWORD', null), + 'database' => 0, + ], + ], + ], + +], +``` + +Notice that we removed the global `'options'` array and created a local +`'options'` array for each connection. In this example setup, we store the +application cache and sessions on one Redis server set, and feed data in +another set. In the first connection block, we set the Redis database for +storing cache data to `0`, and the database for session data to `1`, which +allows us to clear our application cache without erasing user sessions. + +This example setup includes a second set of Redis servers for storing feed data. +The example Sentinel servers contain configuration for the first set with the +service name, `mymaster`, and for the secord set with the service name, +`feed-service`. The local connection options allow us to specify which service +(Redis master group name) the connection makes requests for. In particular, we +set the service name of the `'feed'` connection to `'feed-service'`. + +For more information about Sentinel service configuration, see the [Redis +Sentinel Documentation][sentinel]. + + +[s-appx-env-vars]: #appendix-environment-variables +[s-appx-examples]: #appendix-configuration-examples +[s-dev-vs-prod-example]: #development-vs-production +[s-env-config]: #environment-based-configuration +[s-env-config-examples]: #environment-based-configuration-examples +[s-facade]: #executing-redis-commands-redissentinel-facade +[s-hybrid-config]: #hybrid-configuration +[s-multi-sentinel-example]: #multi-sentinel-connection-configuration +[s-multi-service-example]: #multi-service-connection-configuration +[s-multiple-hosts]: #specifying-multiple-hosts +[s-override-redis-api]: #override-the-standard-redis-api +[s-package-config]: #using-a-package-configuration-file +[s-quickstart]: #quickstart-tldr +[s-standard-config]: #using-standard-configuration-files +[s-standard-config-examples]: #configuration-file-examples + +[laravel-env-docs]: https://laravel.com/docs/configuration#environment-configuration +[laravel-redis-api-docs]: https://laravel.com/docs/redis#interacting-with-redis +[laravel-redis-docs]: https://laravel.com/docs/redis [laravel]: https://laravel.com -[redis]: http://redis.io -[sentinel]: http://redis.io/topics/sentinel -[predis]: https://github.com/nrk/predis -[predis-docs]: https://github.com/nrk/predis/wiki +[lumen-redis-docs]: https://lumen.laravel.com/docs/cache +[lumen]: https://lumen.laravel.com [php-redis]: https://github.com/phpredis/phpredis -[laravel-redis-docs]: https://laravel.com/docs/redis +[phpdotenv-usage-notes]: https://github.com/vlucas/phpdotenv#usage-notes +[predis-docs]: https://github.com/nrk/predis/wiki +[predis]: https://github.com/nrk/predis +[redis]: https://redis.io +[scrutinizer-badge]: https://scrutinizer-ci.com/g/monospice/laravel-redis-sentinel-drivers/badges/quality-score.png?b=2.x +[scrutinizer]: https://scrutinizer-ci.com/g/monospice/laravel-redis-sentinel-drivers/?branch=2.x +[sentinel]: https://redis.io/topics/sentinel +[travis-badge]: https://travis-ci.org/monospice/laravel-redis-sentinel-drivers.svg?branch=2.x +[travis]: https://travis-ci.org/monospice/laravel-redis-sentinel-drivers diff --git a/config/redis-sentinel.php b/config/redis-sentinel.php new file mode 100644 index 0000000..a5d5b3a --- /dev/null +++ b/config/redis-sentinel.php @@ -0,0 +1,283 @@ + true, + + 'clean_config' => true, + + /* + |-------------------------------------------------------------------------- + | Redis Sentinel Database Driver + |-------------------------------------------------------------------------- + | + | The following block configures the Redis Sentinel connections for the + | application. + | + | Each of the connections below contains one or more host definitions for + | the Sentinel servers in the quorum. Each host definition is wrapped in + | an unnamed array that contains the host's IP address or hostname and the + | port number. To specify multiple Sentinel hosts for a connection, add a + | sub-array to the connection's array with the address and port for each + | Sentinel server. Configurations that place multiple Sentinel servers + | behind one aggregate hostname, such as "sentinels.example.com", should + | contain only one host definition per connection. + | + | The main "redis-sentinel" driver configuration array and each of the + | connection arrays within may contain an "options" element that provides + | additional configuration settings for the connections, such as the + | password for the Redis servers behind Sentinel, if needed. Any options + | specified for a connection override the options in the global "options" + | array that defines options for all connections. + | + | We can individually configure each of the application service connections + | ("cache", "session", and "queue") with environment variables by setting + | the variables named for each connection. If more than one connection + | shares a common configuration value, we can instead set the environment + | variable that applies to all of the Sentinel connections. + | + | For example, we may set the following configuration in ".env" for a setup + | that uses the same Sentinel hosts for the application's cache and queue, + | but a different Redis database for each connection: + | + | REDIS_HOST=sentinels.example.com + | REDIS_CACHE_DATABASE=1 + | REDIS_QUEUE_DATABASE=2 + | + | Developers need only supply environment configuration variables for the + | Sentinel connections used by the application. + | + | To simplify environment configuration, this script attempts to read both + | the "REDIS_SENTINEL_*" and the "REDIS_*" environment variables that + | specify values shared by multiple Sentinel connections. If an application + | does not require both Redis Sentinel and standard Redis connections at + | the same time, this feature allows developers to use the same environment + | variale names in development (with a single Redis server) and production + | (with a full set of Sentinel servers). These variables are: + | + | REDIS_SENTINEL_HOST (REDIS_HOST) + | REDIS_SENTINEL_PORT (REDIS_PORT) + | REDIS_SENTINEL_PASSWORD (REDIS_PASSWORD) + | REDIS_SENTINEL_DATABASE (REDIS_DATABASE) + | + | The package supports environment-based configuration for connections with + | multiple hosts by allowing a comma-seperated string of hosts in each of + | the "*_HOST" environment variables. For example: + | + | REDIS_HOST=sentinel1.example.com, sentinel2.example.com + | REDIS_CACHE_HOST=10.0.0.1, 10.0.0.2, 10.0.0.3 + | REDIS_QUEUE_HOST=tcp://10.0.0.3:26379, tcp://10.0.0.3:26380 + | + */ + + 'database' => [ + + 'redis-sentinel' => [ + + 'default' => [ + [ + 'host' => $host, + 'port' => $port, + ], + ], + + 'cache' => [ + [ + 'host' => env('REDIS_CACHE_HOST', $host), + 'port' => env('REDIS_CACHE_PORT', $port), + ], + 'options' => [ + 'service' => env('REDIS_CACHE_SERVICE', $service), + 'parameters' => [ + 'password' => env('REDIS_CACHE_PASSWORD', $password), + 'database' => env('REDIS_CACHE_DATABASE', $database), + ], + ], + ], + + 'session' => [ + [ + 'host' => env('REDIS_SESSION_HOST', $host), + 'port' => env('REDIS_SESSION_PORT', $port), + ], + 'options' => [ + 'service' => env('REDIS_SESSION_SERVICE', $service), + 'parameters' => [ + 'password' => env('REDIS_SESSION_PASSWORD', $password), + 'database' => env('REDIS_SESSION_DATABASE', $database), + ], + ], + ], + + 'queue' => [ + [ + 'host' => env('REDIS_QUEUE_HOST', $host), + 'port' => env('REDIS_QUEUE_PORT', $port), + ], + 'options' => [ + 'service' => env('REDIS_QUEUE_SERVICE', $service), + 'parameters' => [ + 'password' => env('REDIS_QUEUE_PASSWORD', $password), + 'database' => env('REDIS_QUEUE_DATABASE', $database), + ], + ], + ], + + // These options apply to all Redis Sentinel connections unless a + // connection supplies a local options array that overrides the + // values here: + 'options' => [ + 'service' => $service, + + 'parameters' => [ + 'password' => $password, + 'database' => $database, + ], + + 'sentinel_timeout' => env('REDIS_SENTINEL_TIMEOUT', 0.100), + 'retry_limit' => env('REDIS_SENTINEL_RETRY_LIMIT', 20), + 'retry_wait' => env('REDIS_SENTINEL_RETRY_WAIT', 1000), + 'update_sentinels' => env('REDIS_SENTINEL_DISCOVERY', false), + ], + + ], + + // Set the value of "REDIS_DRIVER" to "redis-sentinel" to override + // Laravel's standard Redis API ("Redis" facade and "redis" service + // binding) so that these use the Redis Sentinel connections instead + // of the Redis connections. + 'redis' => [ + 'driver' => env('REDIS_DRIVER', 'default'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Redis Sentinel Cache Store + |-------------------------------------------------------------------------- + | + | Defines the cache store that uses a Redis Sentinel connection for the + | application cache. + | + */ + + 'cache' => [ + 'stores' => [ + 'redis-sentinel' => [ + 'driver' => 'redis-sentinel', + 'connection' => env( + 'CACHE_REDIS_SENTINEL_CONNECTION', + env('CACHE_REDIS_CONNECTION', 'cache') + ), + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Redis Sentinel Session Connection + |-------------------------------------------------------------------------- + | + | Defines the Redis Sentinel connection used store and retrieve sessions + | when "SESSION_DRIVER" ("session.driver") is set to "redis-sentinel". + | + | The package only uses this value if the application supports sessions + | (Lumen applications > 5.2 typically don't). + | + */ + + 'session' => [ + 'connection' => env('SESSION_CONNECTION', 'session'), + ], + + /* + |-------------------------------------------------------------------------- + | Redis Sentinel Queue Connector + |-------------------------------------------------------------------------- + | + | Defines the queue connector that uses a Redis Sentinel connection for the + | application queue. + | + */ + + 'queue' => [ + 'connections' => [ + 'redis-sentinel' => [ + 'driver' => 'redis-sentinel', + 'connection' => env( + 'QUEUE_REDIS_SENTINEL_CONNECTION', + env('QUEUE_REDIS_CONNECTION', 'queue') + ), + 'queue' => 'default', + 'retry_after' => 90, + 'expire' => 90, // Legacy, Laravel < 5.4.30 + ], + ], + ], + +]; diff --git a/src/Configuration/HostNormalizer.php b/src/Configuration/HostNormalizer.php new file mode 100644 index 0000000..4a012ae --- /dev/null +++ b/src/Configuration/HostNormalizer.php @@ -0,0 +1,172 @@ + [ + * [ + * 'host' => 'sentinel1.example.com,sentinel2.example.com', + * 'port' => 26379, + * ] + * ] + * + * This class will convert the connection configuration to: + * + * 'connection' => [ + * [ + * 'host' => 'sentinel1.example.com', + * 'port' => 26379, + * ], + * [ + * 'host' => 'sentinel2.example.com', + * 'port' => 26379, + * ] + * ] + * + * @category Package + * @package Monospice\LaravelRedisSentinel + * @author Cy Rossignol + * @license See LICENSE file + * @link https://github.com/monospice/laravel-redis-sentinel-drivers + */ +class HostNormalizer +{ + /** + * Create single host entries for any host definitions that specify + * multiple hosts in the provided set of connection configurations. + * + * @param array $connections The set of connection configs containing host + * definitions to normalize + * + * @return array The normalized Redis Sentinel connection configuration + */ + public static function normalizeConnections(array $connections) + { + foreach ($connections as $name => $connection) { + if ($name === 'options' || $name === 'clusters') { + continue; + } + + $connections[$name] = static::normalizeConnection($connection); + } + + return $connections; + } + + /** + * Create single host entries for any host definitions that specify + * multiple hosts in the provided connection configuration. + * + * @param array $connection The connection config which contains the host + * definitions for a single Redis Sentinel connection + * + * @return array The normalized connection configuration values + */ + public static function normalizeConnection(array $connection) + { + $normal = [ ]; + + if (array_key_exists('options', $connection)) { + $normal['options'] = $connection['options']; + unset($connection['options']); + } + + foreach ($connection as $host) { + $normal = array_merge($normal, static::normalizeHost($host)); + } + + return $normal; + } + + /** + * Parse the provided host definition into multiple host definitions if it + * specifies more than one host. + * + * @param array|string $host The host definition from a Redis Sentinel + * connection + * + * @return array One or more host definitions parsed from the provided + * host definition + */ + public static function normalizeHost($host) + { + if (is_array($host)) { + return static::normalizeHostArray($host); + } + + if (is_string($host)) { + return static::normalizeHostString($host); + } + + return [ $host ]; + } + + /** + * Parse a host definition in the form of an array into multiple host + * definitions if it specifies more than one host. + * + * @param array $hostArray The host definition from a Redis Sentinel + * connection + * + * @return array One or more host definitions parsed from the provided + * host definition + */ + protected static function normalizeHostArray(array $hostArray) + { + if (! array_key_exists('host', $hostArray)) { + return [ $hostArray ]; + } + + $port = Arr::get($hostArray, 'port', 26379); + + return static::normalizeHostString($hostArray['host'], $port); + } + + /** + * Parse a host definition in the form of a string into multiple host + * definitions it it specifies more than one host. + * + * @param string $hostString The host definition from a Redis Sentinel + * connection + * @param int $port The port number to use for the resulting host + * definitions if the parsed host definition doesn't contain port numbers + * + * @return array One or more host definitions parsed from the provided + * host definition + */ + protected static function normalizeHostString($hostString, $port = 26379) + { + $hosts = [ ]; + + foreach (explode(',', $hostString) as $host) { + $host = trim($host); + + if (Str::contains($host, ':')) { + $hosts[] = $host; + } else { + $hosts[] = [ 'host' => $host, 'port' => $port ]; + } + } + + return $hosts; + } +} diff --git a/src/Configuration/Loader.php b/src/Configuration/Loader.php new file mode 100644 index 0000000..b716522 --- /dev/null +++ b/src/Configuration/Loader.php @@ -0,0 +1,352 @@ + + * @license See LICENSE file + * @link https://github.com/monospice/laravel-redis-sentinel-drivers + */ +class Loader +{ + /** + * The path to the package's default configuration file. + * + * @var string + */ + const CONFIG_PATH = __DIR__ . '/../../config/redis-sentinel.php'; + + /** + * Indicates whether the current application runs the Lumen framework. + * + * @var bool + */ + public $isLumen; + + /** + * Indicates whether the current application supports sessions. + * + * @var bool + */ + public $supportsSessions; + + /** + * The current Laravel or Lumen application instance that provides context + * and services used to load the appropriate configuration. + * + * @var LaravelApplication|LumenApplication + */ + private $app; + + /** + * Used to fetch and set application configuration values. + * + * @var \Illuminate\Contracts\Config\Repository + */ + private $config; + + /** + * Contains the set of configuration values used to configure the package + * as loaded from "config/redis-sentinel.php". Empty when the application's + * standard config files provide all the values needed to configure the + * package (such as when a developer provides a custom config). + * + * @var array + */ + private $packageConfig; + + /** + * Initialize the configuration loader. Any actual loading occurs when + * calling the 'loadConfiguration()' method. + * + * @param LaravelApplication|LumenApplication $app The current application + * instance that provides context and services needed to load the + * appropriate configuration. + */ + public function __construct($app) + { + $this->app = $app; + $this->config = $app->make('config'); + + $lumenApplicationClass = 'Laravel\Lumen\Application'; + + $this->isLumen = $app instanceof $lumenApplicationClass; + $this->supportsSessions = $app->bound('session'); + } + + /** + * Create an instance of the loader and load the configuration in one step. + * + * @param LaravelApplication|LumenApplication $app The current application + * instance that provides context and services needed to load the + * appropriate configuration. + * + * @return self An initialized instance of this class + */ + public static function load($app) + { + $loader = new self($app); + $loader->loadConfiguration(); + + return $loader; + } + + /** + * Load the package configuration. + * + * @return void + */ + public function loadConfiguration() + { + if (! $this->shouldLoadConfiguration()) { + return; + } + + if ($this->isLumen) { + $this->configureLumenComponents(); + } + + $this->loadPackageConfiguration(); + } + + /** + * Determine whether the package should override Laravel's standard Redis + * API ("Redis" facade and "redis" service binding). + * + * @return bool TRUE if the package should override Laravel's standard + * Redis API + */ + public function shouldOverrideLaravelRedisApi() + { + $redisDriver = $this->config->get('database.redis.driver'); + + // Previous versions of the package looked for the value 'sentinel': + return $redisDriver === 'redis-sentinel' || $redisDriver === 'sentinel'; + } + + /** + * Determine if the package should automatically configure itself. + * + * Developers may set the value of "redis-sentinel.load_config" to FALSE to + * disable the package's automatic configuration. This class also sets this + * value to FALSE after loading the package configuration to skip the auto- + * configuration when the application cached its configuration values (via + * "artisan config:cache", for example). + * + * @return bool TRUE if the package should load its configuration + */ + protected function shouldLoadConfiguration() + { + if ($this->isLumen) { + $this->app->configure('redis-sentinel'); + } + + return $this->config->get('redis-sentinel.load_config', true) === true; + } + + /** + * Configure the Lumen components that this package depends on. + * + * Lumen lazily loads many of its components. We must instruct Lumen to + * load the configuration for components that this class configures so + * that the values are accessible and so that the framework does not + * revert the configuration settings that this class changes when one of + * the components initializes later. + * + * @return void + */ + protected function configureLumenComponents() + { + $this->app->configure('database'); + $this->app->configure('cache'); + $this->app->configure('queue'); + } + + /** + * Reconcile the package configuration and use it to set the appropriate + * configuration values for other application components. + * + * @return void + */ + protected function loadPackageConfiguration() + { + $this->setConfigurationFor('database.redis-sentinel'); + $this->setConfigurationFor('database.redis.driver'); + $this->setConfigurationFor('cache.stores.redis-sentinel'); + $this->setConfigurationFor('queue.connections.redis-sentinel'); + $this->setSessionConfiguration(); + + $this->normalizeHosts(); + + if ($this->packageConfig !== null) { + $this->cleanPackageConfiguration(); + } + } + + /** + * Set the application configuration value for the specified key with the + * value from the package configuration. + * + * @param string $configKey The key of the config value to set. Should + * correspond to a key in the package's configuration. + * @param bool $checkExists If TRUE, don't set the value if the key + * already exists in the application configuration. + * + * @return void + */ + protected function setConfigurationFor($configKey, $checkExists = true) + { + if ($checkExists && $this->config->has($configKey)) { + return; + } + + $config = $this->getPackageConfigurationFor($configKey); + + $this->config->set($configKey, $config); + } + + /** + * Set the application session configuration as specified by the package's + * configuration if the app supports sessions. + * + * @return void + */ + protected function setSessionConfiguration() + { + if (! $this->supportsSessions + || $this->config->get('session.driver') !== 'redis-sentinel' + || $this->config->get('session.connection') !== null + ) { + return; + } + + $this->setConfigurationFor('session.connection', false); + } + + /** + * Get the package configuration for the specified key. + * + * @param string $configKey The key of the configuration value to get + * + * @return mixed The value of the configuration with the specified key + */ + protected function getPackageConfigurationFor($configKey) + { + if ($this->packageConfig === null) { + $this->mergePackageConfiguration(); + } + + return Arr::get($this->packageConfig, $configKey); + } + + /** + * Merge the package's default configuration with the override config file + * supplied by the developer, if any. + * + * @return void + */ + protected function mergePackageConfiguration() + { + $defaultConfig = require self::CONFIG_PATH; + $currentConfig = $this->config->get('redis-sentinel', [ ]); + + $this->packageConfig = array_merge($defaultConfig, $currentConfig); + } + + /** + * Parse Redis Sentinel connection host definitions to create single host + * entries for host definitions that specify multiple hosts. + * + * @return void + */ + protected function normalizeHosts() + { + $connections = $this->config->get('database.redis-sentinel'); + + if (! is_array($connections)) { + return; + } + + $this->config->set( + 'database.redis-sentinel', + HostNormalizer::normalizeConnections($connections) + ); + } + + /** + * Remove the package's configuration from the application configuration + * repository. + * + * This package's configuration contains partial elements from several + * other component configurations. By default, the package removes its + * configuration after merging the values into each of the appropriate + * config locations for the components it initializes. This behavior + * prevents the artisan "config:cache" command from saving unnecessary + * configuration values to the cache file. + * + * @return void + */ + protected function cleanPackageConfiguration() + { + if ($this->config->get('redis-sentinel.clean_config', true) !== true) { + return; + } + + $this->config->set('redis-sentinel', [ + 'Config merged. Set "redis-sentinel.clean_config" = false to keep.', + 'load_config' => false, // skip loading package config when cached + ]); + } +} diff --git a/src/RedisSentinelServiceProvider.php b/src/RedisSentinelServiceProvider.php index 6c6f08d..537b9ba 100644 --- a/src/RedisSentinelServiceProvider.php +++ b/src/RedisSentinelServiceProvider.php @@ -10,6 +10,7 @@ use Illuminate\Session\SessionManager; use Illuminate\Support\Arr; use Illuminate\Support\ServiceProvider; +use Monospice\LaravelRedisSentinel\Configuration\Loader as ConfigurationLoader; use Monospice\LaravelRedisSentinel\RedisSentinelManager; use Monospice\LaravelRedisSentinel\Manager; @@ -21,10 +22,17 @@ * @package Monospice\LaravelRedisSentinel * @author Cy Rossignol * @license See LICENSE file - * @link http://github.com/monospice/laravel-redis-sentinel-drivers + * @link https://github.com/monospice/laravel-redis-sentinel-drivers */ class RedisSentinelServiceProvider extends ServiceProvider { + /** + * Loads the package's configuration and provides configuration values. + * + * @var ConfigurationLoader + */ + protected $config; + /** * Boot the service by registering extensions with Laravel's cache, queue, * and session managers for the "redis-sentinel" driver. @@ -36,15 +44,15 @@ public function boot() $this->addRedisSentinelCacheDriver($this->app->make('cache')); $this->addRedisSentinelQueueConnector($this->app->make('queue')); - // Lumen removed session support since version 5.2, so we'll only bind - // the Sentinel session handler if we're running Laravel. - if (! $this->isLumenApplication()) { + // Since version 5.2, Lumen does not include support for sessions by + // default, so we'll only register the session handler if enabled: + if ($this->config->supportsSessions) { $this->addRedisSentinelSessionHandler($this->app->make('session')); } // If we want Laravel's Redis API to use Sentinel, we'll remove the - // "redis" service from the list of deferred services in the container: - if ($this->shouldOverrideLaravelApi()) { + // "redis" service from the deferred services in the container: + if ($this->config->shouldOverrideLaravelRedisApi()) { $this->removeDeferredRedisServices(); } } @@ -57,10 +65,11 @@ public function boot() */ public function register() { - $class = $this->getVersionedRedisSentinelManagerClass(); + $this->config = ConfigurationLoader::load($this->app); - $this->app->singleton('redis-sentinel', function ($app) use ($class) { - $config = $app->make('config')->get('database.redis-sentinel'); + $this->app->singleton('redis-sentinel', function ($app) { + $class = $this->getVersionedRedisSentinelManagerClass(); + $config = $app->make('config')->get('database.redis-sentinel', [ ]); $driver = Arr::pull($config, 'client', 'predis'); return new RedisSentinelManager(new $class($driver, $config)); @@ -69,7 +78,7 @@ public function register() // If we want Laravel's Redis API to use Sentinel, we'll return an // instance of the RedisSentinelManager when requesting the "redis" // service: - if ($this->shouldOverrideLaravelApi()) { + if ($this->config->shouldOverrideLaravelRedisApi()) { $this->registerOverrides(); } } @@ -99,7 +108,7 @@ protected function registerOverrides() */ protected function removeDeferredRedisServices() { - if ($this->isLumenApplication()) { + if ($this->config->isLumen) { return; } @@ -143,7 +152,6 @@ protected function addRedisSentinelSessionHandler(SessionManager $session) { $session->extend('redis-sentinel', function ($app) { $config = $app->make('config'); - $cacheDriver = clone $app->make('cache')->driver('redis-sentinel'); $minutes = $config->get('session.lifetime'); $connection = $config->get('session.connection'); @@ -171,20 +179,6 @@ protected function addRedisSentinelQueueConnector(QueueManager $queue) }); } - /** - * Determine whether this package should replace Laravel's Redis API - * ("Redis" facade and "redis" service binding). - * - * @return bool True if "database.redis.driver" configuration option is - * set to "sentinel" - */ - protected function shouldOverrideLaravelApi() - { - $driver = $this->app->make('config')->get('database.redis.driver'); - - return $driver === 'sentinel'; - } - /** * Get the fully-qualified class name of the RedisSentinelManager class * for the current version of Laravel or Lumen. @@ -194,8 +188,8 @@ protected function shouldOverrideLaravelApi() */ protected function getVersionedRedisSentinelManagerClass() { - if ($this->isLumenApplication()) { - $appVersion = substr($this->app->version(), 7, 3); + if ($this->config->isLumen) { + $appVersion = substr($this->app->version(), 7, 3); // ex. "5.4" $frameworkVersion = '5.4'; } else { $appVersion = \Illuminate\Foundation\Application::VERSION; @@ -208,15 +202,4 @@ protected function getVersionedRedisSentinelManagerClass() return Manager\Laravel5420RedisSentinelManager::class; } - - /** - * Determine if the current application runs the Lumen framework instead of - * Laravel. - * - * @return bool True if running Lumen - */ - protected function isLumenApplication() - { - return $this->app instanceof \Laravel\Lumen\Application; - } } diff --git a/tests/Configuration/LoaderTest.php b/tests/Configuration/LoaderTest.php new file mode 100644 index 0000000..4830b6b --- /dev/null +++ b/tests/Configuration/LoaderTest.php @@ -0,0 +1,490 @@ + 'cache.stores.redis-sentinel', + 'sentinel_connections' => 'database.redis-sentinel', + 'redis_driver' => 'database.redis.driver', + 'queue_connector' => 'queue.connections.redis-sentinel', + 'session_connection' => 'session.connection', + ]; + + /** + * The set of environment variables consumed by this package mapped to the + * configuration keys that each environment variable can set. + * + * @var array + */ + protected $defaultConfigEnvironmentVars = [ + 'REDIS_HOST' => [ + 'database.redis-sentinel.default.0.host', + 'database.redis-sentinel.cache.0.host', + 'database.redis-sentinel.session.0.host', + 'database.redis-sentinel.queue.0.host', + ], + 'REDIS_SENTINEL_HOST' => [ + 'database.redis-sentinel.default.0.host', + 'database.redis-sentinel.cache.0.host', + 'database.redis-sentinel.session.0.host', + 'database.redis-sentinel.queue.0.host', + ], + 'REDIS_PORT' => [ + 'database.redis-sentinel.default.0.port', + 'database.redis-sentinel.cache.0.port', + 'database.redis-sentinel.session.0.port', + 'database.redis-sentinel.queue.0.port', + ], + 'REDIS_SENTINEL_PORT' => [ + 'database.redis-sentinel.default.0.port', + 'database.redis-sentinel.cache.0.port', + 'database.redis-sentinel.session.0.port', + 'database.redis-sentinel.queue.0.port', + ], + 'REDIS_PASSWORD' => [ + 'database.redis-sentinel.options.parameters.password', + 'database.redis-sentinel.cache.options.parameters.password', + 'database.redis-sentinel.session.options.parameters.password', + 'database.redis-sentinel.queue.options.parameters.password', + ], + 'REDIS_SENTINEL_PASSWORD' => [ + 'database.redis-sentinel.options.parameters.password', + 'database.redis-sentinel.cache.options.parameters.password', + 'database.redis-sentinel.session.options.parameters.password', + 'database.redis-sentinel.queue.options.parameters.password', + ], + 'REDIS_DATABASE' => [ + 'database.redis-sentinel.options.parameters.database', + 'database.redis-sentinel.cache.options.parameters.database', + 'database.redis-sentinel.session.options.parameters.database', + 'database.redis-sentinel.queue.options.parameters.database', + ], + 'REDIS_SENTINEL_DATABASE' => [ + 'database.redis-sentinel.options.parameters.database', + 'database.redis-sentinel.cache.options.parameters.database', + 'database.redis-sentinel.session.options.parameters.database', + 'database.redis-sentinel.queue.options.parameters.database', + ], + 'REDIS_SENTINEL_SERVICE' => [ + 'database.redis-sentinel.options.service', + 'database.redis-sentinel.cache.options.service', + 'database.redis-sentinel.session.options.service', + 'database.redis-sentinel.queue.options.service', + ], + 'REDIS_SENTINEL_TIMEOUT' => [ + 'database.redis-sentinel.options.sentinel_timeout', + ], + 'REDIS_SENTINEL_RETRY_LIMIT' => [ + 'database.redis-sentinel.options.retry_limit', + ], + 'REDIS_SENTINEL_RETRY_WAIT' => [ + 'database.redis-sentinel.options.retry_wait', + ], + 'REDIS_SENTINEL_DISCOVERY' => [ + 'database.redis-sentinel.options.update_sentinels', + ], + 'REDIS_CACHE_HOST' => [ + 'database.redis-sentinel.cache.0.host', + ], + 'REDIS_CACHE_PORT' => [ + 'database.redis-sentinel.cache.0.port', + ], + 'REDIS_CACHE_SERVICE' => [ + 'database.redis-sentinel.cache.options.service', + ], + 'REDIS_CACHE_PASSWORD' => [ + 'database.redis-sentinel.cache.options.parameters.password', + ], + 'REDIS_CACHE_DATABASE' => [ + 'database.redis-sentinel.cache.options.parameters.database', + ], + 'REDIS_SESSION_HOST' => [ + 'database.redis-sentinel.session.0.host', + ], + 'REDIS_SESSION_PORT' => [ + 'database.redis-sentinel.session.0.port', + ], + 'REDIS_SESSION_SERVICE' => [ + 'database.redis-sentinel.session.options.service', + ], + 'REDIS_SESSION_PASSWORD' => [ + 'database.redis-sentinel.session.options.parameters.password', + ], + 'REDIS_SESSION_DATABASE' => [ + 'database.redis-sentinel.session.options.parameters.database', + ], + 'REDIS_QUEUE_HOST' => [ + 'database.redis-sentinel.queue.0.host', + ], + 'REDIS_QUEUE_PORT' => [ + 'database.redis-sentinel.queue.0.port', + ], + 'REDIS_QUEUE_SERVICE' => [ + 'database.redis-sentinel.queue.options.service', + ], + 'REDIS_QUEUE_PASSWORD' => [ + 'database.redis-sentinel.queue.options.parameters.password', + ], + 'REDIS_QUEUE_DATABASE' => [ + 'database.redis-sentinel.queue.options.parameters.database', + ], + 'REDIS_DRIVER' => [ + 'database.redis.driver', + ], + 'CACHE_REDIS_CONNECTION' => [ + 'cache.stores.redis-sentinel.connection' + ], + 'CACHE_REDIS_SENTINEL_CONNECTION' => [ + 'cache.stores.redis-sentinel.connection' + ], + 'SESSION_CONNECTION' => [ + 'session.connection', + ], + 'QUEUE_REDIS_CONNECTION' => [ + 'queue.connections.redis-sentinel.connection', + ], + 'QUEUE_REDIS_SENTINEL_CONNECTION' => [ + 'queue.connections.redis-sentinel.connection', + ], + ]; + + /** + * Run this setup before each test + * + * @return void + */ + public function setUp() + { + $this->startTestWithBareApplication(); + + if (ApplicationFactory::isLumen()) { + unset($this->configKeys['session_connection']); + unset($this->defaultConfigEnvironmentVars['SESSION_CONNECTION']); + } + } + + public function testIsInitializable() + { + $this->assertInstanceOf(Loader::class, $this->loader); + } + + public function testIsInitializableWithFactoryMethod() + { + // Don't actually load anything when calling the factory method for + // this test: + $this->config->set('redis-sentinel.load_config', false); + + $this->assertInstanceOf(Loader::class, Loader::load($this->app)); + } + + public function testChecksWhetherApplicationIsLumen() + { + if (ApplicationFactory::isLumen()) { + $this->assertTrue($this->loader->isLumen); + } else { + $this->assertFalse($this->loader->isLumen); + } + } + + public function testChecksWhetherApplicationSupportsSessions() + { + if (ApplicationFactory::isLumen()) { + $this->assertFalse($this->loader->supportsSessions); + } else { + $this->assertTrue($this->loader->supportsSessions); + } + } + + public function testChecksWhetherPackageShouldOverrideRedisApi() + { + $this->config->set('database.redis.driver', 'redis-sentinel'); + $this->assertTrue($this->loader->shouldOverrideLaravelRedisApi()); + + // Previous versios of the package looked for the value 'sentinel': + $this->config->set('database.redis.driver', 'sentinel'); + $this->assertTrue($this->loader->shouldOverrideLaravelRedisApi()); + + $this->config->set('database.redis.driver', 'default'); + $this->assertFalse($this->loader->shouldOverrideLaravelRedisApi()); + } + + public function testLoadsLumenConfigurationDependencies() + { + if (ApplicationFactory::isLumen()) { + $this->loader->loadConfiguration(); + + $this->assertTrue($this->config->has('database')); + $this->assertTrue($this->config->has('cache')); + $this->assertTrue($this->config->has('queue')); + } + } + + public function testLoadsDefaultConfiguration() + { + // The package only sets "session.connection" when "session.driver" + // equals "redis-sentinel" + if (! ApplicationFactory::isLumen()) { + $this->config->set('session.driver', 'redis-sentinel'); + } + + $this->loader->loadConfiguration(); + + foreach ($this->configKeys as $configKey) { + $this->assertTrue($this->config->has($configKey), $configKey); + } + } + + public function testLoadsDefaultConfigurationFromEnvironment() + { + $expected = 'environment variable value'; + + foreach ($this->defaultConfigEnvironmentVars as $env => $configKeys) { + putenv("$env=$expected"); // Set the environment variable + + // Reset the application configuration for each enviroment variable + // because several of the variables set the same config key: + $this->startTestWithBareApplication(); + + // The package only sets "session.connection" when "session.driver" + // equals "redis-sentinel" + if (! ApplicationFactory::isLumen()) { + $this->config->set('session.driver', 'redis-sentinel'); + } + + $this->loader->loadConfiguration(); + + foreach ($configKeys as $configKey) { + $this->assertEquals( + $expected, + $this->config->get($configKey), + "$env -> $configKey" + ); + } + + putenv($env); // Unset the environment variable + } + } + + public function testSkipsLoadingDefaultConfigurationIfDisabled() + { + $this->config->set('redis-sentinel.load_config', false); + $this->loader->loadConfiguration(); + + foreach ($this->configKeys as $configKey) { + $this->assertFalse($this->config->has($configKey), $configKey); + } + } + + public function testSkipsLoadingDefaultConfigurationIfProvidedByApp() + { + $this->startTestWithConfiguredApplication(); + + $expected = 'provided by app config file'; + + foreach ($this->configKeys as $configKey) { + $this->config->set($configKey, $expected); + } + + $this->loader->loadConfiguration(); + + foreach ($this->configKeys as $configKey) { + $this->assertEquals( + $expected, + $this->config->get($configKey), + $configKey + ); + } + + $this->assertFalse($this->config->has('redis-sentinel')); + } + + public function testSetsSessionConnectionIfMissing() + { + if (ApplicationFactory::isLumen()) { + return; + } + + $expected = 'connection name'; + + $this->config->set('session', [ 'driver' => 'redis-sentinel' ]); + $this->config->set('redis-sentinel.session.connection', $expected); + + $this->loader->loadConfiguration(); + + $this->assertEquals( + $expected, + $this->config->get('session.connection') + ); + } + + public function testSkipsSettingSessionConnectionIfExists() + { + if (ApplicationFactory::isLumen()) { + return; + } + + $expected = 'already set'; + + $this->config->set('session', [ + 'driver' => 'redis-sentinel' , + 'connection' => $expected, + ]); + + $this->loader->loadConfiguration(); + + $this->assertEquals( + $expected, + $this->config->get('session.connection') + ); + } + + public function testCleansPackageConfiguration() + { + foreach ($this->configKeys as $configKey) { + $packageConfigKey = "redis-sentinel.$configKey"; + $this->config->set($packageConfigKey, "dummy value"); + } + + $this->loader->loadConfiguration(); + + // The configuration loader under test adds a message that indicates + // that the package cleaned its configuration and sets the value of + // "redis-sentinel.load_config" to FALSE to prevent the package from + // reloading its configuration when cached ("artisan config:cache"). + $this->assertFalse($this->config->get('redis-sentinel.load_config')); + + foreach ($this->configKeys as $configKey) { + $packageConfigKey = "redis-sentinel.$configKey"; + $this->assertFalse($this->config->has($packageConfigKey)); + } + } + + public function testSkipsCleaningPackageConfigurationWhenDisabled() + { + $this->config->set('redis-sentinel.clean_config', false); + + foreach ($this->configKeys as $configKey) { + $packageConfigKey = "redis-sentinel.$configKey"; + $this->config->set($packageConfigKey, "dummy value"); + } + + $this->loader->loadConfiguration(); + + $this->assertFalse($this->config->has('redis-sentinel.load_config')); + + foreach ($this->configKeys as $configKey) { + $packageConfigKey = "redis-sentinel.$configKey"; + $this->assertTrue($this->config->has($packageConfigKey)); + } + } + + public function testNormalizesSentinelConnectionHosts() + { + $this->startTestWithConfiguredApplication(); + + // To support environment-based configuration, the package allows + // developers to provide comma-seperated Redis Sentinel host values + // to specify multiple hosts through a single environment variable. + $this->config->set('database.redis-sentinel', [ + 'connection' => [ + [ + // The package should split a comma-seperated string of + // hosts into individual host definitions. Any hosts that + // contain more than the hostname or IP address don't need + // an array. It should trim any spaces around the comma: + 'host' => 'host1,10.0.0.1,host3:999 , tcp://host4:999', + // Hosts above that don't specify a port should inherit the + // port from this block: + 'port' => 888, + ], + // The package should leave a single host string alone: + 'tcp://host5:999', + // But it should split a comma-seperated string of hosts if the + // developer uses this feature in their own package config: + 'tcp://host6:999,tcp://host7:999', + ], + ]); + + $this->loader->loadConfiguration(); + + $this->assertEquals( + [ + 'connection' => [ + [ + 'host' => 'host1', + 'port' => 888, + ], + [ + 'host' => '10.0.0.1', + 'port' => 888, + ], + 'host3:999', + 'tcp://host4:999', + 'tcp://host5:999', + 'tcp://host6:999', + 'tcp://host7:999', + ], + ], + $this->config->get('database.redis-sentinel') + ); + } + + /** + * Reset the current application, configuration, and loader under test + * with new instances using an un-configured application. + * + * @return void + */ + protected function startTestWithBareApplication() + { + $this->app = ApplicationFactory::make(false); + $this->config = $this->app->config; + $this->loader = new Loader($this->app); + } + + /** + * Reset the current application, configuration, and loader under test + * with new instances using an pre-configured application. + * + * @return void + */ + protected function startTestWithConfiguredApplication() + { + $this->app = ApplicationFactory::make(); + $this->config = $this->app->config; + $this->loader = new Loader($this->app); + } +} diff --git a/tests/RedisSentinelServiceProviderTest.php b/tests/RedisSentinelServiceProviderTest.php index 5bb268f..1c10675 100644 --- a/tests/RedisSentinelServiceProviderTest.php +++ b/tests/RedisSentinelServiceProviderTest.php @@ -48,6 +48,29 @@ public function testIsInitializable() ); } + public function testLoadsDefaultConfiguration() + { + $config = new ConfigRepository(); + $this->app->config = $config; + + // The package only sets "session.connection" when "session.driver" + // equals "redis-sentinel" + if (! ApplicationFactory::isLumen()) { + $config->set('session.driver', 'redis-sentinel'); + } + + $this->provider->register(); + + $this->assertTrue($config->has('database.redis-sentinel')); + $this->assertTrue($config->has('database.redis.driver')); + $this->assertTrue($config->has('cache.stores.redis-sentinel')); + $this->assertTrue($config->has('queue.connections.redis-sentinel')); + + if (! ApplicationFactory::isLumen()) { + $this->assertTrue($config->has('session.connection')); + } + } + public function testRegistersWithApplication() { $this->provider->register(); @@ -72,7 +95,7 @@ public function testRegisterPreservesStandardRedisApi() public function testRegisterOverridesStandardRedisApi() { - $this->app->config->set('database.redis.driver', 'sentinel'); + $this->app->config->set('database.redis.driver', 'redis-sentinel'); $this->provider->register(); $this->provider->boot(); diff --git a/tests/stubs/config.php b/tests/stubs/config.php index 2bb1536..1d8d307 100644 --- a/tests/stubs/config.php +++ b/tests/stubs/config.php @@ -10,19 +10,19 @@ // Represents a subset of config/database.php 'database' => [ 'redis' => [ - 'driver' => 'sentinel', + 'driver' => 'redis-sentinel', ], 'redis-sentinel' => [ 'connection1' => [ [ 'host' => 'localhost', - 'port' => 6379, + 'port' => 26379, ], ], 'connection2' => [ [ 'host' => 'localhost', - 'port' => 6379, + 'port' => 26379, ], 'options' => [ 'service' => 'another-master',