Skip to content

Commit 3bea48a

Browse files
committed
Merge branch 'next-36362/allow-cache-compression-zstd' into 'trunk'
NEXT-36362 - Allow cache compression using zstd See merge request shopware/6/product/platform!13916
2 parents 36bb103 + 8d3cc68 commit 3bea48a

File tree

17 files changed

+322
-64
lines changed

17 files changed

+322
-64
lines changed

config-schema.json

+12
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,12 @@
450450
"default": true,
451451
"description": "Controls the cache compression before the storage"
452452
},
453+
"cache_compression_method": {
454+
"type": "string",
455+
"enum": ["gzip", "zstd"],
456+
"default": "gzip",
457+
"description": "Controls the cache compression method"
458+
},
453459
"invalidation": {
454460
"$ref": "#/definitions/cache_invalidation"
455461
},
@@ -587,6 +593,12 @@
587593
"type": "boolean",
588594
"description": "All carts, which stored in redis, are compressed via gzcompress. This option is only available if redis is used."
589595
},
596+
"compression_method": {
597+
"type": "string",
598+
"enum": ["gzip", "zstd"],
599+
"default": "gzip",
600+
"description": "Controls the cache compression method"
601+
},
590602
"storage": {
591603
"type": "object",
592604
"additionalProperties": false,
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Shopware\Core\Checkout\Cart;
4+
5+
use Shopware\Core\Framework\Log\Package;
6+
7+
#[Package('core')]
8+
class CartCompressor
9+
{
10+
public const COMPRESSION_TYPE_NONE = 0;
11+
public const COMPRESSION_TYPE_GZIP = 1;
12+
public const COMPRESSION_TYPE_ZSTD = 2;
13+
14+
/**
15+
* @var self::COMPRESSION_TYPE_*
16+
*/
17+
private int $compressMethod;
18+
19+
/**
20+
* @internal
21+
*/
22+
public function __construct(private bool $compress, string $compressMethod)
23+
{
24+
$this->compressMethod = match ($compressMethod) {
25+
'zstd' => self::COMPRESSION_TYPE_ZSTD,
26+
'gzip' => self::COMPRESSION_TYPE_GZIP,
27+
default => throw CartException::invalidCompressionMethod($compressMethod),
28+
};
29+
30+
if (!$this->compress) {
31+
$this->compressMethod = self::COMPRESSION_TYPE_NONE;
32+
}
33+
}
34+
35+
/**
36+
* @return array{0: self::COMPRESSION_TYPE_*, 1: string}
37+
*/
38+
public function serialize(mixed $value): array
39+
{
40+
$compressed = serialize($value);
41+
42+
if (!$this->compress) {
43+
return [$this->compressMethod, $compressed];
44+
}
45+
46+
if ($this->compressMethod === self::COMPRESSION_TYPE_ZSTD) {
47+
$compressed = \zstd_compress($compressed);
48+
} elseif ($this->compressMethod === self::COMPRESSION_TYPE_GZIP) {
49+
$compressed = \gzcompress($compressed, 9);
50+
}
51+
52+
if ($compressed === false) {
53+
throw CartException::deserializeFailed();
54+
}
55+
56+
return [$this->compressMethod, $compressed];
57+
}
58+
59+
public function unserialize(string $value, int $compressionMethod): mixed
60+
{
61+
$uncompressed = $value;
62+
63+
if ($compressionMethod === self::COMPRESSION_TYPE_GZIP) {
64+
$uncompressed = @\gzuncompress($uncompressed);
65+
} elseif ($compressionMethod === self::COMPRESSION_TYPE_ZSTD) {
66+
$uncompressed = @\zstd_uncompress($uncompressed);
67+
}
68+
69+
if ($uncompressed === false) {
70+
throw CartException::deserializeFailed();
71+
}
72+
73+
return unserialize($uncompressed);
74+
}
75+
}

src/Core/Checkout/Cart/CartException.php

+10
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class CartException extends HttpException
5050
public const PRICE_PARAMETER_IS_MISSING = 'CHECKOUT__PRICE_PARAMETER_IS_MISSING';
5151
public const PRICES_PARAMETER_IS_MISSING = 'CHECKOUT__PRICES_PARAMETER_IS_MISSING';
5252
public const CART_LINE_ITEM_INVALID = 'CHECKOUT__CART_LINE_ITEM_INVALID';
53+
private const INVALID_COMPRESSION_METHOD = 'CHECKOUT__CART_INVALID_COMPRESSION_METHOD';
5354

5455
public static function deserializeFailed(): self
5556
{
@@ -60,6 +61,15 @@ public static function deserializeFailed(): self
6061
);
6162
}
6263

