I've got a GRPC API that I'm designing. It includes an RPC method that requires the caller follow-up with another API call. Some pseudo-code a client might write:
val apiInstsance = makeAPIInstance()
val questionFromMyAPI = apiInstance.computeQuestion(clientInformation)
val clientResponse = doBusinessLogic(questionFromMyAPI)
apiInstance.offerResult(questionFromMyAPI, clientResponse)
The key thing I want to impart is that computeQuestion
demands a response as per the protocol, IE it is not valid for a client to simply return
after calling computeQuestion
, they must respond with an offerResult
to any computeQuestion
calls.
If an API user fails to call offerResult
the likely result is a kind of live-lock; nothing will be wrong or be waiting on stack, but it will likely be implemented as an infinite spinner or beach-ball. This is tough for a developer because they aren't given any traction by the design of the API to figure out what's wrong.
I want to change my API such that user can more idiomatically write something like:
val apiInstance = makeAPIInstance()
val questionFromMyAPI = apiInstance.computeQuestion(clientInformation)
AlternativeThreadingContext.runAppropriately(() -> {
// with C#'s disposable syntax --you can imagine similar for RAII or try-finally
using(questionFromOurAPI) {
doComplexBuinessLogicAndSendResultTo(apiInstance)
}
})
...With this latter setup where we express protocol semantics through a kind of acquired resource. If the acquired resource is disposed (or the finally block reached) before a call to offerResult
, then we can raise an exception. In this way I can convert from a likely-hanging process to a raised exception, but this requires I throw an exception in the RAII-destructor/finally-block/dispose-method, which seems like its against best-practices.
There is also the problem of exception hiding. If an exception is raised by doBusinessLogic
, then any protocol check I do in the dispose/finally block is likely to find a protocol error and raise another exception effectively hiding the first exception, which is the "real" exception.
In future I hope to write language specific code in each of the major users of our API that will navigate some of these subtleties for you, but for now I'm not sure what to do.
Am I missing something? Is it best to simply document the behaviour and let API callers hang their processes as the failure mode for writing bad code here? Is there some obvious way I can reframe my question such that its expressed as a transaction or other paradigm that GPRC can clearly and easily handle with such-and-such a pattern I'm not aware of? Any help appreciated.