After a year of good and loyal service, the Redis instance used by my service went down—rebooted by AWS—making the service inoperative for about 30 minutes. A situation that could have been avoided if the service was resilient to cache unavailability.
The lifetime of a request
The service implements the command dispatcher pattern: for each request a corresponding query/command object is created, which is then passed to a query/command dispatcher to be executed. The result is serialized and used as the response body. To speed up queries, an event is emitted before their dispatching, if a listener provides a cached response, it is returned immediately, short-circuiting the dispatch/serialize process. To keep the cache consistent with the database, an event is emitted when a command is dispatched to invalidate cache keys.
It is during the dispatching of these events that the application would crash. Not because calls to the cache would fail—I already had try/catch blocks handling those cases—but because the cache was instantiated while the connection to Redis was broken.
The intricacy of dependencies
Event listeners related to cached responses depend on a ResponseCache
instance. The class exposes simple methods to retrieve and store responses using
their corresponding query/command as key. The dependency graph could be
represented as follows:
ResponseCache
└── TagAwareAdapter
└── Adapter (items)
└── Redis
└── Adapter (tags)
└── Redis
Because I'm using the same Redis instance for both items and tags, I figured I
could maybe decorate that instance with try/catch
block around its public
methods, but it proved to be impossible because Symphony's adaptor expects a
very specific set of classes.
Since I couldn't decorate Redis, the next best thing was the ResponseCache
class.
Making the service resilient to cache failure
To avoid instantiating Redis during the initialization of the dependency graph,
I created a proxy to ResponseCache
, that would only require the actual service
when one of its methods is invoked. I started by renaming ResponseCache
as
BasicResponseCache
, then I added a ResponseCache
interface. Because I don't
use the dreadful Interface
suffix I didn't have to modify my code base, only
to update my service configurations.
And that was it!
<?php
namespace HelloFresh\RecipeService\Presentation\HTTP;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
class ResponseCacheProxy implements ResponseCache
{
/**
* @var ContainerInterface
*/
private $container;
/**
* @var string
*/
private $service;
/**
* @var ResponseCache
*/
private $instance;
public function __construct(ContainerInterface $container, string $service)
{
$this->container = $container;
$this->service = $service;
}
/**
* @inheritdoc
*/
public function retrieve(object $query): ?Response
{
return $this->getInstance()->retrieve($query);
}
/**
* @inheritdoc
*/
public function store(object $query, Response $response): void
{
$this->getInstance()->store($query, $response);
}
/**
* @inheritdoc
*/
public function remove(object $command): bool
{
return $this->getInstance()->remove($command);
}
/**
* @inheritdoc
*/
public function clear(): void
{
$this->getInstance()->clear();
}
private function getInstance(): ResponseCache
{
return $this->instance ?: $this->instance = $this->container->get($this->service);
}
}
HelloFresh\RecipeService\Presentation\HTTP\SimpleResponseCache:
arguments:
$cache: "@cache.response"
http.response_cache:
alias: HelloFresh\RecipeService\Presentation\HTTP\SimpleResponseCache
public: true
HelloFresh\RecipeService\Presentation\HTTP\ResponseCacheProxy:
arguments:
$service: http.response_cache
tl;dr
Consider using proxies for dependencies that could bring your application down during their instantiation.