Completed
Push — fix/rate-limit-options-writing ( ff1775 )
by
unknown
133:03 queued 122:53
created

Jetpack_Sync_Module_Full_Sync   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 258
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 258
rs 8.3999
wmc 46
lcom 1
cbo 5

15 Methods

Rating   Name   Duplication   Size   Complexity  
A name() 0 3 1
A init_full_sync_listeners() 0 6 1
A init_before_send() 0 4 1
F start() 0 89 14
D update_sent_progress_action() 0 33 9
A is_started() 0 3 1
A is_finished() 0 3 1
B get_status() 0 34 6
A clear_status() 0 14 2
A reset_data() 0 6 1
A get_status_option() 0 12 3
A update_status_option() 0 4 1
A enable_queue_rate_limit() 0 8 1
A queue_item_added() 0 3 1
A queue_items_added() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like Jetpack_Sync_Module_Full_Sync often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_Sync_Module_Full_Sync, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This class does a full resync of the database by
5
 * enqueuing an outbound action for every single object
6
 * that we care about.
7
 *
8
 * This class, and its related class Jetpack_Sync_Module, contain a few non-obvious optimisations that should be explained:
9
 * - we fire an action called jetpack_full_sync_start so that WPCOM can erase the contents of the cached database
10
 * - for each object type, we page through the object IDs and enqueue them by firing some monitored actions
11
 * - we load the full objects for those IDs in chunks of Jetpack_Sync_Module::ARRAY_CHUNK_SIZE (to reduce the number of MySQL calls)
12
 * - we fire a trigger for the entire array which the Jetpack_Sync_Listener then serializes and queues.
13
 */
14
15
require_once 'class.jetpack-sync-wp-replicastore.php';
16
17
class Jetpack_Sync_Module_Full_Sync extends Jetpack_Sync_Module {
18
	const STATUS_OPTION_PREFIX = 'jetpack_sync_full_';
19
	const FULL_SYNC_TIMEOUT = 3600;
20
21
	private $items_added_since_last_pause;
22
	private $last_pause_time;
23
	private $queue_rate_limit;
24
25
	public function name() {
26
		return 'full-sync';
27
	}
28
29
	function init_full_sync_listeners( $callable ) {
30
		// synthetic actions for full sync
31
		add_action( 'jetpack_full_sync_start', $callable );
32
		add_action( 'jetpack_full_sync_end', $callable );
33
		add_action( 'jetpack_full_sync_cancelled', $callable );
34
	}
35
36
	function init_before_send() {
37
		// this is triggered after actions have been processed on the server
38
		add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) );
39
	}
40
41
	function start( $modules = null ) {
42
		$was_already_running = $this->is_started() && ! $this->is_finished();
43
44
		// remove all evidence of previous full sync items and status
45
		$this->reset_data();
46
47
		$this->enable_queue_rate_limit();
48
49
		if ( $was_already_running ) {
50
			/**
51
			 * Fires when a full sync is cancelled.
52
			 *
53
			 * @since 4.2.0
54
			 */
55
			do_action( 'jetpack_full_sync_cancelled' );
56
		}
57
58
		/**
59
		 * Fires when a full sync begins. This action is serialized
60
		 * and sent to the server so that it knows a full sync is coming.
61
		 *
62
		 * @since 4.2.0
63
		 */
64
		do_action( 'jetpack_full_sync_start', $modules );
65
		$this->update_status_option( 'started', time() );
66
67
		// configure modules
68
		if ( ! is_array( $modules ) ) {
69
			$modules = array();
70
		}
71
72
		// by default, all modules are fully enabled
73
		if ( count( $modules ) === 0 ) {
74
			$default_module_config = true;
75
		} else {
76
			$default_module_config = false;
77
		}
78
79
		// set default configuration, calculate totals, and save configuration if totals > 0
80
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
81
			$module_name = $module->name();
82
			if ( ! isset( $modules[ $module_name ] ) ) {
83
				$modules[ $module_name ] = $default_module_config;
84
			}
85
86
			// check if this module is enabled
87
			if ( ! ( $module_config = $modules[ $module_name ] ) ) {
88
				continue;
89
			}
90
91
			$total_items = $module->estimate_full_sync_actions( $module_config );
92
93
			if ( ! is_null( $total_items ) && $total_items > 0 ) {
94
				$this->update_status_option( "{$module_name}_total", $total_items );
95
				$this->update_status_option( "{$module_name}_config", $module_config );
96
			}
97
		}
98
99
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
100
			$module_name   = $module->name();
101
			$module_config = $modules[ $module_name ];
102
103
			// check if this module is enabled
104
			if ( ! $module_config ) {
105
				continue;
106
			}
107
108
			$items_enqueued = $module->enqueue_full_sync_actions( $module_config );
109
110
			if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) {
111
				$this->update_status_option( "{$module_name}_queued", $items_enqueued );
112
			}
113
		}
114
115
		$this->update_status_option( 'queue_finished', time() );
116
117
		$store = new Jetpack_Sync_WP_Replicastore();
118
119
		/**
120
		 * Fires when a full sync ends. This action is serialized
121
		 * and sent to the server with checksums so that we can confirm the
122
		 * sync was successful.
123
		 *
124
		 * @since 4.2.0
125
		 */
126
		do_action( 'jetpack_full_sync_end', $store->checksum_all() );
