Skip to content
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

[Cache] Allow to use namespace delimiter in cache key #54710

Open
wants to merge 4 commits into
base: 7.1
Choose a base branch
from

Conversation

dorrogeray
Copy link

@dorrogeray dorrogeray commented Apr 23, 2024

Q A
Branch? 7.2
Bug fix? no
New feature? yes
Deprecations? no
Issues #45599
License MIT

Replaces #51603

This PR allow colon char : in cache key. It may be useful for redis grouping keys by pattern without creating a many pools for each namespace.

Difference between implementation #47561

  • colon is only allowed for Symfony contracts cache, the PSR-6/16 adapters keeps this validation as before.
  • Allow colon in namespace name too

@nicolas-grekas I have taken a look at some approaches to how to optimize this, but it looks like:

$reservedChars = null === $allowChars 
    ? self::RESERVED_CHARACTERS
    : str_replace(str_split($allowChars), '', self::RESERVED_CHARACTERS);

is pretty well optimized and is marginally faster than preg_replace and a lot faster than some other for based approaches I tried. I think the main problem is that this code would need to get executed for each validation, of which there can be many in single http request. So I thought that maybe a different approach would could work - simply allowing override of RESERVED_CHARACTERS via the parameter. Here is a simple test for performance comparison.

<?php

class CacheItem {
    private const RESERVED_CHARACTERS = '{}()/\@:';

    /**
     * Validates a cache key according to PSR-6.
     *
     * @param mixed $key The key to validate
     *
     * @throws InvalidArgumentException When $key is not valid
     */
    public static function validateKey($key, string $allowChars = null): string
    {
        if (!\is_string($key)) {
            throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
        }
        if ('' === $key) {
            throw new InvalidArgumentException('Cache key length must be greater than zero.');
        }
        $reservedChars = null === $allowChars ? self::RESERVED_CHARACTERS : str_replace(str_split($allowChars), '', self::RESERVED_CHARACTERS);
        if ('' !== $reservedChars && false !== strpbrk($key, $reservedChars)) {
            throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, $reservedChars));
        }

        return $key;
    }

    /**
     * Validates a cache key according to PSR-6.
     *
     * @param mixed $key The key to validate
     *
     * @throws InvalidArgumentException When $key is not valid
     */
    public static function validateKeyByOverride($key, string $reservedChars = self::RESERVED_CHARACTERS): string
    {
        if (!\is_string($key)) {
            throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
        }
        if ('' === $key) {
            throw new InvalidArgumentException('Cache key length must be greater than zero.');
        }
        if ('' !== $reservedChars && false !== strpbrk($key, $reservedChars)) {
            throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, $reservedChars));
        }

        return $key;
    }
}

$key = "product:123456:discount";
$iterations = 1000000;

// Testing the original approach
$start_time = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    CacheItem::validateKey($key, ':');
}
$end_time = microtime(true);
$time_original = $end_time - $start_time;
echo "Original Method Time: " . $time_original . " seconds\n";

// Testing the optimized approach
$start_time = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    CacheItem::validateKeyByOverride($key, '{}()/\@X');
}
$end_time = microtime(true);
$time_optimized = $end_time - $start_time;
echo "Optimized Method Time: " . $time_optimized . " seconds\n";

Here are the results from 5 executions, 1000000 iterations each:

Original in seconds Optimized in seconds
1.8678679466248 0.62751603126526
1.4007370471954 0.61563205718994
0.89250898361206 0.48939108848572
0.85805201530457 0.41882705688477
0.86538910865784 0.47265005111694

Looks like getting rid of the str_replace an str_split makes the method about 2x faster.

The downside seems relatively small, the logic is moved to the cache adapters. Or we could get rid of the logic completely and just let each adapter define their list of reserved characters..

I am still working on the tests & to ensure that PSR 6 / PSR 16 validation is kept intact.

@carsonbot
Copy link

Hey!

I see that this is your first PR. That is great! Welcome!

Symfony has a contribution guide which I suggest you to read.

In short:

  • Always add tests
  • Keep backward compatibility (see https://symfony.com/bc).
  • Bug fixes must be submitted against the lowest maintained branch where they apply (see https://symfony.com/releases)
  • Features and deprecations must be submitted against the 7.1 branch.

Review the GitHub status checks of your pull request and try to solve the reported issues. If some tests are failing, try to see if they are failing because of this change.

When two Symfony core team members approve this change, it will be merged and you will become an official Symfony contributor!
If this PR is merged in a lower version branch, it will be merged up to all maintained branches within a few days.

I am going to sit back now and wait for the reviews.

Cheers!

Carsonbot

@OskarStark

This comment has been minimized.

@dorrogeray dorrogeray force-pushed the feat/allow-colon-in-redis-adapter branch from 14fd937 to fb44d0e Compare April 27, 2024 19:42
@dorrogeray dorrogeray force-pushed the feat/allow-colon-in-redis-adapter branch from fb44d0e to 48176f6 Compare April 27, 2024 19:44
@dorrogeray
Copy link
Author

Not sure how to fix the psalm error or how it popped up.

@nicolas-grekas nicolas-grekas modified the milestones: 7.1, 7.2 May 2, 2024
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.

None yet

5 participants