mirror of
https://github.com/fafhrd91/actix-web
synced 2024-11-30 18:44:35 +01:00
use mio for accept loop
This commit is contained in:
parent
be1cd2936d
commit
da8aa8b988
@ -46,7 +46,6 @@ regex = "0.2"
|
|||||||
sha1 = "0.2"
|
sha1 = "0.2"
|
||||||
url = "1.5"
|
url = "1.5"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
socket2 = "0.2"
|
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
flate2 = "0.2"
|
flate2 = "0.2"
|
||||||
@ -57,7 +56,9 @@ bitflags = "1.0"
|
|||||||
num_cpus = "1.0"
|
num_cpus = "1.0"
|
||||||
cookie = { version="0.10", features=["percent-encode", "secure"] }
|
cookie = { version="0.10", features=["percent-encode", "secure"] }
|
||||||
|
|
||||||
# tokio
|
# io
|
||||||
|
mio = "0.6"
|
||||||
|
net2 = "0.2"
|
||||||
bytes = "0.4"
|
bytes = "0.4"
|
||||||
futures = "0.1"
|
futures = "0.1"
|
||||||
tokio-io = "0.1"
|
tokio-io = "0.1"
|
||||||
|
@ -50,6 +50,8 @@ extern crate bitflags;
|
|||||||
extern crate futures;
|
extern crate futures;
|
||||||
extern crate tokio_io;
|
extern crate tokio_io;
|
||||||
extern crate tokio_core;
|
extern crate tokio_core;
|
||||||
|
extern crate mio;
|
||||||
|
extern crate net2;
|
||||||
|
|
||||||
extern crate failure;
|
extern crate failure;
|
||||||
#[macro_use] extern crate failure_derive;
|
#[macro_use] extern crate failure_derive;
|
||||||
@ -69,7 +71,6 @@ extern crate brotli2;
|
|||||||
extern crate percent_encoding;
|
extern crate percent_encoding;
|
||||||
extern crate smallvec;
|
extern crate smallvec;
|
||||||
extern crate num_cpus;
|
extern crate num_cpus;
|
||||||
extern crate socket2;
|
|
||||||
extern crate actix;
|
extern crate actix;
|
||||||
extern crate h2 as http2;
|
extern crate h2 as http2;
|
||||||
|
|
||||||
|
147
src/server.rs
147
src/server.rs
@ -10,9 +10,11 @@ use actix::dev::*;
|
|||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use futures::sync::mpsc;
|
use futures::sync::mpsc;
|
||||||
use tokio_io::{AsyncRead, AsyncWrite};
|
use tokio_io::{AsyncRead, AsyncWrite};
|
||||||
|
use tokio_core::reactor::Handle;
|
||||||
use tokio_core::net::TcpStream;
|
use tokio_core::net::TcpStream;
|
||||||
|
use mio;
|
||||||
use num_cpus;
|
use num_cpus;
|
||||||
use socket2::{Socket, Domain, Type};
|
use net2::{TcpBuilder, TcpStreamExt};
|
||||||
|
|
||||||
#[cfg(feature="tls")]
|
#[cfg(feature="tls")]
|
||||||
use futures::{future, Future};
|
use futures::{future, Future};
|
||||||
@ -103,7 +105,7 @@ pub struct HttpServer<T, A, H, U>
|
|||||||
keep_alive: Option<u64>,
|
keep_alive: Option<u64>,
|
||||||
factory: Arc<Fn() -> U + Send + Sync>,
|
factory: Arc<Fn() -> U + Send + Sync>,
|
||||||
workers: Vec<SyncAddress<Worker<H>>>,
|
workers: Vec<SyncAddress<Worker<H>>>,
|
||||||
sockets: HashMap<net::SocketAddr, Socket>,
|
sockets: HashMap<net::SocketAddr, net::TcpListener>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static, A: 'static, H, U: 'static> Actor for HttpServer<T, A, H, U> {
|
impl<T: 'static, A: 'static, H, U: 'static> Actor for HttpServer<T, A, H, U> {
|
||||||
@ -160,6 +162,8 @@ impl<T, A, H, U, V> HttpServer<T, A, H, U>
|
|||||||
/// attempting to connect. It should only affect servers under significant load.
|
/// attempting to connect. It should only affect servers under significant load.
|
||||||
///
|
///
|
||||||
/// Generally set in the 64-2048 range. Default value is 2048.
|
/// Generally set in the 64-2048 range. Default value is 2048.
|
||||||
|
///
|
||||||
|
/// This method should be called before `bind()` method call.
|
||||||
pub fn backlog(mut self, num: i32) -> Self {
|
pub fn backlog(mut self, num: i32) -> Self {
|
||||||
self.backlog = num;
|
self.backlog = num;
|
||||||
self
|
self
|
||||||
@ -202,34 +206,22 @@ impl<T, A, H, U, V> HttpServer<T, A, H, U>
|
|||||||
let mut succ = false;
|
let mut succ = false;
|
||||||
if let Ok(iter) = addr.to_socket_addrs() {
|
if let Ok(iter) = addr.to_socket_addrs() {
|
||||||
for addr in iter {
|
for addr in iter {
|
||||||
let socket = match addr {
|
let builder = match addr {
|
||||||
net::SocketAddr::V4(a) => {
|
net::SocketAddr::V4(_) => TcpBuilder::new_v4()?,
|
||||||
let socket = Socket::new(Domain::ipv4(), Type::stream(), None)?;
|
net::SocketAddr::V6(_) => TcpBuilder::new_v6()?,
|
||||||
match socket.bind(&a.into()) {
|
|
||||||
Ok(_) => socket,
|
|
||||||
Err(e) => {
|
|
||||||
err = Some(e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
net::SocketAddr::V6(a) => {
|
|
||||||
let socket = Socket::new(Domain::ipv6(), Type::stream(), None)?;
|
|
||||||
match socket.bind(&a.into()) {
|
|
||||||
Ok(_) => socket,
|
|
||||||
Err(e) => {
|
|
||||||
err = Some(e);
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
match builder.bind(addr) {
|
||||||
|
Ok(builder) => match builder.reuse_address(true) {
|
||||||
|
Ok(builder) => {
|
||||||
succ = true;
|
succ = true;
|
||||||
socket.listen(self.backlog)
|
let lst = builder.listen(self.backlog)
|
||||||
.expect("failed to set socket backlog");
|
.expect("failed to set socket backlog");
|
||||||
socket.set_reuse_address(true)
|
self.sockets.insert(lst.local_addr().unwrap(), lst);
|
||||||
.expect("failed to set socket reuse address");
|
},
|
||||||
self.sockets.insert(addr, socket);
|
Err(e) => err = Some(e)
|
||||||
|
},
|
||||||
|
Err(e) => err = Some(e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,13 +237,13 @@ impl<T, A, H, U, V> HttpServer<T, A, H, U>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn start_workers(&mut self, settings: &ServerSettings, handler: &StreamHandlerType)
|
fn start_workers(&mut self, settings: &ServerSettings, handler: &StreamHandlerType)
|
||||||
-> Vec<mpsc::UnboundedSender<IoStream<Socket>>>
|
-> Vec<mpsc::UnboundedSender<IoStream<net::TcpStream>>>
|
||||||
{
|
{
|
||||||
// start workers
|
// start workers
|
||||||
let mut workers = Vec::new();
|
let mut workers = Vec::new();
|
||||||
for _ in 0..self.threads {
|
for _ in 0..self.threads {
|
||||||
let s = settings.clone();
|
let s = settings.clone();
|
||||||
let (tx, rx) = mpsc::unbounded::<IoStream<Socket>>();
|
let (tx, rx) = mpsc::unbounded::<IoStream<net::TcpStream>>();
|
||||||
|
|
||||||
let h = handler.clone();
|
let h = handler.clone();
|
||||||
let ka = self.keep_alive;
|
let ka = self.keep_alive;
|
||||||
@ -309,7 +301,8 @@ impl<H: HttpHandler, U, V> HttpServer<TcpStream, net::SocketAddr, H, U>
|
|||||||
if self.sockets.is_empty() {
|
if self.sockets.is_empty() {
|
||||||
panic!("HttpServer::bind() has to be called befor start()");
|
panic!("HttpServer::bind() has to be called befor start()");
|
||||||
} else {
|
} else {
|
||||||
let addrs: Vec<(net::SocketAddr, Socket)> = self.sockets.drain().collect();
|
let addrs: Vec<(net::SocketAddr, net::TcpListener)> =
|
||||||
|
self.sockets.drain().collect();
|
||||||
let settings = ServerSettings::new(Some(addrs[0].0), &self.host, false);
|
let settings = ServerSettings::new(Some(addrs[0].0), &self.host, false);
|
||||||
let workers = self.start_workers(&settings, &StreamHandlerType::Normal);
|
let workers = self.start_workers(&settings, &StreamHandlerType::Normal);
|
||||||
|
|
||||||
@ -413,7 +406,8 @@ impl<T, A, H, U, V> HttpServer<T, A, H, U>
|
|||||||
where S: Stream<Item=(T, A), Error=io::Error> + 'static
|
where S: Stream<Item=(T, A), Error=io::Error> + 'static
|
||||||
{
|
{
|
||||||
if !self.sockets.is_empty() {
|
if !self.sockets.is_empty() {
|
||||||
let addrs: Vec<(net::SocketAddr, Socket)> = self.sockets.drain().collect();
|
let addrs: Vec<(net::SocketAddr, net::TcpListener)> =
|
||||||
|
self.sockets.drain().collect();
|
||||||
let settings = ServerSettings::new(Some(addrs[0].0), &self.host, false);
|
let settings = ServerSettings::new(Some(addrs[0].0), &self.host, false);
|
||||||
let workers = self.start_workers(&settings, &StreamHandlerType::Normal);
|
let workers = self.start_workers(&settings, &StreamHandlerType::Normal);
|
||||||
|
|
||||||
@ -484,6 +478,7 @@ impl<T, A, H, U> Handler<IoStream<T>, io::Error> for HttpServer<T, A, H, U>
|
|||||||
/// Worker accepts Socket objects via unbounded channel and start requests processing.
|
/// Worker accepts Socket objects via unbounded channel and start requests processing.
|
||||||
struct Worker<H> {
|
struct Worker<H> {
|
||||||
h: Rc<WorkerSettings<H>>,
|
h: Rc<WorkerSettings<H>>,
|
||||||
|
hnd: Handle,
|
||||||
handler: StreamHandlerType,
|
handler: StreamHandlerType,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -528,6 +523,7 @@ impl<H: 'static> Worker<H> {
|
|||||||
fn new(h: Vec<H>, handler: StreamHandlerType, keep_alive: Option<u64>) -> Worker<H> {
|
fn new(h: Vec<H>, handler: StreamHandlerType, keep_alive: Option<u64>) -> Worker<H> {
|
||||||
Worker {
|
Worker {
|
||||||
h: Rc::new(WorkerSettings::new(h, keep_alive)),
|
h: Rc::new(WorkerSettings::new(h, keep_alive)),
|
||||||
|
hnd: Arbiter::handle().clone(),
|
||||||
handler: handler,
|
handler: handler,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -546,21 +542,21 @@ impl<H: 'static> Actor for Worker<H> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<H> StreamHandler<IoStream<Socket>> for Worker<H>
|
impl<H> StreamHandler<IoStream<net::TcpStream>> for Worker<H>
|
||||||
where H: HttpHandler + 'static {}
|
where H: HttpHandler + 'static {}
|
||||||
|
|
||||||
impl<H> Handler<IoStream<Socket>> for Worker<H>
|
impl<H> Handler<IoStream<net::TcpStream>> for Worker<H>
|
||||||
where H: HttpHandler + 'static,
|
where H: HttpHandler + 'static,
|
||||||
{
|
{
|
||||||
fn handle(&mut self, msg: IoStream<Socket>, _: &mut Context<Self>)
|
fn handle(&mut self, msg: IoStream<net::TcpStream>, _: &mut Context<Self>)
|
||||||
-> Response<Self, IoStream<Socket>>
|
-> Response<Self, IoStream<net::TcpStream>>
|
||||||
{
|
{
|
||||||
if !self.h.keep_alive_enabled() &&
|
if !self.h.keep_alive_enabled() &&
|
||||||
msg.io.set_keepalive(Some(Duration::new(75, 0))).is_err()
|
msg.io.set_keepalive(Some(Duration::new(75, 0))).is_err()
|
||||||
{
|
{
|
||||||
error!("Can not set socket keep-alive option");
|
error!("Can not set socket keep-alive option");
|
||||||
}
|
}
|
||||||
self.handler.handle(Rc::clone(&self.h), msg);
|
self.handler.handle(Rc::clone(&self.h), &self.hnd, msg);
|
||||||
Self::empty()
|
Self::empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -576,25 +572,27 @@ enum StreamHandlerType {
|
|||||||
|
|
||||||
impl StreamHandlerType {
|
impl StreamHandlerType {
|
||||||
|
|
||||||
fn handle<H: HttpHandler>(&mut self, h: Rc<WorkerSettings<H>>, msg: IoStream<Socket>) {
|
fn handle<H: HttpHandler>(&mut self,
|
||||||
|
h: Rc<WorkerSettings<H>>,
|
||||||
|
hnd: &Handle,
|
||||||
|
msg: IoStream<net::TcpStream>) {
|
||||||
match *self {
|
match *self {
|
||||||
StreamHandlerType::Normal => {
|
StreamHandlerType::Normal => {
|
||||||
let io = TcpStream::from_stream(msg.io.into_tcp_stream(), Arbiter::handle())
|
let io = TcpStream::from_stream(msg.io, hnd)
|
||||||
.expect("failed to associate TCP stream");
|
.expect("failed to associate TCP stream");
|
||||||
|
|
||||||
Arbiter::handle().spawn(HttpChannel::new(h, io, msg.peer, msg.http2));
|
hnd.spawn(HttpChannel::new(h, io, msg.peer, msg.http2));
|
||||||
}
|
}
|
||||||
#[cfg(feature="tls")]
|
#[cfg(feature="tls")]
|
||||||
StreamHandlerType::Tls(ref acceptor) => {
|
StreamHandlerType::Tls(ref acceptor) => {
|
||||||
let IoStream { io, peer, http2 } = msg;
|
let IoStream { io, peer, http2 } = msg;
|
||||||
let io = TcpStream::from_stream(io.into_tcp_stream(), Arbiter::handle())
|
let io = TcpStream::from_stream(io, hnd)
|
||||||
.expect("failed to associate TCP stream");
|
.expect("failed to associate TCP stream");
|
||||||
|
|
||||||
Arbiter::handle().spawn(
|
Arbiter::handle().spawn(
|
||||||
TlsAcceptorExt::accept_async(acceptor, io).then(move |res| {
|
TlsAcceptorExt::accept_async(acceptor, io).then(move |res| {
|
||||||
match res {
|
match res {
|
||||||
Ok(io) => Arbiter::handle().spawn(
|
Ok(io) => hnd.spawn(HttpChannel::new(h, io, peer, http2)),
|
||||||
HttpChannel::new(h, io, peer, http2)),
|
|
||||||
Err(err) =>
|
Err(err) =>
|
||||||
trace!("Error during handling tls connection: {}", err),
|
trace!("Error during handling tls connection: {}", err),
|
||||||
};
|
};
|
||||||
@ -605,10 +603,10 @@ impl StreamHandlerType {
|
|||||||
#[cfg(feature="alpn")]
|
#[cfg(feature="alpn")]
|
||||||
StreamHandlerType::Alpn(ref acceptor) => {
|
StreamHandlerType::Alpn(ref acceptor) => {
|
||||||
let IoStream { io, peer, .. } = msg;
|
let IoStream { io, peer, .. } = msg;
|
||||||
let io = TcpStream::from_stream(io.into_tcp_stream(), Arbiter::handle())
|
let io = TcpStream::from_stream(io, hnd)
|
||||||
.expect("failed to associate TCP stream");
|
.expect("failed to associate TCP stream");
|
||||||
|
|
||||||
Arbiter::handle().spawn(
|
hnd.spawn(
|
||||||
SslAcceptorExt::accept_async(acceptor, io).then(move |res| {
|
SslAcceptorExt::accept_async(acceptor, io).then(move |res| {
|
||||||
match res {
|
match res {
|
||||||
Ok(io) => {
|
Ok(io) => {
|
||||||
@ -631,24 +629,57 @@ impl StreamHandlerType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_accept_thread(sock: Socket, addr: net::SocketAddr,
|
fn start_accept_thread(sock: net::TcpListener, addr: net::SocketAddr,
|
||||||
workers: Vec<mpsc::UnboundedSender<IoStream<Socket>>>) {
|
workers: Vec<mpsc::UnboundedSender<IoStream<net::TcpStream>>>) {
|
||||||
// start acceptors thread
|
// start accept thread
|
||||||
let _ = thread::Builder::new().name(format!("Accept on {}", addr)).spawn(move || {
|
let _ = thread::Builder::new().name(format!("Accept on {}", addr)).spawn(move || {
|
||||||
let mut next = 0;
|
let mut next = 0;
|
||||||
loop {
|
let server = mio::net::TcpListener::from_listener(sock, &addr)
|
||||||
match sock.accept() {
|
.expect("Can not create mio::net::TcpListener");
|
||||||
Ok((socket, addr)) => {
|
const SERVER: mio::Token = mio::Token(0);
|
||||||
let addr = if let Some(addr) = addr.as_inet() {
|
|
||||||
net::SocketAddr::V4(addr)
|
// Create a poll instance
|
||||||
} else {
|
let poll = match mio::Poll::new() {
|
||||||
net::SocketAddr::V6(addr.as_inet6().unwrap())
|
Ok(poll) => poll,
|
||||||
|
Err(err) => panic!("Can not create mio::Poll: {}", err),
|
||||||
};
|
};
|
||||||
let msg = IoStream{io: socket, peer: Some(addr), http2: false};
|
|
||||||
workers[next].unbounded_send(msg).expect("worker thread died");
|
// Start listening for incoming connections
|
||||||
next = (next + 1) % workers.len();
|
if let Err(err) = poll.register(&server, SERVER,
|
||||||
|
mio::Ready::readable(), mio::PollOpt::edge()) {
|
||||||
|
panic!("Can not register io: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create storage for events
|
||||||
|
let mut events = mio::Events::with_capacity(128);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Err(err) = poll.poll(&mut events, None) {
|
||||||
|
panic!("Poll error: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
for event in events.iter() {
|
||||||
|
match event.token() {
|
||||||
|
SERVER => {
|
||||||
|
loop {
|
||||||
|
match server.accept_std() {
|
||||||
|
Ok((sock, addr)) => {
|
||||||
|
let msg = IoStream{io: sock, peer: Some(addr), http2: false};
|
||||||
|
workers[next]
|
||||||
|
.unbounded_send(msg).expect("worker thread died");
|
||||||
|
next = (next + 1) % workers.len();
|
||||||
|
},
|
||||||
|
Err(err) => if err.kind() == io::ErrorKind::WouldBlock {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
error!("Error accepting connection: {:?}", err);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
Err(err) => error!("Error accepting connection: {:?}", err),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
10
src/test.rs
10
src/test.rs
@ -11,9 +11,9 @@ use cookie::Cookie;
|
|||||||
use http::{Uri, Method, Version, HeaderMap, HttpTryFrom};
|
use http::{Uri, Method, Version, HeaderMap, HttpTryFrom};
|
||||||
use http::header::{HeaderName, HeaderValue};
|
use http::header::{HeaderName, HeaderValue};
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use socket2::{Socket, Domain, Type};
|
|
||||||
use tokio_core::net::TcpListener;
|
use tokio_core::net::TcpListener;
|
||||||
use tokio_core::reactor::Core;
|
use tokio_core::reactor::Core;
|
||||||
|
use net2::TcpBuilder;
|
||||||
|
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use server::HttpServer;
|
use server::HttpServer;
|
||||||
@ -139,10 +139,10 @@ impl TestServer {
|
|||||||
/// Get firat available unused address
|
/// Get firat available unused address
|
||||||
pub fn unused_addr() -> net::SocketAddr {
|
pub fn unused_addr() -> net::SocketAddr {
|
||||||
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
|
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||||
let socket = Socket::new(Domain::ipv4(), Type::stream(), None).unwrap();
|
let socket = TcpBuilder::new_v4().unwrap();
|
||||||
socket.bind(&addr.into()).unwrap();
|
socket.bind(&addr).unwrap();
|
||||||
socket.set_reuse_address(true).unwrap();
|
socket.reuse_address(true).unwrap();
|
||||||
let tcp = socket.into_tcp_listener();
|
let tcp = socket.to_tcp_listener().unwrap();
|
||||||
tcp.local_addr().unwrap()
|
tcp.local_addr().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ extern crate tokio_core;
|
|||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, mpsc};
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
use actix_web::*;
|
use actix_web::*;
|
||||||
@ -12,16 +12,20 @@ use actix::System;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_start() {
|
fn test_start() {
|
||||||
let addr = test::TestServer::unused_addr();
|
let _ = test::TestServer::unused_addr();
|
||||||
let srv_addr = addr.clone();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let sys = System::new("test");
|
let sys = System::new("test");
|
||||||
let srv = HttpServer::new(
|
let srv = HttpServer::new(
|
||||||
|| vec![Application::new()
|
|| vec![Application::new()
|
||||||
.resource("/", |r| r.method(Method::GET).h(httpcodes::HTTPOk))]);
|
.resource("/", |r| r.method(Method::GET).h(httpcodes::HTTPOk))]);
|
||||||
srv.bind(srv_addr).unwrap().start();
|
let srv = srv.bind("127.0.0.1:0").unwrap();
|
||||||
|
let _ = tx.send(srv.addrs()[0].clone());
|
||||||
|
srv.start();
|
||||||
sys.run();
|
sys.run();
|
||||||
});
|
});
|
||||||
|
let addr = rx.recv().unwrap();
|
||||||
assert!(reqwest::get(&format!("http://{}/", addr)).unwrap().status().is_success());
|
assert!(reqwest::get(&format!("http://{}/", addr)).unwrap().status().is_success());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user