Understanding when use of ratpack.handling.Context#promise is necessary

classic Classic list List threaded Threaded
9 messages Options
Reply | Threaded
Open this post in threaded view
|

Understanding when use of ratpack.handling.Context#promise is necessary

mhurne
I've been working on a simple application to learn more about Ratpack, and I could use some help understanding when use of ratpack.handling.Context#promise is necessary. My application is using the latest org.mongodb:driver-async:3.0.0-SNAPSHOT build to interact with MongoDB. To get a document from Mongo by ID, I'm calling com.mongodb.async.client.MongoCollection#find(java.lang.Object), which returns a com.mongodb.async.client.FindFluent, which is a com.mongodb.async.client.MongoIterable. Then, I call MongoIterable#first, passing it a com.mongodb.async.SingleResultCallback. My SingleResultCallback is implemented via a Closure.

Initially, my SingleResultCallback called ratpack.handling.Context#render to render the response after the Mongo operation was completed. However, this resulted in response bodies such as "No response sent for GET request to /things/54720972678f8fa30d951732 ..."

Suspecting that the issue lies in Ratpack having no way of knowing when the Mongo callback is finished, I tried wrapping the existing call to the Mongo driver in an Action passed to ratpack.handling.Context#promise. My SingleResultCallback now calls ratpack.exec.Fulfiller#success, and render is called in my Action passed to Promise#then. The application now behaves as I'd expect (though I'm not confident that it is "as asynchronous as possible," if that makes any sense).

Would someone mind providing a link to some material that will help me understand why I needed to involve a call to Context#promise? Also, does anyone have any comments on whether my "working" implementation is optimal?

I also plan to try out org.mongodb:driver-rx:3.0.0-SNAPSHOT with ratpack-rx. Perhaps that approach would be preferred, as RxJava would serve to glue the Mongo driver and Ratpack together in a more familiar fashion?

Here's the complete source of my broken Ratpack.groovy:

import com.mongodb.ConnectionString
import com.mongodb.MongoException
import com.mongodb.async.SingleResultCallback
import com.mongodb.async.client.MongoCollection
import com.thehurnes.inject.MongoModule
import org.bson.Document
import org.bson.types.ObjectId
import ratpack.jackson.JacksonModule

import static ratpack.groovy.Groovy.ratpack
import static ratpack.jackson.Jackson.json

ratpack {

    bindings {
        add MongoModule, {
            it.connectionString = new ConnectionString("mongodb://localhost:27017/")
            it.dbName = "hello-ratpack"
        }
        add JacksonModule
    }

    handlers {

        get("things/:id") { MongoCollection<Document> collection ->
            def id = new ObjectId(pathTokens.get("id"))
            collection.find(new Document().append('_id', id)).first({ Document thing, MongoException e ->
                if (e) {
                    clientError(404)
                } else {
                    render json(thing)
                }
            } as SingleResultCallback)
        }

    }

}


And here's the complete source of my working Ratpack.groovy:

import com.mongodb.ConnectionString
import com.mongodb.MongoException
import com.mongodb.async.SingleResultCallback
import com.mongodb.async.client.MongoCollection
import com.thehurnes.inject.MongoModule
import org.bson.Document
import org.bson.types.ObjectId
import ratpack.jackson.JacksonModule

import static ratpack.groovy.Groovy.ratpack
import static ratpack.jackson.Jackson.json

ratpack {

    bindings {
        add MongoModule, {
            it.connectionString = new ConnectionString("mongodb://localhost:27017/")
            it.dbName = "hello-ratpack"
        }
        add JacksonModule
    }

    handlers {

        get("things/:id") { MongoCollection<Document> collection ->
            def id = new ObjectId(pathTokens.get("id"))
            promise { f ->
                collection.find(new Document().append('_id', id)).first({ Document thing, MongoException e ->
                    if (e) {
                        f.error(e)
                    } else {
                        f.success(thing)
                    }
                } as SingleResultCallback)
            }.then { Document thing ->
                render json(thing)
            }
        }

    }

}

If you'd like to see what the source of MongoModule looks like, I can provide that as well.

Thanks!
Reply | Threaded
Open this post in threaded view
|

Re: Understanding when use of ratpack.handling.Context#promise is necessary

uris77
The api for dealing with async operations is promise{}. Luke Daley explains it here http://ldaley.com/post/97376696242/ratpack-execution-model-part-1

My naive generalization is: use promise{} with async operations and blocking{} with sync/blocking operations.
Reply | Threaded
Open this post in threaded view
|

Re: Understanding when use of ratpack.handling.Context#promise is necessary

mhurne
Thanks for your reply. The article you referenced is actually what led me to my solution of using promise(). :-)

I suppose what has thrown me off is seeing code like the following from https://github.com/ratpack/example-books/blob/master/src/ratpack/Ratpack.groovy:

