<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[5169] sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/libs/site-search/jetpack-search.php: Plugin directory search: randomly throttle back searches for a short window if the API starts returning errors or failing to respond.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/5169">5169</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/5169","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>tellyworth</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-03-27 10:22:40 +0000 (Mon, 27 Mar 2017)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Plugin directory search: randomly throttle back searches for a short window if the API starts returning errors or failing to respond.

This should help limit the load on the ES API. Constants for the window and the threshold curve might need tweaking.

See also <a href="http://meta.trac.wordpress.org/changeset/5124">[5124]</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectorylibssitesearchjetpacksearchphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/libs/site-search/jetpack-search.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectorylibssitesearchjetpacksearchphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/libs/site-search/jetpack-search.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/libs/site-search/jetpack-search.php     2017-03-23 21:27:08 UTC (rev 5168)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/libs/site-search/jetpack-search.php       2017-03-27 10:22:40 UTC (rev 5169)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -72,6 +72,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        const CACHE_GROUP = 'jetpack-search';
</span><span class="cx" style="display: block; padding: 0 10px">        const CACHE_EXPIRY = 300;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        const ERROR_COUNT_KEY = 'error-count-';
+       const ERROR_COUNT_WINDOW = 60; // seconds
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        protected function __construct() {
</span><span class="cx" style="display: block; padding: 0 10px">                /* Don't do anything, needs to be initialized via instance() method */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -167,7 +169,54 @@
</span><span class="cx" style="display: block; padding: 0 10px">        /////////////////////////////////////////////////////////
</span><span class="cx" style="display: block; padding: 0 10px">        // Raw Search Query
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /* 
+        * Return a count of the number of search API errors within the last ERROR_COUNT_WINDOW seconds
+        */
+       protected function get_error_volume() {
+               // Use a dual-tick window like nonces
+               $tick = ceil( time() / (self::ERROR_COUNT_WINDOW/2) );
+
+               return intval( wp_cache_get( self::ERROR_COUNT_KEY . $tick, self::CACHE_GROUP ) ) 
+                        + intval( wp_cache_get( self::ERROR_COUNT_KEY . ($tick -1), self::CACHE_GROUP ) );
+       }
+
+       /* 
+        *      Increment the recent error volume by $count.
+        */
+       protected function increment_error_volume( $count = 1 ) {
+               // wp_cache_incr() bails if the key does not exist
+               $tick = ceil( time() / (self::ERROR_COUNT_WINDOW/2) );
+               wp_cache_add( self::ERROR_COUNT_KEY . $tick, 0, self::CACHE_GROUP, self::ERROR_COUNT_WINDOW );
+               return wp_cache_incr( self::ERROR_COUNT_KEY . $tick, $count, self::CACHE_GROUP );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /*
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Determine whether or not the recent error volume is low enough to allow a fresh API call.
+        */
+       protected function error_volume_is_low() {
+               // This cache key keeps a global-ish count of recent errors (per memcache instance).
+               // Used for random exponential backoff when errors start to pile up, as they will if there is a network outage for example.
+               $error_volume = max( $this->get_error_volume(), 0 ); // >= 0
+
+               // This gives us a threshold with a gentle curve from 10 down to 0
+               // The idea being that for a small volume of errors ( < 10 ) we'll have a 100% chance of attempting a new search
+               // For 10-15 errors, an 80-90% chance
+               // For 20 errors, a 50% chance
+               // For 40+ errors, a 0% chance
+
+               $threshold = ceil( 10 / ( 1 + pow( $error_volume / 20, 4 ) ) );
+               return mt_rand( 1, 10 ) <= $threshold;
+       }
+
+       /*
+        * Trigger a search error message and increment the recent error volume.
+        */
+       protected function search_error( $reason ) {
+               trigger_error( 'Plugin directory search: '.$reason, E_USER_WARNING );
+               return $this->increment_error_volume();
+       }
+
+       /*
</ins><span class="cx" style="display: block; padding: 0 10px">          * Run a search on the WP.com public API.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param object $es_args : Args conforming to the WP.com /sites/<blog_id>/search endpoint
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -184,15 +233,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">                // Use a temporary lock to prevent cache stampedes. This ensures only one process (per search term per memcache instance) will run the remote post.
</span><span class="cx" style="display: block; padding: 0 10px">                // Other processes will use the stale cached value if it's present, even for a while after the expiration time if a fresh value is still being fetched.
</span><span class="cx" style="display: block; padding: 0 10px">                if ( wp_cache_add( $lock_key, 1, self::CACHE_GROUP, 15 ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $request = wp_remote_post( $service_url, array(
-                               'headers' => array(
-                                       'Content-Type' => 'application/json',
-                               ),
-                               'timeout' => 10,
-                               'user-agent' => 'jetpack_search',
-                               'body' => $json_es_args,
-                       ) );
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        // If the error volume is high, there's a proportionally lower chance that we'll actually attempt to hit the API.
+                       if ( $this->error_volume_is_low() ) {
+                               $request = wp_remote_post( $service_url, array(
+                                       'headers' => array(
+                                               'Content-Type' => 'application/json',
+                                       ),
+                                       'timeout' => 10,
+                                       'user-agent' => 'jetpack_search',
+                                       'body' => $json_es_args,
+                               ) );
+                       } else {
+                               trigger_error( 'Plugin directory search: skipping search due to high error volume', E_USER_WARNING );
+                               // Hopefully we still have a cached response to return
+                               return $response;
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         // If there's a network or HTTP error, we'll intentionally leave the temporary lock to expire in a few seconds.
</span><span class="cx" style="display: block; padding: 0 10px">                        // Other requests during that window will use the stale cached value. We'll try another remote request once this lock expires.
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( is_wp_error( $request ) || 200 != wp_remote_retrieve_response_code( $request ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -201,9 +258,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                wp_cache_set( $lock_key, 1, self::CACHE_GROUP, mt_rand( 3, 7 ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( is_wp_error( $request ) )
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        trigger_error( 'Plugin directory search: http error '.$request->get_error_message(), E_USER_WARNING );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 $this->search_error( 'http error '.$request->get_error_message(), E_USER_WARNING );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 else
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        trigger_error( 'Plugin directory search: http status '.wp_remote_retrieve_response_code( $request ), E_USER_WARNING );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 $this->search_error( 'http status '.wp_remote_retrieve_response_code( $request ), E_USER_WARNING );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // If we have a stale cached response, return that. Otherwise, return the error object.
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( $response )
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -218,9 +275,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                wp_cache_set( $lock_key, 1, self::CACHE_GROUP, mt_rand( 3, 7 ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( isset( $fresh_response['error'] ) )
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        trigger_error( 'Plugin directory search: remote error '.$fresh_response['error'], E_USER_WARNING );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 $this->search_error( 'remote error '.$fresh_response['error'], E_USER_WARNING );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 else
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        trigger_error( 'Plugin directory search: invalid json response', E_USER_WARNING );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 $this->search_error( 'invalid json response', E_USER_WARNING );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // Return a stale response if we have one
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( $response )
</span></span></pre>
</div>
</div>

</body>
</html>