Completed
Push — fix/concurrency-race ( 35614e...7b1800 )
by
unknown
90:55 queued 81:30
created

Full_Sync_Immediately::start()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 12
nop 1
dl 0
loc 52
rs 8.1138
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Full sync module.
4
 *
5
 * @package automattic/jetpack-sync
6
 */
7
8
namespace Automattic\Jetpack\Sync\Modules;
9
10
use Automattic\Jetpack\Sync\Actions;
11
use Automattic\Jetpack\Sync\Defaults;
12
use Automattic\Jetpack\Sync\Lock;
13
use Automattic\Jetpack\Sync\Modules;
14
use Automattic\Jetpack\Sync\Settings;
15
16
/**
17
 * This class does a full resync of the database by
18
 * sending an outbound action for every single object
19
 * that we care about.
20
 */
21
class Full_Sync_Immediately extends Module {
22
	/**
23
	 * Prefix of the full sync status option name.
24
	 *
25
	 * @var string
26
	 */
27
	const STATUS_OPTION = 'jetpack_sync_full_status';
28
29
	/**
30
	 * Sync Lock name.
31
	 *
32
	 * @var string
33
	 */
34
	const LOCK_NAME = 'full_sync';
35
36
	/**
37
	 * Sync module name.
38
	 *
39
	 * @access public
40
	 *
41
	 * @return string
42
	 */
43
	public function name() {
44
		return 'full-sync';
45
	}
46
47
	/**
48
	 * Initialize action listeners for full sync.
49
	 *
50
	 * @access public
51
	 *
52
	 * @param callable $callable Action handler callable.
53
	 */
54
	public function init_full_sync_listeners( $callable ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
55
	}
56
57
	/**
58
	 * Start a full sync.
59
	 *
60
	 * @access public
61
	 *
62
	 * @param array $full_sync_config Full sync configuration.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $full_sync_config not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
63
	 *
64
	 * @return bool Always returns true at success.
65
	 */
66
	public function start( $full_sync_config = null ) {
67
		// There was a full sync in progress.
68
		if ( $this->is_started() && ! $this->is_finished() ) {
69
			/**
70
			 * Fires when a full sync is cancelled.
71
			 *
72
			 * @since 4.2.0
73
			 */
74
			do_action( 'jetpack_full_sync_cancelled' );
75
			$this->send_action( 'jetpack_full_sync_cancelled' );
76
		}
77
78
		// Remove all evidence of previous full sync items and status.
79
		$this->reset_data();
80
81
		if ( ! is_array( $full_sync_config ) ) {
82
			$full_sync_config = Defaults::$default_full_sync_config;
83
			if ( is_multisite() ) {
84
				$full_sync_config['network_options'] = 1;
85
			}
86
		}
87
88
		if ( isset( $full_sync_config['users'] ) && 'initial' === $full_sync_config['users'] ) {
89
			$full_sync_config['users'] = Modules::get_module( 'users' )->get_initial_sync_user_config();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Automattic\Jetpack\Sync\Modules\Module as the method get_initial_sync_user_config() does only exist in the following sub-classes of Automattic\Jetpack\Sync\Modules\Module: Automattic\Jetpack\Sync\Modules\Users. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
90
		}
91
92
		$this->update_status(
93
			array(
94
				'started'  => time(),
95
				'config'   => $full_sync_config,
96
				'progress' => $this->get_initial_progress( $full_sync_config ),
97
			)
98
		);
99
100
		$range = $this->get_content_range( $full_sync_config );
0 ignored issues
show
Unused Code introduced by
The call to Full_Sync_Immediately::get_content_range() has too many arguments starting with $full_sync_config.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
101
		/**
102
		 * Fires when a full sync begins. This action is serialized
103
		 * and sent to the server so that it knows a full sync is coming.
104
		 *
105
		 * @param array $full_sync_config Sync configuration for all sync modules.
106
		 * @param array $range Range of the sync items, containing min and max IDs for some item types.
107
		 * @param array $empty The modules with no items to sync during a full sync.
108
		 *
109
		 * @since 4.2.0
110
		 * @since 7.3.0 Added $range arg.
111
		 * @since 7.4.0 Added $empty arg.
112
		 */
113
		do_action( 'jetpack_full_sync_start', $full_sync_config, $range );
114
		$this->send_action( 'jetpack_full_sync_start', array( $full_sync_config, $range ) );
115
116
		return true;
117
	}
118
119
	/**
120
	 * Whether full sync has started.
121
	 *
122
	 * @access public
123
	 *
124
	 * @return boolean
125
	 */
126
	public function is_started() {
127
		return (bool) $this->get_status()['started'];
128
	}
129
130
	/**
131
	 * Retrieve the status of the current full sync.
132
	 *
133
	 * @access public
134
	 *
135
	 * @return array Full sync status.
136
	 */
137
	public function get_status() {
138
		$default = array(
139
			'started'  => false,
140
			'finished' => false,
141
			'progress' => array(),
142
			'config'   => array(),
143
		);
144
145
		return wp_parse_args( \Jetpack_Options::get_raw_option( self::STATUS_OPTION ), $default );
0 ignored issues
show
Documentation introduced by
$default is of type array<string,false|array...ray","config":"array"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
146
	}
147
148
	/**
149
	 * Returns the progress percentage of a full sync.
150
	 *
151
	 * @access public
152
	 *
153
	 * @return int|null
154
	 */
155
	public function get_sync_progress_percentage() {
156
		if ( ! $this->is_started() || $this->is_finished() ) {
157
			return null;
158
		}
159
		$status = $this->get_status();
160
		if ( empty( $status['progress'] ) ) {
161
			return null;
162
		}
163
		$total_items = array_reduce(
164
			array_values( $status['progress'] ),
165
			function ( $sum, $sync_item ) {
166
				return isset( $sync_item['total'] ) ? ( $sum + (int) $sync_item['total'] ) : $sum;
167
			},
168
			0
169
		);
170
		$total_sent  = array_reduce(
171
			array_values( $status['progress'] ),
172
			function ( $sum, $sync_item ) {
173
				return isset( $sync_item['sent'] ) ? ( $sum + (int) $sync_item['sent'] ) : $sum;
174
			},
175
			0
176
		);
177
		return floor( ( $total_sent / $total_items ) * 100 );
178
	}
179
180
	/**
181
	 * Whether full sync has finished.
182
	 *
183
	 * @access public
184
	 *
185
	 * @return boolean
186
	 */
187
	public function is_finished() {
188
		return (bool) $this->get_status()['finished'];
189
	}
190
191
	/**
192
	 * Clear all the full sync data.
193
	 *
194
	 * @access public
195
	 */
196
	public function reset_data() {
197
		$this->clear_status();
198
		( new Lock() )->remove( self::LOCK_NAME, true );
199
	}
200
201
	/**
202
	 * Clear all the full sync status options.
203
	 *
204
	 * @access public
205
	 */
206
	public function clear_status() {
207
		\Jetpack_Options::delete_raw_option( self::STATUS_OPTION );
208
	}
209
210
	/**
211
	 * Updates the status of the current full sync.
212
	 *
213
	 * @access public
214
	 *
215
	 * @param array $values New values to set.
216
	 *
217
	 * @return bool True if success.
218
	 */
219
	public function update_status( $values ) {
220
		return $this->set_status( wp_parse_args( $values, $this->get_status() ) );
0 ignored issues
show
Documentation introduced by
$this->get_status() is of type array|null, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like wp_parse_args($values, $this->get_status()) targeting wp_parse_args() can also be of type null; however, Automattic\Jetpack\Sync\...mediately::set_status() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
221
	}
222
223
	/**
224
	 * Retrieve the status of the current full sync.
225
	 *
226
	 * @param array $values New values to set.
227
	 *
228
	 * @access public
229
	 *
230
	 * @return boolean Full sync status.
231
	 */
232
	public function set_status( $values ) {
233
		return \Jetpack_Options::update_raw_option( self::STATUS_OPTION, $values );
234
	}
235
236
	/**
237
	 * Given an initial Full Sync configuration get the initial status.
238
	 *
239
	 * @param array $full_sync_config Full sync configuration.
240
	 *
241
	 * @return array Initial Sent status.
242
	 */
243
	public function get_initial_progress( $full_sync_config ) {
244
		// Set default configuration, calculate totals, and save configuration if totals > 0.
245
		$status = array();
246
		foreach ( $full_sync_config as $name => $config ) {
247
			$module          = Modules::get_module( $name );
248
			$status[ $name ] = array(
249
				'total'    => $module->total( $config ),
250
				'sent'     => 0,
251
				'finished' => false,
252
			);
253
		}
254
255
		return $status;
256
	}
257
258
	/**
259
	 * Get the range for content (posts and comments) to sync.
260
	 *
261
	 * @access private
262
	 *
263
	 * @return array Array of range (min ID, max ID, total items) for all content types.
264
	 */
265 View Code Duplication
	private function get_content_range() {
266
		$range  = array();
267
		$config = $this->get_status()['config'];
268
		// Add range only when syncing all objects.
269
		if ( true === isset( $config['posts'] ) && $config['posts'] ) {
270
			$range['posts'] = $this->get_range( 'posts' );
271
		}
272
273
		if ( true === isset( $config['comments'] ) && $config['comments'] ) {
274
			$range['comments'] = $this->get_range( 'comments' );
275
		}
276
277
		return $range;
278
	}
279
280
	/**
281
	 * Get the range (min ID, max ID and total items) of items to sync.
282
	 *
283
	 * @access public
284
	 *
285
	 * @param string $type Type of sync item to get the range for.
286
	 *
287
	 * @return array Array of min ID, max ID and total items in the range.
288
	 */
289 View Code Duplication
	public function get_range( $type ) {
290
		global $wpdb;
291
		if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) {
292
			return array();
293
		}
294
295
		switch ( $type ) {
296
			case 'posts':
297
				$table     = $wpdb->posts;
298
				$id        = 'ID';
299
				$where_sql = Settings::get_blacklisted_post_types_sql();
300
301
				break;
302
			case 'comments':
303
				$table     = $wpdb->comments;
304
				$id        = 'comment_ID';
305
				$where_sql = Settings::get_comments_filter_sql();
306
				break;
307
		}
308
309
		// TODO: Call $wpdb->prepare on the following query.
310
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
311
		$results = $wpdb->get_results( "SELECT MAX({$id}) as max, MIN({$id}) as min, COUNT({$id}) as count FROM {$table} WHERE {$where_sql}" );
0 ignored issues
show
Bug introduced by
The variable $id does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $table does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $where_sql does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
312
		if ( isset( $results[0] ) ) {
313
			return $results[0];
314
		}
315
316
		return array();
317
	}
