Создаём REST-сервис на Rust. Часть 4: переходим к REST API

  • Tutorial
В прошлый раз мы реализовали обновление БД.

Осталось сделать только REST-интерфейс. Давайте займёмся этим.

Введение


Эта часть будет, пожалуй, самой сложной — мы близко узнаем типажи Send и Sync, а также тонкости работы замыканий и времён жизни. По-другому я бы озаглавил её «зануда исследует, почему обязательно нужно клонировать данные». Она полезна тем, что затрагивает тонкие места Rust и показывает причины некоторых неочевидных ошибок. Так что если хотите разобраться досконально — добро пожаловать.

Также хочу отметить: если вам что-то непонятно относительно приведённого кода или сами объяснения кажутся недостаточно ясными, не стесняйтесь писать об этом в комментариях. Автор потратил не минуту и не полчаса, пытаясь понять, почему код работает именно в том виде, в котором он написан, и вынужден был не раз сходить в IRC и на форум за разъяснениями.

Обзор кода в целом


В принципе, мы хотим добавить новую команду, которая будет запускать веб-сервер, предоставляющий REST API нашего сервиса «телефонной книги». Нам нужно предоставить стандартные для CRUD точки доступа: GET records, GET records/id, POST records, PUT records/id и DELETE records/id. Для этого мы будем пользоваться router’ом из состава фреймворка iron. Вот код:

Код добавления команды
    "serve" => {
        let sdb = Arc::new(Mutex::new(db));
        let mut router = router::Router::new();
        {
            let sdb_ = sdb.clone();
            router.get("/api/v1/records",
                move |req: &mut Request|
                handlers::get_records(sdb_.clone(), req));
        }
        {
            let sdb_ = sdb.clone();
            router.get("/api/v1/records/:id",
                move |req: &mut Request|
                handlers::get_record(sdb_.clone(), req));
        }
        {
            let sdb_ = sdb.clone();
            router.post("/api/v1/records",
                move |req: &mut Request|
                handlers::add_record(sdb_.clone(), req));
        }
        {
            let sdb_ = sdb.clone();
            router.put("/api/v1/records/:id",
                move |req: &mut Request|
                handlers::update_record(sdb_.clone(), req));
        }
        {
            let sdb_ = sdb.clone();
            router.delete("/api/v1/records/:id",
                move |req: &mut Request|
                handlers::delete_record(sdb_.clone(), req));
        }
        Iron::new(router).http("localhost:3000").unwrap();
    }


Этот вариант работает, но выглядит довольно убого. Есть несколько способов сделать его лучше, но сначала мы разберёмся с тем, что тут происходит. А перепишем его в следующей части. Реализация обработчиков тоже будет рассмотрена в другой раз.

Заворачиваем базу данных


В вышеприведённом коде со второй же строчки что-то новенькое:

        let sdb = Arc::new(Mutex::new(db));

db здесь — это postgres::Connection, т.е. объект, через который идёт вся работа с данной БД.
Мы оборачиваем его в мьютекс, а мьютекс кладём в атомарный счётчик ссылок.

Зачем? Пока запомните лишь, что мы хотим обеспечить синхронизированный доступ к БД из многих потоков — для этого мы используем мьютекс. А для того, чтобы разделить владение объектом между несколькими потоками, мы используем счётчик ссылок — потоки умирают в разные моменты, и БД уничтожится, когда она больше не будет никому нужна.

Это ограничение — следствие архитектуры Iron. Веб-сервер может обрабатывать несколько запросов параллельно, в нескольких потоках.

Определяем пути к API


Далее мы определяем путь для GET-запросов:

        let mut router = router::Router::new();
        {
            let sdb_ = sdb.clone();
            router.get("/api/v1/records",
                move |req: &mut Request|
                handlers::get_records(sdb_.clone(), req));
        }

Для этого мы пользуемся Router::get(). Этот метод принимает glob-шаблон пути (в данном случае, весь путь задан жёстко, без переменных компонентов) и обработчик запросов по данному пути.

Типаж Handler


Обработчик — это любой тип, реализующий типаж Handler. Этот типаж выглядит относительно просто:

pub trait Handler: Send + Sync + Any {
    fn handle(&self, &mut Request) -> IronResult<Response>;
}

Всего один метод — handle, принимающий изменяемую ссылку на запрос и возвращающий HTTP-ответ, завёрнутый в IronResult.

