public void testReplace() throws Exception {
    cache(0).put("myKey", "myValue");

    // add an interceptor on second node that will block REPLACE commands right after
    // EntryWrappingInterceptor until we are ready
    final CountDownLatch replaceStartedLatch = new CountDownLatch(1);
    final CountDownLatch replaceProceedLatch = new CountDownLatch(1);
    boolean isVersioningEnabled = cache(0).getCacheConfiguration().versioning().enabled();
    cacheConfigBuilder
        .customInterceptors()
        .addInterceptor()
        .after(
            isVersioningEnabled
                ? VersionedEntryWrappingInterceptor.class
                : EntryWrappingInterceptor.class)
        .interceptor(
            new CommandInterceptor() {
              @Override
              protected Object handleDefault(InvocationContext ctx, VisitableCommand cmd)
                  throws Throwable {
                if (cmd instanceof ReplaceCommand) {
                  // signal we encounter a REPLACE
                  replaceStartedLatch.countDown();
                  // wait until it is ok to continue with REPLACE
                  if (!replaceProceedLatch.await(15, TimeUnit.SECONDS)) {
                    throw new TimeoutException();
                  }
                }
                return super.handleDefault(ctx, cmd);
              }
            });

    // do not allow coordinator to send topology updates to node B
    final ClusterTopologyManager ctm0 =
        TestingUtil.extractGlobalComponent(manager(0), ClusterTopologyManager.class);
    ctm0.setRebalancingEnabled(false);

    log.info("Adding a new node ..");
    addClusterEnabledCacheManager(cacheConfigBuilder);
    log.info("Added a new node");

    // node B is not a member yet and rebalance has not started yet
    CacheTopology cacheTopology =
        advancedCache(1).getComponentRegistry().getStateTransferManager().getCacheTopology();
    assertNull(cacheTopology.getPendingCH());
    assertTrue(cacheTopology.getMembers().contains(address(0)));
    assertFalse(cacheTopology.getMembers().contains(address(1)));
    assertFalse(cacheTopology.getCurrentCH().getMembers().contains(address(1)));

    // no keys should be present on node B yet because state transfer is blocked
    assertTrue(cache(1).keySet().isEmpty());

    // initiate a REPLACE
    Future<Object> getFuture =
        fork(
            new Callable<Object>() {
              @Override
              public Object call() throws Exception {
                try {
                  return cache(1).replace("myKey", "newValue");
                } catch (Exception e) {
                  log.errorf(e, "REPLACE failed: %s", e.getMessage());
                  throw e;
                }
              }
            });

    // wait for REPLACE command on node B to reach beyond *EntryWrappingInterceptor, where it will
    // block.
    // the value seen so far is null
    if (!replaceStartedLatch.await(15, TimeUnit.SECONDS)) {
      throw new TimeoutException();
    }

    // paranoia, yes the value is still missing from data container
    assertTrue(cache(1).keySet().isEmpty());

    // allow rebalance to start
    ctm0.setRebalancingEnabled(true);

    // wait for state transfer to end
    TestingUtil.waitForRehashToComplete(cache(0), cache(1));

    // the state should be already transferred now
    assertEquals(1, cache(1).keySet().size());

    // allow REPLACE to continue
    replaceProceedLatch.countDown();

    Object oldVal = getFuture.get(15, TimeUnit.SECONDS);
    assertNotNull(oldVal);
    assertEquals("myValue", oldVal);

    assertEquals("newValue", cache(0).get("myKey"));
    assertEquals("newValue", cache(1).get("myKey"));
  }