在这篇文章中,我们将看看如何使用warp与 RabbitMQ 集成一个 Rust Web 应用程序。为此,我们将使用lapin库以及用于池化连接的deadpool。
我们将构建的示例非常简单。有一个端点,您可以发送消息,然后将其发送到 RabbitMQ 实例。同时,该服务会监听新的事件,在它们从队列中取出时对其进行记录。
示例
让我们从一些类型开始。
type WebResult<T> = StdResult<T, Rejection>;
type RMQResult<T> = StdResult<T, PoolError>;
type Result<T> = StdResult<T, Error>;
type Connection = deadpool::managed::Object<deadpool_lapin::Manager>;
#[derive(ThisError, Debug)]
enum Error {
#[error("rmq error: {0}")]
RMQError(#[from] lapin::Error),
#[error("rmq pool error: {0}")]
RMQPoolError(#[from] PoolError),
}
impl warp::reject::Reject for Error {}
这些只是一些辅助类型,所以我们不必输入那么多,还有一个自定义的 Error 类型,它实现了Reject ,所以我们可以用它来返回来自warp 端点的错误。
接下来让我们看看main 。
#[tokio::main]
async fn main() -> Result<()> {
let addr = std::env::var("AMQP_ADDR")
.unwrap_or_else(|_| "amqp://rmq:rmq@127.0.0.1:5672/%2f".into());
let manager = Manager::new(addr, ConnectionProperties::default().with_tokio());
let pool: Pool = deadpool::managed::Pool::builder(manager)
.max_size(10)
.build()
.expect("can create pool");
let health_route = warp::path!("health").and_then(health_handler);
let add_msg_route = warp::path!("msg")
.and(warp::post())
.and(with_rmq(pool.clone()))
.and_then(add_msg_handler);
let routes = health_route
.or(add_msg_route);
println!("Started server at localhost:8000");
let _ = join!(
warp::serve(routes).run(([0, 0, 0, 0], 8000)),
rmq_listen(pool.clone())
);
Ok(())
}
在这个片段中,我们已经可以看到应用程序的基本结构。首先,我们使用deadpool 创建一个 RabbitMQ 连接池。然后,我们定义一些路由 - 一个作为/health 的端点,另一个用于将消息发送到/msg 的队列。
在main 的末尾,我们启动warp 网络服务器,同时,我们使用futures::join! 来启动rmq_listen 函数。这两个期货将一直运行,直到其中一个完成,正如我们将看到的,只有在出现错误时才会出现这种情况。
接下来让我们看看rmq_listen 。
async fn rmq_listen(pool: Pool) -> Result<()> {
let mut retry_interval = tokio::time::interval(Duration::from_secs(5));
loop {
retry_interval.tick().await;
println!("connecting rmq consumer...");
match init_rmq_listen(pool.clone()).await {
Ok(_) => println!("rmq listen returned"),
Err(e) => eprintln!("rmq listen had an error: {}", e),
};
}
}
从高层次来看,我们希望rmq_listen 在网络服务运行时一直运行。因此,例如,如果 RabbitMQ 连接死亡,或者在连接运行期间发生其他错误,我们希望重新连接并继续运行。出于这个原因,实际逻辑被上述重试逻辑所包裹。
在init_rmq_listen 中,我们实际上听取了 RabbitMQ 事件。
async fn init_rmq_listen(pool: Pool) -> Result<()> {
let rmq_con = get_rmq_con(pool).await.map_err(|e| {
eprintln!("could not get rmq con: {}", e);
e
})?;
let channel = rmq_con.create_channel().await?;
let queue = channel
.queue_declare(
"hello",
QueueDeclareOptions::default(),
FieldTable::default(),
)
.await?;
println!("Declared queue {:?}", queue);
let mut consumer = channel
.basic_consume(
"hello",
"my_consumer",
BasicConsumeOptions::default(),
FieldTable::default(),
)
.await?;
println!("rmq consumer connected, waiting for messages");
while let Some(delivery) = consumer.next().await {
if let Ok((channel, delivery)) = delivery {
println!("received msg: {:?}", delivery);
channel
.basic_ack(delivery.delivery_tag, BasicAckOptions::default())
.await?
}
}
Ok(())
}
首先,我们从池中获得一个 RabbitMQ 连接,创建一个通道并确保我们想要监听的队列存在。然后我们为这个队列创建一个消费者。
这个消费者实际上是一个Stream ,所以在函数的最后,我们简单地迭代流。如果任何操作失败,或者没有任何东西可以从流中得到,外部的rmq_listen 函数将重新启动这个过程。如果我们收到一条消息,我们只需记录它并确认它。
好了,现在我们有了一种从队列中接收消息的方法。唯一剩下的就是使用我们的/msg 处理程序向 RabbitMQ 发送事件的方法。为了做到这一点,我们首先需要一种方法来将 RabbitMQ 池传递给处理程序。
fn with_rmq(pool: Pool) -> impl Filter<Extract = (Pool,), Error = Infallible> + Clone {
warp::any().map(move || pool.clone())
}
上述过滤器简单地克隆了该池,并使这个克隆的引用对任何拥有此过滤器的处理程序可用。实际的/msg 处理程序看起来像这样。
async fn add_msg_handler(pool: Pool) -> WebResult<impl Reply> {
let payload = b"Hello world!";
let rmq_con = get_rmq_con(pool).await.map_err(|e| {
eprintln!("can't connect to rmq, {}", e);
warp::reject::custom(Error::RMQPoolError(e))
})?;
let channel = rmq_con.create_channel().await.map_err(|e| {
eprintln!("can't create channel, {}", e);
warp::reject::custom(Error::RMQError(e))
})?;
channel
.basic_publish(
"",
"hello",
BasicPublishOptions::default(),
payload.to_vec(),
BasicProperties::default(),
)
.await
.map_err(|e| {
eprintln!("can't publish: {}", e);
warp::reject::custom(Error::RMQError(e))
})?
.await
.map_err(|e| {
eprintln!("can't publish: {}", e);
warp::reject::custom(Error::RMQError(e))
})?;
Ok("OK")
}
在这个简单的例子中,我们向每个人发送Hello World ,但这很容易被调整为发送,例如,POST 请求的有效载荷。
同样,我们从池中获得一个连接,然后创建一个通道,并在该通道上发布一个消息,将其发送到hello 队列中。
RabbitMQ 中连接池方法的一个问题是,我们仍然必须每次都创建一个通道,为了获得最佳性能,连接和通道都必须被池化。
如果您现在与 RabbitMQ 服务器一起运行此示例(例如使用 docker),您可以调用POST /msg 端点并观察来自监听器中队列的传入消息。
要使用上述凭据启动本地 RabbitMQ 实例,您可以使用以下命令。
docker run -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=rmq -e RABBITMQ_DEFAULT_PASS=rmq rabbitmq:3.8.4-management
完整的示例代码可以在这里找到
结论
自从async/await稳定下来后,围绕分布式系统的生态系统已经发展和成熟了很多,并且还在继续发展。
随着lapin的1.0发布,使用RabbitMQ的系统现在似乎可以用Rust以一种方便和连贯的方式构建,这对网络生态系统来说也是一个很大的进步。
按照这个速度,缺乏生态系统很快就不会成为反对在网络服务中使用Rust的理由了!:):)