mod common;
use std::sync::{Arc, OnceLock};
use common::{connect_with_retry, free_port, http_get};
use function_sdk_rust::proto::v1::function_runner_service_server::FunctionRunnerService;
use function_sdk_rust::proto::v1::{RunFunctionRequest, RunFunctionResponse};
use function_sdk_rust::{Args, Error, Server, response};
use prometheus_client::metrics::{counter::Counter, family::Family};
use prometheus_client::registry::Registry;
use tonic::{Code, Request, Response, Status};
use tonic_health::pb::HealthCheckRequest;
use tonic_health::pb::health_check_response::ServingStatus;
use tonic_health::pb::health_client::HealthClient;
const SERVICES: [&str; 2] = ["", "apiextensions.fn.proto.v1.FunctionRunnerService"];
#[derive(Default)]
struct BundleFunction {
bundle: Arc<OnceLock<String>>,
}
#[tonic::async_trait]
impl FunctionRunnerService for BundleFunction {
async fn run_function(
&self,
request: Request<RunFunctionRequest>,
) -> Result<Response<RunFunctionResponse>, Status> {
let req = request.into_inner();
let mut rsp = response::to(&req, response::DEFAULT_TTL);
let bundle = self
.bundle
.get()
.expect("Server::ready gates on the bundle");
response::normal(&mut rsp, bundle.clone());
Ok(Response::new(rsp))
}
}
fn insecure_args(port: u16) -> Args {
Args {
debug: false,
address: format!("127.0.0.1:{port}"),
tls_certs_dir: None,
insecure: true,
max_recv_message_size: None,
metrics_address: String::new(),
}
}
async fn health_status(health: &mut HealthClient<tonic::transport::Channel>, service: &str) -> i32 {
health
.check(HealthCheckRequest {
service: service.to_string(),
})
.await
.expect("health check must succeed")
.into_inner()
.status
}
#[tokio::test]
async fn ready_holds_the_function_back_until_it_completes() {
let port = free_port();
let (loaded_tx, loaded_rx) = tokio::sync::oneshot::channel::<()>();
let function = BundleFunction::default();
let bundle = function.bundle.clone();
tokio::spawn(async move {
let ready = async move {
loaded_rx.await.expect("the test sends");
bundle.set("v1".to_string()).expect("set once");
Ok::<(), std::io::Error>(())
};
Server::new(function, &insecure_args(port))
.ready(ready)
.serve()
.await
.expect("serve must start");
});
let mut client = connect_with_retry(port).await;
let channel = tonic::transport::Channel::from_shared(format!("http://127.0.0.1:{port}"))
.expect("valid URI")
.connect()
.await
.expect("health channel must connect");
let mut health = HealthClient::new(channel);
for service in SERVICES {
assert_eq!(
health_status(&mut health, service).await,
ServingStatus::NotServing as i32,
"{service:?} before ready"
);
}
let err = client
.run_function(RunFunctionRequest::default())
.await
.expect_err("RunFunction must wait for ready");
assert_eq!(err.code(), Code::Unavailable);
loaded_tx.send(()).expect("the readiness future waits");
for service in SERVICES {
let mut status = 0;
for _ in 0..50 {
status = health_status(&mut health, service).await;
if status == ServingStatus::Serving as i32 {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
assert_eq!(
status,
ServingStatus::Serving as i32,
"{service:?} after ready"
);
}
let rsp = client
.run_function(RunFunctionRequest::default())
.await
.expect("RunFunction must succeed once ready")
.into_inner();
assert_eq!(rsp.results[0].message, "v1");
}
#[tokio::test]
async fn a_failed_ready_stops_the_server() {
let ready = async { Err::<(), _>(std::io::Error::other("cannot pull the bundle")) };
let err = Server::new(BundleFunction::default(), &insecure_args(free_port()))
.ready(ready)
.serve()
.await
.expect_err("serve must return the readiness failure");
assert!(matches!(err, Error::NotReady(_)), "{err:?}");
assert_eq!(
err.to_string(),
"the function did not become ready: cannot pull the bundle"
);
}
#[tokio::test]
async fn a_panicking_ready_stops_the_server() {
let ready = async { panic!("cannot parse the bundle") };
let err = Server::new(BundleFunction::default(), &insecure_args(free_port()))
.ready::<_, std::io::Error>(ready)
.serve()
.await
.expect_err("serve must return the readiness panic");
assert!(matches!(err, Error::NotReady(_)), "{err:?}");
assert!(
err.to_string().contains("cannot parse the bundle"),
"the panic message: {err}"
);
}
#[tokio::test]
async fn metrics_registry_serves_the_function_series_beside_the_grpc_ones() {
let evaluations = Family::<Vec<(String, String)>, Counter>::default();
let mut registry = Registry::default();
registry.sub_registry_with_prefix("my_function").register(
"evaluations",
"Policy evaluations by verdict",
evaluations.clone(),
);
evaluations
.get_or_create(&vec![("verdict".to_string(), "deny".to_string())])
.inc();
let metrics_port = free_port();
let mut args = insecure_args(free_port());
args.metrics_address = format!("127.0.0.1:{metrics_port}");
tokio::spawn(async move {
Server::new(BundleFunction::default(), &args)
.metrics_registry(registry)
.serve()
.await
.expect("serve must start");
});
let om = http_get(metrics_port, None).await;
assert!(
om.contains("my_function_evaluations_total{verdict=\"deny\"} 1"),
"the function's series: {om}"
);
assert!(
om.contains("grpc_server_started_total{grpc_type=\"unary\",grpc_service=\"apiextensions.fn.proto.v1.FunctionRunnerService\",grpc_method=\"RunFunction\"}"),
"the SDK's series: {om}"
);
assert!(
om.trim_end().ends_with("# EOF"),
"one OpenMetrics body: {om}"
);
}