318
319
	/**
320
	 * Continue sending instead of enqueueing.
321
	 *
322
	 * @access public
323
	 */
324
	public function continue_enqueuing() {
325
		$this->continue_sending();
326
	}
327
328
	/**
329
	 * Continue sending.
330
	 *
331
	 * @access public
332
	 */
333
	public function continue_sending() {
334
		// Return early if Full Sync is not running.
335
		if ( ! $this->is_started() || $this->get_status()['finished'] ) {
336
			return;
337
		}
338
339
		// Return early if we've gotten a retry-after header response.
340
		$retry_time = get_option( Actions::RETRY_AFTER_PREFIX . 'immediate-send' );
341
		if ( $retry_time ) {
342
			// If expired delete but don't send. Send will occurr in new request to avoid race conditions.
343
			if ( microtime( true ) > $retry_time ) {
344
				delete_option( Actions::RETRY_AFTER_PREFIX . 'immediate-send' );
345
			}
346
			return false;
347
		}
348
349
		// Obtain send Lock.
350
		$lock            = new Lock();
351
		$lock_expiration = $lock->attempt( self::LOCK_NAME );
352
353
		// Return if unable to obtain lock.
354
		if ( false === $lock_expiration ) {
355
			return;
356
		}
357
358
		// Send Full Sync actions.
359
		$success = $this->send();
360
361
		// Remove lock.
362
		if ( $success ) {
363
			$lock->remove( self::LOCK_NAME, $lock_expiration );
364
		}
365
	}