IronResult — это обычный Result, в котором ошибки специализированы для самого Iron:

type IronResult<T> = Result<T, IronError>;

pub struct IronError {
    ...
}

Теперь посмотрим на определение самого типажа:

pub trait Handler: Send + Sync + Any {

Такого кода мы ещё не видели. Но нова здесь только одна часть определения: Send + Sync + Any.

Эта запись означает, что реализация типажа Handler требует реализации типажей Send, Sync и Any. Вот мы и встретились со столпом Rust: Send и Sync. Any нам пока неинтересен.

Send и Sync


Начнём с Sync.

Значения, реализующие Sync, можно безопасно менять из нескольких потоков одновременно — т.е. при этом не возникает гонки данных.

Например, std::sync::atomic::AtomicIsize представляет собой целое с атомарным доступом. Этот тип реализует Sync, поэтому мы можем делать AtomicIsize::fetch_add из нескольких потоков.

Дальнейшая информация о Sync находится здесь.

Обратите внимание, что возможность синхронизированного доступа не подразумевает возможности передачи владения значением другому потоку. Тип, реализующий Sync, не обязательно реализует Send.

Теперь подробнее о Send. Это типаж-маркер, реализованный для типов, которые можно безопасно передавать в другие потоки. Например, сырой указатель передавать нельзя:

impl<T> !Send for *const T

!Send здесь означает «типаж не реализован».

Зачем нужна такая запись? Затем, что Send реализован для всех типов по умолчанию, если для их составных частей реализован Send. Структура, все поля которой Send, тоже является Send (то же самое касается и Sync). Типы, которые явно небезопасны, должны «отписаться» от реализации этого типажа.

Отсутствие Send (и Sync) тоже распространяется рекурсивно — поэтому если вы используете внутри реализуемого тип, который не реализует Send, ваш тип по умолчанию тоже не будет его реализовывать.

std::sync::atomic::AtomicPtr уже можно передавать в другие потоки:

impl<T> Send for AtomicPtr<T>

На бытовом уровне это значит, что значение можно отдать другому потоку — например, при выполнении thread::spawn. Однако, это не значит, что доступ из многих потоков синхронизирован. Send говорит лишь о том, что тип должно быть можно переместить в память, доступную другому потоку.

Send и Sync часто идут рука об руку — типы, реализующие один типаж, реализуют одновременно и другой. Так, упомянутый выше AtomicPtr является обёрткой над аппаратными атомарными операциями, и благодаря этому он позволяет не только передать значение в другой поток, но и реализовать безопасный одновременный доступ к содержимому. AtomicPtr синхронизирован — он также реализует Sync.

std::sync::mpsc::Sender тоже можно передавать в другие потоки:

impl<T: Send> Send for Sender<T>

Однако, Sender — хороший пример того, что тип можно отправить в другой поток, но сам он не синхронизирован. Это структура, реализующая отправляющую сторону канала — есть соответствующая ему структура Receiver. Так вот, Sender можно передать потоку-отправителю после создания канала, но доступ к одному Sender из многих потоков запрещён.

Запись <T: Send> говорит о том, что для данных, пересылаемых по каналу, должен быть реализован типаж Send. Логично — чтобы данные можно было передавать по каналу, их должно быть можно передавать другому потоку в принципе.

Подробнее о типаже Send — здесь.

Отлично, с типажами вроде разобрались.

Зачем мы обернули БД


Взглянем на наши Arc и Mutex ещё раз:

impl<T> Send for Arc<T> where T: Send + Sync + ?Sized
impl<T> Sync for Arc<T> where T: Send + Sync + ?Sized

impl<T: ?Sized + Send> Send for Mutex<T>
impl<T: ?Sized + Send> Sync for Mutex<T>

Вот зачем мы оборачивали db в мьютекс: он делает вложенные данные Send и Sync — синхронизированными и отправляемыми в другой поток. Только такие данные можно положить в Arc. Сам же Arc опять-таки синхронизирован и может быть отдан в другой поток.

?Sized пока нам незнаком — рассмотрим его в другой раз.

Замыкания


Теперь снова посмотрим на наш код. Вот он:

