官术网_书友最值得收藏!

Building a stateful server

The web app of the file_transfer_stub project was completely stateless, meaning that every operation had the same behavior independently of the previous operations. Other ways to explain this are that no data was kept from one command to the next, or that it computed pure functions only.

The web app of the file_transfer project had a state, but that state was confined to the filesystem. Such a state was the content of the data files. Nevertheless, the application itself was still stateless. No variable survived from one request handling to another request handling.

The REST principles are usually interpreted as prescribing that any API must be stateless. That is a misnomer because REST services can have a state, but they must behave as if they were stateless. To be stateless means that, except for the filesystem and the database, no information survives in the server from one request handling to another request handling. To behave as if stateless means that any sequence of requests should obtain the same results even if the server is terminated and restarted between one request and a successive one.

Clearly, if the server is terminated, its state is lost. So, to behave as stateless means that the behavior should be the same even if the state is reset. So, what is the purpose of the possible server state? It is to store information that can be obtained again with any request, but that would be costly to do so. This is the concept of caching.

Usually, any REST web server has an internal state. The typical information stored in this state is a pool of connections to the database. A pool is initially empty, and when the first handler must connect to the database, it searches the pool for an available connection. If it finds one, it uses it. Otherwise, a new connection is created and added to the pool. A pool is a shared state that must be passed to any request handler.

In the projects of the previous sections, the request handlers were pure functions; they had no possibility of sharing a common state. In the memory_db project, we'll see how we can have a shared state in the Actix web framework that is passed to any request handler.

This web app represents access to a very simple database. Instead of performing actual access to a database, which would require further installations in your computer, it simply invokes some functions exported by the data_access module, defined in the src/data_access.rs file, that keep the database in memory.

A memory database is a state that is shared by all the request handlers. In a more realistic app, a state would contain only one or more connections to an external database.

How to have a stateful server

To have a state in an Actix service, a struct must be declared, and any data that should be part of the state should be a field of that struct.

At the beginning of the main.rs file, there is the following code:

struct AppState {
db: db_access::DbConnection,
}

In the state of our web app, we need only one field, but other fields can be added.

The DbConnection type declared in the db_access module represents the state of our web app. In the main function, just before creating the server, there is the following statement that instantiates the AppState, and then properly encapsulates it:

let db_conn = web::Data::new(Mutex::new(AppState {
db: db_access::DbConnection::new(),
}));

The state is shared by all the requests, and the Actix web framework uses several threads to handle the requests, and so the state must be thread-safe. The typical way of declaring a thread-safe object in Rust is to encapsulate it in a Mutex object. This object is then encapsulated in a Data object.

To ensure that such a state is passed to any handler, the following line must be added before calling the service functions:

.register_data(db_conn.clone())

Here, the db_conn object is cloned (cheaply, as it is a smart pointer), and it is registered into the app.

The effect of this registration is that it is now possible to add another type of argument to the request handlers (both synchronous and asynchronous), as follows:

state: web::Data<Mutex<AppState>>

Such an argument can be used in statements like this:

let db_conn = &mut state.lock().unwrap().db

Here, the state is locked to prevent concurrent access by other requests, and its db field is accessed.

The API of this service

The rest of the code in this app is not particularly surprising. The API is clear from the names used in the main function, as illustrated in the following code block:

.service(
web::resource("/persons/ids")
.route(web::get().to(get_all_persons_ids)))
.service(
web::resource("/person/name_by_id/{id}")
.route(web::get().to(get_person_name_by_id)),
)
.service(
web::resource("/persons")
.route(web::get().to(get_persons)))
.service(
web::resource("/person/{name}")
.route(web::post().to(insert_person)))
.default_service(
web::route().to(invalid_resource))

Notice that the first three patterns use the GET method, and so they query the database. The last one uses the POST method, and so it inserts new records into the database.

Notice also the following lexical conventions.

The path of the URI for the first and third patterns begins with the plural word persons, which means that zero, one, or several items will be managed by this request and that any such item represents a person. Instead, the path of the URI for the second and fourth patterns begins with the singular word person, and this means that no more than one item will be managed by this request. 

The first pattern ends with the plural word ids, and so several items regarding the id will be handled. It has no condition, and so all the IDs are requested. The second pattern contains the word name_by_id, followed by an id parameter, and so it is a request of the name database column for all the records for which the id column has the value specified.

