Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions includes/class-instawp-heartbeat.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
if ( ! class_exists( 'InstaWP_Heartbeat' ) ) {
class InstaWP_Heartbeat {

const MIN_RETRY_DELAY = 3600; // 1 hour in seconds
const MAX_FAILURES_BEFORE_DISABLE = 10;
const MAX_RETRY_DELAY_HOURS = 4;

public function __construct() {
add_action( 'init', array( $this, 'register_events' ) );
add_action( 'add_option_instawp_api_heartbeat', array( $this, 'clear_heartbeat_action' ) );
Expand Down Expand Up @@ -54,26 +58,64 @@ public function send_heartbeat_data() {
$failed_count = Option::get_option( 'instawp_heartbeat_failed', 0 );
$failed_count = $failed_count ? $failed_count : 0;

if ( $failed_count > 10 ) {
if ( $failed_count > self::MAX_FAILURES_BEFORE_DISABLE ) {
Option::delete_option( 'instawp_heartbeat_failed' );
Option::update_option( 'instawp_rm_heartbeat', 'on' );
}
}

public function handle_heartbeat_data() {
// Check if we should skip this heartbeat due to recent failure (minimum 60 minutes)
$last_attempt = Option::get_option( 'instawp_heartbeat_last_attempt', 0 );
$min_retry_delay = self::MIN_RETRY_DELAY;

if ( $last_attempt > 0 && ( time() - $last_attempt ) < $min_retry_delay ) {
// Too soon to retry, reschedule for later
$failed_count = Option::get_option( 'instawp_heartbeat_failed', 0 );
$retry_delay = $this->calculate_retry_delay( $failed_count );
$next_attempt = $last_attempt + $retry_delay;

// Only reschedule if we're not already past the next attempt time
if ( $next_attempt > time() ) {
as_unschedule_all_actions( 'instawp_handle_heartbeat', array(), 'instawp-connect' );
as_schedule_single_action( $next_attempt, 'instawp_handle_heartbeat', array(), 'instawp-connect' );
}
return;
}

// Update last attempt time
Option::update_option( 'instawp_heartbeat_last_attempt', time() );

$heartbeat_response = self::send_heartbeat();

if ( $heartbeat_response['success'] ) {
// Success: reset everything and restore normal schedule
Option::delete_option( 'instawp_heartbeat_failed' );
Option::delete_option( 'instawp_heartbeat_last_attempt' );

// Reschedule recurring action with normal interval
as_unschedule_all_actions( 'instawp_handle_heartbeat', array(), 'instawp-connect' );
$interval = Option::get_option( 'instawp_api_heartbeat', 240 );
$interval = empty( $interval ) ? 240 : (int) $interval;
as_schedule_recurring_action( time(), ( $interval * MINUTE_IN_SECONDS ), 'instawp_handle_heartbeat', array(), 'instawp-connect' );
} else {
// Failure: implement exponential backoff
$failed_count = Option::get_option( 'instawp_heartbeat_failed', 0 );
$failed_count = $failed_count ? $failed_count : 0;

++$failed_count;

Option::update_option( 'instawp_heartbeat_failed', $failed_count );

if ( $failed_count > 10 ) {
// Calculate retry delay with exponential backoff
$retry_delay = $this->calculate_retry_delay( $failed_count );
$next_attempt = time() + $retry_delay;

// Unschedule recurring action and schedule single retry
as_unschedule_all_actions( 'instawp_handle_heartbeat', array(), 'instawp-connect' );
as_schedule_single_action( $next_attempt, 'instawp_handle_heartbeat', array(), 'instawp-connect' );

if ( $failed_count > self::MAX_FAILURES_BEFORE_DISABLE ) {
Option::update_option( 'instawp_rm_heartbeat', 'off' );

if ( intval( $heartbeat_response['response_code'] ) === 404 ) {
Expand All @@ -83,6 +125,29 @@ public function handle_heartbeat_data() {
}
}

/**
* Calculate retry delay based on failed count with exponential backoff
* Pattern: 1hr → 2hrs → 3hrs → 4hrs (capped at 4hrs)
*
* @param int $failed_count Number of consecutive failures
* @return int Delay in seconds
*/
private function calculate_retry_delay( $failed_count ) {
// Minimum delay: 60 minutes (1 hour)
$min_delay = self::MIN_RETRY_DELAY;

// Exponential backoff pattern:
// Failed 1: 1 hour (60 min)
// Failed 2: 2 hours (120 min)
// Failed 3: 3 hours (180 min)
// Failed 4+: 4 hours (240 min) - capped
$delay_hours = min( $failed_count, self::MAX_RETRY_DELAY_HOURS );
$delay_seconds = $delay_hours * self::MIN_RETRY_DELAY;

// Ensure minimum delay of 60 minutes
return max( $delay_seconds, $min_delay );
}

public static function prepare_data() {
if ( ! class_exists( 'WP_Debug_Data' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-debug-data.php';
Expand Down