Timers
When writing a network based application, it is common to need to perform actions 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 in the 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 the
terminal, but any action can be taken at this point.
# #![deny(deprecated)]
# extern crate tokio;
#
use tokio::prelude::*;
use tokio::timer::Delay;
use std::time::{Duration, Instant};
fn main() {
let when = Instant::now() + Duration::from_millis(100);
let task = Delay::new(when)
.and_then(|_| {
println!("Hello world!");
Ok(())
})
.map_err(|e| panic!("delay errored; err={:?}", e));
tokio::run(task);
}
The above example creates a new Delay instance that will complete 100
milliseconds in the future. The new function takes an Instant, so we compute
when to be the instant 100 milliseconds from now.
Once the instant is reached, the Delay future completes, resulting in the
and_then block to be executed.
As with all futures, Delay is lazy. Simply creating a new Delay instance
does nothing. The instance must be used on a task that is spawned onto the Tokio
runtime. The runtime comes preconfigured with a timer implementation to
drive the Delay instance to completion. In the example above, this is done by
passing 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 that operations complete within reasonable amounts of time. This is especially true when waiting for data from external, potentially untrusted, sources.
The Timeout type ensures that an operation completes by a specified
instant in time.
# #![deny(deprecated)]
# extern crate tokio;
#
use tokio::io;
use tokio::net::TcpStream;
use tokio::prelude::*;
use std::time::{Duration, Instant};
fn read_four_bytes(socket: TcpStream)
-> Box<Future<Item = (TcpStream, Vec<u8>), Error = ()> + Send>
{
let buf = vec![0; 4];
let fut = io::read_exact(socket, buf)
.timeout(Duration::from_secs(5))
.map_err(|_| println!("failed to read 4 bytes by timeout"));
Box::new(fut)
}
# pub fn main() {}
The above function takes a socket and returns a future that completes when 4
bytes 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 5
seconds.
The timeout function is defined by FutureExt and is included in the
prelude. As such, use tokio::prelude::* imports FutureExt as well, so
we can call timeout on all futures in order to require them to complete by
the specified instant.
If the timeout is reached without the read completing, the read operation is
automatically canceled. This happens when the future returned by
io::read_exact is dropped. Because of the lazy runtime model, dropping a
future results in the operation being canceled.
Running code on an interval
Repeatedly running code on an interval is useful for cases like sending a PING
message on a socket, or checking a configuration file every so often. This can
be implemented by repeatedly creating Delay values. However, because
this is a common pattern, Interval is provided.
The Interval type implements Stream, yielding at the specified rate.
# #![deny(deprecated)]
# extern crate tokio;
#
use tokio::prelude::*;
use tokio::timer::Interval;
use std::time::{Duration, Instant};
fn main() {
let task = Interval::new(Instant::now(), Duration::from_millis(100))
.take(10)
.for_each(|instant| {
println!("fire; instant={:?}", instant);
Ok(())
})
.map_err(|e| panic!("interval errored; err={:?}", e));
tokio::run(task);
}
The above example creates an Interval that yields every 100 milliseconds
starting now (the first argument is the instant at which the Interval should
first fire).
By default, an Instant stream is unbounded, i.e., it will continue yielding at
the requested interval forever. The example uses Stream::take to limit the
number 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 yielded
immediately.
Notes on the timer
The Tokio timer has a granularity of one millisecond. Any smaller interval is
rounded 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 uses
a hierarchical hashed timer wheel implementation, which provides efficient
constant time complexity when creating, canceling, and firing timeouts.
The Tokio runtime includes one timer instance per worker thread. This means that, if the runtime starts 4 worker threads, there will be 4 timer instances. This allows avoiding synchronization in most cases since the task, when working with a timer, will be operating on state located on the current thread.
That said, the timer implementation is thread safe and supports usage from any thread.