Index: src/wp-includes/class-wp-hook.php
===================================================================
--- src/wp-includes/class-wp-hook.php	(revision 0)
+++ src/wp-includes/class-wp-hook.php	(working copy)
@@ -0,0 +1,282 @@
+<?php
+
+/**
+ * Class WP_Hook
+ */
+class WP_Hook implements IteratorAggregate, Countable {
+	public $callbacks = array();
+
+	/** @var array The priority keys of actively running iterations of a hook */
+	private $iterations = array();
+
+	/** @var int How recursively has this hook been called? */
+	private $nesting_level = 0;
+
+	/**
+	 * Hook a function or method to a specific filter action.
+	 *
+	 * @param callback $function_to_add The callback to be run when the filter is applied.
+	 * @param int      $priority        (optional) The order in which the functions associated with a particular action are executed. Lower numbers correspond with earlier execution, and functions with the same priority are executed in the order in which they were added to the action.
+	 *                                  Default 10.
+	 * @param int      $accepted_args   (optional) The number of arguments the function accepts.
+	 *                                  Default 1.
+	 * @param string   $tag             The name of the filter to hook the $function_to_add callback to.
+	 * @return void
+	 */
+	public function add_filter( $function_to_add, $priority, $accepted_args, $tag ) {
+		$idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
+		$priority_existed = isset($this->callbacks[$priority]);
+		$this->callbacks[$priority][$idx] = array( 'function' => $function_to_add, 'accepted_args' => $accepted_args );
+		if ( !$priority_existed && count($this->callbacks) > 1 ) {
+			ksort($this->callbacks, SORT_NUMERIC);
+		}
+
+		if ( $this->nesting_level > 0 ) {
+			$this->resort_active_iterations();
+		}
+	}
+
+	/**
+	 * When a hook's callbacks are changed mid-iteration, the priority
+	 * keys need to be reset, with the array pointer at the correct
+	 * location
+	 *
+	 * @return void
+	 */
+	private function resort_active_iterations() {
+		$new_priorities = array_keys($this->callbacks);
+
+		// if there are no remaining hooks, clear out all running iterations
+		if ( empty($new_priorities) ) {
+			foreach ( $this->iterations as $index => $iteration ) {
+				$this->iterations[$index] = $new_priorities;
+			}
+			return;
+		}
+
+		$min = min($new_priorities);
+		foreach ( $this->iterations as $index => $iteration ) {
+			$current = current($iteration);
+			$this->iterations[$index] = $new_priorities;
+
+			if ( $current < $min ) {
+				array_unshift( $this->iterations[$index], $current );
+				continue;
+			}
+			while ( current($this->iterations[$index]) < $current ) {
+				if ( next($this->iterations[$index]) === FALSE ) {
+					break;
+				};
+			}
+		}
+	}
+
+	/**
+	 * @param string $function_key
+	 * @param int $priority
+	 *
+	 * @return bool Whether the callback existed before it was removed
+	 */
+	public function remove_filter( $function_key, $priority ) {
+		$exists = isset($this->callbacks[$priority][$function_key]);
+		if ( $exists ) {
+			unset($this->callbacks[$priority][$function_key]);
+			if ( empty($this->callbacks[$priority]) ) {
+				unset($this->callbacks[$priority]);
+				if ( $this->nesting_level > 0 ) {
+					$this->resort_active_iterations();
+				}
+			}
+		}
+		return $exists;
+	}
+
+	/**
+	 * Check if an specific action has been registered for this hook.
+	 *
+	 * @param string $function_key The hashed index of the filter
+	 * @return mixed The priority of that hook is returned, or false if the function is not attached.
+	 */
+	public function has_filter( $function_key ) {
+		foreach ( $this->callbacks as $priority => &$callbacks ) {
+			if ( isset($callbacks[$function_key]) ) {
+				return $priority;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Check if any callbacks have been registered for this hook
+	 *
+	 * @return bool
+	 */
+	public function has_filters() {
+		foreach ( $this->callbacks as &$callbacks ) {
+			if ( !empty($callbacks) ) {
+				return TRUE;
+			}
+		}
+		return FALSE;
+	}
+
+	/**
+	 * Remove all of the callbacks from the filter.
+	 *
+	 * @param int|bool $priority The priority number to remove.
+	 * @return void
+	 */
+	public function remove_all_filters( $priority = false ) {
+		if ( empty($this->callbacks) ) {
+			return;
+		}
+		if( false !== $priority && isset($this->callbacks[$priority]) ) {
+			unset($this->callbacks[$priority]);
+		} else {
+			$this->callbacks = array();
+		}
+		if ( $this->nesting_level > 0 ) {
+			$this->resort_active_iterations();
+		}
+	}
+
+	/**
+	 * Call the functions added to a filter hook.
+	 *
+	 * @param mixed $value The value to filter.
+	 * @param array $args Arguments to pass to callbacks
+	 *
+	 * @return mixed The filtered value after all hooked functions are applied to it
+	 */
+	public function apply_filters( $value, &$args ) {
+		if ( empty($this->callbacks) ) {
+			return $value;
+		}
+		$nesting_level = $this->nesting_level++;
+		$this->iterations[$nesting_level] = array_keys($this->callbacks);
+
+		do {
+			$priority = current($this->iterations[$nesting_level]);
+			foreach ( $this->callbacks[$priority] as $the_ ) {
+				$args[0] = $value;
+				$value = call_user_func_array( $the_['function'], array_slice( $args, 0, (int)$the_['accepted_args'] ) );
+			}
+		} while ( next($this->iterations[$nesting_level]) !== FALSE );
+
+		unset($this->iterations[$nesting_level]);
+		$this->nesting_level--;
+		return $value;
+	}
+
+	/**
+	 * Execute functions hooked on a specific action hook.
+	 *
+	 * @param mixed $args Arguments to pass to callbacks
+	 * @return void
+	 */
+	public function do_action( &$args ) {
+
+		if ( empty($this->callbacks) ) {
+			return;
+		}
+		$nesting_level = $this->nesting_level++;
+		$this->iterations[$nesting_level] = array_keys($this->callbacks);
+		$num_args = count($args);
+
+		do {
+			$priority = current($this->iterations[$nesting_level]);
+			foreach ( $this->callbacks[$priority] as $the_ ) {
+				$func =& $the_['function'];
+				if ( $the_['accepted_args'] == 0 ) {
+					$func_args = array();
+				} elseif ( $the_['accepted_args'] >= $num_args ) {
+					$func_args = $args;
+				} else {
+					$func_args = array_slice( $args, 0, (int) $the_['accepted_args'] );
+				}
+				call_user_func_array($func, $func_args );
+			}
+		} while ( next($this->iterations[$nesting_level]) !== FALSE );
+
+		unset($this->iterations[$nesting_level]);
+		$this->nesting_level--;
+	}
+
+	/**
+	 * Process the functions hooked into the 'all' hook.
+	 *
+	 * @param array $args Arguments to pass to callbacks
+	 * @return void
+	 */
+	public function do_all_hook( &$args ) {
+		$nesting_level = $this->nesting_level++;
+		$this->iterations[$nesting_level] = array_keys($this->callbacks);
+
+		do {
+			$priority = current($this->iterations[$nesting_level]);
+			foreach ( $this->callbacks[$priority] as $the_ ) {
+				call_user_func_array($the_['function'], $args );
+			}
+		} while ( next($this->iterations[$nesting_level]) !== FALSE );
+
+		unset($this->iterations[$nesting_level]);
+		$this->nesting_level--;
+	}
+
+	/**
+	 * Retrieve an external iterator
+	 *
+	 * Provided for backwards compatibility with plugins that iterate over the
+	 * $wp_filter global
+	 *
+	 * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
+	 * @return Traversable An instance of an object implementing Iterator or
+	 * Traversable
+	 */
+	public function getIterator() {
+		return new ArrayIterator($this->callbacks);
+	}
+
+	/**
+	 * Count elements of an object
+	 *
+	 * Provided for backwards compatibility with plugins that access the
+	 * $wp_filter global
+	 *
+	 * @link http://php.net/manual/en/countable.count.php
+	 * @return int The custom count as an integer.
+	 *
+	 * The return value is cast to an integer.
+	 */
+	public function count() {
+		return count($this->callbacks);
+	}
+
+	/**
+	 * Some plugins may set up filters before WordPress has initialized.
+	 * Normalize them to WP_Hook objects.
+	 *
+	 * @param array $filters
+	 * @return WP_Hook[]
+	 */
+	public static function build_preinitialized_hooks( $filters ) {
+		/** @var WP_Hook[] $normalized */
+		$normalized = array();
+		foreach ( $filters as $tag => $callback_groups ) {
+			if ( is_object($callback_groups) && $callback_groups instanceof WP_Hook ) {
+				$normalized[$tag] = $callback_groups;
+				continue;
+			}
+			$hook = new WP_Hook();
+			foreach ( $callback_groups as $priority => $callbacks ) {
+				foreach ( $callbacks as $cb ) {
+					$hook->add_filter( $cb['function'], $priority, $cb['accepted_args'], $tag );
+				}
+			}
+			$normalized[$tag] = $hook;
+		}
+		return $normalized;
+	}
+
+}
+
Index: src/wp-includes/plugin.php
===================================================================
--- src/wp-includes/plugin.php	(revision 29858)
+++ src/wp-includes/plugin.php	(working copy)
@@ -20,17 +20,19 @@
  */
 
 // Initialize the filter globals.
-global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
-
-if ( ! isset( $wp_filter ) )
+require( ABSPATH . '/wp-includes/class-wp-hook.php' );
+/** @var WP_Hook[] $wp_filter */
+global $wp_filter, $wp_actions, $wp_current_filter;
+
+if ( !empty( $wp_filter ) ) {
+	$wp_filter = WP_Hook::build_preinitialized_hooks( $wp_filter );
+} else {
 	$wp_filter = array();
+}
 
 if ( ! isset( $wp_actions ) )
 	$wp_actions = array();
 
-if ( ! isset( $merged_filters ) )
-	$merged_filters = array();
-
 if ( ! isset( $wp_current_filter ) )
 	$wp_current_filter = array();
 
@@ -66,8 +68,6 @@
  * @since 0.71
  *
  * @global array $wp_filter      A multidimensional array of all hooks and the callbacks hooked to them.
- * @global array $merged_filters Tracks the tags that need to be merged for later. If the hook is added,
- *                               it doesn't need to run through that process.
  *
  * @param string   $tag             The name of the filter to hook the $function_to_add callback to.
  * @param callback $function_to_add The callback to be run when the filter is applied.
@@ -80,11 +80,12 @@
  * @return boolean true
  */
 function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
-	global $wp_filter, $merged_filters;
+	global $wp_filter;
+	if ( !isset($wp_filter[$tag]) ) {
+		$wp_filter[$tag] = new WP_Hook();
+	}
+	$wp_filter[$tag]->add_filter( $function_to_add, $priority, $accepted_args, $tag );
 
-	$idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
-	$wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
-	unset( $merged_filters[ $tag ] );
 	return true;
 }
 
@@ -105,25 +106,9 @@
  *                  return value.
  */
 function has_filter($tag, $function_to_check = false) {
-	// Don't reset the internal array pointer
-	$wp_filter = $GLOBALS['wp_filter'];
-
-	$has = ! empty( $wp_filter[ $tag ] );
+	global $wp_filter;
 
-	// Make sure at least one priority has a filter callback
-	if ( $has ) {
-		$exists = false;
-		foreach ( $wp_filter[ $tag ] as $callbacks ) {
-			if ( ! empty( $callbacks ) ) {
-				$exists = true;
-				break;
-			}
-		}
-
-		if ( ! $exists ) {
-			$has = false;
-		}
-	}
+	$has = isset($wp_filter[$tag]) && $wp_filter[$tag]->has_filters();
 
 	if ( false === $function_to_check || false == $has )
 		return $has;
@@ -131,12 +116,7 @@
 	if ( !$idx = _wp_filter_build_unique_id($tag, $function_to_check, false) )
 		return false;
 
-	foreach ( (array) array_keys($wp_filter[$tag]) as $priority ) {
-		if ( isset($wp_filter[$tag][$priority][$idx]) )
-			return $priority;
-	}
-
-	return false;
+	return $wp_filter[$tag]->has_filter($idx);
 }
 
 /**
@@ -167,7 +147,6 @@
  * @since 0.71
  *
  * @global array $wp_filter         Stores all of the filters.
- * @global array $merged_filters    Merges the filter hooks using this function.
  * @global array $wp_current_filter Stores the list of current filters with the current one last.
  *
  * @param string $tag   The name of the filter hook.
@@ -176,7 +155,7 @@
  * @return mixed The filtered value after all hooked functions are applied to it.
  */
 function apply_filters( $tag, $value ) {
-	global $wp_filter, $merged_filters, $wp_current_filter;
+	global $wp_filter, $wp_current_filter;
 
 	$args = array();
 
@@ -185,6 +164,7 @@
 		$wp_current_filter[] = $tag;
 		$args = func_get_args();
 		_wp_call_all_hook($args);
+		array_shift($args);
 	}
 
 	if ( !isset($wp_filter[$tag]) ) {
@@ -196,25 +176,12 @@
 	if ( !isset($wp_filter['all']) )
 		$wp_current_filter[] = $tag;
 
-	// Sort.
-	if ( !isset( $merged_filters[ $tag ] ) ) {
-		ksort($wp_filter[$tag]);
-		$merged_filters[ $tag ] = true;
-	}
-
-	reset( $wp_filter[ $tag ] );
-
-	if ( empty($args) )
+	if ( empty($args) ) {
 		$args = func_get_args();
+		array_shift($args);
+	}
 
-	do {
-		foreach( (array) current($wp_filter[$tag]) as $the_ )
-			if ( !is_null($the_['function']) ){
-				$args[1] = $value;
-				$value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
-			}
-
-	} while ( next($wp_filter[$tag]) !== false );
+	$value = $wp_filter[$tag]->apply_filters( $value, $args );
 
 	array_pop( $wp_current_filter );
 
@@ -230,7 +197,6 @@
  * @since 3.0.0
  *
  * @global array $wp_filter         Stores all of the filters
- * @global array $merged_filters    Merges the filter hooks using this function.
  * @global array $wp_current_filter Stores the list of current filters with the current one last
  *
  * @param string $tag  The name of the filter hook.
@@ -238,7 +204,7 @@
  * @return mixed The filtered value after all hooked functions are applied to it.
  */
 function apply_filters_ref_array($tag, $args) {
-	global $wp_filter, $merged_filters, $wp_current_filter;
+	global $wp_filter, $wp_current_filter;
 
 	// Do 'all' actions first
 	if ( isset($wp_filter['all']) ) {
@@ -256,24 +222,11 @@
 	if ( !isset($wp_filter['all']) )
 		$wp_current_filter[] = $tag;
 
-	// Sort
-	if ( !isset( $merged_filters[ $tag ] ) ) {
-		ksort($wp_filter[$tag]);
-		$merged_filters[ $tag ] = true;
-	}
-
-	reset( $wp_filter[ $tag ] );
-
-	do {
-		foreach( (array) current($wp_filter[$tag]) as $the_ )
-			if ( !is_null($the_['function']) )
-				$args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
-
-	} while ( next($wp_filter[$tag]) !== false );
+	$value = $wp_filter[$tag]->apply_filters( $args[0], $args );
 
 	array_pop( $wp_current_filter );
 
-	return $args[0];
+	return $value;
 }
 
 /**
@@ -295,19 +248,15 @@
  * @return boolean Whether the function existed before it was removed.
  */
 function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
-	$function_to_remove = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
-
-	$r = isset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
+	global $wp_filter;
 
-	if ( true === $r ) {
-		unset( $GLOBALS['wp_filter'][ $tag ][ $priority ][ $function_to_remove ] );
-		if ( empty( $GLOBALS['wp_filter'][ $tag ][ $priority ] ) ) {
-			unset( $GLOBALS['wp_filter'][ $tag ][ $priority ] );
+	$r = false;
+	if ( isset($wp_filter[$tag]) ) {
+		$function_to_remove = _wp_filter_build_unique_id($tag, $function_to_remove, $priority);
+		$r = $wp_filter[$tag]->remove_filter($function_to_remove, $priority);
+		if ( empty($wp_filter[$tag]->callbacks) ) {
+			unset($wp_filter[$tag]);
 		}
-		if ( empty( $GLOBALS['wp_filter'][ $tag ] ) ) {
-			$GLOBALS['wp_filter'][ $tag ] = array();
-		}
-		unset( $GLOBALS['merged_filters'][ $tag ] );
 	}
 
 	return $r;
@@ -323,18 +272,11 @@
  * @return bool True when finished.
  */
 function remove_all_filters( $tag, $priority = false ) {
-	global $wp_filter, $merged_filters;
-
-	if ( isset( $wp_filter[ $tag ]) ) {
-		if ( false !== $priority && isset( $wp_filter[ $tag ][ $priority ] ) ) {
-			$wp_filter[ $tag ][ $priority ] = array();
-		} else {
-			$wp_filter[ $tag ] = array();
-		}
-	}
+	global $wp_filter;
 
-	if ( isset( $merged_filters[ $tag ] ) ) {
-		unset( $merged_filters[ $tag ] );
+	if( isset($wp_filter[$tag]) ) {
+		$wp_filter[$tag]->remove_all_filters($priority);
+		unset($wp_filter[$tag]);
 	}
 
 	return true;
@@ -460,7 +402,7 @@
  * @return null Will return null if $tag does not exist in $wp_filter array.
  */
 function do_action($tag, $arg = '') {
-	global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
+	global $wp_filter, $wp_actions, $wp_current_filter;
 
 	if ( ! isset($wp_actions[$tag]) )
 		$wp_actions[$tag] = 1;
@@ -491,20 +433,7 @@
 	for ( $a = 2; $a < func_num_args(); $a++ )
 		$args[] = func_get_arg($a);
 
-	// Sort
-	if ( !isset( $merged_filters[ $tag ] ) ) {
-		ksort($wp_filter[$tag]);
-		$merged_filters[ $tag ] = true;
-	}
-
-	reset( $wp_filter[ $tag ] );
-
-	do {
-		foreach ( (array) current($wp_filter[$tag]) as $the_ )
-			if ( !is_null($the_['function']) )
-				call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
-
-	} while ( next($wp_filter[$tag]) !== false );
+	$wp_filter[$tag]->do_action( $args );
 
 	array_pop($wp_current_filter);
 }
@@ -543,7 +472,7 @@
  * @return null Will return null if $tag does not exist in $wp_filter array
  */
 function do_action_ref_array($tag, $args) {
-	global $wp_filter, $wp_actions, $merged_filters, $wp_current_filter;
+	global $wp_filter, $wp_actions, $wp_current_filter;
 
 	if ( ! isset($wp_actions[$tag]) )
 		$wp_actions[$tag] = 1;
@@ -566,20 +495,7 @@
 	if ( !isset($wp_filter['all']) )
 		$wp_current_filter[] = $tag;
 
-	// Sort
-	if ( !isset( $merged_filters[ $tag ] ) ) {
-		ksort($wp_filter[$tag]);
-		$merged_filters[ $tag ] = true;
-	}
-
-	reset( $wp_filter[ $tag ] );
-
-	do {
-		foreach( (array) current($wp_filter[$tag]) as $the_ )
-			if ( !is_null($the_['function']) )
-				call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
-
-	} while ( next($wp_filter[$tag]) !== false );
+	$wp_filter[$tag]->do_action( $args );
 
 	array_pop($wp_current_filter);
 }
@@ -838,14 +754,7 @@
  */
 function _wp_call_all_hook($args) {
 	global $wp_filter;
-
-	reset( $wp_filter['all'] );
-	do {
-		foreach( (array) current($wp_filter['all']) as $the_ )
-			if ( !is_null($the_['function']) )
-				call_user_func_array($the_['function'], $args);
-
-	} while ( next($wp_filter['all']) !== false );
+	$wp_filter['all']->do_all_hook($args);
 }
 
 /**
Index: tests/phpunit/includes/functions.php
===================================================================
--- tests/phpunit/includes/functions.php	(revision 29858)
+++ tests/phpunit/includes/functions.php	(working copy)
@@ -2,18 +2,14 @@
 
 // For adding hooks before loading WP
 function tests_add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
-	global $wp_filter, $merged_filters;
+	global $wp_filter;
 
 	$idx = _test_filter_build_unique_id($tag, $function_to_add, $priority);
 	$wp_filter[$tag][$priority][$idx] = array('function' => $function_to_add, 'accepted_args' => $accepted_args);
-	unset( $merged_filters[ $tag ] );
 	return true;
 }
 
 function _test_filter_build_unique_id($tag, $function, $priority) {
-	global $wp_filter;
-	static $filter_id_count = 0;
-
 	if ( is_string($function) )
 		return $function;
 
Index: tests/phpunit/includes/testcase.php
===================================================================
--- tests/phpunit/includes/testcase.php	(revision 29858)
+++ tests/phpunit/includes/testcase.php	(working copy)
@@ -79,7 +79,7 @@
 	 * @return void
 	 */
 	protected function _backup_hooks() {
-		$globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
+		$globals = array( 'wp_actions', 'wp_current_filter', 'wp_filter' );
 		foreach ( $globals as $key ) {
 			self::$hooks_saved[ $key ] = $GLOBALS[ $key ];
 		}
@@ -96,7 +96,7 @@
 	 * @return void
 	 */
 	protected function _restore_hooks() {
-		$globals = array( 'merged_filters', 'wp_actions', 'wp_current_filter', 'wp_filter' );
+		$globals = array( 'wp_actions', 'wp_current_filter', 'wp_filter' );
 		foreach ( $globals as $key ) {
 			if ( isset( self::$hooks_saved[ $key ] ) ) {
 				$GLOBALS[ $key ] = self::$hooks_saved[ $key ];
Index: tests/phpunit/tests/actions.php
===================================================================
--- tests/phpunit/tests/actions.php	(revision 29858)
+++ tests/phpunit/tests/actions.php	(working copy)
@@ -114,6 +114,42 @@
 		$this->assertEquals( array( $val1 ), array_pop( $argsvar2 ) );
 	}
 
+	/**
+	 * Test that multiple callbacks receive the correct number of args even when the number
+	 * is less than, or greater than previous hooks.
+	 */
+	function test_action_args_3() {
+		$a1 = new MockAction();
+		$a2 = new MockAction();
+		$a3 = new MockAction();
+		$tag = rand_str();
+		$val1 = rand_str();
+		$val2 = rand_str();
+
+		// a1 accepts two arguments, a2 doesn't, a3 accepts two arguments
+		add_action($tag, array(&$a1, 'action'), 10, 2);
+		add_action($tag, array(&$a2, 'action'));
+		add_action($tag, array(&$a3, 'action'), 10, 2);
+		// call the action with two arguments
+		do_action($tag, $val1, $val2);
+
+		$call_count = $a1->get_call_count();
+		// a1 should be called with both args
+		$this->assertEquals(1, $call_count);
+		$argsvar1 = $a1->get_args();
+		$this->assertEquals( array( $val1, $val2 ), array_pop( $argsvar1 ) );
+
+		// a2 should be called with one only
+		$this->assertEquals(1, $a2->get_call_count());
+		$argsvar2 = $a2->get_args();
+		$this->assertEquals( array( $val1 ), array_pop( $argsvar2 ) );
+
+		// a3 should be called with both args
+		$this->assertEquals(1, $a3->get_call_count());
+		$argsvar3 = $a3->get_args();
+		$this->assertEquals( array( $val1, $val2 ), array_pop( $argsvar3 ) );
+	}
+
 	function test_action_priority() {
 		$a = new MockAction();
 		$tag = rand_str();
@@ -258,6 +294,97 @@
 	}
 
 	/**
+	 * @ticket 17817
+	 */
+	function test_action_recursion() {
+		$tag = rand_str();
+		$a = new MockAction();
+		$b = new MockAction();
+
+		add_action( $tag, array($a, 'action'), 11, 1 );
+		add_action( $tag, array($b, 'action'), 13, 1 );
+		add_action( $tag, array($this, 'action_that_causes_recursion'), 12, 1 );
+		do_action( $tag, $tag );
+
+		$this->assertEquals( 2, $a->get_call_count(), 'recursive actions should call all callbacks with earlier priority' );
+		$this->assertEquals( 2, $b->get_call_count(), 'recursive actions should call callbacks with later priority' );
+	}
+
+	function action_that_causes_recursion( $tag ) {
+		static $recursing = FALSE;
+		if ( !$recursing ) {
+			$recursing = TRUE;
+			do_action( $tag, $tag );
+		}
+		$recursing = FALSE;
+	}
+
+	/**
+	 * @ticket 9968
+	 */
+	function test_action_callback_manipulation_while_running() {
+		$tag = rand_str();
+		$a = new MockAction();
+		$b = new MockAction();
+		$c = new MockAction();
+		$d = new MockAction();
+		$e = new MockAction();
+
+		add_action( $tag, array($a, 'action'), 11, 2 );
+		add_action( $tag, array($this, 'action_that_manipulates_a_running_hook'), 12, 2 );
+		add_action( $tag, array($b, 'action'), 12, 2 );
+
+		do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
+		do_action( $tag, $tag, array($a,$b,$c,$d,$e) );
+
+		$this->assertEquals( 2, $a->get_call_count(), 'callbacks should run unless otherwise instructed' );
+		$this->assertEquals( 1, $b->get_call_count(), 'callback removed by same priority callback should still get called' );
+		$this->assertEquals( 1, $c->get_call_count(), 'callback added by same priority callback should not get called' );
+		$this->assertEquals( 2, $d->get_call_count(), 'callback added by earlier priority callback should get called' );
+		$this->assertEquals( 1, $e->get_call_count(), 'callback added by later priority callback should not get called' );
+	}
+
+	function action_that_manipulates_a_running_hook( $tag, $mocks ) {
+		remove_action( $tag, array($mocks[1], 'action'), 12, 2 );
+		add_action( $tag, array($mocks[2], 'action' ), 12, 2 );
+		add_action( $tag, array($mocks[3], 'action' ), 13, 2 );
+		add_action( $tag, array($mocks[4], 'action' ), 10, 2 );
+	}
+
+	/**
+	 * @ticket 17817
+	 *
+	 * This specificaly addresses the concern raised at
+	 * https://core.trac.wordpress.org/ticket/17817#comment:52
+	 */
+	function test_remove_anonymous_callback() {
+		$tag = rand_str();
+		$a = new MockAction();
+		add_action( $tag, array( $a, 'action' ), 12, 1 );
+		$this->assertTrue( has_action( $tag ) );
+
+		$hook = $GLOBALS['wp_filter'][ $tag ];
+
+		// From http://wordpress.stackexchange.com/a/57088/6445
+		foreach ( $hook as $priority => $filter ) {
+			foreach ( $filter as $identifier => $function ) {
+				if ( is_array( $function)
+					&& is_a( $function['function'][0], 'MockAction' )
+					&& 'action' === $function['function'][1]
+				) {
+					remove_filter(
+						$tag,
+						array ( $function['function'][0], 'action' ),
+						$priority
+					);
+				}
+			}
+		}
+
+		$this->assertFalse( has_action( $tag ) );
+	}
+
+	/**
 	 * Make sure current_action() behaves as current_filter()
 	 *
 	 * @ticket 14994
Index: tests/phpunit/tests/filters.php
===================================================================
--- tests/phpunit/tests/filters.php	(revision 29858)
+++ tests/phpunit/tests/filters.php	(working copy)
@@ -293,25 +293,4 @@
 		remove_all_filters( $tag, 12 );
 		$this->assertFalse( has_filter( $tag ) );
 	}
-
-	/**
-	 * @ticket 29070
-	 */
-	 function test_has_filter_doesnt_reset_wp_filter() {
-	 	add_action( 'action_test_has_filter_doesnt_reset_wp_filter', '__return_null', 1 );
-	 	add_action( 'action_test_has_filter_doesnt_reset_wp_filter', '__return_null', 2 );
-	 	add_action( 'action_test_has_filter_doesnt_reset_wp_filter', '__return_null', 3 );
-	 	add_action( 'action_test_has_filter_doesnt_reset_wp_filter', array( $this, '_action_test_has_filter_doesnt_reset_wp_filter' ), 4 );
-
-	 	do_action( 'action_test_has_filter_doesnt_reset_wp_filter' );
-	 }
-	 function _action_test_has_filter_doesnt_reset_wp_filter() {
-	 	global $wp_filter;
-
-	 	has_action( 'action_test_has_filter_doesnt_reset_wp_filter', '_function_that_doesnt_exist' );
-
-		$filters = current( $wp_filter['action_test_has_filter_doesnt_reset_wp_filter'] );
-	 	$the_ = current( $filters );
-	 	$this->assertEquals( $the_['function'], array( $this, '_action_test_has_filter_doesnt_reset_wp_filter' ) );
-	 }
 }
