  @Nullable V refreshIfNeeded(Node<K, V> node, long now) {
    if (!refreshAfterWrite()) {
      return null;
    }

    K key;
    V oldValue;
    Object keyReference;
    long writeTime = node.getWriteTime();
    long refreshWriteTime = writeTime | 1L;
    ConcurrentMap<Object, CompletableFuture<?>> refreshes;
    if (((now - writeTime) > refreshAfterWriteNanos())
        && ((key = node.getKey()) != null) && ((oldValue = node.getValue()) != null)
        && !isComputingAsync(oldValue) && ((writeTime & 1L) == 0L)
        && !(refreshes = refreshes()).containsKey(keyReference = node.getKeyReference())
        && node.isAlive() && node.casWriteTime(writeTime, refreshWriteTime)) {
      long[] startTime = new long[1];
      @SuppressWarnings({"rawtypes", "unchecked"})
      @Nullable CompletableFuture<? extends @Nullable V>[] refreshFuture = new CompletableFuture[1];
      try {
        refreshes.computeIfAbsent(keyReference, k -> {
          try {
            startTime[0] = statsTicker().read();
            if (isAsync) {
              @SuppressWarnings("unchecked")
              var future = (CompletableFuture<V>) oldValue;
              if (Async.isReady(future)) {
                requireNonNull(cacheLoader);
                var refresh = cacheLoader.asyncReload(key, future.join(), executor);
                refreshFuture[0] = requireNonNull(refresh, "Null future");
              } else {
                // no-op if the future's completion state was modified (e.g. obtrude methods)
                return null;
              }
            } else {
              requireNonNull(cacheLoader);
              var refresh = cacheLoader.asyncReload(key, oldValue, executor);
              refreshFuture[0] = requireNonNull(refresh, "Null future");
            }
            return refreshFuture[0];
          } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.log(Level.WARNING, "Exception thrown when submitting refresh task", e);
            return null;
          } catch (Throwable e) {
            logger.log(Level.WARNING, "Exception thrown when submitting refresh task", e);
            return null;
          }
        });
      } finally {
        node.casWriteTime(refreshWriteTime, writeTime);
      }

      if (refreshFuture[0] == null) {
        return null;
      }

      var refreshed = refreshFuture[0].handle((newValue, error) -> {
        long loadTime = statsTicker().read() - startTime[0];
        if (error != null) {
          if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
            logger.log(Level.WARNING, "Exception thrown during refresh", error);
          }
          refreshes.remove(keyReference, refreshFuture[0]);
          statsCounter().recordLoadFailure(loadTime);
          return null;
        }

        @SuppressWarnings("unchecked")
        V value = (isAsync && (newValue != null)) ? (V) refreshFuture[0] : newValue;

        @Nullable RemovalCause[] cause = new RemovalCause[1];
        var preserveTimestamps = new boolean[1];
        @Nullable V result;
        try {
          result = compute(key, (K k, @Nullable V currentValue) -> {
            boolean removed = refreshes.remove(keyReference, refreshFuture[0]);
            if (currentValue == null) {
              // If the entry is absent then discard the refresh and maybe notify the listener
              if (value != null) {
                cause[0] = RemovalCause.EXPLICIT;
              }
              return null;
            } else if (currentValue == value) {
              // If the reloaded value is the same instance then no-op
              return currentValue;
            } else if (isAsync &&
                (newValue == Async.getIfReady((CompletableFuture<?>) currentValue))) {
              // If the completed futures hold the same value instance then no-op
              return currentValue;
            } else if (removed && (currentValue == oldValue)
                && (node.getWriteTime() == writeTime)) {
              // If the entry was not modified while in-flight (no ABA) then replace
              return value;
            }
            // Otherwise the refresh is discarded. If a concurrent write changed the value or
            // writeTime, preserve those timestamps so the refresh rejection does not stomp on
            // the user's write. An external discard with no concurrent write falls through to the
            // normal update, which debounces the next refresh attempt.
            if (value != null) {
              cause[0] = RemovalCause.REPLACED;
            }
            if ((currentValue != oldValue) || (node.getWriteTime() != writeTime)) {
              preserveTimestamps[0] = true;
            }
            return currentValue;
          }, expiry(), /* recordLoad= */ false, /* recordLoadFailure= */ true, preserveTimestamps);
        } catch (Throwable t) {
          logger.log(Level.WARNING, "Exception thrown during refresh", t);
          statsCounter().recordLoadFailure(loadTime);
          return null;
        }

        if (cause[0] != null) {
          notifyRemoval(key, value, cause[0]);
        }
        if (newValue == null) {
          statsCounter().recordLoadFailure(loadTime);
        } else {
          statsCounter().recordLoadSuccess(loadTime);
        }
        return result;
      });
      return Async.getIfReady(refreshed);
    }

