Tower, Episode 2: calling a Tower Service

In the previous episode we have seen that a Tower service is made up of the two methods poll_ready and call, which are subject to the following contract: callers first must invoke poll_ready until it returns Poll::Ready before they can invoke call, else call might panic.

This is the signature of poll_ready:

1
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;

In order to invoke poll_ready, we need a std::task::Context. If you are familiar with Futures, you will notice that its poll method also has such an argument. So do we have to implement a Future just to call a service?

Luckily not, because Tower already provides the ServiceExt trait with some helper methods to either invoke poll_ready or even chain invoking poll_ready and call according to their contracts.

Let’s first take a look at ServiceExt::oneshot. According to the documentation, it “consumes this service, calling with the provided request once it is ready”.

1
2
3
let mut service = EchoService;
let response = service.oneshot("Hello, Tower!".into()).await?;
println!("Echo service responded with: {response}");

The Future returned by oneshot first invokes poll_ready when polled and then, if the service returns Poll::Ready, invokes call. Of course it’s not possible to invoke the service again, because oneshot consumes it by taking it as self. But it actually is the only way provided by Tower enforcing the contract between poll_ready and call.

Next let’s take a look at ServiceExt::ready, which, according to the documentation, “yields a mutable reference to the service when it is ready to accept a request”.

1
2
3
4
5
6
7
8
9
10
let mut service = EchoService;

let service = service.ready().await?;
let response = service.call("Hello, Tower!".into()).await?;
println!("Echo service responded with: {response}");

// We should invoke `ready` once again before invoking `call`!
// service.ready().await?;
let response = service.call("Hello again, Tower!".into()).await?;
println!("Echo service responded with: {response}");

The Future returned by ready returns a mutable reference to the service which then can be used to call it. As you can see from the above, we can even call the service several times without checking its readiness. Of course this is against the contract and we should instead invoke ready once again every time before we invoke call, i.e. in the above code snippet we should uncomment line 8.

Finally there is ServiceExt::ready_oneshot. It works like ready, but it consumes the service, yet returns it again.

1
2
3
4
5
6
7
8
9
10
let service = EchoService;

let mut service = service.ready_oneshot().await?;
let response = service.call("Hello, Tower!".into()).await?;
println!("Echo service responded with: {response}");

// We should invoke `ready_oneshot` once again before invoking `call`!
// let mut service = service.ready_oneshot().await?;
let response = service.call("Hello again, Tower!".into()).await?;
println!("Echo service responded with: {response}");

Like for ready, we can call the returned service several times without checking its readiness. Therefore I consider this method misnamed, because “oneshot” for me means, well, no more than one invocation of call.

You might now ask whether this service contract of always invoking poll_ready before call really matters that much. Well, not for our harmless EchoService which is always ready and therefore its call method never panics. Let’s look at some other service, behaving less nicely, in the next episode. As usual the full code is available in tower-experiments on GitHub.