Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Cache] Add stampede protection via probabilistic early expiration #27009

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 11, 2018

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Apr 22, 2018

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? yes
Tests pass? yes
Fixed tickets -
License MIT
Doc PR

This PR implements probabilistic early expiration on top of $cache->get($key, $callback);

It adds a 3rd arg to CacheInterface::get:

float $beta A float that controls the likelyness of triggering early expiration. 0 disables it, INF forces immediate expiration. The default is implementation dependend but should typically be 1.0, which should provide optimal stampede protection.

*/
public function get(string $key, callable $callback, float $beta = null)
{
if (!\is_string($key)) {
Copy link
Member

Choose a reason for hiding this comment

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

Just asking: above we have string $key in the method argument ... will this if (!is_string($key)) code be ever executed if we pass a non-string argument?

$innerItem->set(null);

return $item;
},
null,
CacheItem::class
);
$this->setInnerItem = \Closure::bind(
function (CacheItemInterface $innerItem, array $item) {
if ($stats = $item["\0*\0newStats"]) {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure it it'd help much, but maybe move \0*\0 to private const PREFIX = '\0*\0'; in this class?

Copy link
Member Author

@nicolas-grekas nicolas-grekas Apr 23, 2018

Choose a reason for hiding this comment

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

Not sure also, using the const would just add indirection IMHO, for a purely internal thing.
Other comments addressed thanks.

{
/**
* @param callable(CacheItemInterface $item):mixed $callback Should return the computed value for the given key/item
* @param float $beta A float that controls the likelyness of triggering early expiration.
Copy link
Member

Choose a reason for hiding this comment

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

likelyness -> likeness

/**
* @param callable(CacheItemInterface $item):mixed $callback Should return the computed value for the given key/item
* @param float $beta A float that controls the likelyness of triggering early expiration.
* 0 disables it, INF forces immediate expiration. The default is implementation dependend
Copy link
Member

Choose a reason for hiding this comment

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

dependend -> dependent


/**
* @author Nicolas Grekas <[email protected]>
*/
final class CacheItem implements CacheItemInterface
{
/**
* References the unix timestamp stating when the item will expire, as integer.
Copy link
Member

Choose a reason for hiding this comment

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

unix -> Unix

@nicolas-grekas nicolas-grekas force-pushed the cache-stats branch 3 times, most recently from c2e16bf to 6ae48dc Compare April 23, 2018 08:48
@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 23, 2018

ping @Nyholm FYI: here, I deprecate getPreviousTags() and replace it by getMetadata()

@nicolas-grekas nicolas-grekas force-pushed the cache-stats branch 2 times, most recently from 7a76d9f to af895ff Compare April 23, 2018 19:36
@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 24, 2018

I don't like unclear default values. Why not 1.0 directly?

I designed it this way so that the interface is generic enough. Hardcoding 1.0 as default would prevent any other implementations from providing a different default. Yes, there are no other implementations than the ones in this PR, but that's not a reason to break the abstraction provided by the interface.
Note that PSR-6 does the same for default expiry of items.

@nicolas-grekas nicolas-grekas force-pushed the cache-stats branch 2 times, most recently from 99634fc to 472b593 Compare April 24, 2018 10:07
{
/**
* @param callable(CacheItemInterface $item):mixed $callback Should return the computed value for the given key/item
* @param float|null $beta A float that controls the likeness of triggering early expiration.
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: likeliness

Copy link
Member Author

Choose a reason for hiding this comment

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

I did the same mistake as you, but @javiereguiluz made me fix it: it's really "likeness".

Copy link
Contributor

Choose a reason for hiding this comment

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

They have different meanings. You mean likeliness here.
likeliness = probability
likeness = resemblance

Copy link
Member Author

@nicolas-grekas nicolas-grekas Apr 24, 2018

Choose a reason for hiding this comment

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

indeed, sorry for the misunderstanding (on my side)! (fixed)

@nicolas-grekas
Copy link
Member Author

Rebased, PR ready for review.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented May 28, 2018

PR is ready.

The protection needs storing expiry+computation time. This is done by encoding these values in the key of a wrapping array:
$persistedValue = array("\x9D".$expiry.$computeTime."\x5F" => $cachedValue);

by using this magic-numbered structure, we are able to persist both raw + wrapped values in the same store, providing forward/backward compat at the storage level.

Stampede protection is always enabled when accessing values through the new CacheInterface::get() method (one should use the PSR-6 interface if they don't want the overhead of storing expiry+compute-time, thus opting out from stampede protection.)

@Nyholm
Copy link
Member

Nyholm commented May 28, 2018

Does the getPreviousTags really need to be deprecated? Wouldn't it be good to still have that? As far as I know that would make us compatible with the "soon to be"-PSR

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented May 28, 2018

That's the thing we need to discuss :)
So, my current pov is that accessing tags is useful, but also is accessing the expiry. One use case for the expiry is the one present in this PR, another one is for HTTP caches, allowing to compute the max-age directive when serving cached values.
Which means if we spend some effort having an updated PSR, it could be worth doing it once for all.
This getMetadata() method would be my proposal: an array with keyed values, thus extensible, but still having some keys reserved by const name at least (tags, expiry - computation time could be a custom extension - or in the PSR).

@nicolas-grekas
Copy link
Member Author

Rebased

@nicolas-grekas
Copy link
Member Author

ping @symfony/deciders

Copy link
Member

@Nyholm Nyholm left a comment

Choose a reason for hiding this comment

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

I've looked at this PR. It is real hard to understand what you are doing. Even though I know what you are trying to do. :/

I do not mind the API changes but I would be happy to see some more comments.

$item->isHit = $isHit;
$item->defaultLifetime = $defaultLifetime;
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = \key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
Copy link
Member

Choose a reason for hiding this comment

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

This line looks really complicated. I cannot tell what it is doing. Maybe add a comment?

Copy link
Member Author

Choose a reason for hiding this comment

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

Comments added, I hope it will be more clear.

$innerItem->set(null);

return $item;
},
null,
CacheItem::class
);
$this->setInnerItem = \Closure::bind(
function (CacheItemInterface $innerItem, array $item) {
if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) {
Copy link
Member

Choose a reason for hiding this comment

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

Same here, Im really trying to understand this, but I can't =)

Copy link
Member Author

Choose a reason for hiding this comment

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

Comment added, better?

Copy link
Member

@fabpot fabpot left a comment

Choose a reason for hiding this comment

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

with minor comments

$item->isHit = $isHit;
$item->defaultLifetime = $defaultLifetime;
// Detect wrapped values that encode for their expiry and creation duration
// For compacity, these values are packed in the key of an array using magic numbers
Copy link
Member

Choose a reason for hiding this comment

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

for compactness?

$item->isHit = $isHit;
$item->defaultLifetime = $defaultLifetime;
// Detect wrapped values that encode for their expiry and creation duration
// For compacity, these values are packed in the key of an array using magic numbers
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = \key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
Copy link
Member

Choose a reason for hiding this comment

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

Magic 10 here, not sure if we want to make it explicit (as we would want to make 5 and 9 explicit as well).

}
// For compacity, expiry and creation duration are packed in the key of a array, using magic numbers as separators
Copy link
Member

Choose a reason for hiding this comment

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

compactness as well?

$item->isHit = $innerItem->isHit();
$item->defaultLifetime = $defaultLifetime;
$item->innerItem = $innerItem;
$item->poolHash = $poolHash;
// Detect wrapped values that encode for their expiry and creation duration
// For compacity, these values are packed in the key of an array using magic numbers
Copy link
Member

Choose a reason for hiding this comment

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

same?

@nicolas-grekas
Copy link
Member Author

comments addressed thanks

@@ -140,10 +157,24 @@ public function tag($tags)
* Returns the list of tags bound to the value coming from the pool storage if any.
*
* @return array
*
* @deprecated since Symfony 4.2. Use the "getMetadata()" method instead.
Copy link
Member

Choose a reason for hiding this comment

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

4.2, use the

Copy link
Member Author

Choose a reason for hiding this comment

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

updated

@fabpot
Copy link
Member

fabpot commented Jun 11, 2018

Thank you @nicolas-grekas.

@fabpot fabpot merged commit 13523ad into symfony:master Jun 11, 2018
fabpot added a commit that referenced this pull request Jun 11, 2018
…y expiration (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[Cache] Add stampede protection via probabilistic early expiration

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        |

This PR implements [probabilistic early expiration](https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration) on top of `$cache->get($key, $callback);`

It adds a 3rd arg to `CacheInterface::get`:
> float $beta A float that controls the likelyness of triggering early expiration. 0 disables it, INF forces immediate expiration. The default is implementation dependend but should typically be 1.0, which should provide optimal stampede protection.

Commits
-------

13523ad [Cache] Add stampede protection via probabilistic early expiration
fabpot added a commit that referenced this pull request Jun 11, 2018
…lculations (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[Cache] Use sub-second accuracy for internal expiry calculations

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | not really
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

Embeds #26929, #27009 and #27028, let's focus on the 4th commit for now.

This is my last significant PR in the Cache series :)

By using integer expiries internally, our current implementations are sensitive to abrupt transitions when time() goes to next second: `$s = time(); sleep(1); echo time() - $s;` *can* display 2 from time to time.
This means that we do expire items earlier than required by the expiration settings on items.
This also means that there is no way to have a sub-second expiry. For remote backends, that's fine, but for ArrayAdapter, that's a limitation we can remove.

This PR replaces calls to `time()` by `microtime(true)`, providing more accurate timing measurements internally.

Commits
-------

08554ea [Cache] Use sub-second accuracy for internal expiry calculations
@nicolas-grekas nicolas-grekas deleted the cache-stats branch June 11, 2018 13:38
@nicolas-grekas nicolas-grekas modified the milestones: next, 4.2 Nov 1, 2018
This was referenced Nov 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants