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 ()
.
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 ()
).
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?
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 Promise
s 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.
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.
map
and flatMap
, and not all methods are practical.Blocking
class or theblocking ...
method for blocking operations.then
.Recommended Posts