127
128
		return true;
129
	}
130
131
	function update_sent_progress_action( $actions ) {
132
133
		// quick way to map to first items with an array of arrays
134
		$actions_with_counts = array_count_values( array_map( 'reset', $actions ) );
135
136
		if ( ! $this->is_started() || $this->is_finished() ) {
137
			return;
138
		}
139
140
		if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) {
141
			$this->update_status_option( 'sent_started', time() );
142
		}
143
144
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
145
			$module_actions     = $module->get_full_sync_actions();
146
			$status_option_name = "{$module->name()}_sent";
147
			$items_sent         = $this->get_status_option( $status_option_name, 0 );
148
149
			foreach ( $module_actions as $module_action ) {
150
				if ( isset( $actions_with_counts[ $module_action ] ) ) {
151
					$items_sent += $actions_with_counts[ $module_action ];
152
				}
153
			}
154
155
			if ( $items_sent > 0 ) {
156
				$this->update_status_option( $status_option_name, $items_sent );
157
			}	
158
		}
159
160
		if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) {
161
			$this->update_status_option( 'finished', time() );
162
		}
163
	}
164
165
	public function is_started() {
166
		return !! $this->get_status_option( 'started' );
167
	}
168
169
	public function is_finished() {
170
		return !! $this->get_status_option( 'finished' );
171
	}
172
173
	public function get_status() {
174
		$status = array(
175
			'started'        => $this->get_status_option( 'started' ),
176
			'queue_finished' => $this->get_status_option( 'queue_finished' ),
177
			'sent_started'   => $this->get_status_option( 'sent_started' ),
178
			'finished'       => $this->get_status_option( 'finished' ),
179
			'sent'           => array(),
180
			'queue'          => array(),
181
			'config'         => array(),
182
			'total'          => array(),
183
		);
184
185
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
186
			$name = $module->name();
187
188
			if ( $total = $this->get_status_option( "{$name}_total" ) ) {
189
				$status[ 'total' ][ $name ] = $total;
190
			}
191
192
			if ( $queued = $this->get_status_option( "{$name}_queued" ) ) {
193
				$status[ 'queue' ][ $name ] = $queued;
194
			}
195
			
196
			if ( $sent = $this->get_status_option( "{$name}_sent" ) ) {
197
				$status[ 'sent' ][ $name ] = $sent;
198
			}
199
200
			if ( $config = $this->get_status_option( "{$name}_config" ) ) {
201
				$status[ 'config' ][ $name ] = $config;
202
			}
203
		}
204
205
		return $status;
206
	}
207
208
	public function clear_status() {
209
		$prefix = self::STATUS_OPTION_PREFIX;
210
		delete_option( "{$prefix}_started" );
211
		delete_option( "{$prefix}_queue_finished" );
212
		delete_option( "{$prefix}_sent_started" );
213
		delete_option( "{$prefix}_finished" );
214
215
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
216
			delete_option( "{$prefix}_{$module->name()}_total" );
217
			delete_option( "{$prefix}_{$module->name()}_queued" );
218
			delete_option( "{$prefix}_{$module->name()}_sent" );
219
			delete_option( "{$prefix}_{$module->name()}_config" );
220
		}
221
	}
222
223
	public function reset_data() {
224
		$this->clear_status();
225
		require_once dirname( __FILE__ ) . '/class.jetpack-sync-listener.php';
226
		$listener = Jetpack_Sync_Listener::get_instance();
227
		$listener->get_full_sync_queue()->reset();
228
	}
229
230
	private function get_status_option( $option, $default = null ) {
231
		$prefix = self::STATUS_OPTION_PREFIX;
232
233
		$value = get_option( "{$prefix}_{$option}", $default );
234
		
235
		if ( ! $value ) {
236
			// don't cast to int if we didn't find a value - we want to preserve null or false as sentinals
237
			return $default;
238
		}
239
240
		return is_numeric( $value ) ? intval( $value ) : $value;
241
	}
242
243
	private function update_status_option( $name, $value ) {
244
		$prefix = self::STATUS_OPTION_PREFIX;
245
		update_option( "{$prefix}_{$name}", $value, false );
246
	}
247
248
	private function enable_queue_rate_limit() {
249
		$this->queue_rate_limit = Jetpack_Sync_Settings::get_setting( 'queue_max_writes_sec' );
250
		$this->items_added_since_last_pause = 0;
251
		$this->last_pause_time = microtime( true );
252
253
		add_action( 'jpsq_item_added', array( $this, 'queue_item_added' ) );
254
		add_action( 'jpsq_items_added', array( $this, 'queue_items_added' ) );
255
	}
256
257
	public function queue_item_added() {
258
		$this->queue_items_added( 1 );
259
	}
260
261
	public function queue_items_added( $item_count ) {
262
		$this->items_added_since_last_pause += $item_count;
263
264
		if ( $this->items_added_since_last_pause > $this->queue_rate_limit ) {
265
			// sleep for the rest of the second
266
			$sleep_til = $this->last_pause_time + 1.0;
267
			$sleep_duration = microtime( true ) - $sleep_til;
268
			if ( $sleep_duration > 0.0 ) {
269
				error_log("sleeping for $sleep_duration");
270
				sleep( $sleep_duration );
271
			}
272
		}
273
	}
274
}
275