Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions inc/attachment_cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
*/
class Optml_Attachment_Cache {
const CACHE_GROUP = 'om_att';
/**
* Most URLs kept in the in-memory map during one request.
*
* A page with many distinct image URLs would otherwise grow the map for the whole request.
* When it is full, the older half is dropped; those URLs are still in the persistent cache.
*/
const MAX_MEMORY_ENTRIES = 2000;
/**
* Local cache map.
*
Expand Down Expand Up @@ -32,10 +39,10 @@ public static function get_cached_attachment_id( $url ) {
return self::$cache_map[ $cache_key ];
}

$value = wp_using_ext_object_cache()
$value = wp_using_ext_object_cache()
? wp_cache_get( $cache_key, self::CACHE_GROUP )
: get_transient( self::CACHE_GROUP . $cache_key );
self::$cache_map[ $cache_key ] = $value;
self::remember( $cache_key, $value );

return $value;
}
Expand All @@ -51,7 +58,7 @@ public static function get_cached_attachment_id( $url ) {
public static function set_cached_attachment_id( $url, $id ) {
$cache_key = self::get_cache_key( $url );
// We cache also in memory to avoid calling DB every time when not using Object Cache.
self::$cache_map[ $cache_key ] = $id;
self::remember( $cache_key, $id );
// If the ID is not found we cache for 10 minutes, otherwise for a week.
// We try to reduce the cache time when is not found to
// avoid caching for situation when this might be temporary.
Expand All @@ -61,6 +68,21 @@ public static function set_cached_attachment_id( $url, $id ) {
: set_transient( self::CACHE_GROUP . $cache_key, $id, $expiration );
}

/**
* Keep a value in the in-memory map, dropping the older half when it is full.
*
* @param string $cache_key The cache key.
* @param int|numeric-string|false $value The attachment ID (a numeric string when read back from a transient), or false when it is not cached.
*
* @return void
*/
private static function remember( $cache_key, $value ) {
if ( ! isset( self::$cache_map[ $cache_key ] ) && count( self::$cache_map ) >= self::MAX_MEMORY_ENTRIES ) {
self::$cache_map = array_slice( self::$cache_map, (int) ( self::MAX_MEMORY_ENTRIES / 2 ), null, true );
}
self::$cache_map[ $cache_key ] = $value;
}

/**
* Generate cache key for URL.
*
Expand Down
82 changes: 82 additions & 0 deletions tests/test-attachment-cache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
/**
* Attachment cache tests.
*
* @package Optimole-WP
* @subpackage Tests
*/

/**
* Class Test_Attachment_Cache.
*/
class Test_Attachment_Cache extends WP_UnitTestCase {

public function setUp(): void {
parent::setUp();
Optml_Attachment_Cache::reset();
}

public function tearDown(): void {
Optml_Attachment_Cache::reset();
parent::tearDown();
}

/**
* Read the in-memory map.
*
* @return array<string, int|numeric-string|false>
*/
private function memory_map() {
$property = new ReflectionProperty( Optml_Attachment_Cache::class, 'cache_map' );
$property->setAccessible( true );

return $property->getValue();
}

/**
* A page with many distinct uncached image URLs must not grow the in-memory map for the whole request.
*/
public function test_memory_map_is_bounded_on_cache_misses() {
$limit = Optml_Attachment_Cache::MAX_MEMORY_ENTRIES;

for ( $i = 0; $i <= $limit; $i++ ) {
Optml_Attachment_Cache::get_cached_attachment_id( 'https://example.org/wp-content/uploads/miss-' . $i . '.jpg' );
}

$this->assertCount( (int) ( $limit / 2 ) + 1, $this->memory_map() );
}

/**
* The newest URLs stay in memory when the older half is dropped, and the dropped ones are
* still found in the persistent cache.
*/
public function test_dropped_entries_are_still_found_in_the_persistent_cache() {
$limit = Optml_Attachment_Cache::MAX_MEMORY_ENTRIES;

for ( $i = 0; $i <= $limit; $i++ ) {
Optml_Attachment_Cache::set_cached_attachment_id( 'https://example.org/wp-content/uploads/image-' . $i . '.jpg', $i + 1 );
}

$map = $this->memory_map();
$this->assertCount( (int) ( $limit / 2 ) + 1, $map );
$this->assertContains( $limit + 1, $map );
$this->assertNotContains( 1, $map );

$this->assertSame( 1, (int) Optml_Attachment_Cache::get_cached_attachment_id( 'https://example.org/wp-content/uploads/image-0.jpg' ) );
}

/**
* Updating a URL that is already in memory never drops other entries.
*/
public function test_updating_a_known_url_keeps_the_map() {
$limit = Optml_Attachment_Cache::MAX_MEMORY_ENTRIES;

for ( $i = 0; $i < $limit; $i++ ) {
Optml_Attachment_Cache::set_cached_attachment_id( 'https://example.org/wp-content/uploads/image-' . $i . '.jpg', $i + 1 );
}
Optml_Attachment_Cache::set_cached_attachment_id( 'https://example.org/wp-content/uploads/image-0.jpg', 99 );

$this->assertCount( $limit, $this->memory_map() );
$this->assertSame( 99, Optml_Attachment_Cache::get_cached_attachment_id( 'https://example.org/wp-content/uploads/image-0.jpg' ) );
}
}
Loading