Skip to content

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

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

Closed

Conversation

dorrogeray
Copy link
Contributor

@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
Contributor 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
@upyx
Copy link
Contributor

upyx commented Jul 24, 2024

IMO, it has two really big drawbacks:

  1. It solves the local issue with Redis by cost of portability with different backends that could not support colons. The Symfony cache is designed as an abstraction that can be implemented by various technologies.
  2. It breaks compatibility with PSRs that are gate outside the Symfony world.

I'd rather add a convenient interface to deal with namespaces instead of permitting the namespace separator in keys.

@fabpot fabpot modified the milestones: 7.2, 7.3 Nov 20, 2024
@nicolas-grekas
Copy link
Member

Closing in favor of #59813, thanks for giving this a try!

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.

7 participants