[JAVA] Introduction to Ratpack (6) --Promise

Ratpack introductory series

  1. Introduction to Ratpack (1) --What is Ratpack
  2. Introduction to Ratpack (2) --Architecture
  3. Introduction to Ratpack (3) --Hello world detailed explanation
  4. Introduction to Ratpack (4) --Routing & Static Content
  5. Introduction to Ratpack (5) --Json & Registry
  6. Introduction to Ratpack (6) --Promise
  7. Introduction to Ratpack (7) --Guice & Spring
  8. Introduction to Ratpack (8) --Session
  9. Introduction to Ratpack (9) --Thymeleaf

Promise

Ratpack is a non-blocking event-driven library, so it is assumed that each process is also written asynchronously. If you are an experienced Java user, you are familiar with the fact that writing asynchronous Java processing poorly can be difficult. Ratpack provides a Promise class to describe asynchronous processing concisely. The image is similar to JavaScript's Promise, and you can write a callback when the process is completed withthen ().

Make a Promise

I think IO processing is the most typical blocking operation. You can easily create a Promise by using the Blocking utility class.

chain.all( ctx -> {

    String query = ctx.getRequest().getQueryParams().get( "id" );

    Promise<String> result = Blocking.get( () -> {
        return Database.find( query );
    } );

    ctx.render( result );
} );

Think of Database.find () as the process of finding data in a fictitious database. Blocking.get () executes the argument closure asynchronously and wraps its return value in a Promise. You can also pass a Promise toContext.render ().

Use ʻop () for operations with no return value. The ʻOperation class is a Promise with no return value in Ratpack.

Blocking.op( () -> {
    String data = ctx.getRequest().getQueryParams().get( "data" );
    Database.persist( data );
} ).then( () -> {
    ctx.render( "OK" );
} );

First, save the information in a fictitious database in Blocking.op (). The ʻop () method returns ʻOperation for this operation. Next, describe the process after saving the data in the database with then (). We are calling Context.render () to create a response of ʻOK`.

Promise.sync()

Create a Promise from the factory.

Promise<String> result = Promise.sync( () -> "data" );
result.then( data -> {
    ctx.render( "OK" );
} );

Promise.async()

A Promise.async () static factory is provided for when working with other async libraries.

Promise<String> result = Promise.async( downstream -> {
    downstream.success( "data" );
} );
result.then( data -> {
    ctx.render( "OK" );
} );

Call the success () method to tell you that the process is complete. Note that Promise.async () itself does not perform argument processing asynchronously. You just have to write the asynchronous process yourself (or in the library) (so the official example creates a Thread and callssuccess ()).

Operation of Promise

then

Specifies the callback to be called when the processing of Promise is completed. I think the most common process is to call Context.render () in the callback and create a response.

Note that then () registers the callback with the application and executes it sequentially. Consider the following code.

@Data class Obj {
    public int a;
    public int b;
    public int c;
}
Obj o = new Obj();
Promise.value( 1 ).then( o::setA );
Promise.value( 2 ).then( o::setB );
Promise.value( 3 ).then( o::setC );
Operation.of( () -> ctx.render( o.toString() ) ).then();

Since Promise represents asynchronous processing, at first glance it may seem that the field of ʻo when calling ʻo.toString () is timing dependent. However, calling then () guarantees that Ratpack will execute sequentially in the order of registration, so the value of ʻo.toString ()will always be ʻObj (a = 1, b = 2, c = 3 ). However, this behavior is non-intuitive and confusing, so you shouldn't use it too much.

map

Creates a Promise that adapts the specified function to the result of the Promise. It's the same as map such as streams.

String result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .map( String::toUpperCase );
} ).getValue();

assertThat( result ).isEqualTo( "HOGE" );

blockingMap

It is almost the same as map, but it is executed by the thread for blocking processing. This is an image that wraps the processing in map withBlocking.get ()etc. There is a derived method blockingOp.

flatMap

Replaces the result of Promise with the Promise returned by the specified function. Ratpack has a lot of processing that returns Promise by default, so the frequency of use is unexpectedly high.

String result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .flatMap( v -> {
                      assertThat( v ).isEqualTo( "hoge" );
                      return Promise.value( "piyo" );
                  } );
} ).getValue();

assertThat( result ).isEqualTo( "piyo" );

mapIf

Applies the map function only if the specified Predicate is positive.

mapError flatMapError

If an exception occurs, the result of applying the map function that takes the exception as an argument is returned. You can fluently write branches when it ends normally and when an error occurs.

String result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .mapIf( s -> true, s -> { throw new RuntimeException();} )
                  .mapError( t -> "piyo" );
} ).getValue();

assertThat( result ).isEqualTo( "piyo" );

apply

It takes a function that takes the caller's Promise itself and returns a Promise. I don't know how to use it, but it seems that the purpose is to simplify the description when the process is divided into methods.

String result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" ).apply( p -> {
        assertThat( p == Promise.value( "hoge" ) ).isTrue();
        return p.map( String::toUpperCase );
    } );
} ).getValue();

assertThat( result ).isEqualTo( "HOGE" );

around

Insert processing before and after the calculation of Promise. It looks useful in itself, but it's a shameful method that makes the code meaninglessly verbose because you have to wrap the after result in ʻExecResult`.

