Skip to content
/ cffu Public

🦝 Java CompletableFuture-Fu ("CF-Fu", pronounced "Shifu") is a tiny 0-dependency library that improves the CompletableFuture(CF) usage experience and reduces misuse, enabling more convenient, efficient, and safe use of CF in your application. πŸ˜‹πŸš€πŸ¦Ί

License

Notifications You must be signed in to change notification settings

foldright/cffu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🦝 CompletableFuture-Fu(CF-Fu)

Fast Build CI Strong Build CI Codecov Qodana Code Inspections Java support License Javadocs Maven Central GitHub Releases GitHub Stars GitHub Forks GitHub Issues GitHub Contributors GitHub repo size gitpod: Ready to Code

πŸ“– English Documentation | πŸ“– δΈ­ζ–‡ζ–‡ζ‘£


πŸ‘‰ Java CompletableFuture-Fu ("CF-Fu", pronounced "Shifu" 🦝) is a tiny 0-dependency library that improves the CompletableFuture(CF) usage experience and reduces misuse, enabling more convenient, efficient, and safe use of CF in your application. πŸ˜‹πŸš€πŸ¦Ί

Welcome πŸ‘πŸ’–

shifu

  • For suggestions and questions, submit an Issue
  • For contributions and improvements, play fork and pull request dance


πŸ”§ Features

