Skip to content

Commit 5b764d7

Browse files
authored
Merge pull request #136 from woocommerce/fix/121/ui-batch-generation
Switch Admin UI to generating in batches, using WC's BatchProcessor tool This changes the approach to generating stuff used in the admin UI. Instead of creating a separate Action Scheduler event for every product or order that should get generated, it leverages the batch methods introduced in #125 (and refined by subsequent PRs) to generate batches of items in one event. It also makes use of WooCommerce's BatchProcessor tool to handle batch management when more than one batch is needed (if you want to generate hundreds of products, for example). To go along with this improved background generation process, this makes some improvements to the UI on the Smooth Generator page in WP Admin. It adds a progress bar while a generation job is running, which is updated periodically via WP's heartbeat system. Plus some other miscellaneous tweaks to improve the UX of the page. Fixes #121
2 parents 787f0f3 + 3f2386e commit 5b764d7

File tree

7 files changed

+570
-124
lines changed

7 files changed

+570
-124
lines changed

includes/Admin/AsyncJob.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace WC\SmoothGenerator\Admin;
4+
5+
/**
6+
* Class AsyncJob.
7+
*
8+
* A Record Object to hold the current state of an async job.
9+
*/
10+
class AsyncJob {
11+
/**
12+
* The slug of the generator.
13+
*
14+
* @var string
15+
*/
16+
public string $generator_slug = '';
17+
18+
/**
19+
* The total number of objects to generate.
20+
*
21+
* @var int
22+
*/
23+
public int $amount = 0;
24+
25+
/**
26+
* Additional args for generating the objects.
27+
*
28+
* @var array
29+
*/
30+
public array $args = array();
31+
32+
/**
33+
* The number of objects already generated.
34+
*
35+
* @var int
36+
*/
37+
public int $processed = 0;
38+
39+
/**
40+
* The number of objects that still need to be generated.
41+
*
42+
* @var int
43+
*/
44+
public int $pending = 0;
45+
46+
/**
47+
* AsyncJob class.
48+
*
49+
* @param array $data
50+
*/
51+
public function __construct( array $data = array() ) {
52+
$defaults = array(
53+
'generator_slug' => $this->generator_slug,
54+
'amount' => $this->amount,
55+
'args' => $this->args,
56+
'processed' => $this->processed,
57+
'pending' => $this->pending,
58+
);
59+
$data = wp_parse_args( $data, $defaults );
60+
61+
list(
62+
'generator_slug' => $this->generator_slug,
63+
'amount' => $this->amount,
64+
'args' => $this->args,
65+
'processed' => $this->processed,
66+
'pending' => $this->pending
67+
) = $data;
68+
}
69+
}

