Timers

When writing a network based application, it is common to need to performactions based on time.

  • Run some code after a set period of time.
  • Cancel a running operation that takes too long.
  • Repeatedly perform an action at an interval.
    These use cases are handled by using the various timer APIs that are provided inthe timer module.

Running code after a period of time

In this case, we want to perform a task after a set period of time. To do this,we use the Delay API. All we will do is write "Hello world!" to theterminal, but any action can be taken at this point.

  1. # #![deny(deprecated)]
  2. # extern crate tokio;
  3. #
  4. use tokio::prelude::*;
  5. use tokio::timer::Delay;
  6. use std::time::{Duration, Instant};
  7. fn main() {
  8. let when = Instant::now() + Duration::from_millis(100);
  9. let task = Delay::new(when)
  10. .and_then(|_| {
  11. println!("Hello world!");
  12. Ok(())
  13. })
  14. .map_err(|e| panic!("delay errored; err={:?}", e));
  15. tokio::run(task);
  16. }

The above example creates a new Delay instance that will complete 100milliseconds in the future. The new function takes an Instant, so we computewhen to be the instant 100 milliseconds from now.

Once the instant is reached, the Delay future completes, resulting in theand_then block to be executed.

As with all futures, Delay is lazy. Simply creating a new Delay instancedoes nothing. The instance must be used on a task that is spawned onto the Tokioruntime. The runtime comes preconfigured with a timer implementation todrive the Delay instance to completion. In the example above, this is done bypassing the task to tokio::run. Using tokio::spawn would also work.

Timing out a long running operation

When writing robust networking applications, it’s critical to ensure thatoperations complete within reasonable amounts of time. This is especially truewhen waiting for data from external, potentially untrusted, sources.

The Timeout type ensures that an operation completes by a specifiedinstant in time.

  1. # #![deny(deprecated)]
  2. # extern crate tokio;
  3. #
  4. use tokio::io;
  5. use tokio::net::TcpStream;
  6. use tokio::prelude::*;
  7. use std::time::{Duration, Instant};
  8. fn read_four_bytes(socket: TcpStream)
  9. -> Box<Future<Item = (TcpStream, Vec<u8>), Error = ()> + Send>
  10. {
  11. let buf = vec![0; 4];
  12. let fut = io::read_exact(socket, buf)
  13. .timeout(Duration::from_secs(5))
  14. .map_err(|_| println!("failed to read 4 bytes by timeout"));
  15. Box::new(fut)
  16. }
  17. # pub fn main() {}

The above function takes a socket and returns a future that completes when 4bytes have been read from the socket. The read must complete within 5 seconds.This is ensured by calling timeout on the read future with a duration of 5seconds.

The timeout function is defined by FutureExt and is included in theprelude. As such, use tokio::prelude::* imports FutureExt as well, sowe can call timeout on all futures in order to require them to complete bythe specified instant.

If the timeout is reached without the read completing, the read operation isautomatically canceled. This happens when the future returned byio::read_exact is dropped. Because of the lazy runtime model, dropping afuture results in the operation being canceled.

Running code on an interval

Repeatedly running code on an interval is useful for cases like sending a PINGmessage on a socket, or checking a configuration file every so often. This canbe implemented by repeatedly creating Delay values. However, becausethis is a common pattern, Interval is provided.

The Interval type implements Stream, yielding at the specified rate.

  1. # #![deny(deprecated)]
  2. # extern crate tokio;
  3. #
  4. use tokio::prelude::*;
  5. use tokio::timer::Interval;
  6. use std::time::{Duration, Instant};
  7. fn main() {
  8. let task = Interval::new(Instant::now(), Duration::from_millis(100))
  9. .take(10)
  10. .for_each(|instant| {
  11. println!("fire; instant={:?}", instant);
  12. Ok(())
  13. })
  14. .map_err(|e| panic!("interval errored; err={:?}", e));
  15. tokio::run(task);
  16. }

The above example creates an Interval that yields every 100 millisecondsstarting now (the first argument is the instant at which the Interval shouldfirst fire).

By default, an Instant stream is unbounded, i.e., it will continue yielding atthe requested interval forever. The example uses Stream::take to limit thenumber of times Interval yields, here limiting to a sequence of 10 events.So, the example will run for 0.9 seconds since the first of 10 values is yieldedimmediately.

Notes on the timer

The Tokio timer has a granularity of one millisecond. Any smaller interval isrounded up to the nearest millisecond. The timer is implemented in user land(i.e., does not use an operating system timer like timerfd on linux). It usesa hierarchical hashed timer wheel implementation, which provides efficientconstant time complexity when creating, canceling, and firing timeouts.

The Tokio runtime includes one timer instance per worker thread. This meansthat, if the runtime starts 4 worker threads, there will be 4 timerinstances. This allows avoiding synchronization in most cases since the task,when working with a timer, will be operating on state located on the currentthread.

That said, the timer implementation is thread safe and supports usage fromany thread.

Next up: Essential combinators