1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-25 08:52:42 +01:00
actix-web/actix-http/src/client/pool.rs

670 lines
19 KiB
Rust
Raw Normal View History

2021-02-16 10:08:30 +01:00
//! Client connection pooling keyed on the authority part of the connection URI.
use std::{
cell::RefCell,
collections::VecDeque,
future::Future,
io,
ops::Deref,
pin::Pin,
rc::Rc,
sync::Arc,
task::{Context, Poll},
time::{Duration, Instant},
};
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use actix_rt::time::{sleep, Sleep};
2018-12-11 03:08:33 +01:00
use actix_service::Service;
2021-01-15 03:11:10 +01:00
use ahash::AHashMap;
use futures_core::future::LocalBoxFuture;
use http::uri::Authority;
use pin_project::pin_project;
2021-02-16 09:27:14 +01:00
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
2018-11-12 08:12:54 +01:00
use super::config::ConnectorConfig;
use super::connection::{
ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner,
};
2019-03-13 22:41:40 +01:00
use super::error::ConnectError;
use super::h2proto::handshake;
use super::Connect;
use super::Protocol;
2019-01-29 05:41:09 +01:00
2018-11-12 08:12:54 +01:00
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub struct Key {
2018-11-12 08:12:54 +01:00
authority: Authority,
}
impl From<Authority> for Key {
fn from(authority: Authority) -> Key {
Key { authority }
}
}
#[doc(hidden)]
2021-02-16 10:08:30 +01:00
/// Connections pool for reuse Io type for certain [`http::uri::Authority`] as key.
pub struct ConnectionPool<S, Io>
2018-11-12 08:12:54 +01:00
where
2021-02-16 09:27:14 +01:00
Io: AsyncWrite + Unpin + 'static,
2018-11-12 08:12:54 +01:00
{
connector: S,
2021-02-16 09:27:14 +01:00
inner: ConnectionPoolInner<Io>,
}
2021-02-16 09:27:14 +01:00
/// wrapper type for check the ref count of Rc.
pub struct ConnectionPoolInner<Io>(Rc<ConnectionPoolInnerPriv<Io>>)
2021-02-16 09:27:14 +01:00
where
Io: AsyncWrite + Unpin + 'static;
2021-02-16 09:27:14 +01:00
impl<Io> ConnectionPoolInner<Io>
where
Io: AsyncWrite + Unpin + 'static,
{
fn new(config: ConnectorConfig) -> Self {
let permits = Arc::new(Semaphore::new(config.limit));
let available = RefCell::new(AHashMap::default());
Self(Rc::new(ConnectionPoolInnerPriv {
config,
available,
permits,
}))
}
2021-02-16 09:27:14 +01:00
/// spawn a async for graceful shutdown h1 Io type with a timeout.
fn close(&self, conn: ConnectionInnerType<Io>) {
2021-02-16 09:27:14 +01:00
if let Some(timeout) = self.config.disconnect_timeout {
if let ConnectionInnerType::H1(io) = conn {
2021-02-16 09:27:14 +01:00
actix_rt::spawn(CloseConnection::new(io, timeout));
}
}
2018-11-12 08:12:54 +01:00
}
}
2021-02-16 09:27:14 +01:00
impl<Io> Clone for ConnectionPoolInner<Io>
2018-11-12 08:12:54 +01:00
where
2021-02-16 09:27:14 +01:00
Io: AsyncWrite + Unpin + 'static,
2018-11-12 08:12:54 +01:00
{
fn clone(&self) -> Self {
2021-02-16 09:27:14 +01:00
Self(Rc::clone(&self.0))
}
}
impl<Io> Deref for ConnectionPoolInner<Io>
where
Io: AsyncWrite + Unpin + 'static,
{
type Target = ConnectionPoolInnerPriv<Io>;
fn deref(&self) -> &Self::Target {
&*self.0
2018-11-12 08:12:54 +01:00
}
}
2021-02-16 09:27:14 +01:00
impl<Io> Drop for ConnectionPoolInner<Io>
where
Io: AsyncWrite + Unpin + 'static,
{
2020-08-09 22:49:43 +02:00
fn drop(&mut self) {
2021-02-16 09:27:14 +01:00
// When strong count is one it means the pool is dropped
// remove and drop all Io types.
if Rc::strong_count(&self.0) == 1 {
self.permits.close();
std::mem::take(&mut *self.available.borrow_mut())
.into_iter()
.for_each(|(_, conns)| {
conns.into_iter().for_each(|pooled| self.close(pooled.conn))
});
}
2020-08-09 22:49:43 +02:00
}
}
pub struct ConnectionPoolInnerPriv<Io>
2018-11-12 08:12:54 +01:00
where
2021-02-16 09:27:14 +01:00
Io: AsyncWrite + Unpin + 'static,
{
config: ConnectorConfig,
available: RefCell<AHashMap<Key, VecDeque<PooledConnection<Io>>>>,
permits: Arc<Semaphore>,
}
impl<S, Io> ConnectionPool<S, Io>
where
Io: AsyncWrite + Unpin + 'static,
{
2021-02-16 10:08:30 +01:00
/// Construct a new connection pool.
2021-02-16 09:27:14 +01:00
///
2021-02-16 10:08:30 +01:00
/// [`super::config::ConnectorConfig`]'s `limit` is used as the max permits allowed for
/// in-flight connections.
2021-02-16 09:27:14 +01:00
///
/// The pool can only have equal to `limit` amount of requests spawning/using Io type
/// concurrently.
///
2021-02-16 10:08:30 +01:00
/// Any requests beyond limit would be wait in fifo order and get notified in async manner
/// by [`tokio::sync::Semaphore`]
2021-02-16 09:27:14 +01:00
pub(crate) fn new(connector: S, config: ConnectorConfig) -> Self {
let inner = ConnectionPoolInner::new(config);
2021-02-16 09:27:14 +01:00
Self { connector, inner }
}
}
impl<S, Io> Service<Connect> for ConnectionPool<S, Io>
where
S: Service<Connect, Response = (Io, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io: ConnectionIo,
2018-11-12 08:12:54 +01:00
{
type Response = ConnectionType<Io>;
2019-03-13 22:41:40 +01:00
type Error = ConnectError;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
2018-11-12 08:12:54 +01:00
2021-02-16 10:08:30 +01:00
actix_service::forward_ready!(connector);
2018-11-12 08:12:54 +01:00
fn call(&self, req: Connect) -> Self::Future {
2021-02-16 09:27:14 +01:00
let connector = self.connector.clone();
let inner = self.inner.clone();
2019-11-18 13:42:27 +01:00
2021-02-16 09:27:14 +01:00
Box::pin(async move {
2019-12-05 18:35:43 +01:00
let key = if let Some(authority) = req.uri.authority() {
2019-11-18 13:42:27 +01:00
authority.clone().into()
} else {
return Err(ConnectError::Unresolved);
2019-11-18 13:42:27 +01:00
};
2018-11-12 08:12:54 +01:00
2021-02-16 09:27:14 +01:00
// acquire an owned permit and carry it with connection
2021-02-16 10:08:30 +01:00
let permit = inner.permits.clone().acquire_owned().await.map_err(|_| {
ConnectError::Io(io::Error::new(
io::ErrorKind::Other,
"failed to acquire semaphore on client connection pool",
))
})?;
let conn = {
let mut conn = None;
// check if there is idle connection for given key.
let mut map = inner.available.borrow_mut();
if let Some(conns) = map.get_mut(&key) {
let now = Instant::now();
while let Some(mut c) = conns.pop_front() {
let config = &inner.config;
let idle_dur = now - c.used;
let age = now - c.created;
let conn_ineligible = idle_dur > config.conn_keep_alive
|| age > config.conn_lifetime;
if conn_ineligible {
// drop connections that are too old
inner.close(c.conn);
} else {
// check if the connection is still usable
if let ConnectionInnerType::H1(ref mut io) = c.conn {
2021-02-16 10:08:30 +01:00
let check = ConnectionCheckFuture { io };
match check.await {
ConnectionState::Tainted => {
inner.close(c.conn);
continue;
}
ConnectionState::Skip => continue,
ConnectionState::Live => conn = Some(c),
2021-02-16 09:27:14 +01:00
}
2021-02-16 10:08:30 +01:00
} else {
conn = Some(c);
2021-02-16 09:27:14 +01:00
}
2021-02-16 10:08:30 +01:00
break;
}
2021-02-16 09:27:14 +01:00
}
2021-02-16 10:08:30 +01:00
};
2019-11-18 13:42:27 +01:00
2021-02-16 10:08:30 +01:00
conn
};
2021-02-16 09:27:14 +01:00
// construct acquired. It's used to put Io type back to pool/ close the Io type.
// permit is carried with the whole lifecycle of Acquired.
let acquired = Acquired { key, inner, permit };
2021-02-16 09:27:14 +01:00
// match the connection and spawn new one if did not get anything.
match conn {
Some(conn) => {
Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired))
}
2021-02-16 09:27:14 +01:00
None => {
let (io, proto) = connector.call(req).await?;
2019-11-18 13:42:27 +01:00
// TODO: remove when http3 is added in support.
assert!(proto != Protocol::Http3);
2019-11-18 13:42:27 +01:00
if proto == Protocol::Http1 {
Ok(ConnectionType::from_h1(io, Instant::now(), acquired))
2019-11-18 13:42:27 +01:00
} else {
let config = &acquired.inner.config;
2021-02-16 09:27:14 +01:00
let (sender, connection) = handshake(io, config).await?;
let inner = H2ConnectionInner::new(sender, connection);
Ok(ConnectionType::from_h2(inner, Instant::now(), acquired))
2019-11-18 13:42:27 +01:00
}
}
2018-11-12 08:12:54 +01:00
}
2021-02-16 09:27:14 +01:00
})
2018-11-12 08:12:54 +01:00
}
}
2021-02-16 09:27:14 +01:00
/// Type for check the connection and determine if it's usable.
struct ConnectionCheckFuture<'a, Io> {
io: &'a mut Io,
}
enum ConnectionState {
2021-02-16 10:08:30 +01:00
/// IO is pending and a new request would wake it.
2021-02-16 09:27:14 +01:00
Live,
2021-02-16 10:08:30 +01:00
/// IO unexpectedly has unread data and should be dropped.
Tainted,
/// IO should be skipped but not dropped.
2021-02-16 09:27:14 +01:00
Skip,
}
impl<Io> Future for ConnectionCheckFuture<'_, Io>
2018-11-12 08:12:54 +01:00
where
2021-02-16 09:27:14 +01:00
Io: AsyncRead + Unpin,
2018-11-12 08:12:54 +01:00
{
2021-02-16 09:27:14 +01:00
type Output = ConnectionState;
// this future is only used to get access to Context.
// It should never return Poll::Pending.
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let mut buf = [0; 2];
let mut read_buf = ReadBuf::new(&mut buf);
let state = match Pin::new(&mut this.io).poll_read(cx, &mut read_buf) {
Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => {
2021-02-16 10:08:30 +01:00
ConnectionState::Tainted
2021-02-16 09:27:14 +01:00
}
2021-02-16 10:08:30 +01:00
Poll::Pending => ConnectionState::Live,
2021-02-16 09:27:14 +01:00
_ => ConnectionState::Skip,
};
Poll::Ready(state)
}
}
struct PooledConnection<Io> {
conn: ConnectionInnerType<Io>,
2021-02-16 09:27:14 +01:00
used: Instant,
created: Instant,
2018-11-12 08:12:54 +01:00
}
2021-02-16 09:27:14 +01:00
#[pin_project]
struct CloseConnection<Io> {
io: Io,
#[pin]
timeout: Sleep,
}
impl<Io> CloseConnection<Io>
2018-11-12 08:12:54 +01:00
where
2021-02-16 09:27:14 +01:00
Io: AsyncWrite + Unpin,
2018-11-12 08:12:54 +01:00
{
2021-02-16 09:27:14 +01:00
fn new(io: Io, timeout: Duration) -> Self {
CloseConnection {
io,
timeout: sleep(timeout),
2018-11-12 08:12:54 +01:00
}
}
}
2021-02-16 09:27:14 +01:00
impl<Io> Future for CloseConnection<Io>
2018-11-12 08:12:54 +01:00
where
2021-02-16 09:27:14 +01:00
Io: AsyncWrite + Unpin,
2018-11-12 08:12:54 +01:00
{
2021-02-16 09:27:14 +01:00
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let this = self.project();
match this.timeout.poll(cx) {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => Pin::new(this.io).poll_shutdown(cx).map(|_| ()),
2018-11-12 08:12:54 +01:00
}
}
}
pub struct Acquired<Io>
2018-11-12 08:12:54 +01:00
where
2021-02-16 09:27:14 +01:00
Io: AsyncWrite + Unpin + 'static,
2018-11-12 08:12:54 +01:00
{
/// authority key for identify connection.
2018-11-12 08:12:54 +01:00
key: Key,
/// handle to connection pool.
2021-02-16 09:27:14 +01:00
inner: ConnectionPoolInner<Io>,
/// permit for limit concurrent in-flight connection for a Client object.
2021-02-16 09:27:14 +01:00
permit: OwnedSemaphorePermit,
2018-11-12 08:12:54 +01:00
}
impl<Io: ConnectionIo> Acquired<Io> {
2021-02-16 10:08:30 +01:00
/// Close the IO.
pub(super) fn close(&self, conn: ConnectionInnerType<Io>) {
2021-02-16 09:27:14 +01:00
self.inner.close(conn);
2018-11-12 08:12:54 +01:00
}
2019-11-18 13:42:27 +01:00
2021-02-16 10:08:30 +01:00
/// Release IO back into pool.
pub(super) fn release(&self, conn: ConnectionInnerType<Io>, created: Instant) {
2021-02-16 09:27:14 +01:00
let Acquired { key, inner, .. } = self;
2021-02-16 10:08:30 +01:00
2021-02-16 09:27:14 +01:00
inner
.available
.borrow_mut()
.entry(key.clone())
.or_insert_with(VecDeque::new)
.push_back(PooledConnection {
conn,
2021-02-16 09:27:14 +01:00
created,
used: Instant::now(),
});
2018-11-12 08:12:54 +01:00
let _ = &self.permit;
2018-11-12 08:12:54 +01:00
}
}
2021-02-16 09:27:14 +01:00
#[cfg(test)]
mod test {
2021-02-16 10:08:30 +01:00
use std::{cell::Cell, io};
2019-01-29 05:41:09 +01:00
2021-02-16 09:27:14 +01:00
use http::Uri;
2018-11-12 08:12:54 +01:00
2021-02-16 10:08:30 +01:00
use super::*;
use crate::client::connection::ConnectionType;
2018-11-14 07:53:30 +01:00
2021-02-16 10:08:30 +01:00
/// A stream type that always returns pending on async read.
///
/// Mocks an idle TCP stream that is ready to be used for client connections.
2021-02-16 09:27:14 +01:00
struct TestStream(Rc<Cell<usize>>);
2018-11-14 07:53:30 +01:00
2021-02-16 09:27:14 +01:00
impl Drop for TestStream {
fn drop(&mut self) {
self.0.set(self.0.get() - 1);
}
2018-11-14 07:53:30 +01:00
}
2021-02-16 09:27:14 +01:00
impl AsyncRead for TestStream {
fn poll_read(
self: Pin<&mut Self>,
_: &mut Context<'_>,
_: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Poll::Pending
}
2018-11-12 08:12:54 +01:00
}
2021-02-16 09:27:14 +01:00
impl AsyncWrite for TestStream {
fn poll_write(
self: Pin<&mut Self>,
_: &mut Context<'_>,
_: &[u8],
) -> Poll<io::Result<usize>> {
unimplemented!()
2018-11-12 08:12:54 +01:00
}
2021-02-16 09:27:14 +01:00
fn poll_flush(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<io::Result<()>> {
unimplemented!()
}
fn poll_shutdown(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
2018-11-12 08:12:54 +01:00
}
}
#[derive(Clone)]
2021-02-16 09:27:14 +01:00
struct TestPoolConnector {
generated: Rc<Cell<usize>>,
}
2021-02-16 09:27:14 +01:00
impl Service<Connect> for TestPoolConnector {
type Response = (TestStream, Protocol);
type Error = ConnectError;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
2021-02-16 10:08:30 +01:00
actix_service::always_ready!();
2018-11-12 08:12:54 +01:00
2021-02-16 09:27:14 +01:00
fn call(&self, _: Connect) -> Self::Future {
self.generated.set(self.generated.get() + 1);
let generated = self.generated.clone();
Box::pin(async { Ok((TestStream(generated), Protocol::Http1)) })
2018-11-12 08:12:54 +01:00
}
}
fn release<T>(conn: ConnectionType<T>)
2021-02-16 09:27:14 +01:00
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
{
match conn {
ConnectionType::H1(mut conn) => conn.on_release(true),
ConnectionType::H2(mut conn) => conn.on_release(false),
}
2018-11-12 08:12:54 +01:00
}
2021-02-16 09:27:14 +01:00
#[actix_rt::test]
async fn test_pool_limit() {
let connector = TestPoolConnector {
generated: Rc::new(Cell::new(0)),
};
2019-11-18 13:42:27 +01:00
2021-02-16 09:27:14 +01:00
let config = ConnectorConfig {
limit: 1,
..Default::default()
};
2019-11-19 13:54:19 +01:00
2021-02-16 09:27:14 +01:00
let pool = super::ConnectionPool::new(connector, config);
let req = Connect {
uri: Uri::from_static("http://localhost"),
addr: None,
};
let conn = pool.call(req.clone()).await.unwrap();
let waiting = Rc::new(Cell::new(true));
let waiting_clone = waiting.clone();
actix_rt::spawn(async move {
actix_rt::time::sleep(Duration::from_millis(100)).await;
waiting_clone.set(false);
drop(conn);
});
assert!(waiting.get());
let now = Instant::now();
let conn = pool.call(req).await.unwrap();
release(conn);
assert!(!waiting.get());
assert!(now.elapsed() >= Duration::from_millis(100));
2018-11-12 08:12:54 +01:00
}
2021-02-16 09:27:14 +01:00
#[actix_rt::test]
async fn test_pool_keep_alive() {
let generated = Rc::new(Cell::new(0));
let generated_clone = generated.clone();
2021-02-16 09:27:14 +01:00
let connector = TestPoolConnector { generated };
2019-11-18 13:42:27 +01:00
2021-02-16 09:27:14 +01:00
let config = ConnectorConfig {
conn_keep_alive: Duration::from_secs(1),
..Default::default()
};
2021-02-16 09:27:14 +01:00
let pool = super::ConnectionPool::new(connector, config);
2020-08-09 22:49:43 +02:00
2021-02-16 09:27:14 +01:00
let req = Connect {
uri: Uri::from_static("http://localhost"),
addr: None,
};
2021-02-16 09:27:14 +01:00
let conn = pool.call(req.clone()).await.unwrap();
assert_eq!(1, generated_clone.get());
release(conn);
2019-07-30 17:00:46 +02:00
2021-02-16 09:27:14 +01:00
let conn = pool.call(req.clone()).await.unwrap();
assert_eq!(1, generated_clone.get());
release(conn);
2020-08-09 22:49:43 +02:00
2021-02-16 09:27:14 +01:00
actix_rt::time::sleep(Duration::from_millis(1500)).await;
actix_rt::task::yield_now().await;
2021-02-16 09:27:14 +01:00
let conn = pool.call(req).await.unwrap();
// Note: spawned recycle connection is not ran yet.
// This is tokio current thread runtime specific behavior.
assert_eq!(2, generated_clone.get());
2021-02-16 09:27:14 +01:00
// yield task so the old connection is properly dropped.
actix_rt::task::yield_now().await;
assert_eq!(1, generated_clone.get());
release(conn);
}
2021-02-16 09:27:14 +01:00
#[actix_rt::test]
async fn test_pool_lifetime() {
let generated = Rc::new(Cell::new(0));
let generated_clone = generated.clone();
let connector = TestPoolConnector { generated };
let config = ConnectorConfig {
conn_lifetime: Duration::from_secs(1),
..Default::default()
};
let pool = super::ConnectionPool::new(connector, config);
let req = Connect {
uri: Uri::from_static("http://localhost"),
addr: None,
};
let conn = pool.call(req.clone()).await.unwrap();
assert_eq!(1, generated_clone.get());
release(conn);
let conn = pool.call(req.clone()).await.unwrap();
assert_eq!(1, generated_clone.get());
release(conn);
actix_rt::time::sleep(Duration::from_millis(1500)).await;
actix_rt::task::yield_now().await;
let conn = pool.call(req).await.unwrap();
// Note: spawned recycle connection is not ran yet.
// This is tokio current thread runtime specific behavior.
assert_eq!(2, generated_clone.get());
// yield task so the old connection is properly dropped.
actix_rt::task::yield_now().await;
assert_eq!(1, generated_clone.get());
release(conn);
}
2021-02-16 09:27:14 +01:00
#[actix_rt::test]
async fn test_pool_authority_key() {
let generated = Rc::new(Cell::new(0));
let generated_clone = generated.clone();
2019-11-18 13:42:27 +01:00
2021-02-16 09:27:14 +01:00
let connector = TestPoolConnector { generated };
2021-02-16 09:27:14 +01:00
let config = ConnectorConfig::default();
let pool = super::ConnectionPool::new(connector, config);
let req = Connect {
uri: Uri::from_static("https://crates.io"),
addr: None,
};
let conn = pool.call(req.clone()).await.unwrap();
assert_eq!(1, generated_clone.get());
release(conn);
let conn = pool.call(req).await.unwrap();
assert_eq!(1, generated_clone.get());
release(conn);
let req = Connect {
uri: Uri::from_static("https://google.com"),
addr: None,
};
let conn = pool.call(req.clone()).await.unwrap();
assert_eq!(2, generated_clone.get());
release(conn);
let conn = pool.call(req).await.unwrap();
assert_eq!(2, generated_clone.get());
release(conn);
}
2021-02-16 09:27:14 +01:00
#[actix_rt::test]
async fn test_pool_drop() {
let generated = Rc::new(Cell::new(0));
let generated_clone = generated.clone();
2018-11-12 08:12:54 +01:00
2021-02-16 09:27:14 +01:00
let connector = TestPoolConnector { generated };
let config = ConnectorConfig::default();
let pool = Rc::new(super::ConnectionPool::new(connector, config));
let req = Connect {
uri: Uri::from_static("https://crates.io"),
addr: None,
};
let conn = pool.call(req.clone()).await.unwrap();
assert_eq!(1, generated_clone.get());
release(conn);
let req = Connect {
uri: Uri::from_static("https://google.com"),
addr: None,
};
let conn = pool.call(req.clone()).await.unwrap();
assert_eq!(2, generated_clone.get());
release(conn);
let clone1 = pool.clone();
let clone2 = clone1.clone();
drop(clone2);
for _ in 0..2 {
actix_rt::task::yield_now().await;
2018-11-12 08:12:54 +01:00
}
2021-02-16 09:27:14 +01:00
assert_eq!(2, generated_clone.get());
drop(clone1);
for _ in 0..2 {
actix_rt::task::yield_now().await;
2018-11-12 08:12:54 +01:00
}
2021-02-16 09:27:14 +01:00
assert_eq!(2, generated_clone.get());
2018-11-12 08:12:54 +01:00
2021-02-16 09:27:14 +01:00
drop(pool);
for _ in 0..2 {
actix_rt::task::yield_now().await;
2018-11-12 08:12:54 +01:00
}
2021-02-16 09:27:14 +01:00
assert_eq!(0, generated_clone.get());
2018-11-12 08:12:54 +01:00
}
}