        let sdb = Arc::new(Mutex::new(db));
        let mut router = router::Router::new();
        {
            let sdb_ = sdb.clone();
            router.get("/api/v1/records",
                move |req: &mut Request|
                handlers::get_records(sdb_.clone(), req));
        }

Второй аргумент .get() — это замыкание. Рассмотрим его отдельно:

            move |req: &mut Request|
            handlers::get_records(sdb_.clone(), req));

Давайте начнём с аргументов — они находятся внутри вертикальных черт. Мы принимаем один аргумент req, который представляет собой изменяемую ссылку на запрос. У нас именно такие аргументы, потому что, как вы помните, Handler из Iron должен иметь сигнатуру

fn handle(&self, &mut Request) -> IronResult<Response>;

Тело замыкания просто делает вызов handlers::get_records. Этой функции мы передаём аргумент замыкания и копию нашего Arc<Mutex> — как часть окружения.

Почему копию? Сейчас будет самое интересное.

Попробуем не клонировать данные


Чтобы понять это, давайте подумаем, что происходило бы, если бы мы передавали в замыкание не копию, а сам sdb_. Вот так:

        {
            let sdb_ = sdb.clone();
            router.get("/api/v1/records",
                move |req: &mut Request|
                handlers::get_records(sdb_, req));
        }

Что на это скажет rustc?

main.rs:120:32: 122:69 error: the trait `for<'r, 'r, 'r> core::ops::Fn<(&'r mut iron::request::Request<'r, 'r>,)>` is not implemented for the type `[closure@main.rs:121:36: 122:68 sdb_:alloc::arc::Arc<std::sync::mutex::Mutex<postgres::Connection>>]` [E0277]
main.rs:120                         router.get("/api/v1/records",
main.rs:121                                    move |req: &mut Request|
main.rs:122                                    handlers::get_records(sdb_, req));
main.rs:120:32: 122:69 help: run `rustc --explain E0277` to see a detailed explanation

Видно, он сильно недоволен.

В чём суть ошибки? Типаж Fn(&mut Request,) не реализован для нашего замыкания. Что это за типаж?

Особенности реализации замыканий


Это типаж, реализуемый для замыканий, которые могут быть вызваны многократно. Почему важно знать, сколько раз будет вызвано замыкание? Потому что если вы перемещаете владение чем-то из замыкания, сделать это можно только один раз. В следующий раз перемещать владение будет неоткуда. В таком случае, ваше замыкание реализует FnOnce, но не Fn. В нашем коде мы перемещаем копию sdb_ в сам обработчик — handlers::get_records, т.к. он принимает
Arc<Mutex> с владением. Чтобы лучше понять разницу между типажами замыканий, читайте этот раздел книги.

Почему ошибка говорит о типажах, реализуемых для странно выглядящего типа? Потому что в Rust запись замыкания с помощью синтаксиса |x| x + 1 - это синтаксический сахар. На самом деле компилятор создаёт анонимный тип - структуру, для которой реализуются соответствующие типажи. Подробнее смотрите этот раздел.

Зачем нам вообще сдался этот типаж Fn? Затем, что Handler Iron’а реализован для Fn:

impl<F> Handler for F where F: Send + Sync + Any + Fn(&mut Request) -> IronResult<Response> { fn handle(&self, req: &mut Request) -> IronResult<Response> { (*self)(req) } }

where - это задание краткого имени для группы ограничений на типажи. Везде, где написано F, можно подставить полное Send + Sync + Any + Fn(&mut Request) -> IronResult.

Разумно, что Handler реализован именно для Fn. Iron, конечно, нужно многократно вызываемое замыкание: если запрос к веб-серверу можно сделать только однажды, это никуда не годится.

Мы на полпути к пониманию одной из важнейших частей Rust! Ещё немного.

Перемещение владения в замыкание


Осталось одна незнакомая часть синтаксиса - move перед вертикальными чертами аргументов. move означает, что замыкание должно не заимствовать переменные окружения, а перемещать их внутрь замыкания. Без move мы столкнёмся с другой проблемой:

main.rs:121:36: 122:76 error: closure may outlive the current function, but it borrows `sdb_`, which is owned by the current function [E0373]
main.rs:121                                    |req: &mut Request|
main.rs:122                                    handlers::get_records(sdb_.clone(), req));
main.rs:121:36: 122:76 help: run `rustc --explain E0373` to see a detailed explanation
main.rs:122:58: 122:62 note: `sdb_` is borrowed here
main.rs:122                                    handlers::get_records(sdb_.clone(), req));
                                                                     ^~~~
