use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use agplatform::Env;
use agplatform::Platform;
use agplatform::Request;
use agplatform::RequestBuilderExt;
use agplatform::RequestCancelled;
use agplatform::RequestObserver;
use agplatform::RequestOutput;
use agplatform::RequestRun;
use tokio::task::JoinSet;
async fn fetch_all<P: Platform + 'static>(
platform: &Arc<P>,
urls: Vec<String>,
) -> agplatform::Result<Vec<agplatform::Result<String>>> {
let client = platform.request().client()?;
let mut tasks = JoinSet::new();
for (index, url) in urls.into_iter().enumerate() {
let (platform, client) = (platform.clone(), client.clone());
tasks.spawn(async move {
let output = client
.get(url)
.retry(2, Duration::from_millis(100))
.send_async(platform.request())
.await;
(
index,
output.and_then(|output| Ok(output.text()?.to_string())),
)
});
}
let mut bodies = tasks.join_all().await;
bodies.sort_by_key(|(index, _)| *index);
Ok(bodies.into_iter().map(|(_, body)| body).collect())
}
async fn fetch_within(
platform: &impl Platform,
url: &str,
deadline: Duration,
) -> agplatform::Result<Option<String>> {
let request = platform.request();
let sent = request.send_async(request.client()?.get(url));
match tokio::time::timeout(deadline, sent).await {
Ok(output) => Ok(Some(output?.text()?.to_string())),
Err(_) => Ok(None),
}
}
async fn count_lines(platform: &impl Platform, url: &str) -> agplatform::Result<usize> {
let request = platform.request();
let mut lines = 0;
request
.client()?
.get(url)
.send_async_to(request, &mut |chunk| {
lines += chunk.iter().filter(|&&byte| byte == b'\n').count();
Ok(())
})
.await?;
Ok(lines)
}
#[derive(Default)]
struct Cancellations(Mutex<Vec<String>>);
impl RequestObserver for Cancellations {
fn finished(&self, run: &RequestRun, result: &agplatform::Result<RequestOutput>) {
if let Err(error) = result
&& error.source_as::<RequestCancelled>().is_some()
{
self.0.lock().unwrap().push(run.target());
}
}
}
fn main() -> agplatform::Result<()> {
let cancellations = Arc::new(Cancellations::default());
let platform = agplatform::platform();
platform.request().set_observer(cancellations.clone());
let platform = Arc::new(platform);
let urls: Vec<String> = platform.env().args().skip(1).cloned().collect();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime should start");
runtime.block_on(async {
for (url, body) in urls.iter().zip(fetch_all(&platform, urls.clone()).await?) {
match body {
Ok(body) => println!("{url}: {} bytes", body.len()),
Err(error) => println!("{url}: {error}"),
}
}
for url in &urls {
println!(
"{url}: {} lines",
count_lines(platform.as_ref(), url).await?
);
let page = fetch_within(platform.as_ref(), url, Duration::from_secs(1)).await?;
println!("{url}: fetched within a second: {}", page.is_some());
}
println!("cancelled: {:?}", cancellations.0.lock().unwrap());
Ok(())
})
}
#[cfg(test)]
mod tests {
use super::*;
use agplatform::test_platform::test_platform;
fn block_on<T>(future: impl Future<Output = T>) -> T {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(future)
}
fn ok(body: &str) -> RequestOutput {
RequestOutput {
status: 200,
body: body.as_bytes().to_vec(),
..Default::default()
}
}
mod fetch_all {
use super::*;
#[test]
fn fetch_all_should_send_requests_concurrently() {
let platform = Arc::new(test_platform());
let slow = platform.request.set_pending("/slow", ok("slow"));
platform.request.set_response("/fast", ok("fast"));
let urls = vec![
"https://a.com/slow".to_string(),
"https://b.com/fast".to_string(),
];
let bodies = block_on(async {
let fetching = tokio::spawn({
let platform = platform.clone();
async move { fetch_all(&platform, urls).await }
});
tokio::time::timeout(Duration::from_secs(10), async {
while platform.request.calls().len() < 2 {
tokio::task::yield_now().await;
}
})
.await
.expect("both requests should be sent");
slow.release();
fetching.await.unwrap().unwrap()
});
assert_eq!(bodies[0].as_deref().unwrap(), "slow");
assert_eq!(bodies[1].as_deref().unwrap(), "fast");
}
#[test]
fn fetch_all_should_return_failure_of_each_url() {
let platform = Arc::new(test_platform());
platform.request.set_response("", ok("page"));
platform.request.set_response(
"/missing",
RequestOutput {
status: 404,
..ok("")
},
);
let urls = vec![
"https://a.com/missing".to_string(),
"https://a.com/".to_string(),
];
let bodies = block_on(fetch_all(&platform, urls)).unwrap();
assert_eq!(
bodies[0]
.as_ref()
.unwrap_err()
.request_output()
.unwrap()
.status,
404
);
assert_eq!(bodies[1].as_deref().unwrap(), "page");
}
#[test]
fn fetch_all_client_failure_should_fail() {
let platform = Arc::new(test_platform());
platform.request.set_client_failure(true);
let result = block_on(fetch_all(&platform, vec!["https://a.com/".to_string()]));
assert!(result.is_err());
assert!(platform.request.calls().is_empty());
}
}
mod fetch_within {
use super::*;
#[test]
fn fetch_within_should_return_body_received_in_time() {
let platform = test_platform();
platform.request.set_response("", ok("page"));
let body = block_on(fetch_within(
&platform,
"https://a.com/",
Duration::from_secs(10),
));
assert_eq!(body.unwrap().as_deref(), Some("page"));
}
#[test]
fn fetch_within_missed_deadline_should_cancel_request() {
let cancellations = Arc::new(Cancellations::default());
let platform = test_platform();
platform.request().set_observer(cancellations.clone());
let _gate = platform.request.set_pending("", ok("page"));
let body = block_on(fetch_within(
&platform,
"https://a.com/",
Duration::from_millis(10),
));
assert_eq!(body.unwrap(), None);
assert_eq!(*cancellations.0.lock().unwrap(), ["GET https://a.com/"]);
}
}
mod count_lines {
use super::*;
#[test]
fn count_lines_should_count_across_chunks() {
let platform = test_platform();
platform.request.set_chunks(
"/log",
ok(""),
[b"a\nb".to_vec(), b"\nc\n".to_vec(), b"d".to_vec()],
);
let lines = block_on(count_lines(&platform, "https://a.com/log")).unwrap();
assert_eq!(lines, 3);
}
}
mod cancellations {
use super::*;
mod request_observer {
use super::*;
mod finished {
use super::*;
#[test]
fn finished_should_ignore_requests_not_cancelled() {
let cancellations = Arc::new(Cancellations::default());
let platform = test_platform();
platform.request().set_observer(cancellations.clone());
platform.request.set_response("/ok", ok(""));
platform.request.set_response(
"/error",
RequestOutput {
status: 500,
..ok("")
},
);
let _ = block_on(fetch_within(
&platform,
"https://a.com/ok",
Duration::from_secs(10),
));
let _ = block_on(fetch_within(
&platform,
"https://a.com/error",
Duration::from_secs(10),
));
assert!(cancellations.0.lock().unwrap().is_empty());
}
}
}
}
}