/no-stampede-actions.php

https://github.com/voceconnect/no-stampede-actions · PHP · 204 lines · 126 code · 32 blank · 46 comment · 20 complexity · 2a266a2183b8e18af450799bc8d2b931 MD5 · raw file

  1. <?php
  2. /**
  3. * A WordPress api to (try) kick off globally singleton actions. It will lock the action
  4. * to hopefully prevent other requests from kicking off the same action. This is highly
  5. * based off of Mark Jaquith's NSA_Action_Update_Server @author markjaquith (https://gist.github.com/1149945)
  6. *
  7. */
  8. if( class_exists( 'No_Stampede_Action_Server' ) )
  9. return;
  10. class No_Stampede_Action_Server {
  11. public function __construct() {
  12. add_action( 'init', array( $this, 'init' ) );
  13. }
  14. public function init() {
  15. if( isset( $_POST[ '_nsa_action' ] ) ) {
  16. define( 'DOING_BACKGROUND_ACTION', true );
  17. $action = get_transient( 'nsa_action' . $_POST[ 'key' ] );
  18. if( $action && $action[ 0 ] == $_POST[ '_nsa_action' ] ) {
  19. nsa_action( $action[ 1 ] )
  20. ->action_callback( $action[ 2 ], ( array ) $action[ 3 ] )
  21. ->set_lock( $action[ 0 ] )
  22. ->fire_action();
  23. }
  24. exit();
  25. }
  26. }
  27. }
  28. new No_Stampede_Action_Server();
  29. class NSA_Action {
  30. /**
  31. * Transient key to use
  32. * @var string
  33. */
  34. public $key;
  35. /**
  36. * Unique key to identify that this instance holds the lock
  37. * @var string
  38. */
  39. private $lock_key;
  40. /**
  41. * Callback used to complete the action
  42. * @var callback
  43. */
  44. private $callback;
  45. /**
  46. * Parameters passed into
  47. * @var array
  48. */
  49. private $params;
  50. /**
  51. * Time in seconds that to wait before ever running the action again
  52. * Set to 0 by default, which makes the action only run once
  53. * @var int
  54. */
  55. private $time_til_next_run = 0;
  56. /**
  57. * Whether to go ahead do the action now or later
  58. * @var bool
  59. */
  60. private $force_background_actions = true;
  61. /**
  62. * Maximum number of times to try to wait on cache to be filled bye the
  63. * lock owner before doing the callback on it's own
  64. * @var int
  65. */
  66. private $max_tries = 5;
  67. /**
  68. * Number of microseconds to wait per try when waiting on the lock owner
  69. * @var int
  70. */
  71. private $sleep_time = 300000; //.3 seconds
  72. public function __construct( $key ) {
  73. $this->key = $key;
  74. }
  75. public function do_action() {
  76. if( $this->force_background_actions ) {
  77. $this->schedule_background_action();
  78. return false;
  79. } else {
  80. return $this->fire_action();
  81. }
  82. }
  83. private function schedule_background_action() {
  84. if( $this->get_action_lock() ) {
  85. add_action( 'shutdown', array( $this, 'spawn_server' ) );
  86. }
  87. return $this;
  88. }
  89. public function spawn_server() {
  90. $server_url = home_url( '/?nsa_actions_request' );
  91. wp_remote_post( $server_url, array(
  92. 'body' => array(
  93. '_nsa_action' => $this->lock_key,
  94. 'key' => $this->key
  95. ),
  96. 'timeout' => 0.01,
  97. 'blocking' => false,
  98. 'sslverify' => apply_filters( 'https_local_ssl_verify', true )
  99. ) );
  100. }
  101. public function fire_action() {
  102. // If you don't supply a callback, we can't update it for you!
  103. if( empty( $this->callback ) )
  104. return false;
  105. if( !$this->get_action_lock() ) {
  106. if(!(defined('DOING_BACKGROUND_ACTION') && DOING_BACKGROUND_ACTION)) {
  107. while($this->max_tries > 0 && !$this->action_has_completed()) {
  108. $this->max_tries--;
  109. usleep($this->sleep_time);
  110. }
  111. }
  112. if( $this->action_has_completed() )
  113. return false;
  114. }
  115. call_user_func_array( $this->callback, $this->params );
  116. //set the action lock to expire in time_til_next_run seconds
  117. //time_til_next_run should be 0 to make this action only happen once
  118. set_transient($this->get_lock_name(), 'completed', $this->time_til_next_run);
  119. return true;
  120. }
  121. private function action_has_completed() {
  122. return 'completed' == get_transient($this->get_lock_name());
  123. }
  124. private function get_action_lock() {
  125. //check if action is already locked or the lock is completed
  126. if($this->action_has_lock() && !$this->action_has_completed()) {
  127. if( $this->is_lock_owner() )
  128. return true; //already own it
  129. return false; //someone else owns it
  130. }
  131. //set it for this instance
  132. $this->lock_key = md5( uniqid( microtime() . mt_rand(), true ) );
  133. set_transient($this->get_lock_name(), $this->lock_key);
  134. return true;
  135. }
  136. private function action_has_lock() {
  137. return (bool) get_transient($this->get_lock_name());
  138. }
  139. private function is_lock_owner() {
  140. return $this->lock_key == get_transient($this->get_lock_name());
  141. }
  142. private function get_lock_name() {
  143. return 'nsa_action_' . $this->key;
  144. }
  145. public function set_time_til_next_run( $seconds ) {
  146. $this->time_til_next_run = ( int ) $seconds;
  147. return $this;
  148. }
  149. public function set_lock( $lock ) {
  150. $this->lock_key = $lock;
  151. return $this;
  152. }
  153. public function background_only($val = true) {
  154. $this->force_background_actions = (bool) $val;
  155. return $this;
  156. }
  157. public function action_callback( $callback, $params = array( ) ) {
  158. $this->callback = $callback;
  159. if( is_array( $params ) )
  160. $this->params = $params;
  161. return $this;
  162. }
  163. public function set_max_tries( $max_tries ){
  164. $this->max_tries = (int) $max_tries;
  165. return $this;
  166. }
  167. }
  168. // API so you don't have to use "new"
  169. function nsa_action( $key ) {
  170. $transient = new NSA_Action( $key );
  171. return $transient;
  172. }