RobotCore 1.0
WPILib port to raspberry Pi
Loading...
Searching...
No Matches
Coroutine.java
1// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5package org.wpilib.command3;
6
7import static org.wpilib.command3.CoroutineState.CANCELED;
8import static org.wpilib.command3.CoroutineState.Canceled;
9import static org.wpilib.command3.CoroutineState.FROZEN;
10import static org.wpilib.command3.CoroutineState.ForkFailed;
11import static org.wpilib.command3.CoroutineState.LIVE;
12import static org.wpilib.command3.CoroutineState.forkFailed;
13import static org.wpilib.command3.CoroutineState.parkedUntil;
14import static org.wpilib.command3.CoroutineState.parkedWhile;
15import static org.wpilib.units.Units.Nanoseconds;
16import static org.wpilib.units.Units.Seconds;
17import static org.wpilib.util.ErrorMessages.requireNonNullParam;
18
19import java.util.ArrayList;
20import java.util.Arrays;
21import java.util.Collection;
22import java.util.List;
23import java.util.function.BooleanSupplier;
24import java.util.function.Consumer;
25import org.wpilib.command3.Scheduler.ScheduleResult;
26import org.wpilib.system.RobotController;
27import org.wpilib.units.measure.Time;
28
35public final class Coroutine {
36 private final Scheduler m_scheduler;
37 private final Continuation m_backingContinuation;
38
39 private boolean m_cancelOnForkFailure = true;
40
41 private CoroutineState m_state = LIVE;
42
43 boolean inUnmountableState() {
44 return !m_state.shouldMount();
45 }
46
55 Coroutine(Scheduler scheduler, ContinuationScope scope, Consumer<Coroutine> callback) {
56 m_scheduler = scheduler;
57 m_backingContinuation = new Continuation(scope, () -> callback.accept(this));
58 }
59
79 public void setCancelOnForkFailure(boolean cancelOnForkFailure) {
80 m_cancelOnForkFailure = cancelOnForkFailure;
81 }
82
91 public boolean isCancelOnForkFailure() {
92 return m_cancelOnForkFailure;
93 }
94
95 // Package-private. User code can never access this because setting the flag happens immediately
96 // before the coroutine yields itself.
97 boolean isInterruptRequested() {
98 // Note that we only get into a ForkFailed state if m_cancelOnForkFailure is true,
99 // so there's no point in checking it here
100 return m_state instanceof ForkFailed;
101 }
102
112 public void requestCancellation() {
113 requireMounted();
114
115 m_state = CANCELED;
116 this.yield();
117 }
118
119 // package-private; setting the flag will immediately yield, so user code can never access this
120 // in a state where it returns anything other than `false`
121 boolean isCancellationRequested() {
122 return m_state instanceof Canceled;
123 }
124
125 ForkResult getForkResult() {
126 if (m_state instanceof ForkFailed(ForkResult failure)) {
127 return failure;
128 } else {
129 return null;
130 }
131 }
132
140 public boolean yield() {
141 requireMounted();
142
143 m_backingContinuation.yield();
144
145 // If the coroutine was picked back up by the scheduler, it is definitionally live.
146 // Reset the state to reflect this.
147 m_state = LIVE;
148 return true;
149 }
150
157 public void park() {
158 requireMounted();
159
160 m_state = FROZEN;
161 this.yield();
162 }
163
172 public final class ForkResult {
173 private final List<ScheduleResult.Successful> m_forkedCommands;
174 private final List<ScheduleResult.Failure> m_failedCommands;
175
176 // private - only the coroutine can construct these
177 private ForkResult(
178 Collection<ScheduleResult.Successful> successes, List<ScheduleResult.Failure> failures) {
179 m_forkedCommands = List.copyOf(successes);
180 m_failedCommands = List.copyOf(failures);
181 }
182
188 public boolean successful() {
189 return m_failedCommands.isEmpty();
190 }
191
198 public boolean failed() {
199 return !successful();
200 }
201
211 public boolean partialSuccess() {
212 return !m_forkedCommands.isEmpty() && !m_failedCommands.isEmpty();
213 }
214
222 public void awaitCompletion() {
223 if (m_forkedCommands.isEmpty()) {
224 // Nothing to do; exit early
225 return;
226 }
227 var tracker = CommandRunTracker.of(m_scheduler, m_forkedCommands);
228 while (tracker.isAnyRunning()) {
229 Coroutine.this.yield();
230 }
231 }
232
240 return m_failedCommands;
241 }
242
250 return m_forkedCommands;
251 }
252 }
253
261 private ForkResult checkAllForkable(Collection<? extends Command> commands) {
262 // Check for user error; there's no reason to fork conflicting commands simultaneously
263 // Because this is a bug in user code, throw an error instead of returning a failure result
265
266 var successes = new ArrayList<ScheduleResult.Successful>();
267 var failures = new ArrayList<ScheduleResult.Failure>();
268 for (var command : commands) {
269 var result = m_scheduler.isSchedulable(command);
270 switch (result) {
271 case ScheduleResult.Successful success -> successes.add(success);
272 case ScheduleResult.Failure failure -> failures.add(failure);
273 }
274 }
275
276 var result = new ForkResult(successes, failures);
277 if (result.failed()) {
278 if (m_cancelOnForkFailure) {
279 // Canceling on fork failure means no coroutine or user code gets to run to handle the
280 // failure result
281 m_state = forkFailed(result);
282 this.yield();
283 // could throw an IllegalStateException, but probably shouldn't crash user code
284 return result;
285 } else {
286 // always return the failure object if self-cancellation is disabled
287 return result;
288 }
289 }
290
291 return result;
292 }
293
314 private ForkResult doFork(Collection<? extends Command> commands) {
315 var successes = new ArrayList<ScheduleResult.Successful>();
316 var fails = new ArrayList<ScheduleResult.Failure>();
317 for (var command : commands) {
318 var result = m_scheduler.schedule(command);
319 switch (result) {
320 case ScheduleResult.Successful s -> successes.add(s);
321 case ScheduleResult.Failure f -> fails.add(f);
322 }
323 }
324 ForkResult result = new ForkResult(successes, fails);
325
326 if (!fails.isEmpty() && m_cancelOnForkFailure) {
327 // At least one of the commands couldn't be scheduled due to runtime behavior that can't be
328 // checked ahead of time by `checkAllForkable`.
329 //
330 // We can't roll back the partial success; the commands that successfully started will have
331 // already affected the state of the robot and may have interrupted previously running
332 // commands, which we can't restore. The two options are to either cancel the entire command
333 // composition - which is the default behavior - or to allow user code to handle the failure
334 // and do something with it (eg trying something else, retrying, etc).
335 m_state = forkFailed(result);
336 this.yield();
337 return result; // note: unreachable
338 }
339
340 return result;
341 }
342
380 public ForkResult fork(Command... commands) {
381 requireMounted();
382
383 requireNonNullParam(commands, "commands", "Coroutine.fork");
384 for (int i = 0; i < commands.length; i++) {
385 requireNonNullParam(commands[i], "commands[" + i + "]", "Coroutine.fork");
386 }
387
388 var forkableCheck = checkAllForkable(Arrays.asList(commands));
389 if (forkableCheck.failed()) {
390 return forkableCheck;
391 }
392
393 return doFork(Arrays.asList(commands));
394 }
395
428 public ForkResult fork(Collection<? extends Command> commands) {
429 return fork(commands.toArray(Command[]::new));
430 }
431
450 public ForkResult await(Command command) {
451 requireMounted();
452
453 requireNonNullParam(command, "command", "Coroutine.await");
454
455 // Don't need doFork() because there's no chance of sibling conflicts
456 var scheduleResult = m_scheduler.schedule(command);
457 switch (scheduleResult) {
458 case ScheduleResult.Successful forked -> {
459 int id = forked.runId();
460 // If the command is a one-shot, then the schedule call will completely execute the command.
461 // runId(command) will return 0 and the loop condition will never be met, so we'd return
462 // immediately without yielding.
463 var cond = parkedWhile(() -> m_scheduler.runId(forked.command()) == id);
464 if (!cond.conditionMet()) {
465 m_state = cond;
466 this.yield();
467 }
468
469 return new ForkResult(List.of(forked), List.of());
470 }
471
472 case ScheduleResult.Failure failed -> {
473 // Failed to fork
474 var result = new ForkResult(List.of(), List.of(failed));
475 if (m_cancelOnForkFailure) {
476 m_state = forkFailed(result);
477 this.yield();
478 return result; // note: unreachable because the scheduler will never remount the coroutine
479 }
480 return result;
481 }
482 }
483 }
484
502 public ForkResult awaitAll(Collection<? extends Command> commands) {
503 requireMounted();
504
505 requireNonNullParam(commands, "commands", "Coroutine.awaitAll");
506 int i = 0;
507 for (Command command : commands) {
508 requireNonNullParam(command, "commands[" + i + "]", "Coroutine.awaitAll");
509 i++;
510 }
511
512 var forkableCheck = checkAllForkable(commands);
513 if (forkableCheck.failed()) {
514 return forkableCheck;
515 }
516
517 var forkResult = doFork(commands);
518 if (forkResult.failed()) {
519 return forkResult;
520 }
521
522 var tracker = CommandRunTracker.of(m_scheduler, forkResult.getForkedCommands());
523 var cond = parkedWhile(tracker::isAnyRunning);
524 if (!cond.conditionMet()) {
525 m_state = cond;
526 this.yield();
527 }
528
529 return forkResult;
530 }
531
549 public ForkResult awaitAll(Command... commands) {
550 return awaitAll(Arrays.asList(commands));
551 }
552
570 public ForkResult awaitAny(Collection<? extends Command> commands) {
571 requireMounted();
572
573 requireNonNullParam(commands, "commands", "Coroutine.awaitAny");
574 int i = 0;
575 for (Command command : commands) {
576 requireNonNullParam(command, "commands[" + i + "]", "Coroutine.awaitAny");
577 i++;
578 }
579
580 var forkableCheck = checkAllForkable(commands);
581 if (forkableCheck.failed()) {
582 return forkableCheck;
583 }
584
585 var forkResult = doFork(commands);
586 if (forkResult.failed()) {
587 return forkResult;
588 }
589
590 var tracker = CommandRunTracker.of(m_scheduler, forkResult.getForkedCommands());
591 var cond = parkedWhile(tracker::areAllRunning);
592 if (!cond.conditionMet()) {
593 m_state = cond;
594 this.yield();
595 }
596
597 // At least one command exited; cancel the rest.
598 // We need to use run IDs here because at least one command exited in the previous cycle.
599 // If that command was immediately rescheduled, that would have happened outside this awaitAny
600 // call and must be allowed to continue. Using a naive `commands.forEach(m_scheduler::cancel)`
601 // loop would cause that rescheduled command to be canceled as well.
602 for (var fork : forkResult.getForkedCommands()) {
603 var command = fork.command();
604 int runId = fork.runId();
605
606 if (m_scheduler.runId(command) == runId) {
607 // Only cancel an original invocation of the command. If it exited early and was later
608 // rescheduled, that rescheduled run must be allowed to continue.
609 m_scheduler.cancel(command);
610 }
611 }
612
613 return forkResult;
614 }
615
633 public ForkResult awaitAny(Command... commands) {
634 return awaitAny(Arrays.asList(commands));
635 }
636
670 public void wait(Time duration) {
671 requireMounted();
672
673 requireNonNullParam(duration, "duration", "Coroutine.wait");
674
675 var cond = parkedUntil(RobotController.getTime() + (long) duration.in(Nanoseconds));
676 if (duration.in(Seconds) > 0) {
677 m_state = cond;
678 this.yield();
679 }
680 }
681
686 public enum WaitResult {
689 @Override
690 public boolean timedOut() {
691 return false;
692 }
693 },
694
696 @Override
697 public boolean timedOut() {
698 return true;
699 }
700 },
701 ;
702
708 public abstract boolean timedOut();
709 }
710
722 public WaitResult waitUntil(BooleanSupplier condition) {
723 requireMounted();
724
725 requireNonNullParam(condition, "condition", "Coroutine.waitUntil");
726
727 var cond = parkedUntil(condition);
728 if (!cond.conditionMet()) {
729 m_state = cond;
730 this.yield();
731 }
732
734 }
735
772 public WaitResult waitUntil(BooleanSupplier condition, Time timeout) {
773 requireMounted();
774
775 requireNonNullParam(condition, "condition", "Coroutine.waitUntil");
776 requireNonNullParam(timeout, "timeout", "Coroutine.waitUntil");
777
778 if (condition.getAsBoolean()) {
780 }
781
782 long targetTimestampNanos = RobotController.getTime() + (long) timeout.in(Nanoseconds);
783 m_state =
784 parkedUntil(
785 () -> condition.getAsBoolean() || RobotController.getTime() >= targetTimestampNanos);
786 this.yield();
787
788 // In case of a tiebreaker where the condition and the timeout are both reached in the same
789 // cycle, let the condition win.
790 if (condition.getAsBoolean()) {
792 } else {
793 return WaitResult.TIMED_OUT;
794 }
795 }
796
807 requireMounted();
808
809 return m_scheduler;
810 }
811
812 private boolean isMounted() {
813 return m_backingContinuation.isMounted();
814 }
815
816 private void requireMounted() {
817 // Note: attempting to yield() outside a command will already throw an error due to the
818 // continuation being unmounted, but other actions like forking and awaiting should also
819 // throw errors. For consistent messaging, we use this helper in all places, not just the
820 // ones that interact with the backing continuation.
821
822 if (isMounted()) {
823 return;
824 }
825
826 throw new IllegalStateException("Coroutines can only be used by the command bound to them");
827 }
828
829 // Package-private for interaction with the scheduler.
830 // These functions are not intended for team use.
831
832 void runToYieldPoint() {
833 m_backingContinuation.run();
834 }
835
836 void mount() {
837 Continuation.mountContinuation(m_backingContinuation);
838 }
839
840 boolean isDone() {
841 return m_backingContinuation.isDone();
842 }
843}
static void throwIfConflicts(Collection<? extends Command > commands)
List< ScheduleResult.Failure > getFailedCommands()
List< ScheduleResult.Successful > getForkedCommands()
ForkResult awaitAll(Collection<? extends Command > commands)
void wait(Time duration)
ForkResult await(Command command)
ForkResult fork(Collection<? extends Command > commands)
ForkResult awaitAll(Command... commands)
void setCancelOnForkFailure(boolean cancelOnForkFailure)
WaitResult waitUntil(BooleanSupplier condition, Time timeout)
ForkResult awaitAny(Command... commands)
ForkResult fork(Command... commands)
WaitResult waitUntil(BooleanSupplier condition)
ForkResult awaitAny(Collection<? extends Command > commands)
ScheduleResult isSchedulable(Command command)