blob: b499f3038afb8294cfc808d4e58d9b16513c29d8 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use maka_agent_ports::model::Events;
use tokio::sync::Semaphore;
fn budgeted_stream() -> (ModelStream, Arc<Semaphore>) {
let budget = Arc::new(Semaphore::new(16 * 1024));
let (sender, receiver) = mpsc::channel(4);
let (activity, _) = tokio::sync::watch::channel(tokio::time::Instant::now());
let events = ChannelEvents { sender, activity };
let cancellation = CancellationToken::new();
let producer_cancel = cancellation.clone();
let producer_budget = budget.clone();
let worker = tokio::spawn(async move {
let produce = async {
events
.emit(
ModelEvent::PartStarted {
id: "text".into(),
text_kind: maka_agent_facts::model::TextKind::Text,
provider_options: None,
}
.into(),
)
.await?;
for text in ["a", "b", "c"] {
let reservation = producer_budget
.clone()
.acquire_many_owned(16 * 1024)
.await
.unwrap();
events
.emit(Emission {
event: ModelEvent::PartDelta {
id: "text".into(),
text: text.repeat(16 * 1024),
provider_options: None,
},
reservation: Some(Arc::new(reservation)),
})
.await?;
}
events
.emit(
ModelEvent::PartFinished {
id: "text".into(),
provider_options: None,
}
.into(),
)
.await?;
events
.emit(
ModelEvent::Finished {
reason: maka_agent_facts::model::ModelFinishReason::Stop,
usage: Default::default(),
provider_options: None,
}
.into(),
)
.await
};
tokio::select! {
_ = producer_cancel.cancelled() => {},
result = produce => { let _ = result; },
}
Ok(())
});
(
ModelStream {
receiver,
cancellation,
worker: Some(worker),
cleanup: Ok(()),
finished: false,
pending_delta: None,
ended: false,
},
budget,
)
}
#[tokio::test]
async fn split_output_keeps_backpressure_until_consumed_and_exceeds_one_window() {
tokio::time::timeout(Duration::from_secs(2), async {
let (mut stream, budget) = budgeted_stream();
let mut builder = StepBuilder::default();
builder
.push(stream.next().await.unwrap().unwrap().event)
.unwrap();
let first = stream.next().await.unwrap().unwrap();
let second = stream.next().await.unwrap().unwrap();
assert_eq!(budget.available_permits(), 0);
builder.push(first.event).unwrap();
drop(first.reservation);
assert_eq!(
budget.available_permits(),
0,
"another fragment still owns the bytes"
);
builder.push(second.event).unwrap();
drop(second.reservation);
while let Some(emission) = stream.next().await {
let emission = emission.unwrap();
builder.push(emission.event).unwrap();
}
stream.cancel_and_wait().await.unwrap();
assert_eq!(budget.available_permits(), 16 * 1024);
let output = builder.finish().unwrap();
let [maka_agent_facts::model::ModelPart::Text { text, .. }] = output.parts.as_slice()
else {
panic!("one complete text part expected");
};
assert_eq!(
text,
&format!(
"{}{}{}",
"a".repeat(16 * 1024),
"b".repeat(16 * 1024),
"c".repeat(16 * 1024)
)
);
})
.await
.expect("output larger than its in-flight window must complete");
}
#[tokio::test]
async fn cancellation_releases_split_and_buffered_output_before_joining_producer() {
tokio::time::timeout(Duration::from_secs(2), async {
let (mut stream, budget) = budgeted_stream();
stream.next().await.unwrap().unwrap();
let fragment = stream.next().await.unwrap().unwrap();
assert_eq!(budget.available_permits(), 0);
drop(fragment);
stream.cancel_and_wait().await.unwrap();
assert_eq!(budget.available_permits(), 16 * 1024);
let permit = budget.acquire_many_owned(16 * 1024).await.unwrap();
drop(permit);
})
.await
.expect("cancellation must release bytes and settle the producer");
}