String result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .around(
                          () -> "before",
                          ( before, r ) -> {
                              assertThat( before ).isEqualTo( "before" );
                              assertThat( r.getValue() ).isEqualTo( "hoge" );
                              return ExecResult.of( Result.success( "piyo" ) );
                          }
                  );
} ).getValue();

assertThat( result ).isEqualTo( "piyo" );

replace

Replace Promise with another Promise. In short, it's a version that doesn't take an argument to flatMap (). This is also a method whose necessity is not well understood.

String result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .replace( Promise.value( "piyo" ) );
} ).getValue();

assertThat( result ).isEqualTo( "piyo" );

route

If Predicate is true, execute the specified consumer. As far as JavaDoc is concerned, it seems that it is intended to be used for data validation etc ..., but I feel that it is not very easy to use.

ExecResult<String> result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .route( s -> false, System.out::println );
} );

assertThat( result.getValue() ).isEqualTo( "hoge" );
assertThat( result.isComplete() ).isFalse();

boolean completed = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .route( s -> true, System.out::println );
} ).isComplete();

assertThat( completed ).isTrue();

to

Converts Promise to another type. It may seem awkward to use, but it is used for integration of external libraries. The following is an example of RxRatpack.

List<String> resultHolder = new ArrayList<>();
ExecHarness.runSingle( e -> {
    Promise.value( "hoge" )
           .to( RxRatpack::observe )
           .subscribe( s -> resultHolder.add( s ) );
} );

assertThat( resultHolder ).containsExactly( "hoge" );

next

It has a consumer that takes the result of Promise as an argument. The return value Promise returns the same result as the original Promise. There are derived methods such as nextOp.

String result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .next( System.out::println );
} ).getValue();

assertThat( result ).isEqualTo( "hoge" );

right left

Combines Promise with another Promise and returns it as Promise of Pair.

Pair<String, String> result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .right( Promise.value( "piyo" ) );
} ).getValue();

assertThat( result.getLeft() ).isEqualTo( "hoge" );
assertThat( result.getRight() ).isEqualTo( "piyo" );

cache

Cache the result of Promise. If an exception occurs, that exception is also cached. There are derived methods such as cacheResultIf.

onError

Describes the processing when an error occurs. I think the main usage is to write Context.render () at the time of error. You can have multiple patterns of arguments, such as one that takes an exception class as an argument, one that receives an exception consumer, and one that selects an exception with Predicate.

close