Even in the case of any doubt, the name of the handling functions or comments should make the behavior of the service clear, without having to read the code of the handlers. When looking at the implementation of the handlers, notice that they either return nothing at all or simple text.

Testing the service

Let's test the service with some curl operations.

First of all, we should populate the database that is initially empty. Remember that, being only in memory, it is empty any time you start the service.

After starting the program, type the following commands:

curl -X POST http://localhost:8080/person/John
curl -X POST http://localhost:8080/person/Jonathan
curl -X POST http://localhost:8080/person/Mary%20Jane

After the first command, a number 1 should be printed to the console. After the second command, 2 should be printed, and after the third command, 3 should be printed. They are the IDs of the inserted names of people.

Now, type the following command:

curl -X GET http://localhost:8080/persons/ids

It should print the following: 1, 2, 3. This is the set of all the IDs in the database.

Now, type the following command:

curl -X GET http://localhost:8080/person/name_by_id/3

It should print the following: Mary Jane. This is the name of the unique person for which the id is equal to 3. Notice that the input sequence %20 has been decoded into a blank.

Now, type the following command:

curl -X GET http://localhost:8080/persons?partial_name=an

It should print the following: 2: Jonathan; 3: Mary Jane. This is the set of all the people for which the name column contains the an substring.

Implementing the database

The whole database implementation is kept in the db_access.rs source file. 

The implementation of the database is quite simple. It is a DbConnection type, containing Vec<Person>, where Person is a struct of two fields—id and name.

The methods of DbConnection are described as follows:

  • new: This creates a new database.
  • get_all_persons_ids(&self) -> impl Iterator<Item = u32> + '_: This returns an iterator that provides all the IDs contained in the database. The lifetime of such an iterator must be no more than that of the database itself.
  • get_person_name_by_id(&self, id: u32) -> Option<String>: This returns the name of the unique person having the specified ID if there is one, or zero if there isn't one.
  • get_persons_id_and_name_by_partial_name<'a>(&'a self, subname: &'a str) -> impl Iterator<Item = (u32, String)> + 'a: This returns an iterator that provides the ID and the name of all the people whose name contains the specified string. The lifetime of such an iterator must be no more than that of the database itself, and also no more than that of the specified string.
  • insert_person(&mut self, name: &str) -> u32: This adds a record to the database, containing a generated ID and the specified name. This returns the generated ID.

Handling queries

The request handlers, contained in the main.rs file, get arguments of several types, as follows:

  • web::Data<Mutex<AppState>>: As described previously, this is used to access the shared app state.
  • Path<(String,)>: As described in the previous sections, this is used to access the path of the request.
  • HttpRequest: As described in the previous sections, this is used to access general request information.
But also, the request handlers get the  web::Query<Filter> argument to access the optional arguments of the request.

The get_persons handler has a query argument—it is a generic argument, whose parameter is the Filter type. Such a type is defined as follows:

#[derive(Deserialize)]
pub struct Filter {
partial_name: Option<String>,
}

This definition allows requests such as http://localhost:8080/persons?partial_name=an. In this request, the path is just /persons, while ?partial_name=an is the so-called query. In this case, it contains just one argument whose key is partial_name, and whose value is an. It is a string and it is optional. This is exactly what is described by the Filter struct.

In addition, such a type is deserializable, as such an object must be read by the request through serialization.

The get_persons function accesses the query through the following expression:

&query.partial_name.clone().unwrap_or_else(|| "".to_string()),

The partial_name field is cloned to get a string. If it is nonexistent, it is taken as an empty string.

主站蜘蛛池模板: 台南市| 安溪县| 璧山县| 梁平县| 周至县| 阿尔山市| 扎鲁特旗| 通许县| 梁平县| 女性| 甘孜| 高邑县| 安溪县| 虎林市| 华安县| 资兴市| 南溪县| 黑水县| 兰考县| 苏尼特右旗| 招远市| 通辽市| 土默特右旗| 怀宁县| 五寨县| 于田县| 苍山县| 建阳市| 顺平县| 开平市| 辽中县| 东源县| 塘沽区| 莒南县| 安宁市| 克东县| 香格里拉县| 汉川市| 冕宁县| 嘉义市| 宝坻区|