PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.19
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.19
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
← All changes | includes/Services/Print_Job_Service.php +142 -9 1.10.21.10.19 View file →
@@ -18,8 +18,9 @@
18 18 const META_ORDER_ID = '_wcpos_pj_order_id';
19 19 const META_FORMAT = '_wcpos_pj_format';
20 20 const META_TEMPLATE = '_wcpos_pj_template_id';
21 21 const META_ERROR = '_wcpos_pj_error';
22 + const META_UNCONFIRMED = '_wcpos_pj_unconfirmed';
22 23 const META_RETRIED_TO = '_wcpos_pj_retried_to';
23 24 const META_CLAIMED_AT = '_wcpos_pj_claimed_at';
24 25 const META_PN_KIND = '_wcpos_pj_pn_kind';
25 26 const META_EXTERNAL_PROVIDER = '_wcpos_pj_external_provider';
@@ -44,11 +45,22 @@
44 45
45 46 /** Unix time a job reached a terminal status — the retention clock. */
46 47 const META_TERMINAL_AT = '_wcpos_pj_terminal_at';
47 48
48 - /** Seconds a claimed job stays in-flight before it is treated as stale and re-queued. */
49 + /**
50 + * Seconds a claimed job stays in-flight before it is treated as stale. A stale
51 + * claim is failed as "unconfirmed", never re-queued: the printer may have
52 + * printed it and simply be unable to report yet (see find_unconfirmed()).
53 + */
49 54 const CLAIM_TTL = 120;
50 55
56 + /**
57 + * Seconds after a claim was failed as unconfirmed during which a printer's
58 + * late result is still attributed to it. Printers report within minutes of
59 + * a link coming back; anything older is a stray post, not a late result.
60 + */
61 + const UNCONFIRMED_RESULT_WINDOW = HOUR_IN_SECONDS;
62 +
51 63 const STATUS_PENDING = 'pending';
52 64 const STATUS_CLAIMED = 'claimed';
53 65 const STATUS_PRINTED = 'printed';
54 66 const STATUS_FAILED = 'failed';
@@ -54,12 +66,19 @@
54 66 const STATUS_FAILED = 'failed';
55 67 const STATUS_CANCELLED = 'cancelled';
56 68
57 69 /**
58 - * Constructor — register the CPT on init.
70 + * Constructor — register the CPT on init, or at once when init has passed.
71 + *
72 + * On a storefront request this service is constructed lazily by the first
73 + * order write, long after `init`; a hook added then would never fire.
59 74 */
60 75 public function __construct() {
61 - add_action( 'init', array( $this, 'register_post_type' ) );
76 + if ( did_action( 'init' ) ) {
77 + $this->register_post_type();
78 + } else {
79 + add_action( 'init', array( $this, 'register_post_type' ) );
80 + }
62 81 // A static callback: several services construct Print_Job_Service on
63 82 // every request, and WordPress dedupes identical static callbacks, so
64 83 // the purge runs exactly once per cron event.
65 84 add_action( self::PURGE_HOOK, array( __CLASS__, 'run_purge' ) );
@@ -174,8 +193,11 @@
174 193 'auto_open_drawer' => 'yes' === (string) get_post_meta( $id, self::META_AUTO_OPEN_DRAWER, true ),
175 194 'drawer_connector' => self::normalize_drawer_connector( (string) get_post_meta( $id, self::META_DRAWER_CONNECTOR, true ) ),
176 195 'drawer_error' => (string) get_post_meta( $id, self::META_DRAWER_ERROR, true ),
177 196 'retried_to' => (int) get_post_meta( $id, self::META_RETRIED_TO, true ),
197 + 'error' => (string) get_post_meta( $id, self::META_ERROR, true ),
198 + 'unconfirmed' => '1' === (string) get_post_meta( $id, self::META_UNCONFIRMED, true ),
199 + 'terminal_at' => (int) get_post_meta( $id, self::META_TERMINAL_AT, true ),
178 200 );
179 201 }
180 202
181 203 /**
@@ -453,8 +475,10 @@
453 475 */
454 476 public function query_rows( array $filters = array() ): array {
455 477 global $wpdb;
456 478
479 + $order = isset( $filters['order'] ) && 'DESC' === strtoupper( (string) $filters['order'] ) ? 'DESC' : 'ASC';
480 +
457 481 $query = new \WP_Query(
458 482 array(
459 483 'post_type' => self::POST_TYPE,
460 484 'post_status' => 'publish',
@@ -459,11 +483,14 @@
459 483 'post_type' => self::POST_TYPE,
460 484 'post_status' => 'publish',
461 485 'posts_per_page' => isset( $filters['limit'] ) ? (int) $filters['limit'] : 50,
462 486 'paged' => isset( $filters['page'] ) ? max( 1, (int) $filters['page'] ) : 1,
487 + // Oldest-first by default: oldest_pending_gmt() reads row zero to
488 + // find a printer's longest-waiting job. The queue *view* asks for
489 + // DESC instead, where the newest job is the one being looked for.
463 490 'orderby' => array(
464 - 'date' => 'ASC',
465 - 'ID' => 'ASC',
491 + 'date' => $order,
492 + 'ID' => $order,
466 493 ),
467 494 'fields' => 'ids',
468 495 'no_found_rows' => true,
469 496 'meta_query' => $this->filters_to_meta_query( $filters ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
@@ -495,8 +522,11 @@
495 522 'order_id' => (int) get_post_meta( $id, self::META_ORDER_ID, true ),
496 523 'format' => (string) get_post_meta( $id, self::META_FORMAT, true ),
497 524 'template_id' => (string) get_post_meta( $id, self::META_TEMPLATE, true ),
498 525 'retried_to' => (int) get_post_meta( $id, self::META_RETRIED_TO, true ),
526 + 'error' => (string) get_post_meta( $id, self::META_ERROR, true ),
527 + 'unconfirmed' => '1' === (string) get_post_meta( $id, self::META_UNCONFIRMED, true ),
528 + 'terminal_at' => (int) get_post_meta( $id, self::META_TERMINAL_AT, true ),
499 529 );
500 530 },
501 531 $ids
502 532 );
@@ -653,8 +683,33 @@
653 683 return $cancelled;
654 684 }
655 685
656 686 /**
687 + * Permanently remove a job row.
688 + *
689 + * The retention purge clears terminal jobs on its own schedule; this is the
690 + * admin's manual escape hatch for a queue full of noise they do not want to
691 + * wait out. A still-waiting job is cancelled first so a printer that is
692 + * mid-poll cannot claim a row that is about to vanish.
693 + *
694 + * @param int $id Job ID.
695 + *
696 + * @return bool True when the row was deleted.
697 + */
698 + public function delete( int $id ): bool {
699 + if ( self::POST_TYPE !== get_post_type( $id ) ) {
700 + return false;
701 + }
702 +
703 + $status = (string) get_post_meta( $id, self::META_STATUS, true );
704 + if ( \in_array( $status, array( self::STATUS_PENDING, self::STATUS_CLAIMED ), true ) && ! $this->cancel_if_waiting( $id ) ) {
705 + return false;
706 + }
707 +
708 + return (bool) wp_delete_post( $id, true );
709 + }
710 +
711 + /**
657 712 * Atomically cancel a waiting job while excluding provider submission.
658 713 *
659 714 * @param int $id Job ID.
660 715 *
@@ -1043,11 +1098,77 @@
1043 1098 return $claimed[0];
1044 1099 }
1045 1100
1046 1101 /**
1047 - * Re-queue stale claims for a printer (crashed/aborted prints).
1102 + * The printer's newest unconfirmed, unresolved job within
1103 + * UNCONFIRMED_RESULT_WINDOW, or null.
1048 1104 *
1049 1105 * @param string $printer_id Printer ID.
1106 + *
1107 + * @return array|null
1108 + */
1109 + public function find_unconfirmed( string $printer_id ): ?array {
1110 + $posts = get_posts(
1111 + array(
1112 + 'post_type' => self::POST_TYPE,
1113 + 'post_status' => 'publish',
1114 + 'posts_per_page' => 1,
1115 + // Newest by the time it actually failed, not by creation: two jobs for
1116 + // one printer can go terminal in a different order than they were
1117 + // queued, and it is the most recently failed one a late result
1118 + // belongs to.
1119 + 'orderby' => array(
1120 + 'terminal_at' => 'DESC',
1121 + 'ID' => 'DESC',
1122 + ),
1123 + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
1124 + array(
1125 + 'key' => self::META_PRINTER,
1126 + 'value' => sanitize_text_field( $printer_id ),
1127 + ),
1128 + array(
1129 + 'key' => self::META_STATUS,
1130 + 'value' => self::STATUS_FAILED,
1131 + ),
1132 + array(
1133 + 'key' => self::META_UNCONFIRMED,
1134 + 'value' => '1',
1135 + ),
1136 + array(
1137 + 'key' => self::META_RETRIED_TO,
1138 + 'compare' => 'NOT EXISTS',
1139 + ),
1140 + 'terminal_at' => array(
1141 + 'key' => self::META_TERMINAL_AT,
1142 + 'value' => time() - self::UNCONFIRMED_RESULT_WINDOW,
1143 + 'compare' => '>=',
1144 + 'type' => 'NUMERIC',
1145 + ),
1146 + ),
1147 + )
1148 + );
1149 + return empty( $posts ) ? null : $this->get( (int) $posts[0]->ID );
1150 + }
1151 +
1152 + /**
1153 + * Record the printer's own result for a job — the claim it holds, or one
1154 + * failed as unconfirmed whose result arrived late. Success clears the
1155 + * unconfirmed flag and its explanatory text; a failure's code is recorded by
1156 + * the caller after this.
1157 + *
1158 + * @param int $id Job ID.
1159 + * @param bool $ok Whether the printer reported success.
1160 + */
1161 + public function record_printer_result( int $id, bool $ok ): void {
1162 + $this->set_status( $id, $ok ? self::STATUS_PRINTED : self::STATUS_FAILED );
1163 + delete_post_meta( $id, self::META_UNCONFIRMED );
1164 + delete_post_meta( $id, self::META_ERROR );
1165 + }
1166 +
1167 + /**
1168 + * Fail stale claims without risking an automatic duplicate print.
1169 + *
1170 + * @param string $printer_id Printer ID.
1050 1171 * @param int $ttl Claim TTL in seconds.
1051 1172 */
1052 1173 public function release_stale_claims( string $printer_id, int $ttl = self::CLAIM_TTL ): void {
1053 1174 $claimed = $this->query(
@@ -1061,13 +1182,25 @@
1061 1182 if ( 0 === $claimed_at || ( time() - $claimed_at ) > $ttl ) {
1062 1183 // Drop the timestamp while the job is still claimed — nothing
1063 1184 // can re-claim it until the status flips, so a fresh claim's
1064 1185 // timestamp can never be erased by this cleanup. Then the
1065 - // requeue is conditional on still-claimed: same race as
1186 + // failure is conditional on still-claimed: same race as
1066 1187 // try_claim() — a cancellation landing after the query above
1067 - // must not be overwritten back to pending.
1188 + // must not be overwritten as failed.
1068 1189 delete_post_meta( $job['id'], self::META_CLAIMED_AT );
1069 - update_post_meta( $job['id'], self::META_STATUS, self::STATUS_PENDING, self::STATUS_CLAIMED );
1190 + if ( update_post_meta( $job['id'], self::META_STATUS, self::STATUS_FAILED, self::STATUS_CLAIMED ) ) {
1191 + // A machine code, like every other META_ERROR writer; the queue UI
1192 + // turns the unconfirmed flag into the merchant-facing explanation.
1193 + update_post_meta( $job['id'], self::META_ERROR, 'claim_timeout' );
1194 + // The compare-and-swap above is the only status write; a second,
1195 + // unconditional one would clobber a result or cancellation that
1196 + // landed in between. Only the terminal side effects are wanted.
1197 + $this->finalize_status_change( (int) $job['id'], self::STATUS_FAILED );
1198 + // Flag last: it is what makes the row visible to find_unconfirmed(),
1199 + // so nothing above can race a late result that lands once it is set.
1200 + update_post_meta( $job['id'], self::META_UNCONFIRMED, '1' );
1201 + \WCPOS\WooCommercePOS\Logger::warning( sprintf( 'Printer "%s" did not report a result for print job %d before the claim timeout.', $printer_id, (int) $job['id'] ) );
1202 + }
1070 1203 }
1071 1204 }
1072 1205 }
1073 1206