When the Promise is completed or an exception occurs, the ʻAutoCloseable` specified in the argument is closed. I don't know where to use it.

retry

If the process fails, it will try again after a specified time. This is convenient when calling an external API.

String result = ExecHarness.yieldSingle( e -> {
    return Promise.value( "hoge" )
                  .retry( 3, Duration.ofSeconds( 1 ), ( i, t ) -> System.out.printf( "retry: %d%n", i ) );
} ).getValue();

assertThat( result ).isEqualTo( "hoge" );

time

It takes a consumer as an argument that returns the time taken to execute the Promise. Is performance measurement the main use?

About threads and fork

Ratpack executes Promise in the unit represented by ʻExecution class for asynchronous processing. Normally, you are not aware of this ʻExecution, but in order to execute multiple Promises in parallel, you need to fork () Execution. Promise.fork () is provided as a convenience method, and you can easily execute Promise in another thread.

The code below is a slight modification of the example in the fork JavaDoc.

CyclicBarrier b = new CyclicBarrier( 2 );

Pair<String, String> result = ExecHarness.yieldSingle( e -> {
    Promise<String> p1 = Promise.sync( () -> {
        b.await();
        return "hoge";
    } ).fork();
    Promise<String> p2 = Promise.sync( () -> {
        b.await();
        return "piyo";
    } ).fork();
    return p1.right( p2 );
} ).getValue();

assertThat( result.getLeft() ).isEqualTo( "hoge" );
assertThat( result.getRight() ).isEqualTo( "piyo" );

Now, if you remove the call to fork (), the p1 and p2 will be executed in sequence in the same thread, resulting in a deadlock. If you have created another thread with fork (), it will work normally.

Summary

Ratpack's Promise provides support for asynchronous processing, which Java is not good at. However, there are some parts that have strong habits and there are many methods, so it is difficult to use them properly. The trick may be not to try to be very smart. Ratpack has an RxRatpack module to support RxJava. If you have familiar asynchronous libraries, you can also take advantage of them.

Personal recommendation

Recommended Posts

Introduction to Ratpack (6) --Promise
Introduction to Ratpack (8)-Session
Introduction to Ratpack (9) --Thymeleaf
Introduction to Ratpack (2)-Architecture
Introduction to Ratpack (5) --Json & Registry
Introduction to Ratpack (7) --Guice & Spring
Introduction to Ratpack (1) --What is Ratpack?
Introduction to Ruby 2
Introduction to SWING
Introduction to web3j
Introduction to Micronaut 1 ~ Introduction ~
[Java] Introduction to Java
Introduction to migration
Introduction to java
Introduction to Doma
Introduction to Ratpack (Extra Edition) --Using Sentry
Introduction to Ratpack (3) --hello world detailed explanation
Introduction to JAR files
Introduction to RSpec 1. Test, RSpec
Introduction to bit operation
Introduction to PlayFramework 2.7 ① Overview
Introduction to Android Layout
Introduction to design patterns (introduction)
Introduction to Practical Programming
Introduction to javadoc command
Introduction to jar command
Introduction to lambda expression
Introduction to java command
Introduction to RSpec 2. RSpec setup
Introduction to Keycloak development
Introduction to javac command
Introduction to Design Patterns (Builder)
Introduction to RSpec 6. System specifications
Introduction to Android application development
Introduction to RSpec 3. Model specs
Introduction to Metabase ~ Environment Construction ~
(Dot installation) Introduction to Java8_Impression
Introduction to Design Patterns (Composite)
Introduction to Micronaut 2 ~ Unit test ~
Introduction to JUnit (study memo)
Introduction to Spring Boot ① ~ DI ~
Introduction to design patterns (Flyweight)
[Java] Introduction to lambda expressions
Introduction to Spring Boot ② ~ AOP ~
Introduction to Apache Beam (2) ~ ParDo ~
[Ruby] Introduction to Ruby Error statement
Introduction to EHRbase 2-REST API
Introduction to design patterns Prototype
GitHub Actions Introduction to self-made actions
[Java] Introduction to Stream API
Introduction to Design Patterns (Iterator)
Introduction to Spring Boot Part 1
XVim2 introduction memo to Xcode12.3
Introduction to RSpec-Everyday Rails Summary-
Introduction to Design Patterns (Strategy)
[Introduction to rock-paper-scissors games] Java
[Introduction to Java] About lambda expressions
Introduction to algorithms in java-cumulative sum
Introduction to Functional Programming (Java, Javascript)
Introduction to Ruby processing system self-made
Introduction to algorithms with java-Shakutori method