Skip to content

Commit

Permalink
1.3.6 Retry cache update timeouts three times, change new-user cache …
Browse files Browse the repository at this point in the history
…size default to 16MiB.
  • Loading branch information
OllieJones committed Dec 4, 2023
1 parent c83e9ab commit 61706e4
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 64 deletions.
153 changes: 94 additions & 59 deletions assets/drop-in/object-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -575,9 +575,8 @@ private function open_connection() {
if ( $this->sqlite ) {
return;
}
$max_retries = 3;
$retries = 0;
while ( ++ $retries <= $max_retries ) {
$retries = 3;
while ( $retries -- > 0 ) {
try {
$this->actual_open_connection();

Expand Down Expand Up @@ -950,7 +949,7 @@ public function sqlite_remove_expired() {
$items_removed += $hit;
}
} catch ( Exception $ex ) {
$this->error_log( 'sqlite_clean_up_cache', $ex );
$this->error_log( 'sqlite_remove_expired', $ex );
}

return $items_removed > 0;
Expand Down Expand Up @@ -1396,7 +1395,7 @@ private function get_by_name( $name ) {
$result->finalize();
} catch ( Exception $ex ) {
unset( $this->not_in_persistent_cache [ $name ] );
$this->error_log( 'getone', $ex );
$this->error_log( 'get_by_name', $ex );
$this->delete_offending_files();
self::drop_dead();
}
Expand Down Expand Up @@ -1451,7 +1450,7 @@ public function set( $key, $data, $group = 'default', $expire = 0 ) {
}

/**
* Write to the persistent cache.
* Write to the persistent cache, with timeout retry.
*
* @param string $name What to call the contents in the cache.
* @param mixed $data The contents to store in the cache.
Expand All @@ -1460,51 +1459,76 @@ public function set( $key, $data, $group = 'default', $expire = 0 ) {
* @return void
*/
private function put_by_name( $name, $data, $expire ) {
try {
$start = $this->time_usec();
$value = $this->maybe_serialize( $data );
$expires = $expire ?: $this->noexpire_timestamp_offset;
if ( $this->upsertone ) {
$stmt = $this->upsertone;
$stmt->bindValue( ':name', $name, SQLITE3_TEXT );
$stmt->bindValue( ':value', $value, SQLITE3_BLOB );
$stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
$result = $stmt->execute();
$result->finalize();
} else {
/* Pre-upsert version (pre- 3.24) of SQLite,
* Need to try update, then do insert if need be.
* Race conditions are possible, hence BEGIN / COMMIT
*/
if ( ! $this->transaction_active ) {
$this->sqlite->exec( 'BEGIN' );
$exception = null;
$start = $this->time_usec();
$value = $this->maybe_serialize( $data );
$expires = $expire ?: $this->noexpire_timestamp_offset;
$retries = 3;
while ( $retries -- > 0 ) {
try {
$this->actual_put_by_name( $name, $value, $expires );
unset( $this->not_in_persistent_cache[ $name ] );
$this->insert_times[] = $this->time_usec() - $start;

return;
} catch ( Exception $ex ) {
$exception = $ex;
if ( ! str_contains( $ex->getMessage(), 'database is locked' ) || 5 !== $this->sqlite->lastErrorCode() ) {
break;
}
$stmt = $this->updateone;
}
sleep( 1 );
}
if ( $exception ) {
$this->error_log( 'put_by_name', $exception );
$this->delete_offending_files();
self::drop_dead();
}
}

/**
* Actually write to the cache.
*
* @param string $name What to call the contents in the cache.
* @param string $value The seriolized value.
* @param int $expires Expiration time.
*
* @return void
*/
private function actuaL_put_by_name( $name, $value, $expires ) {
if ( $this->upsertone ) {
$stmt = $this->upsertone;
$stmt->bindValue( ':name', $name, SQLITE3_TEXT );
$stmt->bindValue( ':value', $value, SQLITE3_BLOB );
$stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
$result = $stmt->execute();
$result->finalize();
} else {
/* Pre-upsert version (pre- 3.24) of SQLite,
* Need to try update, then do insert if need be.
* Race conditions are possible, hence BEGIN / COMMIT
*/
if ( ! $this->transaction_active ) {
$this->sqlite->exec( 'BEGIN' );
}
$stmt = $this->updateone;
$stmt->bindValue( ':name', $name, SQLITE3_TEXT );
$stmt->bindValue( ':value', $value, SQLITE3_BLOB );
$stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
$result = $stmt->execute();
$result->finalize();
if ( 0 === $this->sqlite->changes() ) {
/* Updated zero rows, so we need an insert. */
$stmt = $this->insertone;
$stmt->bindValue( ':name', $name, SQLITE3_TEXT );
$stmt->bindValue( ':value', $value, SQLITE3_BLOB );
$stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
$result = $stmt->execute();
$result->finalize();
if ( 0 === $this->sqlite->changes() ) {
/* Updated zero rows, so we need an insert. */
$stmt = $this->insertone;
$stmt->bindValue( ':name', $name, SQLITE3_TEXT );
$stmt->bindValue( ':value', $value, SQLITE3_BLOB );
$stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
$result = $stmt->execute();
$result->finalize();
}
if ( ! $this->transaction_active ) {
$this->sqlite->exec( 'COMMIT' );
}
}
unset( $this->not_in_persistent_cache[ $name ] );
/* track how long it took. */
$this->insert_times[] = $this->time_usec() - $start;
} catch ( Exception $ex ) {
$this->error_log( 'handle_put', $ex );
$this->delete_offending_files();
self::drop_dead();
if ( ! $this->transaction_active ) {
$this->sqlite->exec( 'COMMIT' );
}
}
}

Expand Down Expand Up @@ -1887,7 +1911,6 @@ public function delete( $key, $group = 'default', $deprecated = false ) {
$name = $this->normalize_name( $key, $group );
unset ( $this->cache[ $name ] );
$this->delete_by_name( $name );
$this->not_in_persistent_cache[ $name ] = true;

return true;
}
Expand Down Expand Up @@ -1945,7 +1968,7 @@ public function sqlite_delete_old( $target_size, $current_size ) {
}
$this->checkpoint();
} catch ( Exception $ex ) {
$this->delete_offending_files();
/* Empty, intenionally. */
}
}

Expand All @@ -1957,18 +1980,32 @@ public function sqlite_delete_old( $target_size, $current_size ) {
* @return void
*/
private function delete_by_name( $name ) {
$start = $this->time_usec();
try {
$this->not_in_persistent_cache[ $name ] = true;
$stmt = $this->deleteone;
$stmt->bindValue( ':name', $name, SQLITE3_TEXT );
$result = $stmt->execute();
$result->finalize();
} catch ( Exception $ex ) {
$exception = null;
$retries = 3;
$stmt = $this->deleteone;
$this->not_in_persistent_cache[ $name ] = true;
$start = $this->time_usec();
while ( $retries -- > 0 ) {
try {
$stmt->bindValue( ':name', $name, SQLITE3_TEXT );
$result = $stmt->execute();
$result->finalize();
$this->delete_times[] = $this->time_usec() - $start;

return;
} catch ( Exception $ex ) {
$exception = $ex;
if ( ! str_contains( $ex->getMessage(), 'database is locked' ) || 5 !== $this->sqlite->lastErrorCode() ) {
break;
}
}
sleep( 1 );
}
if ( $exception ) {
$this->error_log( 'delete_by_name', $exception );
$this->delete_offending_files();
self::drop_dead();
}
/* track how long it took. */
$this->delete_times[] = $this->time_usec() - $start;
}

/**
Expand Down Expand Up @@ -2066,9 +2103,8 @@ public function flush( $vacuum = false ) {
$this->sqlite->exec( 'VACUUM;' );
}
} catch ( Exception $ex ) {
$this->error_log( 'flush', $ex );
$this->error_log( 'flush failure, recreate cache.', $ex );
$this->delete_offending_files();
self::drop_dead();
}

return true;
Expand Down Expand Up @@ -2117,7 +2153,6 @@ public function flush_group( $group ) {
} catch ( Exception $ex ) {
$this->error_log( 'flush_group', $ex );
$this->delete_offending_files();
self::drop_dead();
}
/* remove hints about what is in the persistent cache */
$this->not_in_persistent_cache = array();
Expand Down
4 changes: 2 additions & 2 deletions includes/class-sqlite-object-cache-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ private function settings_fields() {
'label' => __( 'Cached data size', 'sqlite-object-cache' ),
'description' => __( 'MiB. When data in the cache grows larger than this, hourly cleanup removes the oldest entries.', 'sqlite-object-cache' ),
'type' => 'number',
'default' => 4,
'min' => 0.5,
'default' => 16,
'min' => 1,
'step' => 'any',
'cssclass' => 'narrow',
'placeholder' => __( 'MiB.', 'sqlite-object-cache' ),
Expand Down
2 changes: 1 addition & 1 deletion includes/class-sqlite-object-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public function load_plugin_textdomain() {
*/
public function clean_job( $grace_factor = 1.0 ) {
$option = get_option( $this->_token . '_settings', array() );
$target_size = empty ( $option['target_size'] ) ? 4 : $option['target_size'];
$target_size = empty ( $option['target_size'] ) ? 16 : $option['target_size'];
$target_size *= ( 1024 * 1024 );
$threshold_size = (int) ( $target_size * $grace_factor );

Expand Down
8 changes: 6 additions & 2 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,15 @@ causes your object cache data to go into the `/tmp` folder in a file named `mysi

`define( 'WP_SQLITE_OBJECT_CACHE_MMAP_SIZE', 32 );`

Notice that using memory-mapped I/O may not help performance in the highly concurrent environment of a busy web server.

= I sometimes get timeout errors from SQLite. How can I fix them? =

Some sites occasionally generate error messages looking like this one:

`Unable to execute statement: database is locked in /var/www/wp-content/object-cache.php:1234`

This can happen if your server places your WordPress files on network-attached storage (that is, on a network drive). To solve this, store your cached data on a locally attached drive. See the question about storing your data in a more secure place.
This can happen if your server places your WordPress files on network-attached storage (that is, on a network drive). To solve this, store your cached data on a locally attached drive. See the question about storing your data in a more secure place. It also can happen in a very busy site.

= Why do I get errors when I use WP-CLI to administer my site? =

Expand Down Expand Up @@ -191,6 +193,8 @@ Please look for more questions and answers [here](https://www.plumislandmedia.ne

* Clean up in chunks in an attempt to reduce contention delays and timeouts.
* Do PRAGMA wal_checkpoint(RESTART) when cleaning up, and also occasionally, to prevent the write-ahead log from growing without bound on busy systems.
* Retry three times if cache updates time out.
* Increase default cache size to 16MiB for new users.

= 1.3.5 =

Expand All @@ -210,6 +214,6 @@ Please look for more questions and answers [here](https://www.plumislandmedia.ne

== Upgrade Notice ==

This release attempts to reduce cache timeouts by doing cleanup operations in chunks. It also does PRAGMA wal_checkpoint(RESTART) when cleaning up, and also occasionally, to prevent the write-ahead log from growing without bound on busy systems.
This release attempts to reduce cache timeouts by doing cleanup operations in chunks, and by retrying timed-out cache update operations. It also does PRAGMA wal_checkpoint(RESTART) when cleaning up, and also occasionally, to prevent the write-ahead log from growing without bound on busy systems.

Thanks, dear users, especially @bourgesloic, @spacedmonkey, @spaceling and @ss88_uk, for letting me know about errors you found, and for your patience as I figure this out. All remaining errors are solely the responsibility of the author.

0 comments on commit 61706e4

Please sign in to comment.