001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.wpilibj2.command;
006
007import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
008
009import edu.wpi.first.hal.FRCNetComm.tInstances;
010import edu.wpi.first.hal.FRCNetComm.tResourceType;
011import edu.wpi.first.hal.HAL;
012import edu.wpi.first.util.sendable.Sendable;
013import edu.wpi.first.util.sendable.SendableBuilder;
014import edu.wpi.first.util.sendable.SendableRegistry;
015import edu.wpi.first.wpilibj.DriverStation;
016import edu.wpi.first.wpilibj.RobotBase;
017import edu.wpi.first.wpilibj.RobotState;
018import edu.wpi.first.wpilibj.TimedRobot;
019import edu.wpi.first.wpilibj.Watchdog;
020import edu.wpi.first.wpilibj.event.EventLoop;
021import edu.wpi.first.wpilibj.livewindow.LiveWindow;
022import edu.wpi.first.wpilibj2.command.Command.InterruptionBehavior;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.Iterator;
027import java.util.LinkedHashMap;
028import java.util.LinkedHashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Optional;
032import java.util.Set;
033import java.util.WeakHashMap;
034import java.util.function.BiConsumer;
035import java.util.function.Consumer;
036
037/**
038 * The scheduler responsible for running {@link Command}s. A Command-based robot should call {@link
039 * CommandScheduler#run()} on the singleton instance in its periodic block in order to run commands
040 * synchronously from the main loop. Subsystems should be registered with the scheduler using {@link
041 * CommandScheduler#registerSubsystem(Subsystem...)} in order for their {@link Subsystem#periodic()}
042 * methods to be called and for their default commands to be scheduled.
043 *
044 * <p>This class is provided by the NewCommands VendorDep
045 */
046public final class CommandScheduler implements Sendable, AutoCloseable {
047  /** The Singleton Instance. */
048  private static CommandScheduler instance;
049
050  /**
051   * Returns the Scheduler instance.
052   *
053   * @return the instance
054   */
055  public static synchronized CommandScheduler getInstance() {
056    if (instance == null) {
057      instance = new CommandScheduler();
058    }
059    return instance;
060  }
061
062  private static final Optional<Command> kNoInterruptor = Optional.empty();
063
064  private final Map<Command, Exception> m_composedCommands = new WeakHashMap<>();
065
066  // A set of the currently-running commands.
067  private final Set<Command> m_scheduledCommands = new LinkedHashSet<>();
068
069  // A map from required subsystems to their requiring commands. Also used as a set of the
070  // currently-required subsystems.
071  private final Map<Subsystem, Command> m_requirements = new LinkedHashMap<>();
072
073  // A map from subsystems registered with the scheduler to their default commands.  Also used
074  // as a list of currently-registered subsystems.
075  private final Map<Subsystem, Command> m_subsystems = new LinkedHashMap<>();
076
077  private final EventLoop m_defaultButtonLoop = new EventLoop();
078  // The set of currently-registered buttons that will be polled every iteration.
079  private EventLoop m_activeButtonLoop = m_defaultButtonLoop;
080
081  private boolean m_disabled;
082
083  // Lists of user-supplied actions to be executed on scheduling events for every command.
084  private final List<Consumer<Command>> m_initActions = new ArrayList<>();
085  private final List<Consumer<Command>> m_executeActions = new ArrayList<>();
086  private final List<BiConsumer<Command, Optional<Command>>> m_interruptActions = new ArrayList<>();
087  private final List<Consumer<Command>> m_finishActions = new ArrayList<>();
088
089  // Flag and queues for avoiding ConcurrentModificationException if commands are
090  // scheduled/canceled during run
091  private boolean m_inRunLoop;
092  private final Set<Command> m_toSchedule = new LinkedHashSet<>();
093  private final List<Command> m_toCancelCommands = new ArrayList<>();
094  private final List<Optional<Command>> m_toCancelInterruptors = new ArrayList<>();
095  private final Set<Command> m_endingCommands = new LinkedHashSet<>();
096
097  private final Watchdog m_watchdog = new Watchdog(TimedRobot.kDefaultPeriod, () -> {});
098
099  CommandScheduler() {
100    HAL.report(tResourceType.kResourceType_Command, tInstances.kCommand2_Scheduler);
101    SendableRegistry.addLW(this, "Scheduler");
102    LiveWindow.setEnabledListener(
103        () -> {
104          disable();
105          cancelAll();
106        });
107    LiveWindow.setDisabledListener(this::enable);
108  }
109
110  /**
111   * Changes the period of the loop overrun watchdog. This should be kept in sync with the
112   * TimedRobot period.
113   *
114   * @param period Period in seconds.
115   */
116  public void setPeriod(double period) {
117    m_watchdog.setTimeout(period);
118  }
119
120  @Override
121  public void close() {
122    SendableRegistry.remove(this);
123    LiveWindow.setEnabledListener(null);
124    LiveWindow.setDisabledListener(null);
125  }
126
127  /**
128   * Get the default button poll.
129   *
130   * @return a reference to the default {@link EventLoop} object polling buttons.
131   */
132  public EventLoop getDefaultButtonLoop() {
133    return m_defaultButtonLoop;
134  }
135
136  /**
137   * Get the active button poll.
138   *
139   * @return a reference to the current {@link EventLoop} object polling buttons.
140   */
141  public EventLoop getActiveButtonLoop() {
142    return m_activeButtonLoop;
143  }
144
145  /**
146   * Replace the button poll with another one.
147   *
148   * @param loop the new button polling loop object.
149   */
150  public void setActiveButtonLoop(EventLoop loop) {
151    m_activeButtonLoop =
152        requireNonNullParam(loop, "loop", "CommandScheduler" + ".replaceButtonEventLoop");
153  }
154
155  /**
156   * Initializes a given command, adds its requirements to the list, and performs the init actions.
157   *
158   * @param command The command to initialize
159   * @param requirements The command requirements
160   */
161  private void initCommand(Command command, Set<Subsystem> requirements) {
162    m_scheduledCommands.add(command);
163    for (Subsystem requirement : requirements) {
164      m_requirements.put(requirement, command);
165    }
166    command.initialize();
167    for (Consumer<Command> action : m_initActions) {
168      action.accept(command);
169    }
170
171    m_watchdog.addEpoch(command.getName() + ".initialize()");
172  }
173
174  /**
175   * Schedules a command for execution. Does nothing if the command is already scheduled. If a
176   * command's requirements are not available, it will only be started if all the commands currently
177   * using those requirements have been scheduled as interruptible. If this is the case, they will
178   * be interrupted and the command will be scheduled.
179   *
180   * @param command the command to schedule. If null, no-op.
181   */
182  private void schedule(Command command) {
183    if (command == null) {
184      DriverStation.reportWarning("Tried to schedule a null command", true);
185      return;
186    }
187    if (m_inRunLoop) {
188      m_toSchedule.add(command);
189      return;
190    }
191
192    requireNotComposed(command);
193
194    // Do nothing if the scheduler is disabled, the robot is disabled and the command doesn't
195    // run when disabled, or the command is already scheduled.
196    if (m_disabled
197        || isScheduled(command)
198        || RobotState.isDisabled() && !command.runsWhenDisabled()) {
199      return;
200    }
201
202    Set<Subsystem> requirements = command.getRequirements();
203
204    // Schedule the command if the requirements are not currently in-use.
205    if (Collections.disjoint(m_requirements.keySet(), requirements)) {
206      initCommand(command, requirements);
207    } else {
208      // Else check if the requirements that are in use have all have interruptible commands,
209      // and if so, interrupt those commands and schedule the new command.
210      for (Subsystem requirement : requirements) {
211        Command requiring = requiring(requirement);
212        if (requiring != null
213            && requiring.getInterruptionBehavior() == InterruptionBehavior.kCancelIncoming) {
214          return;
215        }
216      }
217      for (Subsystem requirement : requirements) {
218        Command requiring = requiring(requirement);
219        if (requiring != null) {
220          cancel(requiring, Optional.of(command));
221        }
222      }
223      initCommand(command, requirements);
224    }
225  }
226
227  /**
228   * Schedules multiple commands for execution. Does nothing for commands already scheduled.
229   *
230   * @param commands the commands to schedule. No-op on null.
231   */
232  public void schedule(Command... commands) {
233    for (Command command : commands) {
234      schedule(command);
235    }
236  }
237
238  /**
239   * Runs a single iteration of the scheduler. The execution occurs in the following order:
240   *
241   * <p>Subsystem periodic methods are called.
242   *
243   * <p>Button bindings are polled, and new commands are scheduled from them.
244   *
245   * <p>Currently-scheduled commands are executed.
246   *
247   * <p>End conditions are checked on currently-scheduled commands, and commands that are finished
248   * have their end methods called and are removed.
249   *
250   * <p>Any subsystems not being used as requirements have their default methods started.
251   */
252  public void run() {
253    if (m_disabled) {
254      return;
255    }
256    m_watchdog.reset();
257
258    // Run the periodic method of all registered subsystems.
259    for (Subsystem subsystem : m_subsystems.keySet()) {
260      subsystem.periodic();
261      if (RobotBase.isSimulation()) {
262        subsystem.simulationPeriodic();
263      }
264      m_watchdog.addEpoch(subsystem.getName() + ".periodic()");
265    }
266
267    // Cache the active instance to avoid concurrency problems if setActiveLoop() is called from
268    // inside the button bindings.
269    EventLoop loopCache = m_activeButtonLoop;
270    // Poll buttons for new commands to add.
271    loopCache.poll();
272    m_watchdog.addEpoch("buttons.run()");
273
274    m_inRunLoop = true;
275    boolean isDisabled = RobotState.isDisabled();
276    // Run scheduled commands, remove finished commands.
277    for (Iterator<Command> iterator = m_scheduledCommands.iterator(); iterator.hasNext(); ) {
278      Command command = iterator.next();
279
280      if (isDisabled && !command.runsWhenDisabled()) {
281        cancel(command, kNoInterruptor);
282        continue;
283      }
284
285      command.execute();
286      for (Consumer<Command> action : m_executeActions) {
287        action.accept(command);
288      }
289      m_watchdog.addEpoch(command.getName() + ".execute()");
290      if (command.isFinished()) {
291        m_endingCommands.add(command);
292        command.end(false);
293        for (Consumer<Command> action : m_finishActions) {
294          action.accept(command);
295        }
296        m_endingCommands.remove(command);
297        iterator.remove();
298
299        m_requirements.keySet().removeAll(command.getRequirements());
300        m_watchdog.addEpoch(command.getName() + ".end(false)");
301      }
302    }
303    m_inRunLoop = false;
304
305    // Schedule/cancel commands from queues populated during loop
306    for (Command command : m_toSchedule) {
307      schedule(command);
308    }
309
310    for (int i = 0; i < m_toCancelCommands.size(); i++) {
311      cancel(m_toCancelCommands.get(i), m_toCancelInterruptors.get(i));
312    }
313
314    m_toSchedule.clear();
315    m_toCancelCommands.clear();
316    m_toCancelInterruptors.clear();
317
318    // Add default commands for un-required registered subsystems.
319    for (Map.Entry<Subsystem, Command> subsystemCommand : m_subsystems.entrySet()) {
320      if (!m_requirements.containsKey(subsystemCommand.getKey())
321          && subsystemCommand.getValue() != null) {
322        schedule(subsystemCommand.getValue());
323      }
324    }
325
326    m_watchdog.disable();
327    if (m_watchdog.isExpired()) {
328      System.out.println("CommandScheduler loop overrun");
329      m_watchdog.printEpochs();
330    }
331  }
332
333  /**
334   * Registers subsystems with the scheduler. This must be called for the subsystem's periodic block
335   * to run when the scheduler is run, and for the subsystem's default command to be scheduled. It
336   * is recommended to call this from the constructor of your subsystem implementations.
337   *
338   * @param subsystems the subsystem to register
339   */
340  public void registerSubsystem(Subsystem... subsystems) {
341    for (Subsystem subsystem : subsystems) {
342      if (subsystem == null) {
343        DriverStation.reportWarning("Tried to register a null subsystem", true);
344        continue;
345      }
346      if (m_subsystems.containsKey(subsystem)) {
347        DriverStation.reportWarning("Tried to register an already-registered subsystem", true);
348        continue;
349      }
350      m_subsystems.put(subsystem, null);
351    }
352  }
353
354  /**
355   * Un-registers subsystems with the scheduler. The subsystem will no longer have its periodic
356   * block called, and will not have its default command scheduled.
357   *
358   * @param subsystems the subsystem to un-register
359   */
360  public void unregisterSubsystem(Subsystem... subsystems) {
361    m_subsystems.keySet().removeAll(Set.of(subsystems));
362  }
363
364  /**
365   * Un-registers all registered Subsystems with the scheduler. All currently registered subsystems
366   * will no longer have their periodic block called, and will not have their default command
367   * scheduled.
368   */
369  public void unregisterAllSubsystems() {
370    m_subsystems.clear();
371  }
372
373  /**
374   * Sets the default command for a subsystem. Registers that subsystem if it is not already
375   * registered. Default commands will run whenever there is no other command currently scheduled
376   * that requires the subsystem. Default commands should be written to never end (i.e. their {@link
377   * Command#isFinished()} method should return false), as they would simply be re-scheduled if they
378   * do. Default commands must also require their subsystem.
379   *
380   * @param subsystem the subsystem whose default command will be set
381   * @param defaultCommand the default command to associate with the subsystem
382   */
383  public void setDefaultCommand(Subsystem subsystem, Command defaultCommand) {
384    if (subsystem == null) {
385      DriverStation.reportWarning("Tried to set a default command for a null subsystem", true);
386      return;
387    }
388    if (defaultCommand == null) {
389      DriverStation.reportWarning("Tried to set a null default command", true);
390      return;
391    }
392
393    requireNotComposed(defaultCommand);
394
395    if (!defaultCommand.getRequirements().contains(subsystem)) {
396      throw new IllegalArgumentException("Default commands must require their subsystem!");
397    }
398
399    if (defaultCommand.getInterruptionBehavior() == InterruptionBehavior.kCancelIncoming) {
400      DriverStation.reportWarning(
401          "Registering a non-interruptible default command!\n"
402              + "This will likely prevent any other commands from requiring this subsystem.",
403          true);
404      // Warn, but allow -- there might be a use case for this.
405    }
406
407    m_subsystems.put(subsystem, defaultCommand);
408  }
409
410  /**
411   * Removes the default command for a subsystem. The current default command will run until another
412   * command is scheduled that requires the subsystem, at which point the current default command
413   * will not be re-scheduled.
414   *
415   * @param subsystem the subsystem whose default command will be removed
416   */
417  public void removeDefaultCommand(Subsystem subsystem) {
418    if (subsystem == null) {
419      DriverStation.reportWarning("Tried to remove a default command for a null subsystem", true);
420      return;
421    }
422
423    m_subsystems.put(subsystem, null);
424  }
425
426  /**
427   * Gets the default command associated with this subsystem. Null if this subsystem has no default
428   * command associated with it.
429   *
430   * @param subsystem the subsystem to inquire about
431   * @return the default command associated with the subsystem
432   */
433  public Command getDefaultCommand(Subsystem subsystem) {
434    return m_subsystems.get(subsystem);
435  }
436
437  /**
438   * Cancels commands. The scheduler will only call {@link Command#end(boolean)} method of the
439   * canceled command with {@code true}, indicating they were canceled (as opposed to finishing
440   * normally).
441   *
442   * <p>Commands will be canceled regardless of {@link InterruptionBehavior interruption behavior}.
443   *
444   * @param commands the commands to cancel
445   */
446  public void cancel(Command... commands) {
447    for (Command command : commands) {
448      cancel(command, kNoInterruptor);
449    }
450  }
451
452  /**
453   * Cancels a command. The scheduler will only call {@link Command#end(boolean)} method of the
454   * canceled command with {@code true}, indicating they were canceled (as opposed to finishing
455   * normally).
456   *
457   * <p>Commands will be canceled regardless of {@link InterruptionBehavior interruption behavior}.
458   *
459   * @param command the command to cancel
460   * @param interruptor the interrupting command, if any
461   */
462  private void cancel(Command command, Optional<Command> interruptor) {
463    if (command == null) {
464      DriverStation.reportWarning("Tried to cancel a null command", true);
465      return;
466    }
467    if (m_endingCommands.contains(command)) {
468      return;
469    }
470    if (m_inRunLoop) {
471      m_toCancelCommands.add(command);
472      m_toCancelInterruptors.add(interruptor);
473      return;
474    }
475    if (!isScheduled(command)) {
476      return;
477    }
478
479    m_endingCommands.add(command);
480    command.end(true);
481    for (BiConsumer<Command, Optional<Command>> action : m_interruptActions) {
482      action.accept(command, interruptor);
483    }
484    m_endingCommands.remove(command);
485    m_scheduledCommands.remove(command);
486    m_requirements.keySet().removeAll(command.getRequirements());
487    m_watchdog.addEpoch(command.getName() + ".end(true)");
488  }
489
490  /** Cancels all commands that are currently scheduled. */
491  public void cancelAll() {
492    // Copy to array to avoid concurrent modification.
493    cancel(m_scheduledCommands.toArray(new Command[0]));
494  }
495
496  /**
497   * Whether the given commands are running. Note that this only works on commands that are directly
498   * scheduled by the scheduler; it will not work on commands inside compositions, as the scheduler
499   * does not see them.
500   *
501   * @param commands the command to query
502   * @return whether the command is currently scheduled
503   */
504  public boolean isScheduled(Command... commands) {
505    return m_scheduledCommands.containsAll(Set.of(commands));
506  }
507
508  /**
509   * Returns the command currently requiring a given subsystem. Null if no command is currently
510   * requiring the subsystem
511   *
512   * @param subsystem the subsystem to be inquired about
513   * @return the command currently requiring the subsystem, or null if no command is currently
514   *     scheduled
515   */
516  public Command requiring(Subsystem subsystem) {
517    return m_requirements.get(subsystem);
518  }
519
520  /** Disables the command scheduler. */
521  public void disable() {
522    m_disabled = true;
523  }
524
525  /** Enables the command scheduler. */
526  public void enable() {
527    m_disabled = false;
528  }
529
530  /**
531   * Adds an action to perform on the initialization of any command by the scheduler.
532   *
533   * @param action the action to perform
534   */
535  public void onCommandInitialize(Consumer<Command> action) {
536    m_initActions.add(requireNonNullParam(action, "action", "onCommandInitialize"));
537  }
538
539  /**
540   * Adds an action to perform on the execution of any command by the scheduler.
541   *
542   * @param action the action to perform
543   */
544  public void onCommandExecute(Consumer<Command> action) {
545    m_executeActions.add(requireNonNullParam(action, "action", "onCommandExecute"));
546  }
547
548  /**
549   * Adds an action to perform on the interruption of any command by the scheduler.
550   *
551   * @param action the action to perform
552   */
553  public void onCommandInterrupt(Consumer<Command> action) {
554    requireNonNullParam(action, "action", "onCommandInterrupt");
555    m_interruptActions.add((command, interruptor) -> action.accept(command));
556  }
557
558  /**
559   * Adds an action to perform on the interruption of any command by the scheduler. The action
560   * receives the interrupted command and an Optional containing the interrupting command, or
561   * Optional.empty() if it was not canceled by a command (e.g., by {@link
562   * CommandScheduler#cancel}).
563   *
564   * @param action the action to perform
565   */
566  public void onCommandInterrupt(BiConsumer<Command, Optional<Command>> action) {
567    m_interruptActions.add(requireNonNullParam(action, "action", "onCommandInterrupt"));
568  }
569
570  /**
571   * Adds an action to perform on the finishing of any command by the scheduler.
572   *
573   * @param action the action to perform
574   */
575  public void onCommandFinish(Consumer<Command> action) {
576    m_finishActions.add(requireNonNullParam(action, "action", "onCommandFinish"));
577  }
578
579  /**
580   * Register commands as composed. An exception will be thrown if these commands are scheduled
581   * directly or added to a composition.
582   *
583   * @param commands the commands to register
584   * @throws IllegalArgumentException if the given commands have already been composed, or the array
585   *     of commands has duplicates.
586   */
587  public void registerComposedCommands(Command... commands) {
588    Set<Command> commandSet;
589    try {
590      commandSet = Set.of(commands);
591    } catch (IllegalArgumentException e) {
592      throw new IllegalArgumentException(
593          "Cannot compose a command twice in the same composition! (Original exception: "
594              + e
595              + ")");
596    }
597    requireNotComposedOrScheduled(commandSet);
598    var exception = new Exception("Originally composed at:");
599    exception.fillInStackTrace();
600    for (var command : commands) {
601      m_composedCommands.put(command, exception);
602    }
603  }
604
605  /**
606   * Clears the list of composed commands, allowing all commands to be freely used again.
607   *
608   * <p>WARNING: Using this haphazardly can result in unexpected/undesirable behavior. Do not use
609   * this unless you fully understand what you are doing.
610   */
611  public void clearComposedCommands() {
612    m_composedCommands.clear();
613  }
614
615  /**
616   * Removes a single command from the list of composed commands, allowing it to be freely used
617   * again.
618   *
619   * <p>WARNING: Using this haphazardly can result in unexpected/undesirable behavior. Do not use
620   * this unless you fully understand what you are doing.
621   *
622   * @param command the command to remove from the list of grouped commands
623   */
624  public void removeComposedCommand(Command command) {
625    m_composedCommands.remove(command);
626  }
627
628  /**
629   * Requires that the specified command hasn't already been added to a composition.
630   *
631   * @param commands The commands to check
632   * @throws IllegalArgumentException if the given commands have already been composed.
633   */
634  public void requireNotComposed(Command... commands) {
635    for (var command : commands) {
636      var exception = m_composedCommands.getOrDefault(command, null);
637      if (exception != null) {
638        throw new IllegalArgumentException(
639            "Commands that have been composed may not be added to another composition or scheduled "
640                + "individually!",
641            exception);
642      }
643    }
644  }
645
646  /**
647   * Requires that the specified commands have not already been added to a composition.
648   *
649   * @param commands The commands to check
650   * @throws IllegalArgumentException if the given commands have already been composed.
651   */
652  public void requireNotComposed(Collection<Command> commands) {
653    requireNotComposed(commands.toArray(Command[]::new));
654  }
655
656  /**
657   * Requires that the specified command hasn't already been added to a composition, and is not
658   * currently scheduled.
659   *
660   * @param command The command to check
661   * @throws IllegalArgumentException if the given command has already been composed or scheduled.
662   */
663  public void requireNotComposedOrScheduled(Command command) {
664    if (isScheduled(command)) {
665      throw new IllegalArgumentException(
666          "Commands that have been scheduled individually may not be added to a composition!");
667    }
668    requireNotComposed(command);
669  }
670
671  /**
672   * Requires that the specified commands have not already been added to a composition, and are not
673   * currently scheduled.
674   *
675   * @param commands The commands to check
676   * @throws IllegalArgumentException if the given commands have already been composed or scheduled.
677   */
678  public void requireNotComposedOrScheduled(Collection<Command> commands) {
679    for (var command : commands) {
680      requireNotComposedOrScheduled(command);
681    }
682  }
683
684  /**
685   * Check if the given command has been composed.
686   *
687   * @param command The command to check
688   * @return true if composed
689   */
690  public boolean isComposed(Command command) {
691    return getComposedCommands().contains(command);
692  }
693
694  Set<Command> getComposedCommands() {
695    return m_composedCommands.keySet();
696  }
697
698  @Override
699  public void initSendable(SendableBuilder builder) {
700    builder.setSmartDashboardType("Scheduler");
701    builder.addStringArrayProperty(
702        "Names",
703        () -> {
704          String[] names = new String[m_scheduledCommands.size()];
705          int i = 0;
706          for (Command command : m_scheduledCommands) {
707            names[i] = command.getName();
708            i++;
709          }
710          return names;
711        },
712        null);
713    builder.addIntegerArrayProperty(
714        "Ids",
715        () -> {
716          long[] ids = new long[m_scheduledCommands.size()];
717          int i = 0;
718          for (Command command : m_scheduledCommands) {
719            ids[i] = command.hashCode();
720            i++;
721          }
722          return ids;
723        },
724        null);
725    builder.addIntegerArrayProperty(
726        "Cancel",
727        () -> new long[] {},
728        toCancel -> {
729          Map<Long, Command> ids = new LinkedHashMap<>();
730          for (Command command : m_scheduledCommands) {
731            long id = command.hashCode();
732            ids.put(id, command);
733          }
734          for (long hash : toCancel) {
735            cancel(ids.get(hash));
736          }
737        });
738  }
739}