From 942f708b61d8d7b79fd3af8415287654e6b79c1c Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Wed, 14 Aug 2019 21:31:48 -0400 Subject: [PATCH] objectcache: fix WRITE_ALLOW_SEGMENTS in BagOStuff cas() and add() methods Add MediumSpecificBagOStuff::getValueOrSegmentList() helper method. Also: * Use $keysMissing variable correctly in CachedBagOStuff::getMulti() to avoid extra overhead. * Optimize mergeViaCas() when the current value matches the new one. Change-Id: I5c4bd74379bc459216ac0278150ce3aecff3b851 --- includes/libs/objectcache/APCBagOStuff.php | 2 +- includes/libs/objectcache/APCUBagOStuff.php | 2 +- includes/libs/objectcache/CachedBagOStuff.php | 2 +- includes/libs/objectcache/EmptyBagOStuff.php | 2 +- includes/libs/objectcache/HashBagOStuff.php | 2 +- .../objectcache/MediumSpecificBagOStuff.php | 195 ++++++++++++------ .../objectcache/MemcachedPeclBagOStuff.php | 4 +- .../objectcache/MemcachedPhpBagOStuff.php | 4 +- includes/libs/objectcache/RESTBagOStuff.php | 2 +- includes/libs/objectcache/RedisBagOStuff.php | 2 +- .../libs/objectcache/WinCacheBagOStuff.php | 9 +- includes/objectcache/SqlBagOStuff.php | 4 +- .../libs/objectcache/BagOStuffTest.php | 45 ++-- 13 files changed, 179 insertions(+), 96 deletions(-) diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php index 0954ac8061..aa83b1ff1e 100644 --- a/includes/libs/objectcache/APCBagOStuff.php +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -73,7 +73,7 @@ class APCBagOStuff extends MediumSpecificBagOStuff { return true; } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return apc_add( $key . self::KEY_SUFFIX, $this->nativeSerialize ? $value : $this->serialize( $value ), diff --git a/includes/libs/objectcache/APCUBagOStuff.php b/includes/libs/objectcache/APCUBagOStuff.php index 021cdf7b76..80383d1fb1 100644 --- a/includes/libs/objectcache/APCUBagOStuff.php +++ b/includes/libs/objectcache/APCUBagOStuff.php @@ -71,7 +71,7 @@ class APCUBagOStuff extends MediumSpecificBagOStuff { ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return apcu_add( $key . self::KEY_SUFFIX, $this->nativeSerialize ? $value : $this->serialize( $value ), diff --git a/includes/libs/objectcache/CachedBagOStuff.php b/includes/libs/objectcache/CachedBagOStuff.php index 0ab26c9520..9fa9a89f9b 100644 --- a/includes/libs/objectcache/CachedBagOStuff.php +++ b/includes/libs/objectcache/CachedBagOStuff.php @@ -79,7 +79,7 @@ class CachedBagOStuff extends BagOStuff { } } - $valuesByKeyFetched = $this->backend->getMulti( $keys, $flags ); + $valuesByKeyFetched = $this->backend->getMulti( $keysMissing, $flags ); $this->setMulti( $valuesByKeyFetched, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY ); return $valuesByKeyCached + $valuesByKeyFetched; diff --git a/includes/libs/objectcache/EmptyBagOStuff.php b/includes/libs/objectcache/EmptyBagOStuff.php index dab8ba1d35..b2613b2e45 100644 --- a/includes/libs/objectcache/EmptyBagOStuff.php +++ b/includes/libs/objectcache/EmptyBagOStuff.php @@ -41,7 +41,7 @@ class EmptyBagOStuff extends MediumSpecificBagOStuff { return true; } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return true; } diff --git a/includes/libs/objectcache/HashBagOStuff.php b/includes/libs/objectcache/HashBagOStuff.php index 1cfa0c7921..b4087bed56 100644 --- a/includes/libs/objectcache/HashBagOStuff.php +++ b/includes/libs/objectcache/HashBagOStuff.php @@ -94,7 +94,7 @@ class HashBagOStuff extends MediumSpecificBagOStuff { return true; } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { if ( $this->hasKey( $key ) && !$this->expire( $key ) ) { return false; // key already set } diff --git a/includes/libs/objectcache/MediumSpecificBagOStuff.php b/includes/libs/objectcache/MediumSpecificBagOStuff.php index 62a8aec967..329e600bb5 100644 --- a/includes/libs/objectcache/MediumSpecificBagOStuff.php +++ b/includes/libs/objectcache/MediumSpecificBagOStuff.php @@ -160,57 +160,9 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @return bool Success */ public function set( $key, $value, $exptime = 0, $flags = 0 ) { - if ( - is_int( $value ) || // avoid breaking incr()/decr() - ( $flags & self::WRITE_ALLOW_SEGMENTS ) != self::WRITE_ALLOW_SEGMENTS || - is_infinite( $this->segmentationSize ) - ) { - return $this->doSet( $key, $value, $exptime, $flags ); - } - - $serialized = $this->serialize( $value ); - $segmentSize = $this->getSegmentationSize(); - $maxTotalSize = $this->getSegmentedValueMaxSize(); - - $size = strlen( $serialized ); - if ( $size <= $segmentSize ) { - // Since the work of serializing it was already done, just use it inline - return $this->doSet( - $key, - SerializedValueContainer::newUnified( $serialized ), - $exptime, - $flags - ); - } elseif ( $size > $maxTotalSize ) { - $this->setLastError( "Key $key exceeded $maxTotalSize bytes." ); - - return false; - } - - $chunksByKey = []; - $segmentHashes = []; - $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 ); - for ( $i = 0; $i < $count; ++$i ) { - $segment = substr( $serialized, $i * $segmentSize, $segmentSize ); - $hash = sha1( $segment ); - $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash ); - $chunksByKey[$chunkKey] = $segment; - $segmentHashes[] = $hash; - } - - $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity - $ok = $this->setMulti( $chunksByKey, $exptime, $flags ); - if ( $ok ) { - // Only when all segments are stored should the main key be changed - $ok = $this->doSet( - $key, - SerializedValueContainer::newSegmented( $segmentHashes ), - $exptime, - $flags - ); - } - - return $ok; + list( $entry, $usable ) = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags ); + // Only when all segments (if any) are stored should the main key be changed + return $usable ? $this->doSet( $key, $entry, $exptime, $flags ) : false; } /** @@ -268,6 +220,23 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { */ abstract protected function doDelete( $key, $flags = 0 ); + public function add( $key, $value, $exptime = 0, $flags = 0 ) { + list( $entry, $usable ) = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags ); + // Only when all segments (if any) are stored should the main key be changed + return $usable ? $this->doAdd( $key, $entry, $exptime, $flags ) : false; + } + + /** + * Insert an item if it does not already exist + * + * @param string $key + * @param mixed $value + * @param int $exptime + * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) + * @return bool Success + */ + abstract protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ); + /** * Merge changes into the existing cache value (possibly creating a new one) * @@ -283,7 +252,6 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @param int $attempts The amount of times to attempt a merge in case of failure * @param int $flags Bitfield of BagOStuff::WRITE_* constants * @return bool Success - * @throws InvalidArgumentException */ public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) { return $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags ); @@ -297,9 +265,9 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @param int $flags Bitfield of BagOStuff::WRITE_* constants * @return bool Success * @see BagOStuff::merge() - * */ final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) { + $attemptsLeft = $attempts; do { $casToken = null; // passed by reference // Get the old value and CAS token from cache @@ -309,23 +277,27 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { $this->doGet( $key, self::READ_LATEST, $casToken ) ); if ( $this->getLastError() ) { + // Don't spam slow retries due to network problems (retry only on races) $this->logger->warning( - __METHOD__ . ' failed due to I/O error on get() for {key}.', + __METHOD__ . ' failed due to read I/O error on get() for {key}.', [ 'key' => $key ] ); - - return false; // don't spam retries (retry only on races) + $success = false; + break; } // Derive the new value from the old value $value = call_user_func( $callback, $this, $key, $currentValue, $exptime ); - $hadNoCurrentValue = ( $currentValue === false ); + $keyWasNonexistant = ( $currentValue === false ); + $valueMatchesOldValue = ( $value === $currentValue ); unset( $currentValue ); // free RAM in case the value is large $this->clearLastError(); if ( $value === false ) { $success = true; // do nothing - } elseif ( $hadNoCurrentValue ) { + } elseif ( $valueMatchesOldValue && $attemptsLeft !== $attempts ) { + $success = true; // recently set by another thread to the same value + } elseif ( $keyWasNonexistant ) { // Try to create the key, failing if it gets created in the meantime $success = $this->add( $key, $value, $exptime, $flags ); } else { @@ -333,15 +305,16 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { $success = $this->cas( $casToken, $key, $value, $exptime, $flags ); } if ( $this->getLastError() ) { + // Don't spam slow retries due to network problems (retry only on races) $this->logger->warning( - __METHOD__ . ' failed due to I/O error for {key}.', + __METHOD__ . ' failed due to write I/O error for {key}.', [ 'key' => $key ] ); - - return false; // IO error; don't spam retries + $success = false; + break; } - } while ( !$success && --$attempts ); + } while ( !$success && --$attemptsLeft ); return $success; } @@ -357,21 +330,58 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @return bool Success */ protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + if ( $casToken === null ) { + $this->logger->warning( + __METHOD__ . ' got empty CAS token for {key}.', + [ 'key' => $key ] + ); + + return false; // caller may have meant to use add()? + } + + list( $entry, $usable ) = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags ); + // Only when all segments (if any) are stored should the main key be changed + return $usable ? $this->doCas( $casToken, $key, $entry, $exptime, $flags ) : false; + } + + /** + * Check and set an item + * + * @param mixed $casToken + * @param string $key + * @param mixed $value + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @param int $flags Bitfield of BagOStuff::WRITE_* constants + * @return bool Success + */ + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + // @TODO: the lock() call assumes that all other relavent sets() use one if ( !$this->lock( $key, 0 ) ) { return false; // non-blocking } $curCasToken = null; // passed by reference + $this->clearLastError(); $this->doGet( $key, self::READ_LATEST, $curCasToken ); - if ( $casToken === $curCasToken ) { - $success = $this->set( $key, $value, $exptime, $flags ); + if ( is_object( $curCasToken ) ) { + // Using === does not work with objects since it checks for instance identity + throw new UnexpectedValueException( "CAS token cannot be an object" ); + } + if ( $this->getLastError() ) { + // Fail if the old CAS token could not be read + $success = false; + $this->logger->warning( + __METHOD__ . ' failed due to write I/O error for {key}.', + [ 'key' => $key ] + ); + } elseif ( $casToken === $curCasToken ) { + $success = $this->doSet( $key, $value, $exptime, $flags ); } else { + $success = false; // mismatched or failed $this->logger->info( __METHOD__ . ' failed due to race condition for {key}.', [ 'key' => $key ] ); - - $success = false; // mismatched or failed } $this->unlock( $key ); @@ -782,6 +792,59 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { $this->busyCallbacks[] = $workCallback; } + /** + * Determine the entry (inline or segment list) to store under a key to save the value + * + * @param string $key + * @param mixed $value + * @param int $exptime + * @param int $flags + * @return array (inline value or segment list, whether the entry is usable) + * @since 1.34 + */ + final protected function makeValueOrSegmentList( $key, $value, $exptime, $flags ) { + $entry = $value; + $usable = true; + + if ( + ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS && + !is_int( $value ) && // avoid breaking incr()/decr() + is_finite( $this->segmentationSize ) + ) { + $segmentSize = $this->segmentationSize; + $maxTotalSize = $this->segmentedValueMaxSize; + + $serialized = $this->serialize( $value ); + $size = strlen( $serialized ); + if ( $size > $maxTotalSize ) { + $this->logger->warning( + "Value for {key} exceeds $maxTotalSize bytes; cannot segment.", + [ 'key' => $key ] + ); + } elseif ( $size <= $segmentSize ) { + // The serialized value was already computed, so just use it inline + $entry = SerializedValueContainer::newUnified( $serialized ); + } else { + // Split the serialized value into chunks and store them at different keys + $chunksByKey = []; + $segmentHashes = []; + $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 ); + for ( $i = 0; $i < $count; ++$i ) { + $segment = substr( $serialized, $i * $segmentSize, $segmentSize ); + $hash = sha1( $segment ); + $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash ); + $chunksByKey[$chunkKey] = $segment; + $segmentHashes[] = $hash; + } + $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity + $usable = $this->setMulti( $chunksByKey, $exptime, $flags ); + $entry = SerializedValueContainer::newSegmented( $segmentHashes ); + } + } + + return [ $entry, $usable ]; + } + /** * @param int|float $exptime * @return bool Whether the expiry is non-infinite, and, negative or not a UNIX timestamp diff --git a/includes/libs/objectcache/MemcachedPeclBagOStuff.php b/includes/libs/objectcache/MemcachedPeclBagOStuff.php index cc7ee2a5f5..3df483def1 100644 --- a/includes/libs/objectcache/MemcachedPeclBagOStuff.php +++ b/includes/libs/objectcache/MemcachedPeclBagOStuff.php @@ -214,7 +214,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { : $this->checkResult( $key, $result ); } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { $this->debug( "cas($key)" ); $result = $this->acquireSyncClient()->cas( @@ -238,7 +238,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { : $this->checkResult( $key, $result ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { $this->debug( "add($key)" ); $result = $this->acquireSyncClient()->add( diff --git a/includes/libs/objectcache/MemcachedPhpBagOStuff.php b/includes/libs/objectcache/MemcachedPhpBagOStuff.php index b1d5d29f16..81442314c9 100644 --- a/includes/libs/objectcache/MemcachedPhpBagOStuff.php +++ b/includes/libs/objectcache/MemcachedPhpBagOStuff.php @@ -76,7 +76,7 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff { return $this->client->delete( $this->validateKeyEncoding( $key ) ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return $this->client->add( $this->validateKeyEncoding( $key ), $value, @@ -84,7 +84,7 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff { ); } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { return $this->client->cas( $casToken, $this->validateKeyEncoding( $key ), diff --git a/includes/libs/objectcache/RESTBagOStuff.php b/includes/libs/objectcache/RESTBagOStuff.php index aa4a9b31fc..b8ce38b1cc 100644 --- a/includes/libs/objectcache/RESTBagOStuff.php +++ b/includes/libs/objectcache/RESTBagOStuff.php @@ -164,7 +164,7 @@ class RESTBagOStuff extends MediumSpecificBagOStuff { return $this->handleError( "Failed to store $key", $rcode, $rerr, $rhdrs, $rbody ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { // @TODO: make this atomic if ( $this->get( $key ) === false ) { return $this->set( $key, $value, $exptime, $flags ); diff --git a/includes/libs/objectcache/RedisBagOStuff.php b/includes/libs/objectcache/RedisBagOStuff.php index f75d3a1015..252b1faf8c 100644 --- a/includes/libs/objectcache/RedisBagOStuff.php +++ b/includes/libs/objectcache/RedisBagOStuff.php @@ -341,7 +341,7 @@ class RedisBagOStuff extends MediumSpecificBagOStuff { return $result; } - public function add( $key, $value, $expiry = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $expiry = 0, $flags = 0 ) { $conn = $this->getConnection( $key ); if ( !$conn ) { return false; diff --git a/includes/libs/objectcache/WinCacheBagOStuff.php b/includes/libs/objectcache/WinCacheBagOStuff.php index 0e4e3fb63d..3c4efbbedd 100644 --- a/includes/libs/objectcache/WinCacheBagOStuff.php +++ b/includes/libs/objectcache/WinCacheBagOStuff.php @@ -28,6 +28,11 @@ * @ingroup Cache */ class WinCacheBagOStuff extends MediumSpecificBagOStuff { + public function __construct( array $params = [] ) { + $params['segmentationSize'] = $params['segmentationSize'] ?? INF; + parent::__construct( $params ); + } + protected function doGet( $key, $flags = 0, &$casToken = null ) { $casToken = null; @@ -44,7 +49,7 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff { return $value; } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { if ( !wincache_lock( $key ) ) { // optimize with FIFO lock return false; } @@ -76,7 +81,7 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff { return ( $result === [] || $result === true ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { if ( wincache_ucache_exists( $key ) ) { return false; // avoid warnings } diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index e97dc41af7..d9fe319277 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -467,11 +467,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { return $this->modifyMulti( [ $key => $value ], $exptime, $flags, self::$OP_SET ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return $this->modifyMulti( [ $key => $value ], $exptime, $flags, self::$OP_ADD ); } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); $exptime = $this->getExpirationAsTimestamp( $exptime ); diff --git a/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php b/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php index 9ec86bce79..7d3e82c786 100644 --- a/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php +++ b/tests/phpunit/includes/libs/objectcache/BagOStuffTest.php @@ -380,24 +380,39 @@ class BagOStuffTest extends MediaWikiTestCase { return $oldValue . '!'; }; - foreach ( [ $tiny, $small, $big ] as $value ) { + $cases = [ 'tiny' => $tiny, 'small' => $small, 'big' => $big ]; + foreach ( $cases as $case => $value ) { $this->cache->set( $key, $value, 10, BagOStuff::WRITE_ALLOW_SEGMENTS ); - $this->assertEquals( $value, $this->cache->get( $key ) ); - $this->assertEquals( $value, $this->cache->getMulti( [ $key ] )[$key] ); - - $this->assertTrue( $this->cache->merge( $key, $callback, 5 ) ); - $this->assertEquals( "$value!", $this->cache->get( $key ) ); - $this->assertEquals( "$value!", $this->cache->getMulti( [ $key ] )[$key] ); - - $this->assertTrue( $this->cache->deleteMulti( [ $key ] ) ); - $this->assertFalse( $this->cache->get( $key ) ); - $this->assertEquals( [], $this->cache->getMulti( [ $key ] ) ); + $this->assertEquals( $value, $this->cache->get( $key ), "get $case" ); + $this->assertEquals( $value, $this->cache->getMulti( [ $key ] )[$key], "get $case" ); + + $this->assertTrue( + $this->cache->merge( $key, $callback, 5, 1, BagOStuff::WRITE_ALLOW_SEGMENTS ), + "merge $case" + ); + $this->assertEquals( + "$value!", + $this->cache->get( $key ), + "merged $case" + ); + $this->assertEquals( + "$value!", + $this->cache->getMulti( [ $key ] )[$key], + "merged $case" + ); + + $this->assertTrue( $this->cache->deleteMulti( [ $key ] ), "delete $case" ); + $this->assertFalse( $this->cache->get( $key ), "deleted $case" ); + $this->assertEquals( [], $this->cache->getMulti( [ $key ] ), "deletd $case" ); $this->cache->set( $key, "@$value", 10, BagOStuff::WRITE_ALLOW_SEGMENTS ); - $this->assertEquals( "@$value", $this->cache->get( $key ) ); - $this->assertTrue( $this->cache->delete( $key, BagOStuff::WRITE_PRUNE_SEGMENTS ) ); - $this->assertFalse( $this->cache->get( $key ) ); - $this->assertEquals( [], $this->cache->getMulti( [ $key ] ) ); + $this->assertEquals( "@$value", $this->cache->get( $key ), "get $case" ); + $this->assertTrue( + $this->cache->delete( $key, BagOStuff::WRITE_PRUNE_SEGMENTS ), + "prune $case" + ); + $this->assertFalse( $this->cache->get( $key ), "pruned $case" ); + $this->assertEquals( [], $this->cache->getMulti( [ $key ] ), "pruned $case" ); } $this->cache->set( $key, 666, 10, BagOStuff::WRITE_ALLOW_SEGMENTS ); -- 2.20.1