use std::time::Instant;
use prost::Message;
use tonic::metadata::AsciiMetadataValue;
use tonic::transport::Channel;
use tonic::{Code, Request, Status};
use crate::channels::ChannelCache;
use crate::error::SailError;
use crate::pb::imagebuilder::v1 as pb;
use crate::worker::{
retry_deadline, sleep_before_retry, EXEC_TRANSIENT_RETRY_INITIAL_DELAY_SECONDS,
};
use pb::image_builder_service_client::ImageBuilderServiceClient;
fn is_transient_imagebuilder_transport(status: &Status) -> bool {
status.code() == Code::Unavailable
&& crate::worker::is_transient_transport_message(status.message())
}
pub struct ImageBuilder {
endpoint: String,
channels: ChannelCache,
authorization: AsciiMetadataValue,
}
fn decode<T: Message + Default>(bytes: &[u8]) -> Result<T, SailError> {
T::decode(bytes).map_err(|err| SailError::Internal {
message: format!("failed to decode imagebuilder request: {err}"),
})
}
impl ImageBuilder {
pub fn new(endpoint: &str, api_key: &str) -> Result<ImageBuilder, SailError> {
let authorization = format!("Bearer {api_key}")
.parse()
.map_err(|_| SailError::Config {
message: "SAIL_API_KEY contains characters invalid in a gRPC metadata value"
.to_string(),
})?;
Ok(ImageBuilder {
endpoint: endpoint.to_string(),
channels: ChannelCache::new(),
authorization,
})
}
fn client(&self) -> Result<ImageBuilderServiceClient<Channel>, SailError> {
Ok(ImageBuilderServiceClient::new(
self.channels.get(&self.endpoint)?,
))
}
fn request<T>(&self, message: T) -> Request<T> {
let mut request = Request::new(message);
request
.metadata_mut()
.insert("authorization", self.authorization.clone());
request
}
async fn handle_retry(
&self,
status: &Status,
deadline: Instant,
delay: &mut f64,
) -> Result<(), SailError> {
if Instant::now() >= deadline || !is_transient_imagebuilder_transport(status) {
return Err(SailError::from_rpc_status(status));
}
self.channels.invalidate(&self.endpoint);
*delay = sleep_before_retry(*delay, deadline).await;
Ok(())
}
pub async fn build_image(
&self,
request: &[u8],
retry_timeout: f64,
) -> Result<Vec<u8>, SailError> {
let message = decode::<pb::BuildImageRequest>(request)?;
let deadline = retry_deadline(retry_timeout);
let mut delay = EXEC_TRANSIENT_RETRY_INITIAL_DELAY_SECONDS;
loop {
match self
.client()?
.build_image(self.request(message.clone()))
.await
{
Ok(resp) => return Ok(resp.into_inner().encode_to_vec()),
Err(status) => self.handle_retry(&status, deadline, &mut delay).await?,
}
}
}
pub async fn get_image_build_status(
&self,
request: &[u8],
retry_timeout: f64,
) -> Result<Vec<u8>, SailError> {
let message = decode::<pb::GetImageBuildStatusRequest>(request)?;
let deadline = retry_deadline(retry_timeout);
let mut delay = EXEC_TRANSIENT_RETRY_INITIAL_DELAY_SECONDS;
loop {
match self
.client()?
.get_image_build_status(self.request(message.clone()))
.await
{
Ok(resp) => return Ok(resp.into_inner().encode_to_vec()),
Err(status) => self.handle_retry(&status, deadline, &mut delay).await?,
}
}
}
pub async fn prepare_local_file_upload(&self, request: &[u8]) -> Result<Vec<u8>, SailError> {
let message = decode::<pb::PrepareLocalFileUploadRequest>(request)?;
let resp = self
.client()?
.prepare_local_file_upload(self.request(message))
.await
.map_err(|status| SailError::from_rpc_status(&status))?;
Ok(resp.into_inner().encode_to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drain_race_is_transient_but_connect_failure_is_not() {
assert!(is_transient_imagebuilder_transport(&Status::unavailable(
"endpoint closing"
)));
assert!(is_transient_imagebuilder_transport(&Status::unavailable(
"error reading server preface: connection reset"
)));
assert!(!is_transient_imagebuilder_transport(&Status::unavailable(
"tcp connect error"
)));
assert!(!is_transient_imagebuilder_transport(&Status::internal(
"boom"
)));
}
}