Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- 33306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:5.0
image: redis:7.0
ports:
- 6379:6379
options: --entrypoint redis-server
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ services:
- "3306:3306"
restart: always
redis:
image: redis:5.0-alpine
image: redis:7.0-alpine
ports:
- "6379:6379"
restart: always
2 changes: 1 addition & 1 deletion src/Illuminate/Cache/RedisStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ public function flush()
public function tags($names)
{
return new RedisTaggedCache(
$this, new TagSet($this, is_array($names) ? $names : func_get_args())
$this, new RedisTagSet($this, is_array($names) ? $names : func_get_args())
);
}

Expand Down
124 changes: 124 additions & 0 deletions src/Illuminate/Cache/RedisTagSet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

namespace Illuminate\Cache;

use Illuminate\Support\LazyCollection;

class RedisTagSet extends TagSet
{
/**
* Add a reference entry to the tag set's underlying sorted set.
*
* @param string $key
* @param int $ttl
* @param string $updateWhen
* @return void
*/
public function addEntry(string $key, int $ttl = 0, $updateWhen = null)
{
$ttl = $ttl > 0 ? now()->addSeconds($ttl)->getTimestamp() : -1;

foreach ($this->tagIds() as $tagKey) {
if ($updateWhen) {
$this->store->connection()->zadd($this->store->getPrefix().$tagKey, $updateWhen, $ttl, $key);
} else {
$this->store->connection()->zadd($this->store->getPrefix().$tagKey, $ttl, $key);
}
}
}

/**
* Get all of the cache entry keys for the tag set.
*
* @return \Illuminate\Support\LazyCollection
*/
public function entries()
{
return LazyCollection::make(function () {
foreach ($this->tagIds() as $tagKey) {
$cursor = $defaultCursorValue = '0';

do {
[$cursor, $entries] = $this->store->connection()->zscan(
$this->store->getPrefix().$tagKey,
$cursor,
['match' => '*', 'count' => 1000]
);

if (! is_array($entries)) {
break;
}

$entries = array_unique(array_keys($entries));

if (count($entries) === 0) {
continue;
}

foreach ($entries as $entry) {
yield $entry;
}
} while (((string) $cursor) !== $defaultCursorValue);
}
});
}

/**
* Remove the stale entries from the tag set.
*
* @return void
*/
public function flushStaleEntries()
{
$this->store->connection()->pipeline(function ($pipe) {
foreach ($this->tagIds() as $tagKey) {
$pipe->zremrangebyscore($this->store->getPrefix().$tagKey, 0, now()->getTimestamp());
}
});
}

/**
* Flush the tag from the cache.
*
* @param string $name
*/
public function flushTag($name)
{
return $this->resetTag($name);
}

/**
* Reset the tag and return the new tag identifier.
*
* @param string $name
* @return string
*/
public function resetTag($name)
{
$this->store->forget($this->tagKey($name));

return $this->tagId($name);
}

/**
* Get the unique tag identifier for a given tag.
*
* @param string $name
* @return string
*/
public function tagId($name)
{
return "tag:{$name}:entries";
}

/**
* Get the tag identifier key for a given tag.
*
* @param string $name
* @return string
*/
public function tagKey($name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tagKey used to return just "tag:$name:key" whose value pointed to the name of tag reference key, while tagId returned the reference key itself (laravel:6307213384953462982890:standard_ref). Technically, if somebody used those for whatever reason, it will be a breaking change for them.

Same goes for resetTag - if somebody calls that and expects the tag key to be present in cache afterwards (which is what used to happen), this is also technically a breaking change.

Both of these aren't major at all and I doubt many people used these at all, but it's worth mentioning.

Copy link
Contributor

@clemblanco clemblanco Jan 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't see a change in the tests for this kind of intermediate entries being stored properly either. Unless I missed it.

{
return "tag:{$name}:entries";
}
}
153 changes: 34 additions & 119 deletions src/Illuminate/Cache/RedisTaggedCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@
class RedisTaggedCache extends TaggedCache
{
/**
* Forever reference key.
* Store an item in the cache if the key does not exist.
*
* @var string
* @param string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
*/
const REFERENCE_KEY_FOREVER = 'forever_ref';
public function add($key, $value, $ttl = null)
{
$this->tags->addEntry(
$this->itemKey($key),
! is_null($ttl) ? $this->getSeconds($ttl) : 0
);

/**
* Standard reference key.
*
* @var string
*/
const REFERENCE_KEY_STANDARD = 'standard_ref';
return parent::add($key, $value, $ttl);
}

/**
* Store an item in the cache.
Expand All @@ -28,11 +32,14 @@ class RedisTaggedCache extends TaggedCache
*/
public function put($key, $value, $ttl = null)
{
if ($ttl === null) {
if (is_null($ttl)) {
return $this->forever($key, $value);
}

$this->pushStandardKeys($this->tags->getNamespace(), $key);
$this->tags->addEntry(
$this->itemKey($key),
$this->getSeconds($ttl)
);

return parent::put($key, $value, $ttl);
}
Expand All @@ -46,7 +53,7 @@ public function put($key, $value, $ttl = null)
*/
public function increment($key, $value = 1)
{
$this->pushStandardKeys($this->tags->getNamespace(), $key);
$this->tags->addEntry($this->itemKey($key), updateWhen: 'NX');

return parent::increment($key, $value);
}
Expand All @@ -60,7 +67,7 @@ public function increment($key, $value = 1)
*/
public function decrement($key, $value = 1)
{
$this->pushStandardKeys($this->tags->getNamespace(), $key);
$this->tags->addEntry($this->itemKey($key), updateWhen: 'NX');

return parent::decrement($key, $value);
}
Expand All @@ -74,7 +81,7 @@ public function decrement($key, $value = 1)
*/
public function forever($key, $value)
{
$this->pushForeverKeys($this->tags->getNamespace(), $key);
$this->tags->addEntry($this->itemKey($key));

return parent::forever($key, $value);
}
Expand All @@ -86,129 +93,37 @@ public function forever($key, $value)
*/
public function flush()
{
$this->deleteForeverKeys();
$this->deleteStandardKeys();

$this->flushValues();
$this->tags->flush();

return true;
}

/**
* Store standard key references into store.
*
* @param string $namespace
* @param string $key
* @return void
*/
protected function pushStandardKeys($namespace, $key)
{
$this->pushKeys($namespace, $key, self::REFERENCE_KEY_STANDARD);
}

/**
* Store forever key references into store.
*
* @param string $namespace
* @param string $key
* @return void
*/
protected function pushForeverKeys($namespace, $key)
{
$this->pushKeys($namespace, $key, self::REFERENCE_KEY_FOREVER);
}

/**
* Store a reference to the cache key against the reference key.
*
* @param string $namespace
* @param string $key
* @param string $reference
* @return void
*/
protected function pushKeys($namespace, $key, $reference)
{
$fullKey = $this->store->getPrefix().sha1($namespace).':'.$key;

foreach (explode('|', $namespace) as $segment) {
$this->store->connection()->sadd($this->referenceKey($segment, $reference), $fullKey);
}
}

/**
* Delete all of the items that were stored forever.
* Flush the individual cache entries for the tags.
*
* @return void
*/
protected function deleteForeverKeys()
protected function flushValues()
{
$this->deleteKeysByReference(self::REFERENCE_KEY_FOREVER);
}

/**
* Delete all standard items.
*
* @return void
*/
protected function deleteStandardKeys()
{
$this->deleteKeysByReference(self::REFERENCE_KEY_STANDARD);
}
$entries = $this->tags->entries()
->map(fn (string $key) => $this->store->getPrefix().$key)
->chunk(1000);

/**
* Find and delete all of the items that were stored against a reference.
*
* @param string $reference
* @return void
*/
protected function deleteKeysByReference($reference)
{
foreach (explode('|', $this->tags->getNamespace()) as $segment) {
$this->deleteValues($segment = $this->referenceKey($segment, $reference));

$this->store->connection()->del($segment);
foreach ($entries as $cacheKeys) {
$this->store->connection()->del(...$cacheKeys);
}
}

/**
* Delete item keys that have been stored against a reference.
* Remove all stale reference entries from the tag set.
*
* @param string $referenceKey
* @return void
* @return bool
*/
protected function deleteValues($referenceKey)
public function flushStale()
{
$cursor = $defaultCursorValue = '0';

do {
[$cursor, $valuesChunk] = $this->store->connection()->sscan(
$referenceKey, $cursor, ['match' => '*', 'count' => 1000]
);

// PhpRedis client returns false if set does not exist or empty. Array destruction
// on false stores null in each variable. If valuesChunk is null, it means that
// there were not results from the previously executed "sscan" Redis command.
if (is_null($valuesChunk)) {
break;
}

$valuesChunk = array_unique($valuesChunk);

if (count($valuesChunk) > 0) {
$this->store->connection()->del(...$valuesChunk);
}
} while (((string) $cursor) !== $defaultCursorValue);
}
$this->tags->flushStaleEntries();

/**
* Get the reference key for the segment.
*
* @param string $segment
* @param string $suffix
* @return string
*/
protected function referenceKey($segment, $suffix)
{
return $this->store->getPrefix().$segment.':'.$suffix;
return true;
}
}
Loading