Skip to content

Commit 13d1f0e

Browse files
committed
Feature: Add "Batch Size" to the workflow split controller, to send multiple items to each job.
1 parent 28dbba1 commit 13d1f0e

5 files changed

Lines changed: 56 additions & 10 deletions

File tree

docs/data.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3226,9 +3226,9 @@ For controller nodes, see the following table for details on how the `data` prop
32263226
| `multiplex` | Will contain `stagger` (delay in seconds) for staggering jobs across servers, and `continue` (percentage) for gating success. |
32273227
| `wait` | Will contain `wait` (delay in seconds). |
32283228
| `repeat` | Will contain `repeat` (iteration count), and `continue` (percentage) for gating success. |
3229-
| `split` | Will contain `split` (expression to split on), and `continue` (percentage) for gating success. |
3229+
| `split` | Will contain `split` (expression to split on), `continue` (percentage) for gating success, an optional `filter` for filtering items, and optional `chunk` for batch size. |
32303230
| `join` | Not used. |
3231-
| `decision` | Will contain `label` (custom title), `icon` (custom icon), and `decision` (expression to evaluate). |
3231+
| `decision` | Will contain `label` (custom title), `icon` (custom icon), `decision` (expression to evaluate), and `abort` (abort entire workflow on false eval). |
32323232

32333233
#### WorkflowNode.x
32343234

docs/workflows.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ The Split controller fans out work by taking an input list and launching one sub
8484

8585
In the UI, the split controller configuration dialog provides an "Expression Builder" button, which allows you to explore output data from recently completed jobs, and pick out a specific JSON key path to use for the expression string.
8686

87-
Each individual sub-job will receive one item from the split data. It will arrive in the job's [Job.input](data.md#job-input), either in `data` as a property named `item`, or as a [File](data.md#file) in the `files` array.
87+
Each individual sub-job will receive one item from the split data. It will arrive in the job's [Job.input](data.md#job-input), either in `data` as a property named `item` (or `items` if multiple are batched), or as a [File](data.md#file) in the `files` array.
8888

8989
Split requires exactly one output connection to the Event or Job node it will run per item. Concurrency and queuing are governed by the limits attached to that node. After all items complete, you can continue the flow using a `continue` wire from the controlled node. The controller includes a "continue percentage" setting so you can require that at least N% of the sub-jobs succeed before continuing.
9090

