refactor(cache): implement graceful degradation for BrandCache
This commit is contained in:
parent
0766e9dc8f
commit
daa8d1e0fa
3 changed files with 151 additions and 44 deletions
|
|
@ -20,22 +20,38 @@ final readonly class BrandCache
|
|||
|
||||
public function __construct(
|
||||
private CacheInterface $cacheStore,
|
||||
private \Psr\Log\LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getById(BrandId $id): ?Brand
|
||||
{
|
||||
return $this->fromCacheData($this->cacheStore->get($this->idKey($id)));
|
||||
try {
|
||||
return $this->fromCacheData($this->cacheStore->get($this->idKey($id)));
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::getById failed', ['exception' => $e->getMessage()]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getByCode(BrandCode $code): ?Brand
|
||||
{
|
||||
return $this->fromCacheData($this->cacheStore->get($this->codeKey($code)));
|
||||
try {
|
||||
return $this->fromCacheData($this->cacheStore->get($this->codeKey($code)));
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::getByCode failed', ['exception' => $e->getMessage()]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getByArcadiaId(ArcadiaId $arcadiaId): ?Brand
|
||||
{
|
||||
return $this->fromCacheData($this->cacheStore->get($this->arcadiaKey($arcadiaId)));
|
||||
try {
|
||||
return $this->fromCacheData($this->cacheStore->get($this->arcadiaKey($arcadiaId)));
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::getByArcadiaId failed', ['exception' => $e->getMessage()]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -43,65 +59,86 @@ final readonly class BrandCache
|
|||
*/
|
||||
public function getAll(): ?array
|
||||
{
|
||||
$data = $this->cacheStore->get(self::KEY_ALL);
|
||||
try {
|
||||
$data = $this->cacheStore->get(self::KEY_ALL);
|
||||
|
||||
if (! is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$brands = [];
|
||||
foreach ($data as $item) {
|
||||
$brand = $this->fromCacheData($item);
|
||||
if ($brand !== null) {
|
||||
$brands[] = $brand;
|
||||
if (! is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($brands) !== count($data) || empty($brands)) {
|
||||
$brands = [];
|
||||
foreach ($data as $item) {
|
||||
$brand = $this->fromCacheData($item);
|
||||
if ($brand !== null) {
|
||||
$brands[] = $brand;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($brands) !== count($data) || empty($brands)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $brands;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::getAll failed', ['exception' => $e->getMessage()]);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $brands;
|
||||
}
|
||||
|
||||
public function put(Brand $brand): void
|
||||
{
|
||||
$data = $this->toCacheData($brand);
|
||||
try {
|
||||
$data = $this->toCacheData($brand);
|
||||
|
||||
$this->cacheStore->set($this->idKey($brand->id()), $data, self::CACHE_TTL_SECONDS);
|
||||
$this->cacheStore->set($this->codeKey($brand->code), $data, self::CACHE_TTL_SECONDS);
|
||||
$this->cacheStore->set($this->arcadiaKey($brand->arcadiaId), $data, self::CACHE_TTL_SECONDS);
|
||||
$this->cacheStore->set($this->idKey($brand->id()), $data, self::CACHE_TTL_SECONDS);
|
||||
$this->cacheStore->set($this->codeKey($brand->code), $data, self::CACHE_TTL_SECONDS);
|
||||
$this->cacheStore->set($this->arcadiaKey($brand->arcadiaId), $data, self::CACHE_TTL_SECONDS);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::put failed', ['exception' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function putAll(Brand ...$brands): void
|
||||
{
|
||||
$cachedList = array_map(fn (Brand $brand): array => $this->toCacheData($brand), $brands);
|
||||
$this->cacheStore->set(self::KEY_ALL, $cachedList, self::CACHE_TTL_SECONDS);
|
||||
try {
|
||||
$cachedList = array_map(fn (Brand $brand): array => $this->toCacheData($brand), $brands);
|
||||
$this->cacheStore->set(self::KEY_ALL, $cachedList, self::CACHE_TTL_SECONDS);
|
||||
|
||||
foreach ($brands as $brand) {
|
||||
$this->put($brand);
|
||||
foreach ($brands as $brand) {
|
||||
$this->put($brand);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::putAll failed', ['exception' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function invalidate(Brand $brand): void
|
||||
{
|
||||
$this->cacheStore->delete(self::KEY_ALL);
|
||||
$this->cacheStore->delete($this->idKey($brand->id()));
|
||||
$this->cacheStore->delete($this->codeKey($brand->code));
|
||||
$this->cacheStore->delete($this->arcadiaKey($brand->arcadiaId));
|
||||
try {
|
||||
$this->cacheStore->delete(self::KEY_ALL);
|
||||
$this->cacheStore->delete($this->idKey($brand->id()));
|
||||
$this->cacheStore->delete($this->codeKey($brand->code));
|
||||
$this->cacheStore->delete($this->arcadiaKey($brand->arcadiaId));
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::invalidate failed', ['exception' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->cacheStore->delete(self::KEY_ALL);
|
||||
try {
|
||||
$this->cacheStore->delete(self::KEY_ALL);
|
||||
|
||||
$all = $this->getAll();
|
||||
if ($all !== null) {
|
||||
foreach ($all as $brand) {
|
||||
$this->cacheStore->delete($this->idKey($brand->id()));
|
||||
$this->cacheStore->delete($this->codeKey($brand->code));
|
||||
$this->cacheStore->delete($this->arcadiaKey($brand->arcadiaId));
|
||||
$all = $this->getAll();
|
||||
if ($all !== null) {
|
||||
foreach ($all as $brand) {
|
||||
$this->cacheStore->delete($this->idKey($brand->id()));
|
||||
$this->cacheStore->delete($this->codeKey($brand->code));
|
||||
$this->cacheStore->delete($this->arcadiaKey($brand->arcadiaId));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('BrandCache::reset failed', ['exception' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ class DomainServiceProvider extends ServiceProvider
|
|||
$this->app->bind(BrandCache::class, static function (Application $app): BrandCache {
|
||||
$storageConfig = $app->make(ArcadiaStorageConfig::class);
|
||||
$cache = Cache::store($storageConfig->cacheStore);
|
||||
$logger = $app->make(\Psr\Log\LoggerInterface::class);
|
||||
|
||||
return new BrandCache($cache);
|
||||
return new BrandCache($cache, $logger);
|
||||
});
|
||||
|
||||
$this->app->scoped(LaravelTransactionalDomainEventDispatcher::class);
|
||||
|
|
|
|||
|
|
@ -36,12 +36,16 @@ describe('BrandCache Unit Tests', function (): void {
|
|||
->with("brand:id:{$uuid}")
|
||||
->andReturn($cacheData);
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
/** @var \Psr\Log\LoggerInterface&Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(\Psr\Log\LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->zeroOrMoreTimes();
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$result = $brandCache->getById($brandId);
|
||||
|
||||
// THEN
|
||||
assert($result !== null);
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result->id()->value)->toBe($brandId->value)
|
||||
->and($result->name->value)->toBe('Le Gaulois')
|
||||
|
|
@ -68,12 +72,16 @@ describe('BrandCache Unit Tests', function (): void {
|
|||
->with('brand:code:LGL01')
|
||||
->andReturn($cacheData);
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
/** @var \Psr\Log\LoggerInterface&Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(\Psr\Log\LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->zeroOrMoreTimes();
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$result = $brandCache->getByCode($brandCode);
|
||||
|
||||
// THEN
|
||||
assert($result !== null);
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result->code->value)->toBe('LGL01')
|
||||
->and($result->name->value)->toBe('Le Gaulois');
|
||||
|
|
@ -98,12 +106,16 @@ describe('BrandCache Unit Tests', function (): void {
|
|||
->with('brand:arcadia:10')
|
||||
->andReturn($cacheData);
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
/** @var \Psr\Log\LoggerInterface&Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(\Psr\Log\LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->zeroOrMoreTimes();
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$result = $brandCache->getByArcadiaId($arcadiaId);
|
||||
|
||||
// THEN
|
||||
assert($result !== null);
|
||||
expect($result)->not->toBeNull()
|
||||
->and($result->arcadiaId->value)->toBe('10')
|
||||
->and($result->name->value)->toBe('Maître CoQ');
|
||||
|
|
@ -120,7 +132,10 @@ describe('BrandCache Unit Tests', function (): void {
|
|||
->with("brand:id:{$brandId->value}")
|
||||
->andReturn('corrupted_string');
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
/** @var \Psr\Log\LoggerInterface&Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(\Psr\Log\LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->zeroOrMoreTimes();
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$result = $brandCache->getById($brandId);
|
||||
|
|
@ -158,7 +173,10 @@ describe('BrandCache Unit Tests', function (): void {
|
|||
->once()
|
||||
->with('brand:arcadia:20', $expectedData, 86400);
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
/** @var \Psr\Log\LoggerInterface&Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(\Psr\Log\LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->zeroOrMoreTimes();
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$brandCache->put($brand);
|
||||
|
|
@ -192,7 +210,10 @@ describe('BrandCache Unit Tests', function (): void {
|
|||
->once()
|
||||
->with('brand:arcadia:30');
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
/** @var \Psr\Log\LoggerInterface&Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(\Psr\Log\LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->zeroOrMoreTimes();
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$brandCache->invalidate($brand);
|
||||
|
|
@ -233,7 +254,10 @@ describe('BrandCache Unit Tests', function (): void {
|
|||
->once()
|
||||
->with('brand:arcadia:1');
|
||||
|
||||
$brandCache = new BrandCache($cacheMock);
|
||||
/** @var \Psr\Log\LoggerInterface&Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(\Psr\Log\LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->zeroOrMoreTimes();
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$brandCache->reset();
|
||||
|
|
@ -241,4 +265,49 @@ describe('BrandCache Unit Tests', function (): void {
|
|||
// THEN
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
it('returns null and logs warning when getById throws exception', function (): void {
|
||||
// GIVEN
|
||||
$brandId = new BrandId(Uuid::uuid7());
|
||||
/** @var CacheInterface&\Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('get')->andThrow(new \RuntimeException('Redis error'));
|
||||
|
||||
/** @var \Psr\Log\LoggerInterface&\Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(\Psr\Log\LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->once();
|
||||
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$result = $brandCache->getById($brandId);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
it('logs warning and does not throw when put throws exception', function (): void {
|
||||
// GIVEN
|
||||
$brand = Brand::hydrate(
|
||||
id: new BrandId(Uuid::uuid7()),
|
||||
name: new BrandName('Ghost'),
|
||||
code: new BrandCode('GHO01'),
|
||||
arcadiaId: new ArcadiaId('999'),
|
||||
);
|
||||
/** @var CacheInterface&\Mockery\MockInterface $cacheMock */
|
||||
$cacheMock = Mockery::mock(CacheInterface::class);
|
||||
$cacheMock->shouldReceive('set')->andThrow(new \RuntimeException('Redis error'));
|
||||
|
||||
/** @var \Psr\Log\LoggerInterface&\Mockery\MockInterface $loggerMock */
|
||||
$loggerMock = Mockery::mock(\Psr\Log\LoggerInterface::class);
|
||||
$loggerMock->shouldReceive('warning')->once();
|
||||
|
||||
$brandCache = new BrandCache($cacheMock, $loggerMock);
|
||||
|
||||
// WHEN
|
||||
$brandCache->put($brand);
|
||||
|
||||
// THEN
|
||||
// No exception is thrown
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue