RuntimeException getTransactionException(KeyInfo keyInfo, Throwable cause) {
    region.getCancelCriterion().checkCancelInProgress(cause); // fixes bug 44567
    Throwable ex = cause;
    while (ex != null) {
      if (ex instanceof CacheClosedException) {
        return new TransactionDataNodeHasDepartedException(ex.getMessage());
      }
      ex = ex.getCause();
    }

    if (isKeyInNonColocatedBucket(keyInfo)) {
      return new TransactionDataNotColocatedException(
          String.format("Key %s is not colocated with transaction",
              keyInfo.getKey()));
    }
    ex = cause;
    while (ex != null) {
      if (ex instanceof PrimaryBucketException || ex instanceof BucketNotFoundException) {
        return new TransactionDataRebalancedException(
            "Transactional data moved, due to rebalancing.");
      }
      ex = ex.getCause();
    }
    return new TransactionDataNodeHasDepartedException(cause.getLocalizedMessage());
  }
tracks bucketIds of transactional operations so as to distinguish between
TransactionDataNotColocated and TransactionDataRebalanced exceptions. Map rather than set, as a
HashSet is backed by a HashMap. (avoids one "new" call).
geode-core/src/main/java/org/apache/geode/internal/cache/tx/PartitionedTXRegionStub.java