@@ -102,6 +102,12 @@ Also available in context here is `index` (the 0-based index of the current item
102102
index % 2 == 0
103103
```
104104

105+
##### Split Batch Size
106+
107+
By default, each split item launches one downstream job. To process multiple items per job, increase the Split controller's Batch Size setting. xyOps will chunk the split items in order, using one downstream job per batch, with the final batch containing any remaining items.
108+
109+
For batched splits, each job receives an array in `data.items` (note: plural). For `files` splits, each batched job receives multiple [File](data.md#file) objects in its `files` array. A special case is when the Batch Size is `1`, which keeps the original legacy behavior, where each job receives a single `data.item` (note: singular) or one file.
110+
105111
#### Join Controller
106112

107113
The Join controller waits for multiple incoming flows to finish, then passes a combined result to the next step. You can wire multiple inputs into a Join; it initializes when the first input arrives and completes after all of its inputs have fired.

htdocs/js/pages/Workflows.class.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2215,6 +2215,21 @@ Page.Workflows = class Workflows extends Page.Events {
22152215
})
22162216
});
22172217

2218+
// split chunk
2219+
html += this.getFormRow({
2220+
id: 'd_wfd_split_chunk',
2221+
content: this.getFormText({
2222+
id: 'fe_wfd_split_chunk',
2223+
type: 'number',
2224+
spellcheck: 'false',
2225+
autocomplete: 'off',
2226+
maxlength: 32,
2227+
min: 1,
2228+
step: 1,
2229+
value: node.data.chunk || '1'
2230+
})
2231+
});
2232+
22182233
// decision expression
22192234
html += this.getFormRow({
22202235
id: 'd_wfd_if',
@@ -2300,6 +2315,7 @@ Page.Workflows = class Workflows extends Page.Events {
23002315
node.data.split = $('#fe_wfd_split').val().trim();
23012316
if (!node.data.split.length) return app.badField('#fe_wfd_split');
23022317
node.data.filter = $('#fe_wfd_split_filter').val().trim();
2318+
node.data.chunk = parseInt( $('#fe_wfd_split_chunk').val() ) || 1;
23032319
node.data.continue = parseInt( $('#fe_wfd_continue').val() ) || 0;
23042320
break;
23052321

@@ -2355,14 +2371,14 @@ Page.Workflows = class Workflows extends Page.Events {
23552371
// handle type change
23562372
var do_change_type = function() {
23572373
// show/hide sections based on type
2358-
$('#d_wfd_stagger, #d_wfd_wait, #d_wfd_repeat, #d_wfd_split, #d_wfd_split_filter, #d_wfd_if, #d_wfd_if_abort, #d_wfd_title, #d_wfd_icon, #d_wfd_continue').hide();
2374+
$('#d_wfd_stagger, #d_wfd_wait, #d_wfd_repeat, #d_wfd_split, #d_wfd_split_filter, #d_wfd_split_chunk, #d_wfd_if, #d_wfd_if_abort, #d_wfd_title, #d_wfd_icon, #d_wfd_continue').hide();
23592375

23602376
var type = $('#fe_wfd_type').val();
23612377
switch (type) {
23622378
case 'multiplex': $('#d_wfd_stagger, #d_wfd_continue').show(); break;
23632379
case 'wait': $('#d_wfd_wait').show(); break;
23642380
case 'repeat': $('#d_wfd_repeat, #d_wfd_continue').show(); break;
2365-
case 'split': $('#d_wfd_split, #d_wfd_split_filter, #d_wfd_continue').show(); break;
2381+
case 'split': $('#d_wfd_split, #d_wfd_split_filter, #d_wfd_split_chunk, #d_wfd_continue').show(); break;
23662382
case 'decision': $('#d_wfd_if, #d_wfd_if_abort, #d_wfd_title, #d_wfd_icon').show(); break;
23672383
}
23682384

internal/ui.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,12 +604,16 @@
604604
},
605605
"d_wfd_split": {
606606
"label": "Split Expression:",
607-
"caption": "Specify the path to the array for splitting, using [XYEXP](#Docs/xyexp), e.g. `data.items`. Use the special `files` keyword to split the input files."
607+
"caption": "Specify the path to the array for splitting, using [XYEXP](#Docs/xyexp), e.g. `data.myItems`. Use the special `files` keyword to split the input files."
608608
},
609609
"d_wfd_split_filter": {
610610
"label": "Item Filter:",
611611
"caption": "Optionally enter an expression to filter items, e.g. `item.random > 0.5`. If false, the item will be excluded from the set. [Learn More](#Docs/workflows/split-item-filter)"
612612
},
613+
"d_wfd_split_chunk": {
614+
"label": "Batch Size:",
615+
"caption": "Optionally batch multiple items per job by increasing this value. The default is `1`, meaning each job receives exactly one item. [Learn More](#Docs/workflows/split-batch-size)"
616+
},
613617
"d_wfd_if": {
614618
"label": "Expression:",
615619
"caption": "Enter the expression to evaluate using [XYEXP](#Docs/xyexp), e.g. `data.random > 0.5`. If true, control will pass onto the next node."

lib/workflow.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,25 @@ class Workflows {
721721
return;
722722
}
723723

724+
// optionally chunk the list
725+
var is_chunked = false;
726+
if (node.data.chunk && (node.data.chunk > 1)) {
727+
var chunks = [];
728+
var chunk = [];
729+
730+
value.forEach( function(item) {
731+
chunk.push(item);
732+
if (chunk.length >= node.data.chunk) {
733+
chunks.push(chunk);
734+
chunk = [];
735+
}
736+
});
737+
if (chunk.length) chunks.push(chunk);
738+
739+
value = chunks;
740+
is_chunked = true;
741+
}
742+
724743
// find single node we need to control
725744
var conns = Tools.findObjects( workflow.connections, { source: node.id } );
726745
if (!conns.length) {
@@ -760,12 +779,13 @@ class Workflows {
760779
value.forEach( function(item) {
761780
var overrides = Tools.copyHash(opts.overrides, true);
762781

763-
if (exp == 'files') {
764-
// special mode for splitting on files instead of data
765-
overrides.input.files = [ item ];
782+
if (is_chunked) {
783+
if (exp == 'files') overrides.input.files = [ ...item ];
784+
else overrides.input.data = { items: item };
766785
}
767786
else {
768-
overrides.input.data = { item };
787+
if (exp == 'files') overrides.input.files = [ item ];
788+
else overrides.input.data = { item };
769789
}
770790

771791
self.runWorkflowNode({

0 commit comments

Comments
 (0)