main.rs:121:36: 122:76 help: to force the closure to take ownership of `sdb_` (and any other referenced variables), use the `move` keyword, as shown:
main.rs:                                       move |req: &mut Request|
main.rs:                                       handlers::get_records(sdb_.clone(), req));
error: aborting due to previous error

Замыкание попыталось позаимствовать sdb_, но оно может пережить функцию main - потому что Iron может запустить его в отдельном потоке. Если бы мы разрешили эту ситуацию, порождённый поток мог бы обратиться к данным, которые уже невалидны, когда поток-родитель вышел из main, но ещё не завершился.

Rust ещё раз не дал нам сделать глупость. Замечательно.

Последнее препятствие


Скептик заметит последний камень преткновения: зачем же мы тогда клонировали sdb выше?

                            let sdb_ = sdb.clone();

Потому что иначе мы получили бы в следующих блоках ошибку использования перемещённого значения:

main.rs:125:36: 125:39 error: use of moved value: `sdb` [E0382]
main.rs:125                         let sdb_ = sdb.clone();
                                               ^~~
main.rs:125:36: 125:39 help: run `rustc --explain E0382` to see a detailed explanation
main.rs:119:29: 119:33 note: `sdb` moved here because it has type `alloc::arc::Arc<std::sync::mutex::Mutex<postgres::Connection>>`, which is moved by default
main.rs:119                         let sdb_ = sdb;
                                        ^~~~

Дальше всё тривиально и аналогично. Вот, например, как выглядит определение точки доступа GET/id:

        {
            let sdb_ = sdb.clone();
            router.get("/api/v1/records/:id",
                move |req: &mut Request|
                handlers::get_record(sdb_.clone(), req));
        }

Здесь мы используем в шаблоне пути :id, чтобы указать Router’у, что на этом месте в URL будет идентификатор записи, которую мы хотим получить.

А так мы запускаем HTTP-сервер с нашим Router’ом на указанном порту:

        Iron::new(router).http("localhost:3000").unwrap();


Заключение


Вы всё ещё здесь? Поздравляю! Если вы поняли написанное выше, то сможете разобраться в большой части кода на Rust. Теперь вы сможете понять истинную причину ошибок в случае работы с типичным многопоточным кодом на замыканиях, и знаете распространённый шаблон разделяемого доступа к данным - Mutex внутри Arc.

Если у вас возникли вопросы - прошу в комментарии или в чат.
  • +11
  • 15,1k
  • 2
Поделиться публикацией

Комментарии 2

    +1
    Большое спасибо за статью! Очень здорово, что по Rust они появляются и на русском.

    Пара замечаний. Во-первых, идиоматично переиспользовать имена переменных в паттернах наподобие того, что демонстрирует ваш код:

    let sdb = Arc::new(Mutex::new(db));
    {
        let sdb = sdb.clone();
        thread::spawn(move || do_something(sdb));
    }
    


    Т.е. не нужно добавлять суффиксов-префиксов, можно просто переопределить (в смысле rebind, а не reassign) существующую переменную.

    Во-вторых, where — это не «задание краткого имени для длинного типа», это определение ограничений на дженериковую ти́повую переменную:

        fn do_something<T: 'a + Trait1 + Trait2 + Trait3>() { ... }
        // equivalent to
        fn do_something<T>() where T: 'a + Trait1 + Trait2 + Trait3 { ... }
    


    Обычно where используется, если ограничений много или если они большие (например, большая сигнатура замыкания), потому что в таком случае их проще будет распределить по нескольким строчкам. Если ограничения простые (один-два коротких трейта), то чаще используется синтаксис с ограничениями в списке параметров.
      0
      Пара замечаний. Во-первых, идиоматично переиспользовать имена переменных в паттернах наподобие того, что демонстрирует ваш код:

      Я знаю про эту практику, но мне она не нравится. Маскировать переменные можно и в Си, однако обычно я этого избегаю, т.к. это вносит путаницу и обычно сигнализирует о чрезмерной вложенности или непродуманности имён.
      Во-вторых, where — это не «задание краткого имени для длинного типа», это определение ограничений на дженериковую ти́повую переменную:

      Разумеется. Спасибо. Подумал про одно, написал про другое.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое