<!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>[33615] trunk/src: Term splitting routine should be run in a separate process, triggered via wp-cron.</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="https://core.trac.wordpress.org/changeset/33615">33615</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/33615","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>boonebgorges</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2015-08-14 03:58:41 +0000 (Fri, 14 Aug 2015)</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'>Term splitting routine should be run in a separate process, triggered via wp-cron.

<a href="https://core.trac.wordpress.org/changeset/32814">[32814]</a> introduced a routine to split shared terms, which was run during the
regular WP database upgrade. This turned out to be problematic because plugins
are not loaded during the db upgrade (due to `WP_INSTALLING`), with the result
that plugins were not able to hook into the 'split_shared_term' action during
the bulk split. We work around this limitation by moving the term splitting
routine to a separate process, triggered by a wp-cron hook.

Props boonebgorges, Chouby, peterwilsoncc, pento, dd32.
Fixes <a href="https://core.trac.wordpress.org/ticket/30261">#30261</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminincludesupgradephp">trunk/src/wp-admin/includes/upgrade.php</a></li>
<li><a href="#trunksrcwpincludesdefaultfiltersphp">trunk/src/wp-includes/default-filters.php</a></li>
<li><a href="#trunksrcwpincludestaxonomyphp">trunk/src/wp-includes/taxonomy.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminincludesupgradephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/upgrade.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/upgrade.php   2015-08-13 22:30:26 UTC (rev 33614)
+++ trunk/src/wp-admin/includes/upgrade.php     2015-08-14 03:58:41 UTC (rev 33615)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1505,8 +1505,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                upgrade_430_fix_comments();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // Shared terms are split in a separate process.
</ins><span class="cx" style="display: block; padding: 0 10px">         if ( $wp_current_db_version < 32814 ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                split_all_shared_terms();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         wp_schedule_single_event( time() + ( 1 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( $wp_current_db_version < 33055 && 'utf8mb4' === $wpdb->charset ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1881,76 +1882,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Splits all shared taxonomy terms.
- *
- * @since 4.3.0
- *
- * @global wpdb $wpdb WordPress database abstraction object.
- */
-function split_all_shared_terms() {
-       global $wpdb;
-
-       // Get a list of shared terms (those with more than one associated row in term_taxonomy).
-       $shared_terms = $wpdb->get_results(
-               "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt
-                LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
-                GROUP BY t.term_id
-                HAVING term_tt_count > 1"
-       );
-
-       if ( empty( $shared_terms ) ) {
-               return;
-       }
-
-       // Rekey shared term array for faster lookups.
-       $_shared_terms = array();
-       foreach ( $shared_terms as $shared_term ) {
-               $term_id = intval( $shared_term->term_id );
-               $_shared_terms[ $term_id ] = $shared_term;
-       }
-       $shared_terms = $_shared_terms;
-
-       // Get term taxonomy data for all shared terms.
-       $shared_term_ids = implode( ',', array_keys( $shared_terms ) );
-       $shared_tts = $wpdb->get_results( "SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})" );
-
-       // Split term data recording is slow, so we do it just once, outside the loop.
-       $suspend = wp_suspend_cache_invalidation( true );
-       $split_term_data = get_option( '_split_terms', array() );
-       $skipped_first_term = $taxonomies = array();
-       foreach ( $shared_tts as $shared_tt ) {
-               $term_id = intval( $shared_tt->term_id );
-
-               // Don't split the first tt belonging to a given term_id.
-               if ( ! isset( $skipped_first_term[ $term_id ] ) ) {
-                       $skipped_first_term[ $term_id ] = 1;
-                       continue;
-               }
-
-               if ( ! isset( $split_term_data[ $term_id ] ) ) {
-                       $split_term_data[ $term_id ] = array();
-               }
-
-               // Keep track of taxonomies whose hierarchies need flushing.
-               if ( ! isset( $taxonomies[ $shared_tt->taxonomy ] ) ) {
-                       $taxonomies[ $shared_tt->taxonomy ] = 1;
-               }
-
-               // Split the term.
-               $split_term_data[ $term_id ][ $shared_tt->taxonomy ] = _split_shared_term( $shared_terms[ $term_id ], $shared_tt, false );
-       }
-
-       // Rebuild the cached hierarchy for each affected taxonomy.
-       foreach ( array_keys( $taxonomies ) as $tax ) {
-               delete_option( "{$tax}_children" );
-               _get_term_hierarchy( $tax );
-       }
-
-       wp_suspend_cache_invalidation( $suspend );
-       update_option( '_split_terms', $split_term_data );
-}
-
-/**
</del><span class="cx" style="display: block; padding: 0 10px">  * Retrieve all options as it was for 1.2.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 1.2.0
</span></span></pre></div>
<a id="trunksrcwpincludesdefaultfiltersphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/default-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-filters.php 2015-08-13 22:30:26 UTC (rev 33614)
+++ trunk/src/wp-includes/default-filters.php   2015-08-14 03:58:41 UTC (rev 33615)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -328,9 +328,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'determine_current_user', 'wp_validate_logged_in_cookie', 20 );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> // Split term updates.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_action( 'admin_init',        '_wp_check_for_scheduled_split_terms' );
</ins><span class="cx" style="display: block; padding: 0 10px"> add_action( 'split_shared_term', '_wp_check_split_default_terms',  10, 4 );
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'split_shared_term', '_wp_check_split_terms_in_menus', 10, 4 );
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'split_shared_term', '_wp_check_split_nav_menu_terms', 10, 4 );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_action( 'wp_split_shared_term_batch', '_wp_batch_split_terms' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * Filters formerly mixed into wp-includes
</span></span></pre></div>
<a id="trunksrcwpincludestaxonomyphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/taxonomy.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/taxonomy.php        2015-08-13 22:30:26 UTC (rev 33614)
+++ trunk/src/wp-includes/taxonomy.php  2015-08-14 03:58:41 UTC (rev 33615)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4249,12 +4249,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $term_taxonomy_id = intval( $term_taxonomy->term_taxonomy_id );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        // Don't try to split terms if database schema does not support shared slugs.
-       $current_db_version = get_option( 'db_version' );
-       if ( $current_db_version < 30133 ) {
-               return $term_id;
-       }
-
</del><span class="cx" style="display: block; padding: 0 10px">         // If there are no shared term_taxonomy rows, there's nothing to do here.
</span><span class="cx" style="display: block; padding: 0 10px">        $shared_tt_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy tt WHERE tt.term_id = %d AND tt.term_taxonomy_id != %d", $term_id, $term_taxonomy_id ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4262,6 +4256,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return $term_id;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /*
+        * Verify that the term_taxonomy_id passed to the function is actually associated with the term_id.
+        * If there's a mismatch, it may mean that the term is already split. Return the actual term_id from the db.
+        */
+       $check_term_id = $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) );
+       if ( $check_term_id != $term_id ) {
+               return $check_term_id;
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         // Pull up data about the currently shared slug, which we'll use to populate the new one.
</span><span class="cx" style="display: block; padding: 0 10px">        if ( empty( $shared_term ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $shared_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.* FROM $wpdb->terms t WHERE t.term_id = %d", $term_id ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4339,6 +4342,116 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Splits a batch of shared taxonomy terms.
+ *
+ * @since 4.3.0
+ *
+ * @global wpdb $wpdb WordPress database abstraction object.
+ */
+function _wp_batch_split_terms() {
+       global $wpdb;
+
+       $lock_name = 'term_split.lock';
+
+       // Try to lock.
+       $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time() ) );
+
+       if ( ! $lock_result ) {
+               $lock_result = get_option( $lock_name );
+
+               // Bail if we were unable to create a lock, or if the existing lock is still valid.
+               if ( ! $lock_result || ( $lock_result > ( time() - HOUR_IN_SECONDS ) ) ) {
+                       wp_schedule_single_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' );
+                       return;
+               }
+       }
+
+       // Update the lock, as by this point we've definitely got a lock, just need to fire the actions.
+       update_option( $lock_name, time() );
+
+       // Get a list of shared terms (those with more than one associated row in term_taxonomy).
+       $shared_terms = $wpdb->get_results(
+               "SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt
+                LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
+                GROUP BY t.term_id
+                HAVING term_tt_count > 1
+                LIMIT 20"
+       );
+
+       // No more terms, we're done here.
+       if ( ! $shared_terms ) {
+               update_option( 'finished_splitting_shared_terms', true );
+               delete_option( $lock_name );
+               return;
+       }
+
+       // Shared terms found? We'll need to run this script again.
+       wp_schedule_single_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' );
+
+       // Rekey shared term array for faster lookups.
+       $_shared_terms = array();
+       foreach ( $shared_terms as $shared_term ) {
+               $term_id = intval( $shared_term->term_id );
+               $_shared_terms[ $term_id ] = $shared_term;
+       }
+       $shared_terms = $_shared_terms;
+
+       // Get term taxonomy data for all shared terms.
+       $shared_term_ids = implode( ',', array_keys( $shared_terms ) );
+       $shared_tts = $wpdb->get_results( "SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})" );
+
+       // Split term data recording is slow, so we do it just once, outside the loop.
+       $suspend = wp_suspend_cache_invalidation( true );
+       $split_term_data = get_option( '_split_terms', array() );
+       $skipped_first_term = $taxonomies = array();
+       foreach ( $shared_tts as $shared_tt ) {
+               $term_id = intval( $shared_tt->term_id );
+
+               // Don't split the first tt belonging to a given term_id.
+               if ( ! isset( $skipped_first_term[ $term_id ] ) ) {
+                       $skipped_first_term[ $term_id ] = 1;
+                       continue;
+               }
+
+               if ( ! isset( $split_term_data[ $term_id ] ) ) {
+                       $split_term_data[ $term_id ] = array();
+               }
+
+               // Keep track of taxonomies whose hierarchies need flushing.
+               if ( ! isset( $taxonomies[ $shared_tt->taxonomy ] ) ) {
+                       $taxonomies[ $shared_tt->taxonomy ] = 1;
+               }
+
+               // Split the term.
+               $split_term_data[ $term_id ][ $shared_tt->taxonomy ] = _split_shared_term( $shared_terms[ $term_id ], $shared_tt, false );
+       }
+
+       // Rebuild the cached hierarchy for each affected taxonomy.
+       foreach ( array_keys( $taxonomies ) as $tax ) {
+               delete_option( "{$tax}_children" );
+               _get_term_hierarchy( $tax );
+       }
+
+       wp_suspend_cache_invalidation( $suspend );
+       update_option( '_split_terms', $split_term_data );
+
+       delete_option( $lock_name );
+}
+
+/**
+ * In order to avoid the wp_batch_split_terms() job being accidentally removed,
+ * check that it's still scheduled while we haven't finished splitting terms.
+ *
+ * @ignore
+ * @since 4.3.0
+ */
+function _wp_check_for_scheduled_split_terms() {
+       if ( ! get_option( 'finished_splitting_shared_terms' ) && ! wp_next_scheduled( 'wp_batch_split_terms' ) ) {
+               wp_schedule_single_event( 'wp_batch_split_terms', time() + MINUTE_IN_SECONDS );
+       }
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Check default categories when a term gets split to see if any of them need to be updated.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @ignore
</span></span></pre>
</div>
</div>

</body>
</html>