/**
     * Waits until total number of events processed is equal or greater then argument passed.
     *
     * @param cnt Number of events to wait.
     * @param timeout Timeout to wait.
     * @return {@code True} if successfully waited, {@code false} if timeout happened.
     * @throws InterruptedException If thread is interrupted.
     */
    public synchronized boolean awaitEvents(int cnt, long timeout) throws InterruptedException {
      long start = U.currentTimeMillis();

      long now = start;

      while (start + timeout > now) {
        if (evtCnt >= cnt) return true;

        wait(start + timeout - now);

        now = U.currentTimeMillis();
      }

      return false;
    }
  /** @return Nodes to execute on. */
  private Collection<ClusterNode> nodes() {
    CacheMode cacheMode = cctx.config().getCacheMode();

    switch (cacheMode) {
      case LOCAL:
        if (prj != null)
          U.warn(
              log,
              "Ignoring query projection because it's executed over LOCAL cache "
                  + "(only local node will be queried): "
                  + this);

        return Collections.singletonList(cctx.localNode());

      case REPLICATED:
        if (prj != null || partition() != null) return nodes(cctx, prj, partition());

        return cctx.affinityNode()
            ? Collections.singletonList(cctx.localNode())
            : Collections.singletonList(F.rand(nodes(cctx, null, partition())));

      case PARTITIONED:
        return nodes(cctx, prj, partition());

      default:
        throw new IllegalStateException("Unknown cache distribution mode: " + cacheMode);
    }
  }