#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use futures_util::StreamExt as _;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio::net::{TcpListener, TcpStream};
use lightstreamer_rs::{
Client, ClientConfig, ConnectionOptions, FieldSchema, ItemGroup, ServerAddress, Subscription,
SubscriptionEvent, SubscriptionMode, Transport,
};
const HOLD: Duration = Duration::from_millis(1500);
const OPEN_TIMEOUT: Duration = Duration::from_secs(1);
async fn read_request(socket: &mut TcpStream) -> String {
let mut buffer = Vec::new();
let mut scratch = [0u8; 4096];
loop {
if let Some(head_end) = buffer.windows(4).position(|window| window == b"\r\n\r\n") {
let head = String::from_utf8_lossy(&buffer[..head_end]);
let content_length = head
.lines()
.find_map(|line| {
let lower = line.to_ascii_lowercase();
lower
.strip_prefix("content-length:")
.and_then(|value| value.trim().parse::<usize>().ok())
})
.unwrap_or(0);
if buffer.len() >= head_end + 4 + content_length {
return String::from_utf8_lossy(&buffer).into_owned();
}
}
match socket.read(&mut scratch).await {
Ok(0) | Err(_) => return String::from_utf8_lossy(&buffer).into_owned(),
Ok(read) => buffer.extend_from_slice(&scratch[..read]),
}
}
}
const STREAM_HEAD: &[u8] = b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n";
const BOUND_WINDOW: Duration = Duration::from_millis(500);
async fn write_fixed(socket: &mut TcpStream, body: &str) {
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
let _ = socket.write_all(response.as_bytes()).await;
let _ = socket.flush().await;
}
fn request_id(request: &str) -> String {
request
.rsplit("LS_reqId=")
.next()
.map(|tail| {
tail.chars()
.take_while(char::is_ascii_alphanumeric)
.collect::<String>()
})
.filter(|id| !id.is_empty())
.unwrap_or_else(|| "1".to_owned())
}
async fn serve_connection(mut socket: TcpStream, subscribed: Arc<AtomicBool>) {
let request = read_request(&mut socket).await;
let path = request.lines().next().unwrap_or_default();
if path.contains("create_session") {
tokio::time::sleep(HOLD).await;
let _ = socket.write_all(STREAM_HEAD).await;
let _ = socket
.write_all(b"CONOK,S1,50000,15000,*\r\nLOOP,0\r\n")
.await;
let _ = socket.flush().await;
} else if path.contains("bind_session") {
tokio::time::sleep(HOLD).await;
let _ = socket.write_all(STREAM_HEAD).await;
let _ = socket.write_all(b"CONOK,S1,50000,15000,*\r\n").await;
let _ = socket.flush().await;
tokio::time::sleep(BOUND_WINDOW).await;
if subscribed.load(Ordering::SeqCst) {
let _ = socket.write_all(b"SUBOK,1,1,2\r\nU,1,1,ACME|1.5\r\n").await;
}
let _ = socket.write_all(b"LOOP,0\r\n").await;
let _ = socket.flush().await;
} else if path.contains("control") {
subscribed.store(true, Ordering::SeqCst);
write_fixed(&mut socket, &format!("REQOK,{}\r\n", request_id(&request))).await;
} else {
write_fixed(&mut socket, "").await;
}
}
async fn serve(listener: TcpListener, subscribed: Arc<AtomicBool>) {
loop {
match listener.accept().await {
Ok((socket, _)) => {
let subscribed = Arc::clone(&subscribed);
tokio::spawn(serve_connection(socket, subscribed));
}
Err(_) => return,
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn http_polling_waits_out_a_withheld_poll_and_delivers_data() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind loopback");
let address: SocketAddr = listener.local_addr().expect("local addr");
let subscribed = Arc::new(AtomicBool::new(false));
let server = tokio::spawn(serve(listener, Arc::clone(&subscribed)));
let config = ClientConfig::builder(
ServerAddress::try_new(format!("http://{address}")).expect("valid address"),
)
.with_transport(Transport::HttpPolling)
.with_options(ConnectionOptions::default().with_open_timeout(OPEN_TIMEOUT))
.build()
.expect("valid config");
let (client, _events) = tokio::time::timeout(Duration::from_secs(20), Client::connect(config))
.await
.expect("connect must not hang waiting out the poll")
.expect("connect must succeed despite the withheld poll response");
let mut updates = client
.subscribe(Subscription::new(
SubscriptionMode::Merge,
ItemGroup::from_items(["item1"]).expect("valid item"),
FieldSchema::from_fields(["stock_name", "last_price"]).expect("valid schema"),
))
.await
.expect("subscribe queued");
let update = tokio::time::timeout(Duration::from_secs(25), async {
while let Some(event) = updates.next().await {
if let SubscriptionEvent::Update(update) = event {
return Some(update);
}
}
None
})
.await
.expect("an update must arrive within the budget")
.expect("the subscription stream must yield an update");
assert_eq!(
update
.field_by_name("last_price")
.and_then(|value| value.text()),
Some("1.5"),
"the withheld poll cycle delivered its data"
);
drop(client);
server.abort();
}