get {
    bookService.all().toList().subscribe { List<Book> books ->
        ...
        render groovyMarkupTemplate(...)
    }
}

Why doesn't that handler need to use promise()?
Reply | Threaded
Open this post in threaded view
|

Re: Understanding when use of ratpack.handling.Context#promise is necessary

mhurne
Hmm, perhaps the reason the example-books handler I referenced doesn't call promise() is because it relies on code in https://github.com/ratpack/example-books/blob/master/src/main/groovy/ratpack/example/books/BookDbCommands.groovy that calls blocking()?
Reply | Threaded
Open this post in threaded view
|

Re: Understanding when use of ratpack.handling.Context#promise is necessary

danhyun
Hi Matt,

uris77 is correct about when to use promise vs blocking.

The reason example-books uses Context#blocking is because it's integrating with a traditional blocking API call.
The Groovy Sql interface uses JDBC calls under the hood, which is blocking API call. Since we can't change the Sql implementation to be asynchronous, Context#blocking provides a way to let Ratpack run this code in its managed I/O threadpool.

In the case where you have non-blocking APIs, Context#blocking does not suffice. Context#promise is the adapter provided by Ratpack for you to hook into Ratpack's execution model. You're provided a Fulfiller that you can use to notify subscribers of the outcome of your async call. Read more here and here.

Keep in mind that for any of Ratpack's async functionality, you must "subscribe" to the outcome of the async call for the promise or blocking action to execute.
get {
  promise {
   it.success("foo") // will not run
  }
}
vs
get {
  promise {
   it.success("foo")
  } then { success ->
    response.send(success) // sends the string "foo" to the client
  }
}

As an aside, it's nicer to hide all the async activity in the implementation of a service then just to subscribe to the async calls within the handler code.

class MongoService {
    private final ExecControl execControl
    private final MongoCollection<Document> collection

    @Inject
     public MongoService(ExecControl exec, MongoCollection<Document> collection) {
        this.execControl = exec
        this.collection = collection
    }

    Promise<Document> findById(ObjectId id) {
        execControl.promise { f ->
            collection.find(new Document().append('_id', id)).first({ Document thing, MongoException e ->
                if (e) {
                    f.error(e)
                } else {
                    f.success(thing)
                }
            } as SingleResultCallback)
        }
    }
}


bindings {
  bind(MongoService).in(Scopes.SINGLETON)
}

handlers {
  get("things/:id") { MongoService service ->
    service.findById(new ObjectId(pathTokens.get("id")))
    .onError { response.send "error occured" }
    .then { Document thing -> render json(thing) }
  }
}
Reply | Threaded
Open this post in threaded view
|

Re: Understanding when use of ratpack.handling.Context#promise is necessary

mhurne
Thank you both! This is making a lot more sense to me now. The statement that "Context#promise is the adapter provided by Ratpack for you to hook into Ratpack's execution model" is key to my understanding. I understand that one could say "Context#blocking is another adapter provided by Ratpack for you to hook into Ratpack's execution model, for execution of blocking operations." In other words, unless a handler has nothing to do other than computation, there ought to be either a Context#promise call or a Context#blocking call, or both, and as many as are appropriate for the non-compute operations that need to happen. I appreciate the clarification on the need to subscribe to the promise and the example code showing how I might extract the Mongo-related code into a service as well. Gold. Thanks again.
Reply | Threaded
Open this post in threaded view
|

Re: Understanding when use of ratpack.handling.Context#promise is necessary

danhyun
Matt,

You've got it. Happy coding! Please explore Ratpack and submit any bugs you encounter or file a feature request that you think would be good to have.
Reply | Threaded
Open this post in threaded view
|

Re: Understanding when use of ratpack.handling.Context#promise is necessary

Luke Daley
Administrator
You could extract the promise adapting out to a general routine…

<code>
import com.mongodb.async.SingleResultCallback
import ratpack.exec.ExecControl
import ratpack.exec.Promise
import ratpack.func.Action

import javax.inject.Inject

class MongoService {

    private final ExecControl execControl

    @Inject
    MongoService(ExecControl execControl) {
        this.execControl = execControl
    }

    public <T> Promise<T> promise(Action<SingleResultCallback<T>> action) {
        return execControl.promise { f ->
            action.execute { result, error ->
                if (error) {
                    f.error(error)
                } else {
                    f.success(result)
                }
            }
        }
    }

}
</code>

Your handlers would then look like this…

<code>
    handlers { MongoService mongoService ->

        handler("things") {
            byMethod {
                get { MongoCollection<Document> collection ->
                    mongoService.promise {
                        collection.find().into([], it)
                    } then { List<Document> things ->
                        render json(things)
                    }
                }
</code>
Reply | Threaded
Open this post in threaded view
|

Re: Understanding when use of ratpack.handling.Context#promise is necessary

mhurne
That makes sense to me, Luke. Thanks!