64+
public static function invalidCompressionMethod(string $method): self
65+
{
66+
return new self(
67+
Response::HTTP_INTERNAL_SERVER_ERROR,
68+
self::INVALID_COMPRESSION_METHOD,
69+
\sprintf('Invalid cache compression method: %s', $method),
70+
);
71+
}
72+
6373
public static function tokenNotFound(string $token): self
6474
{
6575
return new CartTokenNotFoundException(Response::HTTP_NOT_FOUND, self::TOKEN_NOT_FOUND_CODE, 'Cart with token {{ token }} not found.', ['token' => $token]);

src/Core/Checkout/Cart/CartPersister.php

+16-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Shopware\Core\Checkout\Cart\Event\CartSavedEvent;
99
use Shopware\Core\Checkout\Cart\Event\CartVerifyPersistEvent;
1010
use Shopware\Core\Defaults;
11-
use Shopware\Core\Framework\Adapter\Cache\CacheValueCompressor;
1211
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
1312
use Shopware\Core\Framework\Log\Package;
1413
use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
@@ -26,7 +25,7 @@ public function __construct(
2625
private readonly Connection $connection,
2726
private readonly EventDispatcherInterface $eventDispatcher,
2827
private readonly CartSerializationCleaner $cartSerializationCleaner,
29-
private readonly bool $compress
28+
private readonly CartCompressor $compressor
3029
) {
3130
}
3231

@@ -47,7 +46,12 @@ public function load(string $token, SalesChannelContext $context): Cart
4746
throw CartException::tokenNotFound($token);
4847
}
4948

50-
$cart = $content['compressed'] ? CacheValueCompressor::uncompress($content['payload']) : unserialize((string) $content['payload']);
49+
try {
50+
$cart = $this->compressor->unserialize($content['payload'], (int) $content['compressed']);
51+
} catch (\Exception) {
52+
// When we can't decode it, we have to delete it
53+
throw CartException::tokenNotFound($token);
54+
}
5155

5256
if (!$cart instanceof Cart) {
5357
throw CartException::deserializeFailed();
@@ -87,12 +91,14 @@ public function save(Cart $cart, SalesChannelContext $context): void
8791
ON DUPLICATE KEY UPDATE `payload` = :payload, `compressed` = :compressed, `rule_ids` = :rule_ids, `created_at` = :now;
8892
SQL;
8993

94+
[$compressed, $serializeCart] = $this->serializeCart($cart);
95+
9096
$data = [
9197
'token' => $cart->getToken(),
92-
'payload' => $this->serializeCart($cart),
98+
'payload' => $serializeCart,
9399
'rule_ids' => json_encode($context->getRuleIds(), \JSON_THROW_ON_ERROR),
94100
'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
95-
'compressed' => (int) $this->compress,
101+
'compressed' => $compressed,
96102
];
97103

98104
$query = new RetryableQuery($this->connection, $this->connection->prepare($sql));
@@ -136,7 +142,10 @@ public function prune(int $days): void
136142
} while ($result > 0);
137143
}
138144