366
367
	/**
368
	 * Immediately send the next items to full sync.
369
	 *
370
	 * @access public
371
	 */
372
	public function send() {
373
		$config = $this->get_status()['config'];
374
375
		$max_duration = Settings::get_setting( 'full_sync_send_duration' );
376
		$send_until   = microtime( true ) + $max_duration;
377
378
		$progress = $this->get_status()['progress'];
379
380
		foreach ( $this->get_remaining_modules_to_send() as $module ) {
381
			$progress[ $module->name() ] = $module->send_full_sync_actions( $config[ $module->name() ], $progress[ $module->name() ], $send_until );
382
			if ( isset( $progress[ $module->name() ]['error'] ) ) {
383
				unset( $progress[ $module->name() ]['error'] );
384
				$this->update_status( array( 'progress' => $progress ) );
385
				return false;
386
			} elseif ( ! $progress[ $module->name() ]['finished'] ) {
387
				$this->update_status( array( 'progress' => $progress ) );
388
				return true;
389
			}
390
		}
391
392
		$this->send_full_sync_end();
393
		$this->update_status( array( 'progress' => $progress ) );
394
		return true;
395
	}
396
397
	/**
398
	 * Get Modules that are configured to Full Sync and haven't finished sending
399
	 *
400
	 * @return array
401
	 */