☘️ Completing missing functionality in business development

  • πŸͺ More convenient features, such as:
    • Support for returning overall results of multiple input CFs instead of returning CF<Void> without input CF results (CompletableFuture#allOf)
      • Such as methods allResultsFailFastOf / mSupplyFailFastAsync / thenMApplyMostSuccessAsync
    • Support for directly running multiple Actions instead of wrapping them into CompletableFutures first
      • Such as methods mSupplyAsync / mRunFailFastAsync / thenMApplyAllSuccessAsync
      • Aka. multiple instruction, single data (MISD) style processing
    • Support for async parallel processing of collection data, instead of wrapping data with actions into CompletableFutures first
      • Such as methods CfParallelUtils#parApplyFailFastAsync / CfParallelUtils#thenParAcceptAnySuccessAsync
      • Aka. single instruction, multiple data (SIMD) style processing
    • Support for inputting collections of CFs / Actions instead of converting them to array first
      • Such as methods CfIterableUtils#allResultsFailFastOf / CfIterableUtils#mSupplyFailFastAsync / CfIterableUtils#thenMApplyMostSuccessAsync
    • Support for setting a default business thread pool via CffuFactory#builder(executor) method, instead of repeatedly passing business thread pool parameters during async execution
    • Support for handling specific exception types via catching methods instead of handling all Throwable exceptions (CompletableFuture#exceptionally)
  • 🚦 More efficient and flexible concurrent execution strategies, such as:
    • AllFailFast strategy: fail fast when any of the multiple input CFs complete exceptionally instead of futilely waiting for all CFs to complete (CompletableFuture#allOf)
    • AnySuccess strategy: Return the first successful CF result instead of the first completed but possibly exceptional CF (CompletableFuture#anyOf)
    • AllSuccess strategy: Return successful results from multiple CFs, returning specified default values for exceptional CFs
    • MostSuccess strategy: Return successful results from multiple CFs within a specified time, returning specified default values for exceptional or timed-out CFs
    • All(Complete) / Any(Complete) strategies: These two are strategies already supported by CompletableFuture
  • 🦺 Safer usage patterns, such as:
    • Timeout-safe new implementation of orTimeout / completeOnTimeout methods
      • The CF#orTimeout / CF#completeOnTimeout methods can break CompletableFuture's timeout and delay functionality❗️
    • peek processing method that definitely won't modify CF results
      • The whenComplete method may modify the CF result, and the returned CF result may not be consistent with the input
    • Support for timeout-enabled join(timeout, unit) method
    • Support for the forbidding obtrude methods via CffuFactoryBuilder#forbidObtrudeMethods method
    • Complete code quality annotations attached to class methods, enabling IDEs to provide early problem warnings during coding
      • Such as @NonNull, @Nullable, @CheckReturnValue, @Contract, etc.
  • 🧩 Missing basic functionality, in addition to the safety-oriented new implementations above, including:
    • Async exception completion, completeExceptionallyAsync method
    • Non-blocking retrieval of successful results, returning specified default values for exceptional or incomplete CFs, getSuccessNow method
    • Unwrapping CF exceptions into business exceptions, unwrapCfException method

⏳ Backport support for Java 8, all new CF methods from Java 9+ versions are directly available in Java 8 version, such as:

  • Timeout control: orTimeout / completeOnTimeout
  • Delayed execution: delayedExecutor
  • Factory methods: failedFuture / completedStage / failedStage
  • Processing operations: completeAsync / exceptionallyAsync / exceptionallyCompose / copy
  • Non-blocking reads: resultNow / exceptionNow / state

πŸ’ͺ Enhancement of existing features, such as:

  • anyOf method: Returns specific type T (type-safe) instead of returning Object (CompletableFuture#anyOf)
  • allOf / anyOf methods: Accept broader CompletionStage parameter types instead of CompletableFuture class (CompletableFuture#allOf/anyOf)

For more information about cffu features and usage, see the cffu Feature Introduction.

About CompletableFuture

Managing concurrent execution is a complex and error-prone problem, and the industry has a large number of tools and frameworks available.

For a broad understanding of concurrency tools and frameworks, you can check out books like "Seven Concurrency Models in Seven Weeks", "Programming Concurrency on the JVM", "Learning Concurrent Programming in Scala (2nd Edition)".

Among them, CompletableFuture(CF) has its advantages:

  • Widely Known and Widely Used
    • CompletableFuture was provided in Java 8 released in 2014, 10 years ago
    • The parent interface Future of CompletableFuture was provided as early as Java 5 released in 2004, 20 years ago. Although the Future interface doesn't support asynchronous retrieval of execution results and orchestration of concurrent execution logic, it has made the majority of Java developers familiar with the typical concept and tool of Future
  • Powerful but Not Excessively Complex
    • Sufficient to handle daily business asynchronous concurrency needs
    • Other large-scale concurrency frameworks (such as Akka, RxJava) require much more understanding to use. Of course, basic concurrency concerns and their complexity are independent of which specific tool is used and must all be understood and paid attention to
  • High-Level Abstraction
  • Built into Java Standard Library
    • No additional dependencies required, almost always available
    • Believed to have extremely high implementation quality

Like other concurrency tools and frameworks, CompletableFuture is used for:

  • Concurrent execution of business logic, or orchestrating concurrent processing flows or asynchronous tasks
  • Multicore parallel processing, making full use of resources
  • Shortening request response time and improving business responsiveness

It's worth understanding and applying in more depth. πŸ’•

πŸ‘₯ User Guide

1. cffu usage modes

  • 🦝 Using the Cffu class
  • πŸ”§ Using the CompletableFutureUtils utility class

1.1 recommended Cffu class usage 🌟

Compared to calling static methods of the CompletableFutureUtils utility class:

  • Using the Cffu class is like using CompletableFuture, with new features as instance methods of the Cffu class, which can be called naturally and conveniently
    • The Cffu class to the CompletableFuture utility class CompletableFutureUtils is like Guava's FluentFuture to the ListenableFuture utility class Futures
  • The Java language doesn't support extending methods on existing classes (CompletableFuture), so a new wrapper class (Cffu) is needed

If you don't want to introduce a new class (Cffu class) to your project and feel that this adds complexity, you can completely use the cffu library as a utility class:

  • Utility methods for optimizing CompletableFuture usage are very common in business projects
  • CompletableFutureUtils provides a series of practical, efficient, safe, and reliable utility methods
  • Some cffu features are not provided in this usage mode (and no suitable implementation approach has been found)
    Such as support for setting default business thread pools and forbidding obtrude methods

1.2 migrating code from using CompletableFuture class to using Cffu class

To conveniently and naturally use the enhanced features and methods of the cffu library, you can migrate existing code that uses the CompletableFuture class to the Cffu class.

1) If you can modify code that uses CompletableFuture

Migrating to the Cffu class involves 2 simple changes:

  • In type declaration places, change the CompletableFuture class to the Cffu class
  • In CompletableFuture static method call places, change the class name CompletableFuture to a cffuFactory instance

The reason this migration is possible is that:

  • All instance methods of the CompletableFuture class are implemented in the Cffu class with the same method signatures and functionality
  • All static methods of the CompletableFuture class are implemented in the CffuFactory class with the same method signatures and functionality

2) If you cannot modify code that uses CompletableFuture (such as CF returned from external libraries)

Use the CffuFactory.toCffu(CompletionStage) method to convert CompletableFuture or CompletionStage to Cffu type.

1.3 dependencies (including CompletableFutureUtils utility class)

  • For Maven projects:

    <dependency>
      <groupId>io.foldright</groupId>
      <artifactId>cffu2</artifactId>
      <version>2.0.0</version>
    </dependency>
  • For Gradle projects:

    Gradle Kotlin DSL

    implementation("io.foldright:cffu2:2.0.0")

    Gradle Groovy DSL

    implementation 'io.foldright:cffu2:2.0.0'

2. cffu feature introduction

2.1 support for returning overall results of multiple input CFs

The return type of CompletableFuture#allOf method is CF<Void> without the execution results of the input CFs. To get the execution results of input CFs, you need to:

  • After the allOf method, get results through explicit read operations (like join / get) on the input parameter CFs
  • Or set external variables in the passed CompletableFuture Action
    • Need to pay attention to thread safety issues of multithreaded read-write βš οΈπŸ”€
      Multi-threaded read-write involves the complexity of multithreaded data transfer; omitting correct handling of concurrent logic data read-write is a common problem in business code❗️
    • Avoid concurrent pitfalls; concurrent logic is complex and bug-prone 🐞
      If timeouts are involved, it becomes even more complex; even JDK CompletableFuture itself has bug fixes in this area in Java 21 ⏰

Methods in cffu like allResultsFailFastOf / mSupplyFailFastAsync / thenMApplyMostSuccessAsync provide functionality to return results of input CFs. Using these methods to get overall execution results of input CFs:

  • Convenient and straightforward
  • Because the returned result is a CF with overall results, you can continue chaining non-blocking operations, naturally reducing the use of blocking read methods (like join / get) and minimizing deadlock risk in business logic
  • Avoids complex thread safety issues and logic errors of directly implementing multithreaded read-write logic in business logic
  • Using "reliably implemented and tested" library-provided concurrency features instead of implementing them directly is a best practice πŸ†βœ…

Example code:

public class AllResultsOfDemo {
  private static final ExecutorService myBizExecutor = Executors.newCachedThreadPool();
  private static final CffuFactory cffuFactory = CffuFactory.builder(myBizExecutor).build();

  public static void main(String[] args) throws Exception {
    //////////////////////////////////////////////////
    // CffuFactory#allResultsOf
    //////////////////////////////////////////////////
    Cffu<Integer> cffu1 = cffuFactory.completedFuture(21);
    Cffu<Integer> cffu2 = cffuFactory.completedFuture(42);

    Cffu<Void> all = cffuFactory.allOf(cffu1, cffu2);
    // result type is Void!
    //
    // the result can be got by input argument `cf1.get()`, but it's cumbersome.
    // so we can see a lot of util methods to enhance `allOf` with result in our project.

    MCffu<Integer, List<Integer>> allResults = cffuFactory.allResultsOf(cffu1, cffu2);
    System.out.println(allResults.get());
    // output: [21, 42]

    //////////////////////////////////////////////////
    // or CompletableFutureUtils#allResultsOf
    //////////////////////////////////////////////////
    CompletableFuture<Integer> cf1 = CompletableFuture.completedFuture(21);
    CompletableFuture<Integer> cf2 = CompletableFuture.completedFuture(42);

    CompletableFuture<Void> all2 = CompletableFuture.allOf(cf1, cf2);
    // result type is Void!

    CompletableFuture<List<Integer>> allResults2 = allResultsOf(cf1, cf2);
    System.out.println(allResults2.get());
    // output: [21, 42]
  }
}

# Complete runnable demo code can be found at AllResultsOfDemo.java.

2.2 support for setting default business thread pool

The default thread pool used by CompletableFuture async execution (i.e., *Async methods) is ForkJoinPool.commonPool(); using this default thread pool in business is very dangerous❗

  • ForkJoinPool.commonPool() has about as many threads as CPUs, suitable for executing CPU-intensive tasks; for business logic, there are often many waiting operations (such as network IO, blocking waits) that are not CPU-intensive, leading to low business processing capabilities 🐌
  • ForkJoinPool uses unbounded queues; during high traffic, tasks will accumulate, causing memory exhaustion and service crashes 🚨
    For more information about this problem and its causes, see this article

The result is that in business logic, when calling CompletableFuture's *Async methods, you almost always need to repeatedly pass in a specified business thread pool; this makes using CompletableFuture cumbersome and error-prone 🀯❌

Additionally, when under-layer operations call back to business logic (such as RPC callbacks), it's not appropriate or convenient to provide a thread pool for the business; using Cffu to set the default business thread pool specified by upper-level business is both convenient, reasonable, and safe.
For more information about this usage scenario, see CompletableFuture Principles and Practice - 4.2.3 Asynchronous RPC Calls Should Not Block IO Thread Pools

Example code:

public class NoDefaultExecutorSettingForCompletableFuture {
  public static final Executor myBizExecutor = Executors.newCachedThreadPool();

  public static void main(String[] args) {
    CompletableFuture<Void> cf1 = CompletableFuture.runAsync(
        () -> System.out.println("doing a long time work!"),
        myBizExecutor);

    CompletableFuture<Void> cf2 = CompletableFuture
        .supplyAsync(
            () -> {
              System.out.println("doing another long time work!");
              return 42;
            },
            myBizExecutor)
        .thenAcceptAsync(
            i -> System.out.println("doing third long time work!"),
            myBizExecutor);

    CompletableFuture.allOf(cf1, cf2).join();
  }
}

# Complete runnable demo code can be found at NoDefaultExecutorSettingForCompletableFuture.java.

The Cffu class supports setting a default business thread pool, avoiding the above cumbersomeness and dangers. Example code:

public class DefaultExecutorSettingForCffu {
  private static final ExecutorService myBizExecutor = Executors.newCachedThreadPool();
  private static final CffuFactory cffuFactory = CffuFactory.builder(myBizExecutor).build();

  public static void main(String[] args) {
    Cffu<Void> cf1 = cffuFactory.runAsync(() -> System.out.println("doing a long time work!"));

    Cffu<Void> cf2 = cffuFactory.supplyAsync(() -> {
      System.out.println("doing another long time work!");
      return 42;
    }).thenAcceptAsync(i -> System.out.println("doing third long time work!"));

    cffuFactory.allOf(cf1, cf2).join();
  }
}

# Complete runnable demo code can be found at DefaultExecutorSettingForCffu.java.

2.3 efficient and flexible concurrent execution strategies (AllFailFast / AnySuccess / AllSuccess / MostSuccess)

  • CompletableFuture's allOf method waits for all input CFs to complete; even if a CF completes exceptionally, it still waits for subsequent CFs to complete before returning an exceptional CF.
    • For business logic, this failure-and-continue-waiting strategy (AllComplete) slows down business responsiveness; businesses would prefer fail fast when any of input CF complete exceptionally, instead of futilely waiting
    • cffu provides corresponding methods like allResultsFailFastOf that support the AllFailFast concurrent execution strategy
    • Both allOf and allResultsFailFastOf return successful results only when all input CFs successful
  • CompletableFuture's anyOf method returns the first completed CF without waiting for subsequent incomplete CFs; even if the first completed CF is exceptional, it returns this exceptional CF result.
    • For business logic, what's often wanted is not the first completed but exceptional CF result (AnyComplete), but rather the first successful CF result
    • cffu provides corresponding methods like anySuccessOf that support the AnySuccess concurrent execution strategy
    • anySuccessOf returns a exceptional result only when all input CFs exceptional
  • Return successful results from multiple CFs, returning specified default values for exceptional CFs
    • When business logic includes fault tolerance, successful partial results can be used, instead of overall failure when some CFs fail
    • cffu provides corresponding methods like allSuccessOf that support the AllSuccess concurrent execution strategy
  • Return successful results from multiple CFs within a specified time, returning specified default values for incomplete or timed-out CFs
    • When business is eventually consistent, return available results as much as possible; for CFs that couldn't return in time, results will be written to distributed cache for the next business request, avoiding duplicate calculations
    • This is a common business usage pattern; cffu provides corresponding methods like mostSuccessResultsOf that support the MostSuccess concurrent execution strategy

πŸ“” For more about concurrent execution strategies for multiple CFs, see the JavaScript specification Promise Concurrency; in JavaScript, Promise corresponds to CompletableFuture.

JavaScript Promise provides 4 concurrent execution methods:

  • Promise.all(): Wait for all Promises to complete, immediately return exceptionally if any input complete exceptionally (AllFailFast)
  • Promise.allSettled(): Wait for all Promises to complete, regardless of success or failure (AllComplete)
  • Promise.any(): Racing mode, immediately return the first successful Promise (AnySuccess)
  • Promise.race(): Racing mode, immediately return the first completed Promise (AnyComplete)

PS: The method naming of JavaScript Promise is well-considered~ πŸ‘

cffu's new methods support the concurrent execution approaches of the JavaScript Promise specification~

Example code:

public class ConcurrencyStrategyDemo {
  private static final ExecutorService myBizExecutor = Executors.newCachedThreadPool();
  private static final CffuFactory cffuFactory = CffuFactory.builder(myBizExecutor).build();

  public static void main(String[] args) throws Exception {
    ////////////////////////////////////////////////////////////////////////
    // CffuFactory#allResultsFailFastOf
    // CffuFactory#anySuccessOf
    // CffuFactory#mostSuccessResultsOf
    ////////////////////////////////////////////////////////////////////////
    final Cffu<Integer> success = cffuFactory.supplyAsync(() -> {
      sleep(300); // sleep SHORT time
      return 42;
    });
    final Cffu<Integer> successAfterLongTime = cffuFactory.supplyAsync(() -> {
      sleep(3000); // sleep LONG time
      return 4242;
    });
    final Cffu<Integer> failed = cffuFactory.failedFuture(new RuntimeException("Bang!"));

    MCffu<Integer, List<Integer>> failFast = cffuFactory.allResultsFailFastOf(success, successAfterLongTime, failed);
    // fail fast without waiting successAfterLongTime
    System.out.println(failFast.exceptionNow());
    // output: java.lang.RuntimeException: Bang!

    Cffu<Integer> anySuccess = cffuFactory.anySuccessOf(success, successAfterLongTime, failed);
    System.out.println(anySuccess.get());
    // output: 42

    MCffu<Integer, List<Integer>> mostSuccess = cffuFactory.mostSuccessResultsOf(
        -1, 100, TimeUnit.MILLISECONDS, success, successAfterLongTime, failed);
    System.out.println(mostSuccess.get());
    // output: [42, -1, -1]

    ////////////////////////////////////////////////////////////////////////
    // or CompletableFutureUtils#allResultsFailFastOf
    //    CompletableFutureUtils#anySuccessOf
    //    CompletableFutureUtils#mostSuccessResultsOf
    ////////////////////////////////////////////////////////////////////////
    final CompletableFuture<Integer> successCf = CompletableFuture.supplyAsync(() -> {
      sleep(300); // sleep SHORT time
      return 42;
    });
    final CompletableFuture<Integer> successAfterLongTimeCf = CompletableFuture.supplyAsync(() -> {
      sleep(3000); // sleep LONG time
      return 4242;
    });
    final CompletableFuture<Integer> failedCf = failedFuture(new RuntimeException("Bang!"));

    CompletableFuture<List<Integer>> failFast2 = allResultsFailFastOf(successCf, successAfterLongTimeCf, failedCf);
    // fail fast without waiting successAfterLongTime
    System.out.println(exceptionNow(failFast2));
    // output: java.lang.RuntimeException: Bang!

    CompletableFuture<Integer> anySuccess2 = anySuccessOf(successCf, successAfterLongTimeCf, failedCf);
    System.out.println(anySuccess2.get());
    // output: 42

    CompletableFuture<List<Integer>> mostSuccess2 = mostSuccessResultsOf(
        -1, 100, TimeUnit.MILLISECONDS, successCf, successAfterLongTime, failed);
    System.out.println(mostSuccess2.get());
    // output: [42, -1, -1]
  }
}

# Complete runnable demo code can be found at ConcurrencyStrategyDemo.java.

2.4 support for directly running multiple actions instead of wrapping them into Completablefutures first

The allOf/anyOf methods of CompletableFuture take CompletableFuture as input; when business logic directly has methods to orchestrate, you still need to wrap them into CompletableFuture first before running:

  • Cumbersome
  • Blurs business processes
  • Simply wrapping multiple Actions into CFs and submitting them to allOf/anyOf (which is how business code often implements this) will swallow exceptions❗️
    • When multiple input Actions throw exceptions during execution, at most one of these exceptions can be fed back to the business through the returned CF; other exceptions are silently swallowed, affecting business problem troubleshooting

cffu provides methods for directly running multiple Actions, solving the above problems:

  • Convenient and clear expression and orchestration of business processes
  • Doesn't swallow exceptions, facilitating business problem troubleshooting
    • When multiple input Actions throw exceptions during execution, log reports will be printed for exceptions not fed back to the business through the returned CF

Example code:

public class MultipleActionsDemo {
  private static final ExecutorService myBizExecutor = Executors.newCachedThreadPool();
  private static final CffuFactory cffuFactory = CffuFactory.builder(myBizExecutor).build();

  static void mRunAsyncDemo() {
    // wrap actions to CompletableFutures first, AWKWARD! πŸ˜–
    CompletableFuture.allOf(
        CompletableFuture.runAsync(() -> System.out.println("task1")),
        CompletableFuture.runAsync(() -> System.out.println("task2")),
        CompletableFuture.runAsync(() -> System.out.println("task3"))
    );
    completedFuture("task").thenCompose(v ->
        CompletableFuture.allOf(
            CompletableFuture.runAsync(() -> System.out.println(v + "1")),
            CompletableFuture.runAsync(() -> System.out.println(v + "2")),
            CompletableFuture.runAsync(() -> System.out.println(v + "3"))
        )
    );

    // just run multiple actions, fresh and cool πŸ˜‹
    CompletableFutureUtils.mRunAsync(
        () -> System.out.println("task1"),
        () -> System.out.println("task2"),
        () -> System.out.println("task3")
    );
    cffuFactory.completedFuture("task").thenMAcceptAsync(
        (String v) -> System.out.println(v + "1"),
        v -> System.out.println(v + "2"),
        v -> System.out.println(v + "3")
    );
  }
}

These multiple-Action methods also support "different concurrent execution strategies" and "returning results of multiple input CFs".

Example code:

public class MultipleActionsDemo {
  private static final ExecutorService myBizExecutor = Executors.newCachedThreadPool();
  private static final CffuFactory cffuFactory = CffuFactory.builder(myBizExecutor).build();

  static void thenMApplyAsyncDemo() {
    // wrap actions to CompletableFutures first, AWKWARD! πŸ˜–
    completedFuture(42).thenCompose(v ->
        CompletableFutureUtils.allResultsFailFastOf(
            CompletableFuture.supplyAsync(() -> v + 1),
            CompletableFuture.supplyAsync(() -> v + 2),
            CompletableFuture.supplyAsync(() -> v + 3)
        )
    ).thenAccept(System.out::println);
    // output: [43, 44, 45]
    cffuFactory.completedFuture(42).thenCompose(v ->
        CompletableFutureUtils.allSuccessResultsOf(
            -1,
            CompletableFuture.supplyAsync(() -> v + 1),
            CompletableFuture.supplyAsync(() -> v + 2),
            CompletableFuture.supplyAsync(() -> v + 3)
        )
    ).thenAccept(System.out::println);
    // output: [43, 44, 45]

    // just run multiple actions, fresh and cool πŸ˜‹
    CompletableFutureUtils.thenMApplyFailFastAsync(
        completedFuture(42),
        v -> v + 1,
        v -> v + 2,
        v -> v + 3
    ).thenAccept(System.out::println);
    // output: [43, 44, 45]
    cffuFactory.completedFuture(42).thenMApplyAllSuccessAsync(
        -1,
        v -> v + 1,
        v -> v + 2,
        v -> v + 3
    ).thenAccept(System.out::println);
    // output: [43, 44, 45]
  }
}

# Complete runnable demo code can be found at MultipleActionsDemo.java.

2.5 Support for async parallel processing of collection data, instead of wrapping data and Action into CompletableFutures first

Async parallel processing of multiple data items is a common business requirement, but implementing it via the CompletableFuture is cumbersome and complex, also obscures the business logic, and a simple implementation may swallow exceptions❗

cffu provides methods for async parallel data processing to solve these problems.

Example code:

public class CfParallelDemo {
  private static final ExecutorService myBizExecutor = Executors.newCachedThreadPool();
  private static final CffuFactory cffuFactory = CffuFactory.builder(myBizExecutor).build();

  static void parApplyFailFastAsyncDemo() {
    final Function<Integer, Integer> fn = x -> x + 1;
    final List<Integer> list = asList(42, 43, 44);

    // wrap data with action to CompletableFutures first, AWKWARD and COMPLEX! πŸ˜–
    CompletableFuture<Integer>[] cfs = new CompletableFuture[list.size()];
    for (int i = 0; i < list.size(); i++) {
      Integer e = list.get(i);
      cfs[i] = CompletableFuture.supplyAsync(() -> fn.apply(e));
    }
    CompletableFutureUtils.allResultsFailFastOf(cfs).thenAccept(System.out::println);
    // output: [43, 44, 45]
    cffuFactory.allResultsFailFastOf(cfs).thenAccept(System.out::println);
    // output: [43, 44, 45]

    // just parallel process multiple data, fresh and cool πŸ˜‹
    CfParallelUtils.parApplyFailFastAsync(
        asList(42, 43, 44),
        x -> x + 1
    ).thenAccept(System.out::println);
    // output: [43, 44, 45]
    cffuFactory.parOps().parApplyFailFastAsync(
        asList(42, 43, 44),
        x -> x + 1
    ).thenAccept(System.out::println);
    // output: [43, 44, 45]
  }

  static void thenParApplyFailFastAsyncDemo() {
    final Function<Integer, Integer> fn = x -> x + 1;
    final CompletableFuture<List<Integer>> cf = completedFuture(asList(42, 43, 44));

    // wrap data with action to CompletableFutures first, AWKWARD and COMPLEX! πŸ˜–
    cf.thenCompose(list -> {
      CompletableFuture<Integer>[] cfs = new CompletableFuture[list.size()];
      for (int i = 0; i < list.size(); i++) {
        Integer e = list.get(i);
        cfs[i] = CompletableFuture.supplyAsync(() -> fn.apply(e));
      }
      return CompletableFutureUtils.allResultsFailFastOf(cfs);
    }).thenAccept(System.out::println);
    // output: [43, 44, 45]
    final MCffu<Integer, List<Integer>> mCffu = cffuFactory.completedMCffu(asList(42, 43, 44));
    mCffu.thenCompose(list -> {
      CompletableFuture<Integer>[] cfs = new CompletableFuture[list.size()];
      for (int i = 0; i < list.size(); i++) {
        Integer e = list.get(i);
        cfs[i] = CompletableFuture.supplyAsync(() -> fn.apply(e));
      }
      return CompletableFutureUtils.allResultsFailFastOf(cfs);
    }).thenAccept(System.out::println);
    // output: [43, 44, 45]

    // just parallel process multiple data, fresh and cool πŸ˜‹
    CfParallelUtils.thenParApplyFailFastAsync(cf, x -> x + 1)
        .thenAccept(System.out::println);
    // output: [43, 44, 45]
    mCffu.parOps().thenParApplyFailFastAsync(x -> x + 1)
        .thenAccept(System.out::println);
    // output: [43, 44, 45]
  }
}

# Complete runnable demo code can be found at CfParallelDemo.java.

2.6 support for handling specific exception types instead of all Throwable exceptions

In business processing try-catch statements, catching all exceptions (Throwable) is often not a good practice. Similarly, the CompletableFuture#exceptionally method also handles all exceptions (Throwable).

You should only handle specific exceptions that the current business clearly understands and can recover from, letting outer layers handle other exceptions; avoid masking bugs or incorrectly handling exceptions that you cannot recover from.

cffu provides corresponding catching* methods that support specifying exception types to handle; compared to the CF#exceptionally method, it adds an exception type parameter, with similar usage, so no code example is provided.

2.7 backport support for Java 8

All new CF methods from Java 9+ higher versions are directly available in Java 8 lower versions.

Important backport features include:

  • Timeout control: orTimeout / completeOnTimeout
  • Delayed execution: delayedExecutor
  • Factory methods: failedFuture / completedStage / failedStage
  • Processing operations: completeAsync / exceptionallyAsync / exceptionallyCompose / copy
  • Non-blocking reads: resultNow / exceptionNow / state

These backport methods are existing functionality of CompletableFuture, so no code examples are provided.

2.8 timeout-safe new implementation of orTimeout / completeOnTimeout

The CF#orTimeout() / CF#completeOnTimeout() methods use the internal single-threaded ScheduledThreadPoolExecutor of CF to trigger business logic execution when timeouts occur, which can break CompletableFuture's timeout and delay functionality❗

Because timeout and delayed execution are basic functionalities, once they fail, it can lead to:

  • Business functionality correctness issues, with timeout triggers being inaccurate and delayed
  • System stability issues, such as waiting operations in threads not returning, other dependent CFs not completing, and thread pool exhaustion and memory leaks

The cffu library provides timeout-safe new implementation methods:

Ensuring business logic won't execute in CF's single-threaded ScheduledThreadPoolExecutor.

For more information, see:

2.9 support for timeout-enabled join method

The cf.join() method "waits forever without timeout", which is very dangerous in business❗️ When unexpected long waits occur, it can lead to:

  • Main business logic blocking, with no opportunity for appropriate handling to respond to users in time
  • Consuming a thread, and threads are very limited resources (usually a few hundred); exhausting threads means service paralysis and failure

The join(timeout, unit) method is a join method that supports timeout; it's like cf.get(timeout, unit) compared to cf.get().

This new method is simple and similar to use, so no code example is provided.

2.10 anyOf method that returns specific types

CompletableFuture's anyOf() method returns type Object, losing specific types, making it inconvenient to use return values requiring casting operations, and it's not type-safe.

cffu's anySuccessOf() / anyOf() methods return specific type T instead of returning Object.

This method is simple and similar to use, so no code example is provided.

2.11 allOf/anyOf methods that accept broader input types

CompletableFuture's allOf() / anyOf() methods take CompletableFuture parameter types, not the broader CompletionStage types; for CompletionStage type inputs, you need to call the CompletionStage#toCompletableFuture method for conversion.

cffu's allOf() / anyOf() methods accept broader CompletionStage parameter types, making them more convenient to use.

The method usage is simple and similar, so no code example is provided.

more feature documentation

You can refer to:

πŸ”Œ API Docs

πŸͺ Dependencies

You can check the latest available version list at central.sonatype.com.

  • cffu library (including enhanced CompletableFutureUtils for Java CompletableFuture):
    • For Maven projects:

      <dependency>
        <groupId>io.foldright</groupId>
        <artifactId>cffu2</artifactId>
        <version>2.0.0</version>
      </dependency>
    • For Gradle projects:

      Gradle Kotlin DSL

      implementation("io.foldright:cffu2:2.0.0")

      Gradle Groovy DSL

      implementation 'io.foldright:cffu2:2.0.0'
  • πŸ“Œ TransmittableThreadLocal(TTL) cffu executor wrapper SPI implementation:
    • For Maven projects:

      <dependency>
        <groupId>io.foldright</groupId>
        <artifactId>cffu2-ttl-executor-wrapper</artifactId>
        <version>2.0.0</version>
        <scope>runtime</scope>
      </dependency>
    • For Gradle projects:

      Gradle Kotlin DSL

      runtimeOnly("io.foldright:cffu2-ttl-executor-wrapper:2.0.0")

      Gradle Groovy DSL

      runtimeOnly 'io.foldright:cffu2-ttl-executor-wrapper:2.0.0'
  • cffu bom:
    • For Maven projects:

      <dependency>
        <groupId>io.foldright</groupId>
        <artifactId>cffu2-bom</artifactId>
        <version>2.0.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    • For Gradle projects:

      Gradle Kotlin DSL

      implementation(platform("io.foldright:cffu2-bom:2.0.0"))

      Gradle Groovy DSL

      implementation platform('io.foldright:cffu2-bom:2.0.0')

πŸ“š More Resources

πŸ‘‹ About the Library Name

cffu is short for CompletableFuture-Fu; pronounced as C Fu, which sounds like Shifu.

Yes, it may remind you of the cute raccoon shifu from "Kung Fu Panda"~ 🦝

shifu

About

🦝 Java CompletableFuture-Fu ("CF-Fu", pronounced "Shifu") is a tiny 0-dependency library that improves the CompletableFuture(CF) usage experience and reduces misuse, enabling more convenient, efficient, and safe use of CF in your application. πŸ˜‹πŸš€πŸ¦Ί

Topics

Resources

License

Stars

Watchers

Forks