Make job completion order deterministic, improve tests - #42
Conversation
When multiple jobs complete in the same "tick" of the parallel execution loop, the `done_futures` set can result in different colcon return codes depending on which one happens to get processed first. A more desirable behavior is to prefer the return code of the job which was started first. While it is currently unlikely to see multiple jobs finish in the same tick due to strict concurrency pre-limiting, this becomes much more common under dynamic scheduling and resource-guarded execution. This change also includes an additional test for the parallel executor, and switches all of the job identifiers to use consistent identifiers, where they were previously using different values in the overall job dictionary from the identifier used on the job objects themselves. Assisted-by: Gemini 3.5 Flash <gemini@google.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #42 +/- ##
==========================================
+ Coverage 96.73% 97.38% +0.65%
==========================================
Files 4 4
Lines 153 153
Branches 41 41
==========================================
+ Hits 148 149 +1
Misses 1 1
+ Partials 4 3 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
I'll review this so add me as reviewer @knmcguire |
knmcguire
left a comment
There was a problem hiding this comment.
Should we also have a test on the first addition you made?
Claude Opus 5.1 generated this:
class ImmediateJob(Job):
def __init__(self, identifier, rc):
super().__init__(
identifier=identifier, dependencies=set(), task=None,
task_context=None)
self.rc = rc
async def __call__(self, *args, **kwargs):
return self.rc
def test_simultaneous_completion_prefers_first_started():
extension = ParallelExecutorExtension()
args = SimpleNamespace(parallel_workers=2)
# repeat: set iteration order varies run to run, so a single
# pass would only catch the bug ~50% of the time
for _ in range(50):
jobs = OrderedDict()
jobs['early'] = ImmediateJob('early', 3)
jobs['late'] = ImmediateJob('late', 7)
rc = extension.execute(args, jobs, on_error=OnError.continue_)
assert rc == 3, f'expected first-started rc 3, got {rc}'
I've tested it out on the current master and the patch, and indeed it passes on your fix.
FYI, I had to use Claude Opus 5.1 to check out a couple of assumptions for the review as here but I tested off of the changes myself.
| # check results of done futures | ||
| for done_future in done_futures: | ||
| for done_future in [ | ||
| f for f in futures.keys() if f in done_futures |
There was a problem hiding this comment.
lgtm! If any done_future is not in future keys, this is silently dropped. Which is perhaps what we want? before we got an error in the next line
| args = SimpleNamespace(parallel_workers=2) | ||
| jobs = OrderedDict() | ||
| jobs['one'] = Job1() | ||
| jobs['job1'] = Job1() |
There was a problem hiding this comment.
Interestingly, it seems that these renames actually has an unexpected effect with the dependency checker that before would just give everything the same priority, namely this one, since one/two/three didn't match the keys job1, job2:
colcon-parallel-executor/colcon_parallel_executor/executor/parallel.py
Lines 116 to 121 in 94dfeff
I believe that now that the scheduler actually gives a priority to the jobs that others depend on.
I do wonder if that will interfere with the behavior of having first initialized jobs finish first.
Anyway, seems harmless but just something to keep in mind. Otherwise I'm always for consistency!
When multiple jobs complete in the same "tick" of the parallel execution loop, the
done_futuresset can result in different colcon return codes depending on which one happens to get processed first. A more desirable behavior is to prefer the return code of the job which was started first.While it is currently unlikely to see multiple jobs finish in the same tick due to strict concurrency pre-limiting, this becomes much more common under dynamic scheduling and resource-guarded execution.
This change also includes an additional test for the parallel executor, and switches all of the job identifiers to use consistent identifiers, where they were previously using different values in the overall job dictionary from the identifier used on the job objects themselves.
Assisted-by: Gemini 3.5 Flash