use std::time::Instant;
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,
}
impl ImageBuilder {
pub fn new(endpoint: &str, api_key: &str) -> Result<ImageBuilder, SailError> {
let authorization = crate::worker::bearer_metadata(api_key)?;
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,
message: pb::BuildImageRequest,
retry_timeout: f64,
) -> Result<pb::BuildImageResponse, SailError> {
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()),
Err(status) => self.handle_retry(&status, deadline, &mut delay).await?,
}
}
}
pub async fn get_image_build_status(
&self,
message: pb::GetImageBuildStatusRequest,
retry_timeout: f64,
) -> Result<pb::GetImageBuildStatusResponse, SailError> {
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()),
Err(status) => self.handle_retry(&status, deadline, &mut delay).await?,
}
}
}
pub async fn prepare_local_file_upload(
&self,
message: pb::PrepareLocalFileUploadRequest,
) -> Result<pb::PrepareLocalFileUploadResponse, SailError> {
let resp = self
.client()?
.prepare_local_file_upload(self.request(message))
.await
.map_err(|status| SailError::from_rpc_status(&status))?;
Ok(resp.into_inner())
}
}
#[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"
)));
}
}