/** * 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; }
/** {@inheritDoc} */ @Override protected final boolean tryReleaseShared(int ignore) { endTime = U.currentTimeMillis(); // Always signal after setting final done status. return true; }
/** Key partition. */ public class GridDhtLocalPartition implements Comparable<GridDhtLocalPartition>, GridReservable { /** Maximum size for delete queue. */ public static final int MAX_DELETE_QUEUE_SIZE = Integer.getInteger(IGNITE_ATOMIC_CACHE_DELETE_HISTORY_SIZE, 200_000); /** Static logger to avoid re-creation. */ private static final AtomicReference<IgniteLogger> logRef = new AtomicReference<>(); /** Logger. */ private static volatile IgniteLogger log; /** Partition ID. */ private final int id; /** State. */ @GridToStringExclude private final AtomicStampedReference<GridDhtPartitionState> state = new AtomicStampedReference<>(MOVING, 0); /** Rent future. */ @GridToStringExclude private final GridFutureAdapter<?> rent; /** Entries map. */ private final ConcurrentMap<KeyCacheObject, GridDhtCacheEntry> map; /** Context. */ private final GridCacheContext cctx; /** Create time. */ @GridToStringExclude private final long createTime = U.currentTimeMillis(); /** Eviction history. */ private volatile Map<KeyCacheObject, GridCacheVersion> evictHist = new HashMap<>(); /** Lock. */ private final ReentrantLock lock = new ReentrantLock(); /** Public size counter. */ private final LongAdder8 mapPubSize = new LongAdder8(); /** Remove queue. */ private final GridCircularBuffer<T2<KeyCacheObject, GridCacheVersion>> rmvQueue; /** Group reservations. */ private final CopyOnWriteArrayList<GridDhtPartitionsReservation> reservations = new CopyOnWriteArrayList<>(); /** * @param cctx Context. * @param id Partition ID. */ @SuppressWarnings("ExternalizableWithoutPublicNoArgConstructor") GridDhtLocalPartition(GridCacheContext cctx, int id) { assert cctx != null; this.id = id; this.cctx = cctx; log = U.logger(cctx.kernalContext(), logRef, this); rent = new GridFutureAdapter<Object>() { @Override public String toString() { return "PartitionRentFuture [part=" + GridDhtLocalPartition.this + ", map=" + map + ']'; } }; map = new ConcurrentHashMap8<>(cctx.config().getStartSize() / cctx.affinity().partitions()); int delQueueSize = CU.isSystemCache(cctx.name()) ? 100 : Math.max(MAX_DELETE_QUEUE_SIZE / cctx.affinity().partitions(), 20); rmvQueue = new GridCircularBuffer<>(U.ceilPow2(delQueueSize)); } /** * Adds group reservation to this partition. * * @param r Reservation. * @return {@code false} If such reservation already added. */ public boolean addReservation(GridDhtPartitionsReservation r) { assert state.getReference() != EVICTED : "we can reserve only active partitions"; assert state.getStamp() != 0 : "partition must be already reserved before adding group reservation"; return reservations.addIfAbsent(r); } /** @param r Reservation. */ public void removeReservation(GridDhtPartitionsReservation r) { if (!reservations.remove(r)) throw new IllegalStateException("Reservation was already removed."); } /** @return Partition ID. */ public int id() { return id; } /** @return Create time. */ long createTime() { return createTime; } /** @return Partition state. */ public GridDhtPartitionState state() { return state.getReference(); } /** @return Reservations. */ public int reservations() { return state.getStamp(); } /** @return Keys belonging to partition. */ public Set<KeyCacheObject> keySet() { return map.keySet(); } /** @return Entries belonging to partition. */ public Collection<GridDhtCacheEntry> entries() { return map.values(); } /** @return {@code True} if partition is empty. */ public boolean isEmpty() { return map.isEmpty(); } /** @return Number of entries in this partition (constant-time method). */ public int size() { return map.size(); } /** Increments public size of the map. */ public void incrementPublicSize() { mapPubSize.increment(); } /** Decrements public size of the map. */ public void decrementPublicSize() { mapPubSize.decrement(); } /** @return Number of public (non-internal) entries in this partition. */ public int publicSize() { return mapPubSize.intValue(); } /** @return If partition is moving or owning or renting. */ public boolean valid() { GridDhtPartitionState state = state(); return state == MOVING || state == OWNING || state == RENTING; } /** @param entry Entry to add. */ void onAdded(GridDhtCacheEntry entry) { GridDhtPartitionState state = state(); if (state == EVICTED) throw new GridDhtInvalidPartitionException( id, "Adding entry to invalid partition [part=" + id + ']'); map.put(entry.key(), entry); if (!entry.isInternal()) mapPubSize.increment(); } /** @param entry Entry to remove. */ @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") void onRemoved(GridDhtCacheEntry entry) { assert entry.obsolete(); // Make sure to remove exactly this entry. synchronized (entry) { map.remove(entry.key(), entry); if (!entry.isInternal() && !entry.deleted()) mapPubSize.decrement(); } // Attempt to evict. tryEvict(true); } /** * @param key Removed key. * @param ver Removed version. * @throws IgniteCheckedException If failed. */ public void onDeferredDelete(KeyCacheObject key, GridCacheVersion ver) throws IgniteCheckedException { try { T2<KeyCacheObject, GridCacheVersion> evicted = rmvQueue.add(new T2<>(key, ver)); if (evicted != null) cctx.dht().removeVersionedEntry(evicted.get1(), evicted.get2()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IgniteInterruptedCheckedException(e); } } /** Locks partition. */ @SuppressWarnings({"LockAcquiredButNotSafelyReleased"}) public void lock() { lock.lock(); } /** Unlocks partition. */ public void unlock() { lock.unlock(); } /** * @param key Key. * @param ver Version. */ public void onEntryEvicted(KeyCacheObject key, GridCacheVersion ver) { assert key != null; assert ver != null; assert lock.isHeldByCurrentThread(); // Only one thread can enter this method at a time. if (state() != MOVING) return; Map<KeyCacheObject, GridCacheVersion> evictHist0 = evictHist; if (evictHist0 != null) { GridCacheVersion ver0 = evictHist0.get(key); if (ver0 == null || ver0.isLess(ver)) { GridCacheVersion ver1 = evictHist0.put(key, ver); assert ver1 == ver0; } } } /** * Cache preloader should call this method within partition lock. * * @param key Key. * @param ver Version. * @return {@code True} if preloading is permitted. */ public boolean preloadingPermitted(KeyCacheObject key, GridCacheVersion ver) { assert key != null; assert ver != null; assert lock.isHeldByCurrentThread(); // Only one thread can enter this method at a time. if (state() != MOVING) return false; Map<KeyCacheObject, GridCacheVersion> evictHist0 = evictHist; if (evictHist0 != null) { GridCacheVersion ver0 = evictHist0.get(key); // Permit preloading if version in history // is missing or less than passed in. return ver0 == null || ver0.isLess(ver); } return false; } /** * Reserves a partition so it won't be cleared. * * @return {@code True} if reserved. */ @Override public boolean reserve() { while (true) { int reservations = state.getStamp(); GridDhtPartitionState s = state.getReference(); if (s == EVICTED) return false; if (state.compareAndSet(s, s, reservations, reservations + 1)) return true; } } /** Releases previously reserved partition. */ @Override public void release() { while (true) { int reservations = state.getStamp(); if (reservations == 0) return; GridDhtPartitionState s = state.getReference(); assert s != EVICTED; // Decrement reservations. if (state.compareAndSet(s, s, reservations, --reservations)) { tryEvict(true); break; } } } /** @return {@code True} if transitioned to OWNING state. */ boolean own() { while (true) { int reservations = state.getStamp(); GridDhtPartitionState s = state.getReference(); if (s == RENTING || s == EVICTED) return false; if (s == OWNING) return true; assert s == MOVING; if (state.compareAndSet(MOVING, OWNING, reservations, reservations)) { if (log.isDebugEnabled()) log.debug("Owned partition: " + this); // No need to keep history any more. evictHist = null; return true; } } } /** * @param updateSeq Update sequence. * @return Future to signal that this node is no longer an owner or backup. */ IgniteInternalFuture<?> rent(boolean updateSeq) { while (true) { int reservations = state.getStamp(); GridDhtPartitionState s = state.getReference(); if (s == RENTING || s == EVICTED) return rent; if (state.compareAndSet(s, RENTING, reservations, reservations)) { if (log.isDebugEnabled()) log.debug("Moved partition to RENTING state: " + this); // Evict asynchronously, as the 'rent' method may be called // from within write locks on local partition. tryEvictAsync(updateSeq); break; } } return rent; } /** * @param updateSeq Update sequence. * @return Future for evict attempt. */ IgniteInternalFuture<Boolean> tryEvictAsync(boolean updateSeq) { if (map.isEmpty() && !GridQueryProcessor.isEnabled(cctx.config()) && state.compareAndSet(RENTING, EVICTED, 0, 0)) { if (log.isDebugEnabled()) log.debug("Evicted partition: " + this); clearSwap(); if (cctx.isDrEnabled()) cctx.dr().partitionEvicted(id); cctx.dataStructures().onPartitionEvicted(id); rent.onDone(); ((GridDhtPreloader) cctx.preloader()).onPartitionEvicted(this, updateSeq); clearDeferredDeletes(); return new GridFinishedFuture<>(true); } return cctx.closures() .callLocalSafe( new GPC<Boolean>() { @Override public Boolean call() { return tryEvict(true); } }, /*system pool*/ true); } /** @return {@code true} If there is a group reservation. */ private boolean groupReserved() { for (GridDhtPartitionsReservation reservation : reservations) { if (!reservation.invalidate()) return true; // Failed to invalidate reservation -> we are reserved. } return false; } /** * @param updateSeq Update sequence. * @return {@code True} if entry has been transitioned to state EVICTED. */ boolean tryEvict(boolean updateSeq) { if (state.getReference() != RENTING || state.getStamp() != 0 || groupReserved()) return false; // Attempt to evict partition entries from cache. clearAll(); if (map.isEmpty() && state.compareAndSet(RENTING, EVICTED, 0, 0)) { if (log.isDebugEnabled()) log.debug("Evicted partition: " + this); if (!GridQueryProcessor.isEnabled(cctx.config())) clearSwap(); if (cctx.isDrEnabled()) cctx.dr().partitionEvicted(id); cctx.dataStructures().onPartitionEvicted(id); rent.onDone(); ((GridDhtPreloader) cctx.preloader()).onPartitionEvicted(this, updateSeq); clearDeferredDeletes(); return true; } return false; } /** Clears swap entries for evicted partition. */ private void clearSwap() { assert state() == EVICTED; assert !GridQueryProcessor.isEnabled(cctx.config()) : "Indexing needs to have unswapped values."; try { GridCloseableIterator<Map.Entry<byte[], GridCacheSwapEntry>> it = cctx.swap().iterator(id); boolean isLocStore = cctx.store().isLocal(); if (it != null) { // We can safely remove these values because no entries will be created for evicted // partition. while (it.hasNext()) { Map.Entry<byte[], GridCacheSwapEntry> entry = it.next(); byte[] keyBytes = entry.getKey(); KeyCacheObject key = cctx.toCacheKeyObject(keyBytes); cctx.swap().remove(key); if (isLocStore) cctx.store().remove(null, key.value(cctx.cacheObjectContext(), false)); } } } catch (IgniteCheckedException e) { U.error(log, "Failed to clear swap for evicted partition: " + this, e); } } /** */ void onUnlock() { tryEvict(true); } /** * @param topVer Topology version. * @return {@code True} if local node is primary for this partition. */ public boolean primary(AffinityTopologyVersion topVer) { return cctx.affinity().primary(cctx.localNode(), id, topVer); } /** Clears values for this partition. */ private void clearAll() { GridCacheVersion clearVer = cctx.versions().next(); boolean swap = cctx.isSwapOrOffheapEnabled(); boolean rec = cctx.events().isRecordable(EVT_CACHE_REBALANCE_OBJECT_UNLOADED); Iterator<GridDhtCacheEntry> it = map.values().iterator(); GridCloseableIterator<Map.Entry<byte[], GridCacheSwapEntry>> swapIt = null; if (swap && GridQueryProcessor.isEnabled(cctx.config())) { // Indexing needs to unswap cache values. Iterator<GridDhtCacheEntry> unswapIt = null; try { swapIt = cctx.swap().iterator(id); unswapIt = unswapIterator(swapIt); } catch (Exception e) { U.error(log, "Failed to clear swap for evicted partition: " + this, e); } if (unswapIt != null) it = F.concat(it, unswapIt); } try { while (it.hasNext()) { GridDhtCacheEntry cached = it.next(); try { if (cached.clearInternal(clearVer, swap)) { map.remove(cached.key(), cached); if (!cached.isInternal()) { mapPubSize.decrement(); if (rec) cctx.events() .addEvent( cached.partition(), cached.key(), cctx.localNodeId(), (IgniteUuid) null, null, EVT_CACHE_REBALANCE_OBJECT_UNLOADED, null, false, cached.rawGet(), cached.hasValue(), null, null, null); } } } catch (IgniteCheckedException e) { U.error(log, "Failed to clear cache entry for evicted partition: " + cached, e); } } } finally { U.close(swapIt, log); } } /** * @param it Swap iterator. * @return Unswapping iterator over swapped entries. */ private Iterator<GridDhtCacheEntry> unswapIterator( final GridCloseableIterator<Map.Entry<byte[], GridCacheSwapEntry>> it) { if (it == null) return null; return new Iterator<GridDhtCacheEntry>() { /** */ GridDhtCacheEntry lastEntry; @Override public boolean hasNext() { return it.hasNext(); } @Override public GridDhtCacheEntry next() { Map.Entry<byte[], GridCacheSwapEntry> entry = it.next(); byte[] keyBytes = entry.getKey(); try { KeyCacheObject key = cctx.toCacheKeyObject(keyBytes); lastEntry = (GridDhtCacheEntry) cctx.cache().entryEx(key, false); lastEntry.unswap(true); return lastEntry; } catch (IgniteCheckedException e) { throw new CacheException(e); } } @Override public void remove() { map.remove(lastEntry.key(), lastEntry); } }; } /** */ private void clearDeferredDeletes() { rmvQueue.forEach( new CI1<T2<KeyCacheObject, GridCacheVersion>>() { @Override public void apply(T2<KeyCacheObject, GridCacheVersion> t) { cctx.dht().removeVersionedEntry(t.get1(), t.get2()); } }); } /** {@inheritDoc} */ @Override public int hashCode() { return id; } /** {@inheritDoc} */ @SuppressWarnings({"OverlyStrongTypeCast"}) @Override public boolean equals(Object obj) { return obj instanceof GridDhtLocalPartition && (obj == this || ((GridDhtLocalPartition) obj).id() == id); } /** {@inheritDoc} */ @Override public int compareTo(@NotNull GridDhtLocalPartition part) { if (part == null) return 1; return Integer.compare(id, part.id()); } /** {@inheritDoc} */ @Override public String toString() { return S.toString( GridDhtLocalPartition.class, this, "state", state(), "reservations", reservations(), "empty", map.isEmpty(), "createTime", U.format(createTime), "mapPubSize", mapPubSize); } }
/** {@inheritDoc} */ @Override public long duration() { long endTime = this.endTime; return endTime == 0 ? U.currentTimeMillis() - startTime : endTime - startTime; }
/** Future adapter. */ public class GridFutureAdapter<R> extends AbstractQueuedSynchronizer implements IgniteInternalFuture<R> { /** */ private static final long serialVersionUID = 0L; /** Initial state. */ private static final int INIT = 0; /** Cancelled state. */ private static final int CANCELLED = 1; /** Done state. */ private static final int DONE = 2; /** */ private static final byte ERR = 1; /** */ private static final byte RES = 2; /** */ private byte resFlag; /** Result. */ @GridToStringInclude private Object res; /** Future start time. */ private final long startTime = U.currentTimeMillis(); /** Future end time. */ private volatile long endTime; /** */ private boolean ignoreInterrupts; /** */ private IgniteInClosure<? super IgniteInternalFuture<R>> lsnr; /** {@inheritDoc} */ @Override public long startTime() { return startTime; } /** {@inheritDoc} */ @Override public long duration() { long endTime = this.endTime; return endTime == 0 ? U.currentTimeMillis() - startTime : endTime - startTime; } /** @param ignoreInterrupts Ignore interrupts flag. */ public void ignoreInterrupts(boolean ignoreInterrupts) { this.ignoreInterrupts = ignoreInterrupts; } /** @return Future end time. */ public long endTime() { return endTime; } /** {@inheritDoc} */ @Override public Throwable error() { return (resFlag == ERR) ? (Throwable) res : null; } /** {@inheritDoc} */ @Override public R result() { return resFlag == RES ? (R) res : null; } /** {@inheritDoc} */ @Override public R get() throws IgniteCheckedException { try { if (endTime == 0) { if (ignoreInterrupts) acquireShared(0); else acquireSharedInterruptibly(0); } if (getState() == CANCELLED) throw new IgniteFutureCancelledCheckedException("Future was cancelled: " + this); assert resFlag != 0; if (resFlag == ERR) throw U.cast((Throwable) res); return (R) res; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IgniteInterruptedCheckedException(e); } } /** {@inheritDoc} */ @Override public R get(long timeout) throws IgniteCheckedException { // Do not replace with static import, as it may not compile. return get(timeout, TimeUnit.MILLISECONDS); } /** {@inheritDoc} */ @Override public R get(long timeout, TimeUnit unit) throws IgniteCheckedException { A.ensure(timeout >= 0, "timeout cannot be negative: " + timeout); A.notNull(unit, "unit"); try { return get0(unit.toNanos(timeout)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IgniteInterruptedCheckedException( "Got interrupted while waiting for future to complete.", e); } } /** * @param nanosTimeout Timeout (nanoseconds). * @return Result. * @throws InterruptedException If interrupted. * @throws IgniteFutureTimeoutCheckedException If timeout reached before computation completed. * @throws IgniteCheckedException If error occurred. */ @Nullable protected R get0(long nanosTimeout) throws InterruptedException, IgniteCheckedException { if (endTime == 0 && !tryAcquireSharedNanos(0, nanosTimeout)) throw new IgniteFutureTimeoutCheckedException( "Timeout was reached before computation completed."); if (getState() == CANCELLED) throw new IgniteFutureCancelledCheckedException("Future was cancelled: " + this); assert resFlag != 0; if (resFlag == ERR) throw U.cast((Throwable) res); return (R) res; } /** {@inheritDoc} */ @Override public void listen(IgniteInClosure<? super IgniteInternalFuture<R>> lsnr0) { assert lsnr0 != null; boolean done = isDone(); if (!done) { synchronized (this) { done = isDone(); // Double check. if (!done) { if (lsnr == null) lsnr = lsnr0; else if (lsnr instanceof ArrayListener) ((ArrayListener) lsnr).add(lsnr0); else { lsnr = (IgniteInClosure) new ArrayListener<IgniteInternalFuture>(lsnr, lsnr0); } return; } } } assert done; notifyListener(lsnr0); } /** {@inheritDoc} */ @Override public <T> IgniteInternalFuture<T> chain( final IgniteClosure<? super IgniteInternalFuture<R>, T> doneCb) { return new ChainFuture<>(this, doneCb); } /** Notifies all registered listeners. */ private void notifyListeners() { IgniteInClosure<? super IgniteInternalFuture<R>> lsnr0; synchronized (this) { lsnr0 = lsnr; if (lsnr0 == null) return; lsnr = null; } assert lsnr0 != null; notifyListener(lsnr0); } /** * Notifies single listener. * * @param lsnr Listener. */ private void notifyListener(IgniteInClosure<? super IgniteInternalFuture<R>> lsnr) { assert lsnr != null; try { lsnr.apply(this); } catch (IllegalStateException e) { U.error( null, "Failed to notify listener (is grid stopped?) [fut=" + this + ", lsnr=" + lsnr + ", err=" + e.getMessage() + ']', e); } catch (RuntimeException | Error e) { U.error(null, "Failed to notify listener: " + lsnr, e); throw e; } } /** * Default no-op implementation that always returns {@code false}. Futures that do support * cancellation should override this method and call {@link #onCancelled()} callback explicitly if * cancellation indeed did happen. */ @Override public boolean cancel() throws IgniteCheckedException { return false; } /** {@inheritDoc} */ @Override public boolean isDone() { // Don't check for "valid" here, as "done" flag can be read // even in invalid state. return endTime != 0; } /** @return Checks is future is completed with exception. */ public boolean isFailed() { // Must read endTime first. return endTime != 0 && resFlag == ERR; } /** {@inheritDoc} */ @Override public boolean isCancelled() { return getState() == CANCELLED; } /** * Callback to notify that future is finished with {@code null} result. This method must delegate * to {@link #onDone(Object, Throwable)} method. * * @return {@code True} if result was set by this call. */ public final boolean onDone() { return onDone(null, null); } /** * Callback to notify that future is finished. This method must delegate to {@link #onDone(Object, * Throwable)} method. * * @param res Result. * @return {@code True} if result was set by this call. */ public final boolean onDone(@Nullable R res) { return onDone(res, null); } /** * Callback to notify that future is finished. This method must delegate to {@link #onDone(Object, * Throwable)} method. * * @param err Error. * @return {@code True} if result was set by this call. */ public final boolean onDone(@Nullable Throwable err) { return onDone(null, err); } /** * Callback to notify that future is finished. Note that if non-{@code null} exception is passed * in the result value will be ignored. * * @param res Optional result. * @param err Optional error. * @return {@code True} if result was set by this call. */ public boolean onDone(@Nullable R res, @Nullable Throwable err) { return onDone(res, err, false); } /** * @param res Result. * @param err Error. * @param cancel {@code True} if future is being cancelled. * @return {@code True} if result was set by this call. */ private boolean onDone(@Nullable R res, @Nullable Throwable err, boolean cancel) { boolean notify = false; try { if (compareAndSetState(INIT, cancel ? CANCELLED : DONE)) { if (err != null) { resFlag = ERR; this.res = err; } else { resFlag = RES; this.res = res; } notify = true; releaseShared(0); return true; } return false; } finally { if (notify) notifyListeners(); } } /** * Callback to notify that future is cancelled. * * @return {@code True} if cancel flag was set by this call. */ public boolean onCancelled() { return onDone(null, null, true); } /** {@inheritDoc} */ @Override protected final int tryAcquireShared(int ignore) { return endTime != 0 ? 1 : -1; } /** {@inheritDoc} */ @Override protected final boolean tryReleaseShared(int ignore) { endTime = U.currentTimeMillis(); // Always signal after setting final done status. return true; } /** @return String representation of state. */ private String state() { int s = getState(); return s == INIT ? "INIT" : s == CANCELLED ? "CANCELLED" : "DONE"; } /** {@inheritDoc} */ @Override public String toString() { return S.toString(GridFutureAdapter.class, this, "state", state()); } /** */ private static class ArrayListener<R> implements IgniteInClosure<IgniteInternalFuture<R>> { /** */ private static final long serialVersionUID = 0L; /** */ private IgniteInClosure<? super IgniteInternalFuture<R>>[] arr; /** @param lsnrs Listeners. */ private ArrayListener(IgniteInClosure... lsnrs) { this.arr = lsnrs; } /** {@inheritDoc} */ @Override public void apply(IgniteInternalFuture<R> fut) { for (int i = 0; i < arr.length; i++) arr[i].apply(fut); } /** @param lsnr Listener. */ void add(IgniteInClosure<? super IgniteInternalFuture<R>> lsnr) { arr = Arrays.copyOf(arr, arr.length + 1); arr[arr.length - 1] = lsnr; } /** {@inheritDoc} */ @Override public String toString() { return S.toString(ArrayListener.class, this, "arrSize", arr.length); } } /** */ private static class ChainFuture<R, T> extends GridFutureAdapter<T> { /** */ private static final long serialVersionUID = 0L; /** */ private GridFutureAdapter<R> fut; /** */ private IgniteClosure<? super IgniteInternalFuture<R>, T> doneCb; /** */ public ChainFuture() { // No-op. } /** * @param fut Future. * @param doneCb Closure. */ ChainFuture( GridFutureAdapter<R> fut, IgniteClosure<? super IgniteInternalFuture<R>, T> doneCb) { this.fut = fut; this.doneCb = doneCb; fut.listen(new GridFutureChainListener<>(this, doneCb)); } /** {@inheritDoc} */ @Override public String toString() { return "ChainFuture [orig=" + fut + ", doneCb=" + doneCb + ']'; } } }