Adding State to Our Web App
Recap and overview#
We built a basic web server in the previous chapter which is the foundation for this chapter and the next. In this chapter we are going to add some in-memory state to the server. This will require us to explore the concepts of interior mutability, atomic types, as well as other tools which make it safe to share data across threads. The next chapter is focused on state backed by a database which is distinctly different from what we are covering here.
We are going to make the server more complex in this chapter, and sometimes this is exactly what you need to achieve your goals. However you always want to use the right tools for your job, so do not feel like each piece is required for every problem you might be solving.
We're going to start with the Actix server we created in the previous chapter, but we are going to make our server slightly more complex by adding state that persists across requests.
First let's update our set of import statements to bring in some new types that we will use for managing state:
Cell type we will discuss shortly. The group of things we pull in from
std::sync::atomic are the tools we need to work with a
usize that can be modified atomically and therefore is thread-safe. Finally, we pull in
std::sync which are tools we will use to safely share and mutate things that are not atomic across multiple threads.
Actix by default will create a number of workers to enable handling concurrent requests. One piece of state we are going to maintain is a unique
usize for each worker. We will create an atomic
usize to track this count of workers because it needs to be thread-safe however it only ever needs to increase. This is a good use case for an atomic integer. Let's define a static variable to hold our server counter atomic:
Static versus const#
There are two things that you will see in Rust code which look similar and which live for the entire lifetime of your program, one is denoted
const and the other is denoted
Items marked with
const are effectively inlined at each site they are used. Therefore references to the same constant do not necessarily point to the same memory address.
On the other hand,
static items are not inlined, they have a fixed address as there is only one instance for each value. Hence
static must be used for a shared global variable.
It is possible to have
static mut variables, but mutable global variables are bad and therefore in order to read/write mutable statics requires the use of the
Atomics, on the other hand, can be modified in such a way that we do not need to mark the variable as mutable. The
mut keyword is really a marker for the compiler to guarantee that certain memory safety properties are upheld for which atomics are immune.
const variables must have their types given explicitly, so we write the type
AtomicUsize for our variable. The
new function on
AtomicUsize is marked
const which is what allows it to be called in this static context.
Now we can define the struct which will hold the state for our app:
Each worker thread gets its own instance of this state struct. Actix takes an application factory because it will create many instances of the application, and therefore many instances of the state struct. Therefore in order to share information across the different instances we will have to use different mechanisms than we have seen so far.
Defining our state#
The first part of the state will be set from the atomic
usize we declared earlier. We will see how this is set when we get to our updated factory function, but for now we can note that this will just be a normal
usize that gets set once when this struct is initialized.
The second piece of data will keep track of the number of requests seen by the particular worker that owns this instance of state.
The request count is owned by each worker and changes are not meant to be shared across threads, however we do want to mutate this value within a single request. We cannot just use a normal
usize variable because we can only get an immutable reference to the state inside a request handler. Rust has a pattern for mutating a piece of data inside a struct which itself is immutable known as interior mutability.
Two special types enable this,
Cell implements interior mutability by moving values in and out of a shared memory location.
RefCell implements interior mutability by using borrow checking at runtime to enforce the constraint that only one mutable reference can be live at any given time.
If one tries to mutably borrow a
RefCell that is already mutably borrowed the calling thread will panic. As we are dealing with a primitive type as the interior value, namely
usize, we can take advantage of
Cell copying the value in and out and avoid the overhead of the extra lock associated with a
RefCell are not needed that often in everyday Rust, but they are absolutely necessary in some situations so it is useful to be aware of them.
Finally, the last piece of state is going to be a vector of strings that represent messages shared across all of the workers. We want each worker thread to be able to read and write this state, and we want updates to be shared amongst the workers.
In other words, we want shared mutable state, which is typically where bugs happen. Rust provides us with tools that makes writing safe and correct code involving shared mutable state relatively painless. The state we care about is a vector of strings, so we know we want a
Sharing across threads#
We also want to be able to read and write this vector on multiple threads in a way that is safe. We can ensure mutually exclusive access to the vector by creating a
Mutex that wraps our vector.
Mutex<Vec<String>> is a type that provides an interface for coordinating access to the inner object (
Vec<String>) across multiple threads. We will see how this works in the implementation of our handler.