(string) $native_id), $event_id, 'order' ); } catch (Throwable $error) { unset($error); return false; } $date = self::date($now); $written = $wpdb->query($wpdb->prepare( "INSERT INTO {$table} (event_id,order_key,operation,envelope,state,capture_attempts,attempts, next_attempt_gmt,created_gmt,updated_gmt) VALUES (%s,%s,'order',%s,'capture_pending',0,0,%s,%s,%s) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id), next_attempt_gmt=IF(state='capture_pending' AND %d=1, LEAST(next_attempt_gmt,VALUES(next_attempt_gmt)),next_attempt_gmt), updated_gmt=IF(state='capture_pending' AND %d=1,VALUES(updated_gmt),updated_gmt)", $event_id, $order_key, $envelope, $date, $date, $date, $accelerate ? 1 : 0, $accelerate ? 1 : 0 )); unset($envelope); return false !== $written && (int) $wpdb->insert_id > 0 ? (int) $wpdb->insert_id : false; } public function order_state($event_id) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); $state = $wpdb->get_var($wpdb->prepare( "SELECT state FROM {$table} WHERE event_id=%s AND operation='order' LIMIT 1", $event_id )); return is_string($state) ? $state : ''; } public function order_provenance($event_id) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); try { $wpdb->flush(); $state = $wpdb->get_var($wpdb->prepare( "SELECT state FROM {$table} WHERE event_id=%s AND operation='order' LIMIT 1", $event_id )); if (!property_exists($wpdb, 'last_error') || '' !== (string) $wpdb->last_error) return null; return null === $state ? false : (is_string($state) ? true : null); } catch (Throwable $error) { unset($error); return null; } } public function insert_status($event_id, $order_key, $envelope, $now) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); $date = self::date($now); $written = $wpdb->query($wpdb->prepare( "INSERT INTO {$table} (event_id,order_key,operation,envelope,state,capture_attempts,attempts, next_attempt_gmt,created_gmt,updated_gmt) VALUES (%s,%s,'status',%s,'pending',0,0,%s,%s,%s)", $event_id, $order_key, $envelope, $date, $date, $date )); return 1 === $written && (int) $wpdb->insert_id > 0 ? (int) $wpdb->insert_id : false; } public function claim_capture($id, $owner, $now) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); $date = self::date($now); $expires = self::date($now + self::LEASE_SECONDS); $changed = $wpdb->query($wpdb->prepare( "UPDATE {$table} SET state='capture_processing', capture_attempts=LEAST(capture_attempts+1,255), lease_owner=%s,lease_expires_gmt=%s,updated_gmt=%s WHERE id=%d AND operation='order' AND next_attempt_gmt<=%s AND ((state='capture_pending' AND (lease_expires_gmt IS NULL OR lease_expires_gmt<=%s)) OR (state='capture_processing' AND lease_expires_gmt<=%s))", $owner, $expires, $date, $id, $date, $date, $date )); return 1 === $changed ? $this->claimed($id, $owner, 'capture_processing') : false; } public function claim_delivery($id, $owner, $now) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); $date = self::date($now); $expires = self::date($now + self::LEASE_SECONDS); $changed = $wpdb->query($wpdb->prepare( "UPDATE {$table} candidate LEFT JOIN {$table} earlier ON earlier.order_key=candidate.order_key AND earlier.idclaimed($id, $owner, 'processing') : false; } public function freeze($id, $owner, $envelope, $now) { return $this->transition($id, $owner, 'capture_processing', array( 'state' => 'pending', 'envelope' => $envelope, 'next_attempt_gmt' => self::date($now), 'last_outcome' => 'capture_ready', 'completed_gmt' => null, ), $now); } public function retry_capture($id, $owner, $delay, $now) { return $this->transition($id, $owner, 'capture_processing', array( 'state' => 'capture_pending', 'next_attempt_gmt' => self::date($now + $delay), 'last_outcome' => 'capture_incomplete', 'completed_gmt' => null, ), $now); } public function terminal_capture($id, $owner, $outcome, $now) { return $this->transition($id, $owner, 'capture_processing', array( 'state' => 'capture_failed', 'envelope' => null, 'last_outcome' => $outcome, 'completed_gmt' => self::date($now), ), $now); } public function retry_delivery($id, $owner, $delay, $now) { return $this->transition($id, $owner, 'processing', array( 'state' => 'retry', 'next_attempt_gmt' => self::date($now + $delay), 'last_outcome' => 'retryable', 'completed_gmt' => null, ), $now); } public function pause_delivery($id, $owner, $outcome, $now) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); return 1 === $wpdb->query($wpdb->prepare( "UPDATE {$table} SET state='paused',attempts=IF(attempts>0,attempts-1,0), last_outcome=%s,lease_owner=NULL,lease_expires_gmt=NULL,updated_gmt=%s WHERE id=%d AND lease_owner=%s AND state='processing'", $outcome, self::date($now), $id, $owner )); } public function terminal_delivery($id, $owner, $state, $outcome, $now) { if (!in_array($state, array('confirmed', 'skipped', 'rejected', 'exhausted'), true)) { return false; } $fields = array('state' => $state, 'last_outcome' => $outcome, 'completed_gmt' => self::date($now)); if ('exhausted' !== $state) $fields['envelope'] = null; return $this->transition($id, $owner, 'processing', $fields, $now); } public function quarantine_delivery(array $row, $owner, $outcome, $now) { if (!in_array($outcome, array('delivery_invalid', 'invalid_request', 'idempotency_conflict', 'order_conflict', 'invalid_status_transition', 'unknown_terminal'), true)) return false; $changed = $this->transition((int) $row['id'], $owner, 'processing', array( 'state' => 'rejected', 'last_outcome' => $outcome, 'completed_gmt' => self::date($now), ), $now); if ($changed && 'order' === $row['operation']) $this->block_dependents($row['order_key'], $now); return $changed; } public function block_dependents($order_key, $now) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); return $wpdb->query($wpdb->prepare( "UPDATE {$table} SET state='rejected',envelope=NULL,last_outcome='order_blocked', lease_owner=NULL,lease_expires_gmt=NULL,completed_gmt=%s,updated_gmt=%s WHERE order_key=%s AND operation='status' AND (state IN ('pending','retry') OR (state='processing' AND lease_expires_gmt<=%s))", self::date($now), self::date($now), $order_key, self::date($now) )); } public function recoverable($now, $limit) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); return $wpdb->get_results($wpdb->prepare( "SELECT id,operation,state FROM {$table} WHERE ((state IN ('capture_pending','pending','retry') AND next_attempt_gmt<=%s AND NOT (state='retry' AND last_outcome IN ('local_not_ready','connector_disabled') AND updated_gmt>%s)) OR (state IN ('capture_processing','processing') AND lease_expires_gmt<=%s)) ORDER BY id ASC LIMIT %d", self::date($now), self::date($now - self::LEASE_SECONDS), self::date($now), $limit ), ARRAY_A); } public function block_orphans($now, $limit) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); $ids = $wpdb->get_col($wpdb->prepare( "SELECT child.id FROM {$table} child INNER JOIN {$table} parent ON parent.order_key=child.order_key AND parent.operation='order' AND parent.state IN ('skipped','rejected','exhausted','capture_failed') WHERE child.operation='status' AND (child.state IN ('pending','retry') OR (child.state='processing' AND child.lease_expires_gmt<=%s)) ORDER BY child.id ASC LIMIT %d", self::date($now), $limit )); $blocked = 0; foreach (is_array($ids) ? $ids : array() as $id) { $changed = $wpdb->query($wpdb->prepare( "UPDATE {$table} SET state='rejected',envelope=NULL,last_outcome='order_blocked', lease_owner=NULL,lease_expires_gmt=NULL,completed_gmt=%s,updated_gmt=%s WHERE id=%d AND operation='status' AND (state IN ('pending','retry') OR (state='processing' AND lease_expires_gmt<=%s))", self::date($now), self::date($now), $id, self::date($now) )); $blocked += 1 === $changed ? 1 : 0; } return $blocked; } public function cleanup($cutoff, $limit) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); return $wpdb->query($wpdb->prepare( "DELETE FROM {$table} WHERE operation='status' AND envelope IS NULL AND state IN ('confirmed','rejected','exhausted') AND completed_gmt<%s LIMIT %d", self::date($cutoff), $limit )); } private function claimed($id, $owner, $state) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); return $wpdb->get_row($wpdb->prepare( "SELECT id,event_id,order_key,operation,envelope,state,capture_attempts,attempts, last_outcome,created_gmt FROM {$table} WHERE id=%d AND lease_owner=%s AND state=%s LIMIT 1", $id, $owner, $state ), ARRAY_A); } private function transition($id, $owner, $expected, array $fields, $now) { global $wpdb; $table = Akkedli_V2_Outbox_Schema::table(); $sets = array(); $values = array(); foreach ($fields as $field => $value) { $sets[] = "{$field}=" . (null === $value ? 'NULL' : '%s'); if (null !== $value) { $values[] = $value; } } $sets[] = 'lease_owner=NULL'; $sets[] = 'lease_expires_gmt=NULL'; $sets[] = 'updated_gmt=%s'; $values[] = self::date($now); $values[] = $id; $values[] = $owner; $values[] = $expected; $query = "UPDATE {$table} SET " . implode(',', $sets) . ' WHERE id=%d AND lease_owner=%s AND state=%s'; return 1 === $wpdb->query($wpdb->prepare($query, $values)); } private static function date($timestamp) { return gmdate('Y-m-d H:i:s', (int) $timestamp); } }