PageRenderTime 48ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/jetpack/sync/class.jetpack-sync-queue.php

https://gitlab.com/vanafroo/landingpage
PHP | 459 lines | 334 code | 92 blank | 33 comment | 35 complexity | 5c1d76b2ec09ea89bdf04ee4174449eb MD5 | raw file
  1. <?php
  2. /**
  3. * A buffer of items from the queue that can be checked out
  4. */
  5. class Jetpack_Sync_Queue_Buffer {
  6. public $id;
  7. public $items_with_ids;
  8. public function __construct( $id, $items_with_ids ) {
  9. $this->id = $id;
  10. $this->items_with_ids = $items_with_ids;
  11. }
  12. public function get_items() {
  13. return array_combine( $this->get_item_ids(), $this->get_item_values() );
  14. }
  15. public function get_item_values() {
  16. return Jetpack_Sync_Utils::get_item_values( $this->items_with_ids );
  17. }
  18. public function get_item_ids() {
  19. return Jetpack_Sync_Utils::get_item_ids( $this->items_with_ids );
  20. }
  21. }
  22. /**
  23. * A persistent queue that can be flushed in increments of N items,
  24. * and which blocks reads until checked-out buffers are checked in or
  25. * closed. This uses raw SQL for two reasons: speed, and not triggering
  26. * tons of added_option callbacks.
  27. */
  28. class Jetpack_Sync_Queue {
  29. public $id;
  30. private $row_iterator;
  31. function __construct( $id ) {
  32. $this->id = str_replace( '-', '_', $id ); // necessary to ensure we don't have ID collisions in the SQL
  33. $this->row_iterator = 0;
  34. $this->random_int = mt_rand( 1, 1000000 );
  35. }
  36. function add( $item ) {
  37. global $wpdb;
  38. $added = false;
  39. // this basically tries to add the option until enough time has elapsed that
  40. // it has a unique (microtime-based) option key
  41. while ( ! $added ) {
  42. $rows_added = $wpdb->query( $wpdb->prepare(
  43. "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES (%s, %s,%s)",
  44. $this->get_next_data_row_option_name(),
  45. serialize( $item ),
  46. 'no'
  47. ) );
  48. $added = ( 0 !== $rows_added );
  49. }
  50. do_action( 'jpsq_item_added' );
  51. }
  52. // Attempts to insert all the items in a single SQL query. May be subject to query size limits!
  53. function add_all( $items ) {
  54. global $wpdb;
  55. $base_option_name = $this->get_next_data_row_option_name();
  56. $query = "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES ";
  57. $rows = array();
  58. for ( $i = 0; $i < count( $items ); $i += 1 ) {
  59. $option_name = esc_sql( $base_option_name . '-' . $i );
  60. $option_value = esc_sql( serialize( $items[ $i ] ) );
  61. $rows[] = "('$option_name', '$option_value', 'no')";
  62. }
  63. $rows_added = $wpdb->query( $query . join( ',', $rows ) );
  64. if ( count( $items ) === $rows_added ) {
  65. return new WP_Error( 'row_count_mismatch', "The number of rows inserted didn't match the size of the input array" );
  66. }
  67. do_action( 'jpsq_items_added', $rows_added );
  68. }
  69. // Peek at the front-most item on the queue without checking it out
  70. function peek( $count = 1 ) {
  71. $items = $this->fetch_items( $count );
  72. if ( $items ) {
  73. return Jetpack_Sync_Utils::get_item_values( $items );
  74. }
  75. return array();
  76. }
  77. // lag is the difference in time between the age of the oldest item
  78. // (aka first or frontmost item) and the current time
  79. function lag() {
  80. global $wpdb;
  81. $first_item_name = $wpdb->get_var( $wpdb->prepare(
  82. "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT 1",
  83. "jpsq_{$this->id}-%"
  84. ) );
  85. if ( ! $first_item_name ) {
  86. return 0;
  87. }
  88. // break apart the item name to get the timestamp
  89. $matches = null;
  90. if ( preg_match( '/^jpsq_' . $this->id . '-(\d+\.\d+)-/', $first_item_name, $matches ) ) {
  91. return microtime( true ) - floatval( $matches[1] );
  92. } else {
  93. return 0;
  94. }
  95. }
  96. function reset() {
  97. global $wpdb;
  98. $this->delete_checkout_id();
  99. $wpdb->query( $wpdb->prepare(
  100. "DELETE FROM $wpdb->options WHERE option_name LIKE %s", "jpsq_{$this->id}-%"
  101. ) );
  102. }
  103. function size() {
  104. global $wpdb;
  105. return (int) $wpdb->get_var( $wpdb->prepare(
  106. "SELECT count(*) FROM $wpdb->options WHERE option_name LIKE %s", "jpsq_{$this->id}-%"
  107. ) );
  108. }
  109. // we use this peculiar implementation because it's much faster than count(*)
  110. function has_any_items() {
  111. global $wpdb;
  112. $value = $wpdb->get_var( $wpdb->prepare(
  113. "SELECT exists( SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s )", "jpsq_{$this->id}-%"
  114. ) );
  115. return ( $value === '1' );
  116. }
  117. function checkout( $buffer_size ) {
  118. if ( $this->get_checkout_id() ) {
  119. return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
  120. }
  121. $buffer_id = uniqid();
  122. $result = $this->set_checkout_id( $buffer_id );
  123. if ( ! $result || is_wp_error( $result ) ) {
  124. return $result;
  125. }
  126. $items = $this->fetch_items( $buffer_size );
  127. if ( count( $items ) === 0 ) {
  128. return false;
  129. }
  130. $buffer = new Jetpack_Sync_Queue_Buffer( $buffer_id, array_slice( $items, 0, $buffer_size ) );
  131. return $buffer;
  132. }
  133. // this checks out rows until it either empties the queue or hits a certain memory limit
  134. // it loads the sizes from the DB first so that it doesn't accidentally
  135. // load more data into memory than it needs to.
  136. // The only way it will load more items than $max_size is if a single queue item
  137. // exceeds the memory limit, but in that case it will send that item by itself.
  138. function checkout_with_memory_limit( $max_memory, $max_buffer_size = 500 ) {
  139. if ( $this->get_checkout_id() ) {
  140. return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
  141. }
  142. $buffer_id = uniqid();
  143. $result = $this->set_checkout_id( $buffer_id );
  144. if ( ! $result || is_wp_error( $result ) ) {
  145. return $result;
  146. }
  147. // get the map of buffer_id -> memory_size
  148. global $wpdb;
  149. $items_with_size = $wpdb->get_results(
  150. $wpdb->prepare(
  151. "SELECT option_name AS id, LENGTH(option_value) AS value_size FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d",
  152. "jpsq_{$this->id}-%",
  153. $max_buffer_size
  154. ),
  155. OBJECT
  156. );
  157. if ( count( $items_with_size ) === 0 ) {
  158. return false;
  159. }
  160. $total_memory = 0;
  161. $min_item_id = $max_item_id = $items_with_size[0]->id;
  162. foreach ( $items_with_size as $id => $item_with_size ) {
  163. $total_memory += $item_with_size->value_size;
  164. // if this is the first item and it exceeds memory, allow loop to continue
  165. // we will exit on the next iteration instead
  166. if ( $total_memory > $max_memory && $id > 0 ) {
  167. break;
  168. }
  169. $max_item_id = $item_with_size->id;
  170. }
  171. $query = $wpdb->prepare(
  172. "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name >= %s and option_name <= %s ORDER BY option_name ASC",
  173. $min_item_id,
  174. $max_item_id
  175. );
  176. $items = $wpdb->get_results( $query, OBJECT );
  177. foreach ( $items as $item ) {
  178. $item->value = maybe_unserialize( $item->value );
  179. }
  180. if ( count( $items ) === 0 ) {
  181. $this->delete_checkout_id();
  182. return false;
  183. }
  184. $buffer = new Jetpack_Sync_Queue_Buffer( $buffer_id, $items );
  185. return $buffer;
  186. }
  187. function checkin( $buffer ) {
  188. $is_valid = $this->validate_checkout( $buffer );
  189. if ( is_wp_error( $is_valid ) ) {
  190. return $is_valid;
  191. }
  192. $this->delete_checkout_id();
  193. return true;
  194. }
  195. function close( $buffer, $ids_to_remove = null ) {
  196. $is_valid = $this->validate_checkout( $buffer );
  197. if ( is_wp_error( $is_valid ) ) {
  198. return $is_valid;
  199. }
  200. $this->delete_checkout_id();
  201. // by default clear all items in the buffer
  202. if ( is_null( $ids_to_remove ) ) {
  203. $ids_to_remove = $buffer->get_item_ids();
  204. }
  205. global $wpdb;
  206. if ( count( $ids_to_remove ) > 0 ) {
  207. $sql = "DELETE FROM $wpdb->options WHERE option_name IN (" . implode( ', ', array_fill( 0, count( $ids_to_remove ), '%s' ) ) . ')';
  208. $query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $ids_to_remove ) );
  209. $wpdb->query( $query );
  210. }
  211. return true;
  212. }
  213. function flush_all() {
  214. $items = Jetpack_Sync_Utils::get_item_values( $this->fetch_items() );
  215. $this->reset();
  216. return $items;
  217. }
  218. function get_all() {
  219. return $this->fetch_items();
  220. }
  221. // use with caution, this could allow multiple processes to delete
  222. // and send from the queue at the same time
  223. function force_checkin() {
  224. $this->delete_checkout_id();
  225. }
  226. // used to lock checkouts from the queue.
  227. // tries to wait up to $timeout seconds for the queue to be empty
  228. function lock( $timeout = 30 ) {
  229. $tries = 0;
  230. while ( $this->has_any_items() && $tries < $timeout ) {
  231. sleep( 1 );
  232. $tries += 1;
  233. }
  234. if ( $tries === 30 ) {
  235. return new WP_Error( 'lock_timeout', 'Timeout waiting for sync queue to empty' );
  236. }
  237. if ( $this->get_checkout_id() ) {
  238. return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
  239. }
  240. // hopefully this means we can acquire a checkout?
  241. $result = $this->set_checkout_id( 'lock' );
  242. if ( ! $result || is_wp_error( $result ) ) {
  243. return $result;
  244. }
  245. return true;
  246. }
  247. function unlock() {
  248. return $this->delete_checkout_id();
  249. }
  250. private function get_checkout_id() {
  251. global $wpdb;
  252. $checkout_value = $wpdb->get_var(
  253. $wpdb->prepare(
  254. "SELECT option_value FROM $wpdb->options WHERE option_name = %s",
  255. $this->get_lock_option_name()
  256. )
  257. );
  258. if ( $checkout_value ) {
  259. list( $checkout_id, $timestamp ) = explode( ':', $checkout_value );
  260. if ( intval( $timestamp ) > time() ) {
  261. return $checkout_id;
  262. }
  263. }
  264. return false;
  265. }
  266. private function set_checkout_id( $checkout_id ) {
  267. global $wpdb;
  268. $expires = time() + Jetpack_Sync_Defaults::$default_sync_queue_lock_timeout;
  269. $updated_num = $wpdb->query(
  270. $wpdb->prepare(
  271. "UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
  272. "$checkout_id:$expires",
  273. $this->get_lock_option_name()
  274. )
  275. );
  276. if ( ! $updated_num ) {
  277. $updated_num = $wpdb->query(
  278. $wpdb->prepare(
  279. "INSERT INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )",
  280. $this->get_lock_option_name(),
  281. "$checkout_id:$expires"
  282. )
  283. );
  284. }
  285. return $updated_num;
  286. }
  287. private function delete_checkout_id() {
  288. global $wpdb;
  289. // rather than delete, which causes fragmentation, we update in place
  290. return $wpdb->query(
  291. $wpdb->prepare(
  292. "UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
  293. "0:0",
  294. $this->get_lock_option_name()
  295. )
  296. );
  297. }
  298. private function get_lock_option_name() {
  299. return "jpsq_{$this->id}_checkout";
  300. }
  301. private function get_next_data_row_option_name() {
  302. // this option is specifically chosen to, as much as possible, preserve time order
  303. // and minimise the possibility of collisions between multiple processes working
  304. // at the same time
  305. // TODO: confirm we only need to support PHP 5.05+ (otherwise we'll need to emulate microtime as float, and avoid PHP_INT_MAX)
  306. // @see: http://php.net/manual/en/function.microtime.php
  307. $timestamp = sprintf( '%.6f', microtime( true ) );
  308. // row iterator is used to avoid collisions where we're writing data waaay fast in a single process
  309. if ( $this->row_iterator === PHP_INT_MAX ) {
  310. $this->row_iterator = 0;
  311. } else {
  312. $this->row_iterator += 1;
  313. }
  314. return 'jpsq_' . $this->id . '-' . $timestamp . '-' . $this->random_int . '-' . $this->row_iterator;
  315. }
  316. private function fetch_items( $limit = null ) {
  317. global $wpdb;
  318. if ( $limit ) {
  319. $query_sql = $wpdb->prepare( "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d", "jpsq_{$this->id}-%", $limit );
  320. } else {
  321. $query_sql = $wpdb->prepare( "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC", "jpsq_{$this->id}-%" );
  322. }
  323. $items = $wpdb->get_results( $query_sql, OBJECT );
  324. foreach ( $items as $item ) {
  325. $item->value = maybe_unserialize( $item->value );
  326. }
  327. return $items;
  328. }
  329. private function validate_checkout( $buffer ) {
  330. if ( ! $buffer instanceof Jetpack_Sync_Queue_Buffer ) {
  331. return new WP_Error( 'not_a_buffer', 'You must checkin an instance of Jetpack_Sync_Queue_Buffer' );
  332. }
  333. $checkout_id = $this->get_checkout_id();
  334. if ( ! $checkout_id ) {
  335. return new WP_Error( 'buffer_not_checked_out', 'There are no checked out buffers' );
  336. }
  337. if ( $checkout_id != $buffer->id ) {
  338. return new WP_Error( 'buffer_mismatch', 'The buffer you checked in was not checked out' );
  339. }
  340. return true;
  341. }
  342. }
  343. class Jetpack_Sync_Utils {
  344. static function get_item_values( $items ) {
  345. return array_map( array( __CLASS__, 'get_item_value' ), $items );
  346. }
  347. static function get_item_ids( $items ) {
  348. return array_map( array( __CLASS__, 'get_item_id' ), $items );
  349. }
  350. static private function get_item_value( $item ) {
  351. return $item->value;
  352. }
  353. static private function get_item_id( $item ) {
  354. return $item->id;
  355. }
  356. }