includes/Admin/BatchProcessor.php

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
<?php
2+
3+
namespace WC\SmoothGenerator\Admin;
4+
5+
use Automattic\WooCommerce\Internal\BatchProcessing\{ BatchProcessorInterface, BatchProcessingController };
6+
use WC\SmoothGenerator\Router;
7+
8+
/**
9+
* Class BatchProcessor.
10+
*
11+
* A class for asynchronously generating batches of objects using WooCommerce's internal batch processing tool.
12+
* (This might break if changes are made to the tool.)
13+
*/
14+
class BatchProcessor implements BatchProcessorInterface {
15+
/**
16+
* The key used to store the state of the current job in the options table.
17+
*/
18+
const OPTION_KEY = 'smoothgenerator_async_job';
19+
20+
/**
21+
* Get the state of the current job.
22+
*
23+
* @return ?AsyncJob Null if there is no current job.
24+
*/
25+
public static function get_current_job() {
26+
$current_job = get_option( self::OPTION_KEY, null );
27+
28+
if ( ! $current_job instanceof AsyncJob && wc_get_container()->get( BatchProcessingController::class )->is_enqueued( self::class ) ) {
29+
wc_get_container()->get( BatchProcessingController::class )->remove_processor( self::class );
30+
} elseif ( $current_job instanceof AsyncJob && ! wc_get_container()->get( BatchProcessingController::class )->is_enqueued( self::class ) ) {
31+
self::delete_current_job();
32+
$current_job = null;
33+
}
34+
35+
return $current_job;
36+
}
37+
38+
/**
39+
* Create a new AsyncJob object.
40+
*
41+
* @param string $generator_slug The slug identifier of the generator to use.
42+
* @param int $amount The number of objects to generate.
43+
* @param array $args Additional args for object generation.
44+
*
45+
* @return AsyncJob|\WP_Error
46+
*/
47+
public static function create_new_job( string $generator_slug, int $amount, array $args = array() ) {
48+
if ( self::get_current_job() instanceof AsyncJob ) {
49+
return new \WP_Error(
50+
'smoothgenerator_async_job_already_exists',
51+
'Can\'t create a new Smooth Generator job because one is already in progress.'
52+
);
53+
}
54+
55+
$job = new AsyncJob( array(
56+
'generator_slug' => $generator_slug,
57+
'amount' => $amount,
58+
'args' => $args,
59+
'pending' => $amount,
60+
) );
61+
62+
update_option( self::OPTION_KEY, $job, false );
63+
64+
wc_get_container()->get( BatchProcessingController::class )->enqueue_processor( self::class );
65+
66+
return $job;
67+
}
68+
69+
/**
70+
* Update the state of the current job.
71+
*
72+
* @param int $processed The amount to change the state values by.
73+
*
74+
* @return AsyncJob|\WP_Error
75+
*/
76+
public static function update_current_job( int $processed ) {
77+
$current_job = self::get_current_job();
78+
79+
if ( ! $current_job instanceof AsyncJob ) {
80+
return new \WP_Error(
81+
'smoothgenerator_async_job_does_not_exist',
82+
'There is no Smooth Generator job to update.'
83+
);
84+
}
85+
86+
$current_job->processed += $processed;
87+
$current_job->pending = max( $current_job->pending - $processed, 0 );
88+
89+
update_option( self::OPTION_KEY, $current_job, false );
90+
91+
return $current_job;
92+
}
93+
94+
/**
95+
* Delete the AsyncJob object.
96+
*
97+
* @return bool
98+
*/
99+
public static function delete_current_job() {
100+
wc_get_container()->get( BatchProcessingController::class )->remove_processor( self::class );
101+
delete_option( self::OPTION_KEY );
102+
}
103+
104+
/**
105+
* Get a user-friendly name for this processor.
106+
*
107+
* @return string Name of the processor.
108+
*/
109+
public function get_name(): string {
110+
return 'Smooth Generator';
111+
}
112+
113+
/**
114+
* Get a user-friendly description for this processor.
115+
*
116+
* @return string Description of what this processor does.
117+
*/
118+
public function get_description(): string {
119+
return 'Generates various types of WooCommerce data objects with randomized data for use in testing.';
120+
}
121+
122+
/**
123+
* Get the total number of pending items that require processing.
124+
* Once an item is successfully processed by 'process_batch' it shouldn't be included in this count.
125+
*
126+
* Note that the once the processor is enqueued the batch processor controller will keep
127+
* invoking `get_next_batch_to_process` and `process_batch` repeatedly until this method returns zero.
128+
*
129+
* @return int Number of items pending processing.
130+
*/
131+
public function get_total_pending_count(): int {
132+
$current_job = self::get_current_job();
133+
134+
if ( ! $current_job instanceof AsyncJob ) {
135+
return 0;
136+
}
137+
138+
return $current_job->pending;
139+
}
140+
141+
/**
142+
* Returns the next batch of items that need to be processed.
143+
*
144+
* A batch item can be anything needed to identify the actual processing to be done,
145+
* but whenever possible items should be numbers (e.g. database record ids)
146+
* or at least strings, to ease troubleshooting and logging in case of problems.
147+
*
148+
* The size of the batch returned can be less than $size if there aren't that
149+
* many items pending processing (and it can be zero if there isn't anything to process),
150+
* but the size should always be consistent with what 'get_total_pending_count' returns
151+
* (i.e. the size of the returned batch shouldn't be larger than the pending items count).
152+
*
153+
* @param int $size Maximum size of the batch to be returned.
154+
*
155+
* @return array Batch of items to process, containing $size or less items.
156+
*/
157+
public function get_next_batch_to_process( int $size ): array {
158+
$current_job = self::get_current_job();
159+
$max_batch = self::get_default_batch_size();
160+
161+
if ( ! $current_job instanceof AsyncJob ) {
162+
$current_job = new AsyncJob();
163+
}
164+
165+
$amount = min( $size, $current_job->pending, $max_batch );
166+
167+
// The batch processing controller counts items in the array to determine if there are still pending items.
168+
if ( $amount < 1 ) {
169+
return array();
170+
}
171+
172+
return array(
173+
'generator_slug' => $current_job->generator_slug,
174+
'amount' => $amount,
175+
'args' => $current_job->args,
176+
);
177+
}
178+
179+
/**
180+
* Process data for the supplied batch.
181+
*
182+
* This method should be prepared to receive items that don't actually need processing
183+
* (because they have been processed before) and ignore them, but if at least
184+
* one of the batch items that actually need processing can't be processed, an exception should be thrown.
185+
*
186+
* Once an item has been processed it shouldn't be counted in 'get_total_pending_count'
187+
* nor included in 'get_next_batch_to_process' anymore (unless something happens that causes it
188+
* to actually require further processing).
189+
*
190+
* @throw \Exception Something went wrong while processing the batch.
191+
*
192+
* @param array $batch Batch to process, as returned by 'get_next_batch_to_process'.
193+
*/
194+
public function process_batch( array $batch ): void {
195+
list( 'generator_slug' => $slug, 'amount' => $amount, 'args' => $args ) = $batch;
196+
197+
$result = Router::generate_batch( $slug, $amount, $args );
198+
199+
if ( is_wp_error( $result ) ) {
200+
throw new \Exception( $result->get_error_message() );
201+
}
202+
203+
self::update_current_job( count( $result ) );
204+
}
205+
206+
/**
207+
* Default (preferred) batch size to pass to 'get_next_batch_to_process'.
208+
* The controller will pass this size unless it's externally configured
209+
* to use a different size.
210+
*
211+
* @return int Default batch size.
212+
*/
213+
public function get_default_batch_size(): int {
214+
$current_job = self::get_current_job() ?: new AsyncJob();
215+
$generator = Router::get_generator_class( $current_job->generator_slug );
216+
217+
if ( is_wp_error( $generator ) ) {
218+
return 0;
219+
}
220+
221+
return $generator::MAX_BATCH_SIZE;
222+
}
223+
}

0 commit comments

Comments
 (0)