Skip to content

runtime: Enable customization of parallel workers #1588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Feb 20, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6774382
[#1044] allow assignment of work using api hook
eman2673 Feb 25, 2021
a4de83b
[#1044] Using array instead of iterable
eman2673 Feb 25, 2021
9223aba
[#1044] Adding type for parallelCanAssign
eman2673 Feb 25, 2021
587187d
[#1044] Using variable for workers instead of repeating _.values.
eman2673 Feb 25, 2021
eab037e
[#1044] Mansplaining the custom worker assignment process
eman2673 Feb 26, 2021
179b12d
[#1044] Making casing consistent
eman2673 Feb 26, 2021
f501380
Instead of killing the job, make sure at least 1 worker is assigned
eman2673 Feb 26, 2021
13ada90
Detailing example a bit more.
eman2673 Feb 26, 2021
37b23e1
Put close worker back. No longer reused
eman2673 Feb 26, 2021
6654b6e
Utilizing ParallelAssignmentValidator type for ISupportCodeLibrary.pa…
eman2673 Feb 26, 2021
ffac8c5
Moving idle state to worker ready message
eman2673 Feb 26, 2021
981574c
[#1044] Emitting warning for all workers idle
eman2673 Mar 9, 2021
381d5aa
Resolving some minor README issues
eman2673 Mar 9, 2021
628e90a
[#1044] Refactoring out nextPickleIndex + README example as test
eman2673 Mar 11, 2021
ef33a9a
Omitting complex 3 cause cannot guaranty the worker as both will be r…
eman2673 Mar 11, 2021
b085009
Dropping use of _.values to iterate for waking workers
eman2673 Mar 11, 2021
189a1d5
copy the pickleIds to leave the passed argument intact
eman2673 Mar 11, 2021
e512a2b
Parsing test cases to verify order and parallelism
eman2673 Mar 11, 2021
7cfb9b3
Using spawn tag to get errorOutput for warning validation
eman2673 Apr 1, 2021
d173e64
Merge pull request #1 from cucumber/master
eman2673 Apr 2, 2021
5634903
Merge pull request #2 from cucumber/master
eman2673 Apr 20, 2021
40290f3
Merging from cucumber-main
eman2673 May 21, 2021
865894f
Merge branch 'cucumber-main'
eman2673 May 21, 2021
cddc5a5
Simplify tests (#4)
eman2673 May 21, 2021
753a5e6
Resolve conflicts (#5)
eman2673 Jan 28, 2022
50a9f62
merge main
davidjgoss Jan 29, 2022
248d45a
Merge branch 'main' into master
eman2673 Feb 5, 2022
975cf41
Merge branch 'main' into master
aurelien-reeves Feb 10, 2022
15c0fe5
update feature helper
charlierudolph Feb 16, 2022
ba96d2a
change table structure
charlierudolph Feb 16, 2022
5ae125c
reduce timeout, attempt to make more reliable on windows
charlierudolph Feb 16, 2022
6e4f781
fix timing issue, larger time window to reduce flakes
charlierudolph Feb 16, 2022
bc66b40
Merge branch 'main' into master
davidjgoss Feb 18, 2022
5426a09
Update CHANGELOG.md
davidjgoss Feb 18, 2022
23339e9
update docs
charlierudolph Feb 19, 2022
10a88f2
Merge branch 'master' of github.com:eman2673/cucumber-js into eman267…
charlierudolph Feb 19, 2022
4187469
increase time to decrease chance of flakes
charlierudolph Feb 19, 2022
d70c4df
Merge branch 'main' into master
davidjgoss Feb 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions features/parallel_custom_assign.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
Feature: Running scenarios in parallel with custom assignment

@spawn
Scenario: Bad parallel assignment helper uses 1 worker
Given a file named "features/step_definitions/cucumber_steps.js" with:
"""
const {Given, setParallelCanAssign} = require('@cucumber/cucumber')

setParallelCanAssign(() => false)

Given('slow step', (done) => setTimeout(done, 100))
"""
And a file named "features/a.feature" with:
"""
Feature: only one worker works
Scenario: someone must do work
Given slow step

Scenario: even if it's all the work
Given slow step
"""
When I run cucumber-js with `--parallel 2`
Then the error output contains the text:
"""
WARNING: All workers went idle 2 time(s). Consider revising handler passed to setParallelCanAssign.
"""
And no pickles run at the same time

Scenario: assignment is appropriately applied
Given a file named "features/step_definitions/cucumber_steps.js" with:
"""
const {Given, setParallelCanAssign} = require('@cucumber/cucumber')
const {atMostOnePicklePerTag} = require('@cucumber/cucumber/lib/support_code_library_builder/parallel_can_assign_helpers')

setParallelCanAssign(atMostOnePicklePerTag(["@complex", "@simple"]))

Given('complex step', (done) => setTimeout(done, 325))
Given('simple step', (done) => setTimeout(done, 200))
"""
And a file named "features/a.feature" with:
"""
Feature: adheres to setParallelCanAssign handler
@complex
Scenario: complex1
Given complex step

@complex
Scenario: complex2
Given complex step

@complex
Scenario: complex3
Given complex step

@simple
Scenario: simple1
Given simple step

@simple
Scenario: simple2
Given simple step

@simple
Scenario: simple3
Given simple step
"""
When I run cucumber-js with `--parallel 2`
Then it passes
And the following pairs of pickles execute at the same time:
| complex1 | simple1 |
| complex1 | simple2 |
| simple2 | complex2 |
| complex2 | simple3 |
53 changes: 53 additions & 0 deletions features/step_definitions/parallel_steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { DataTable, Then } from '../../'
import { World } from '../support/world'
import messages from '@cucumber/messages'
import { expect } from 'chai'

function getPairsOfPicklesRunningAtTheSameTime(
envelopes: messages.Envelope[]
): string[][] {
const pickleIdToName: Record<string, string> = {}
const testCaseIdToPickleId: Record<string, string> = {}
const testCaseStarteIdToPickleId: Record<string, string> = {}
let currentRunningPickleIds: string[] = []
const result: string[][] = []
envelopes.forEach((envelope) => {
if (envelope.pickle != null) {
pickleIdToName[envelope.pickle.id] = envelope.pickle.name
} else if (envelope.testCase != null) {
testCaseIdToPickleId[envelope.testCase.id] = envelope.testCase.pickleId
} else if (envelope.testCaseStarted != null) {
const pickleId = testCaseIdToPickleId[envelope.testCaseStarted.testCaseId]
testCaseStarteIdToPickleId[envelope.testCaseStarted.id] = pickleId
currentRunningPickleIds.forEach((x) => {
result.push([pickleIdToName[x], pickleIdToName[pickleId]])
})
currentRunningPickleIds.push(pickleId)
} else if (envelope.testCaseFinished != null) {
const pickleId =
testCaseStarteIdToPickleId[envelope.testCaseFinished.testCaseStartedId]
currentRunningPickleIds = currentRunningPickleIds.filter(
(x) => x != pickleId
)
}
})
return result
}

Then('no pickles run at the same time', function (this: World) {
const actualPairs = getPairsOfPicklesRunningAtTheSameTime(
this.lastRun.envelopes
)
expect(actualPairs).to.eql([])
})

Then(
'the following pairs of pickles execute at the same time:',
function (this: World, dataTable: DataTable) {
const expectedPairs = dataTable.raw()
const actualPairs = getPairsOfPicklesRunningAtTheSameTime(
this.lastRun.envelopes
)
expect(actualPairs).to.eql(expectedPairs)
}
)
1 change: 1 addition & 0 deletions src/cli/helpers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function testEmitSupportCodeMessages(
parameterTypeRegistry: new ParameterTypeRegistry(),
undefinedParameterTypes: [],
World: null,
parallelCanAssign: () => true,
},
supportCode
),
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const Given = methods.Given
export const setDefaultTimeout = methods.setDefaultTimeout
export const setDefinitionFunctionWrapper = methods.setDefinitionFunctionWrapper
export const setWorldConstructor = methods.setWorldConstructor
export const setParallelCanAssign = methods.setParallelCanAssign
export const Then = methods.Then
export const When = methods.When
export {
Expand Down
50 changes: 49 additions & 1 deletion src/runtime/parallel/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,59 @@
Parallelization is achieved by having multiple child processes running scenarios.

#### Customizable work assignment
Cucumber exposes customization of worker assignment via `setParallelCanAssign`.
The example below overrides the default, `() => true` which processes test cases
indiscriminately, with a scheme that accepts untagged test cases as well as test cases
where the first tag doesn't match the first tag of any in progress tests.

```typescript
import { setParallelCanAssign } from '@cucumber/cucumber'
// Accept tests missing tags or no test is running having the same first tag
setParallelCanAssign((pickleInQuestion, picklesInProgress) => _.isEmpty(pickleInQuestion.tags)
|| _.every(picklesInProgress, ({tags}) => _.isEmpty(tags) || tags[0].name !== pickleInQuestion.tags[0].name))
```
* Example using the handler above
* 2 workers, `A` and `B`
* Scenarios tagged as `@simple` (2 secs) or `@complex` (3 secs)
* The first tag of the scenarios: `[@complex, @complex, @complex, @simple, @simple, @simple]`

| Time | WIP | Events |
|---|---|---|
| 0 | | assigned `1 (@complex)` to `worker A` |
| 0 | `@complex` | skip `2 & 3 (@complex)` - assign `4 (@simple)` to `worker B` |
| 2 | `@complex` | skip `2 & 3 (@complex)` - assign `5 (@simple)` to `worker B` |
| 3 | `@simple` | assign `2 (@complex)` to `worker A` |
| 4 | `@complex` | skip `3 (@complex)` - assign `6 (@simple)` to `worker B` |
| 6 | | assign `3 (@complex)` to `worker A` |
| 9 | | done |


#### Note
The coordinator doesn't reorder work as it skips un-assignable tests. Also, it always
returns to the beginning of the unprocessed list when attempting to make assignments
to an idle worker. If there was a worker C in the example above, assignment to worker B
would skip 2 & 3 as shown; then assignment to worker C would also skip 2 & 3 upon
checking the test cases against the handler to determine if they have become assignable.

Custom work assignment prioritizes your definition of assignable work over efficiency.
The exception to this rule is if all remaining work is un-assignable, such that all
workers are idle. In this case Cucumber assigns the next test to the first worker
before continuing to utilize the handler to determine assignable work. Workers become
idle after checking all remaining test cases against the handler. Assignment is
attempted on all idle workers when a busy worker becomes `ready`.

#### Coordinator
- load all features, generate test cases
- broadcast `test-run-started`
- create workers and for each worker
- send an `initialize` command
- when a worker outputs a `ready` command, send it a `run` command with a test case. If there are no more test cases, send a `finalize` command
- when a worker outputs a `ready` command
- if there are no more test cases, send a `finalize` command
- identify the next processable test case (the next test by default)
- when there are no processable test cases all idle workers remain idle
- send a `run` command with the test case to an idle worker
- repeat if there are still idle workers
- if all workers become idle and there are more tests, process the next test case
- when a worker outputs an `event` command,
broadcast the event to the formatters,
and on `test-case-finished` update the overall result
Expand Down
Loading