139-
private function serializeCart(Cart $cart): string
145+
/**
146+
* @return array{0: int, 1: string}
147+
*/
148+
private function serializeCart(Cart $cart): array
140149
{
141150
$errors = $cart->getErrors();
142151
$data = $cart->getData();
@@ -146,7 +155,7 @@ private function serializeCart(Cart $cart): string
146155

147156
$this->cartSerializationCleaner->cleanupCart($cart);
148157

149-
$serialized = $this->compress ? CacheValueCompressor::compress($cart) : serialize($cart);
158+
$serialized = $this->compressor->serialize($cart);
150159

151160
$cart->setErrors($errors);
152161
$cart->setData($data);

src/Core/Checkout/Cart/Command/CartMigrateCommand.php

+12-10
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
use Doctrine\DBAL\ArrayParameterType;
66
use Doctrine\DBAL\Connection;
7+
use Shopware\Core\Checkout\Cart\CartCompressor;
78
use Shopware\Core\Checkout\Cart\RedisCartPersister;
89
use Shopware\Core\Defaults;
9-
use Shopware\Core\Framework\Adapter\Cache\CacheValueCompressor;
1010
use Shopware\Core\Framework\Adapter\Cache\RedisConnectionFactory;
1111
use Shopware\Core\Framework\Adapter\Console\ShopwareStyle;
1212
use Shopware\Core\Framework\DataAbstractionLayer\Command\ConsoleProgressTrait;
@@ -38,9 +38,9 @@ class CartMigrateCommand extends Command
3838
public function __construct(
3939
private $redis,
4040
private readonly Connection $connection,
41-
private readonly bool $compress,
4241
private readonly int $expireDays,
43-
private readonly RedisConnectionFactory $factory
42+
private readonly RedisConnectionFactory $factory,
43+
private readonly CartCompressor $cartCompressor
4444
) {
4545
parent::__construct();
4646
}
@@ -128,12 +128,14 @@ private function redisToSql(InputInterface $input, OutputInterface $output): int
128128

129129
$value = \unserialize($value);
130130

131-
$content = $value['compressed'] ? CacheValueCompressor::uncompress($value['content']) : \unserialize($value['content']);
131+
$content = $this->cartCompressor->unserialize($value['content'], (int) $value['compressed']);
132+
133+
[$newCompression, $newCart] = $this->cartCompressor->serialize($content['cart']);
132134

133135
$migratedCart = [];
134136
$migratedCart['token'] = substr($key, \strlen(RedisCartPersister::PREFIX));
135-
$migratedCart['payload'] = $this->compress ? CacheValueCompressor::compress($content['cart']) : serialize($content['cart']);
136-
$migratedCart['compressed'] = $this->compress ? 1 : 0;
137+
$migratedCart['payload'] = $newCart;
138+
$migratedCart['compressed'] = $newCompression;
137139
$migratedCart['rule_ids'] = \json_encode($content['rule_ids'], \JSON_THROW_ON_ERROR);
138140
$migratedCart['created_at'] = $created;
139141

@@ -186,16 +188,16 @@ private function sqlToRedis(InputInterface $input, OutputInterface $output): int
186188
foreach ($rows as $row) {
187189
$key = RedisCartPersister::PREFIX . $row['token'];
188190

189-
$cart = $row['compressed'] ? CacheValueCompressor::uncompress($row['payload']) : unserialize((string) $row['payload']);
191+
$cart = $this->cartCompressor->unserialize($row['payload'], (int) $row['compressed']);
190192

191193
$content = ['cart' => $cart, 'rule_ids' => \json_decode((string) $row['rule_ids'], true, 512, \JSON_THROW_ON_ERROR)];
192194

193-
$content = $this->compress ? CacheValueCompressor::compress($content) : \serialize($content);
195+
[$newCompression, $newCart] = $this->cartCompressor->serialize($content);
194196

195197
$values[$key] = $row['token'];
196198
$value = \serialize([
197-
'compressed' => $this->compress,
198-
'content' => $content,
199+
'compressed' => $newCompression,
200+
'content' => $newCart,
199201
]);
200202

201203
$this->redis->set($key, $value, ['EX' => $this->expireDays * 86400]);

src/Core/Checkout/Cart/RedisCartPersister.php

+11-9
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Shopware\Core\Checkout\Cart\Event\CartSavedEvent;
88
use Shopware\Core\Checkout\Cart\Event\CartVerifyPersistEvent;
99
use Shopware\Core\Checkout\Cart\Exception\CartTokenNotFoundException;
10-
use Shopware\Core\Framework\Adapter\Cache\CacheValueCompressor;
1110
use Shopware\Core\Framework\Log\Package;
1211
use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
1312
use Shopware\Core\System\SalesChannel\SalesChannelContext;
@@ -19,17 +18,17 @@ class RedisCartPersister extends AbstractCartPersister
1918
final public const PREFIX = 'cart-persister-';
2019

2120
/**
21+
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay $redis
22+
*
2223
* @internal
2324
*
2425
* param cannot be natively typed, as symfony might change the type in the future
25-
*
26-
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay $redis
2726
*/
2827
public function __construct(
2928
private $redis,
3029
private readonly EventDispatcherInterface $eventDispatcher,
3130
private readonly CartSerializationCleaner $cartSerializationCleaner,
32-
private readonly bool $compress,
31+
private readonly CartCompressor $compressor,
3332
private readonly int $expireDays
3433
) {
3534
}
@@ -58,7 +57,12 @@ public function load(string $token, SalesChannelContext $context): Cart
5857
throw CartException::tokenNotFound($token);
5958
}
6059

61-
$content = $value['compressed'] ? CacheValueCompressor::uncompress($value['content']) : \unserialize((string) $value['content']);
60+
try {
61+
$content = $this->compressor->unserialize($value['content'], (int) $value['compressed']);
62+
} catch (\Exception) {
63+
// When we can't decode it, we have to delete it
64+
throw CartException::tokenNotFound($token);
65+
}
6266

6367
if (!\is_array($content)) {
6468
throw CartException::tokenNotFound($token);
@@ -131,15 +135,13 @@ private function serializeCart(Cart $cart, SalesChannelContext $context): string
131135

132136
$this->cartSerializationCleaner->cleanupCart($cart);
133137

134-
$content = ['cart' => $cart, 'rule_ids' => $context->getRuleIds()];
135-
136-
$content = $this->compress ? CacheValueCompressor::compress($content) : \serialize($content);
138+
[$compressed, $content] = $this->compressor->serialize(['cart' => $cart, 'rule_ids' => $context->getRuleIds()]);
137139

138140
$cart->setErrors($errors);
139141
$cart->setData($data);
140142

141143
return \serialize([
142-
'compressed' => $this->compress,
144+
'compressed' => $compressed,
143145
'content' => $content,
144146
]);
145147
}

src/Core/Checkout/DependencyInjection/cart.xml

+8-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
<argument type="service" id="Doctrine\DBAL\Connection"/>
5050
<argument type="service" id="event_dispatcher"/>
5151
<argument type="service" id="Shopware\Core\Checkout\Cart\CartSerializationCleaner"/>
52-
<argument>%shopware.cart.compress%</argument>
52+
<argument type="service" id="Shopware\Core\Checkout\Cart\CartCompressor"/>
5353
</service>
5454

5555
<service id="Shopware\Core\Checkout\Cart\CartSerializationCleaner">
@@ -499,11 +499,16 @@
499499
<argument type="service" id="Shopware\Core\Checkout\Cart\Price\CurrencyPriceCalculator"/>
500500
</service>
501501

502+
<service id="Shopware\Core\Checkout\Cart\CartCompressor">
503+
<argument>%shopware.cart.compress%</argument>
504+
<argument>%shopware.cart.compression_method%</argument>
505+
</service>
506+
502507
<service id="Shopware\Core\Checkout\Cart\RedisCartPersister">
503508
<argument type="service" id="shopware.cart.redis"/>
504509
<argument type="service" id="event_dispatcher"/>
505510
<argument type="service" id="Shopware\Core\Checkout\Cart\CartSerializationCleaner"/>
506-
<argument>%shopware.cart.compress%</argument>
511+
<argument type="service" id="Shopware\Core\Checkout\Cart\CartCompressor"/>
507512
<argument>%shopware.cart.expire_days%</argument>
508513
</service>
509514

@@ -515,9 +520,9 @@
515520
<service id="Shopware\Core\Checkout\Cart\Command\CartMigrateCommand">
516521
<argument type="service" id="shopware.cart.redis" on-invalid="null"/>
517522
<argument type="service" id="Doctrine\DBAL\Connection"/>
518-
<argument>%shopware.cart.compress%</argument>
519523
<argument>%shopware.cart.expire_days%</argument>
520524
<argument type="service" id="Shopware\Core\Framework\Adapter\Cache\RedisConnectionFactory"/>
525+
<argument type="service" id="Shopware\Core\Checkout\Cart\CartCompressor"/>
521526
<tag name="console.command"/>
522527
</service>
523528
</services>

0 commit comments

Comments
 (0)