storage = $storage; $this->client = $client; $this->clock = null === $clock ? 'time' : $clock; } /** * GET authoritative status only when due. This does not extend liveness. * * @return array|\WP_Error */ public function maybe_status( $reason = 'request', $force = false ) { $settings = $this->storage->get(); $now = call_user_func( $this->clock ); if ( ! $this->storage->has_management_binding( $settings ) ) { return new \WP_Error( 'status_requires_binding', __( 'Reconnect this site to read its Achla status.', 'achlaai' ), array( 'status' => 409 ) ); } if ( ! $force && (int) $settings['lifecycle']['next_status_check_at'] > $now ) { return $settings['lifecycle']; } if ( ! $this->acquire_lock( self::STATUS_LOCK_OPTION, $now ) ) { return $settings['lifecycle']; } try { $result = $this->client->status( $settings['connection'], $this->timeout_for( $reason ) ); if ( is_wp_error( $result ) ) { $this->handle_error( $result, 'status', $now ); return $result; } $this->persist_management_status( $settings, $result, false, $now ); return $result; } finally { $this->release_lock( self::STATUS_LOCK_OPTION ); } } /** @return array|\WP_Error */ public function maybe_heartbeat( $reason = 'request', $force = false ) { $settings = $this->storage->get(); $now = call_user_func( $this->clock ); if ( ! $this->storage->has_management_binding( $settings ) ) { return new \WP_Error( 'heartbeat_requires_binding', __( 'Reconnect this site before sending a heartbeat.', 'achlaai' ), array( 'status' => 409 ) ); } if ( ! $force && (int) $settings['lifecycle']['next_heartbeat_at'] > $now ) { return $settings['lifecycle']; } if ( ! $this->acquire_lock( self::HEARTBEAT_LOCK_OPTION, $now ) ) { return $settings['lifecycle']; } try { return $this->heartbeat( $now ); } finally { $this->release_lock( self::HEARTBEAT_LOCK_OPTION ); } } /** Run bounded status and liveness maintenance from real requests. */ public function request_maintenance( $reason = 'request' ) { $status = $this->maybe_status( $reason, false ); if ( is_wp_error( $status ) ) { return $status; } return $this->maybe_heartbeat( $reason, false ); } /** @return array|\WP_Error */ public function heartbeat( $now = null ) { $now = null === $now ? call_user_func( $this->clock ) : (int) $now; $settings = $this->storage->get(); $connection = $settings['connection']; if ( ! $this->storage->has_management_binding( $settings ) ) { return new \WP_Error( 'heartbeat_requires_binding', __( 'Reconnect this site before sending a heartbeat.', 'achlaai' ), array( 'status' => 409 ) ); } $origin_status = $this->storage->reconcile_origin(); if ( 'verified' !== $origin_status || in_array( $settings['release']['status'], array( 'blocked', 'killed' ), true ) || in_array( $settings['lifecycle']['project_status'], array( 'suspended', 'canceled' ), true ) || 'failed' === $settings['lifecycle']['index_status'] ) { $connection_status = 'error'; } elseif ( 'active' !== $settings['lifecycle']['project_status'] || 'ready' !== $settings['lifecycle']['index_status'] || 'ready' !== $settings['release']['status'] ) { $connection_status = 'degraded'; } else { $connection_status = 'connected'; } $index_status = in_array( $settings['lifecycle']['index_status'], array( 'pending', 'indexing', 'ready', 'failed' ), true ) ? $settings['lifecycle']['index_status'] : 'unknown'; $result = $this->client->heartbeat( $connection, array( 'protocol' => 'aip-management-v1', 'plugin_version' => ACHLAAI_VERSION, 'cms' => 'wordpress', 'cms_version' => get_bloginfo( 'version' ), 'runtime' => 'php', 'runtime_version' => Protocol::php_runtime_version(), 'connection_status' => $connection_status, 'index_status' => $index_status, ) ); if ( is_wp_error( $result ) ) { $this->handle_error( $result, 'heartbeat', $now ); return $result; } $this->persist_management_status( $settings, $result, true, $now ); return $result; } private function handle_error( $error, $kind, $now ) { $data = $error->get_error_data(); if ( is_array( $data ) && isset( $data['status'] ) && in_array( (int) $data['status'], array( 401, 403 ), true ) ) { $this->storage->revoke_management( 'management_revoked' ); return; } $settings = $this->storage->get(); $key = 'status' === $kind ? 'status_failures' : 'heartbeat_failures'; $next_key = 'status' === $kind ? 'next_status_check_at' : 'next_heartbeat_at'; $failures = min( 6, (int) $settings['lifecycle'][ $key ] + 1 ); $delay = min( 900, 30 * ( 2 ** ( $failures - 1 ) ) ); $settings['lifecycle'][ $key ] = $failures; $settings['lifecycle'][ $next_key ] = $now + $delay; $this->storage->save( $settings ); } /** @return float */ private function timeout_for( $reason ) { return in_array( $reason, array( 'manual', 'admin' ), true ) ? 4.0 : 2.0; } /** @return bool */ private function uses_provisioning_cadence( $settings, $status ) { return 'verified' === $settings['connection']['status'] && in_array( $status['project']['status'], array( 'registered', 'active' ), true ) && in_array( $status['project']['index_status'], array( 'pending', 'indexing' ), true ); } private function persist_management_status( $settings, $status, $heartbeat, $now ) { $this->storage->apply_management_status( $status, $heartbeat, $now ); if ( ! $this->uses_provisioning_cadence( $settings, $status ) ) { return; } $settings = $this->storage->get(); $settings['lifecycle']['next_status_check_at'] = $now + self::PROVISIONING_STATUS_INTERVAL; $this->storage->save( $settings ); } /** @return bool */ private function acquire_lock( $name, $now ) { $expires = get_option( $name, 0 ); if ( is_numeric( $expires ) && (int) $expires <= $now ) { delete_option( $name ); } return add_option( $name, $now + self::LOCK_TTL, '', 'no' ); } private function release_lock( $name ) { delete_option( $name ); } }