402
	public function get_remaining_modules_to_send() {
403
		$status = $this->get_status();
404
405
		return array_filter(
406
			Modules::get_modules(),
407
			/**
408
			 * Select configured and not finished modules.
409
			 *
410
			 * @return bool
411
			 * @var $module Module
412
			 */
413
			function ( $module ) use ( $status ) {
414
				// Skip module if not configured for this sync or module is done.
415
				if ( ! isset( $status['config'][ $module->name() ] ) ) {
416
					return false;
417
				}
418
				if ( ! $status['config'][ $module->name() ] ) {
419
					return false;
420
				}
421
				if ( isset( $status['progress'][ $module->name() ]['finished'] ) ) {
422
					if ( true === $status['progress'][ $module->name() ]['finished'] ) {
423
						return false;
424
					}
425
				}
426
427
				return true;
428
			}
429
		);
430
	}
431
432
	/**
433
	 * Send 'jetpack_full_sync_end' and update 'finished' status.
434
	 *
435
	 * @access public
436
	 */
437
	public function send_full_sync_end() {
438
		$range = $this->get_content_range();
439
440
		/**
441
		 * Fires when a full sync ends. This action is serialized
442
		 * and sent to the server.
443
		 *
444
		 * @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/
445
		 * @param array $range Range of the sync items, containing min and max IDs for some item types.
446
		 *
447
		 * @since 4.2.0
448
		 * @since 7.3.0 Added $range arg.
449
		 */
450
		do_action( 'jetpack_full_sync_end', '', $range );
451
		$this->send_action( 'jetpack_full_sync_end', array( '', $range ) );
452
453
		// Setting autoload to true means that it's faster to check whether we should continue enqueuing.
454
		$this->update_status( array( 'finished' => time() ) );
455
	}
456
457
	/**
458
	 * Empty Function as we don't close buffers on Immediate Full Sync.
459
	 *
460
	 * @param array $actions an array of actions, ignored for queueless sync.
461
	 */
462
	public function update_sent_progress_action( $actions ) { } // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
463
464
}
465