149 lines
4.9 KiB
Rust
149 lines
4.9 KiB
Rust
use crate::Router;
|
|
use crate::api::{ExternalHttpRequest, InternalSpeakRequest, ErrorResponse, ErrorType};
|
|
use crate::newrelic_logger::{MaybeNewRelicTransaction};
|
|
use hyper::Method;
|
|
use hyper::header::HeaderValue;
|
|
use hyper::http::header::HeaderName;
|
|
use hyper::{Body, Request, Response, HeaderMap};
|
|
use std::fmt::Debug;
|
|
use std::net::IpAddr;
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
pub async fn speak_proxy_with_retry(
|
|
ip_addr: IpAddr,
|
|
request: Request<Body>,
|
|
router: Arc<Router>,
|
|
_transaction: MaybeNewRelicTransaction,
|
|
max_retries: u8,
|
|
retry_wait_ms: u64,
|
|
proxy_hostname: &str,
|
|
endpoint: &'static str) -> Result<Response<Body>, ErrorResponse>
|
|
{
|
|
let mut attempt : u8 = 0;
|
|
|
|
let external_http_request = ExternalHttpRequest::decode_request(request).await.unwrap();
|
|
|
|
// This is the IP of the Digital Ocean load balancer.
|
|
let gateway_ip = HeaderValue::from_str(&ip_addr.to_string())
|
|
.ok()
|
|
.unwrap_or(HeaderValue::from_static(""));
|
|
|
|
// This is the IP of the upstream client browser.
|
|
let upstream_client_ip = external_http_request.headers.get(HeaderName::from_static("x-forwarded-for"))
|
|
.map(|ip| ip.to_str().unwrap_or(""))
|
|
.unwrap_or("");
|
|
|
|
let upstream_client_ip = HeaderValue::from_str(upstream_client_ip)
|
|
.ok()
|
|
.unwrap_or(HeaderValue::from_static(""));
|
|
|
|
let mut headers_to_proxy = external_http_request.headers.clone();
|
|
|
|
headers_to_proxy.insert(HeaderName::from_static("forwarded"), upstream_client_ip.clone());
|
|
headers_to_proxy.insert(HeaderName::from_static("x-forwarded-for"), upstream_client_ip.clone());
|
|
|
|
// Unfortunately it looks like this middleware trounces the standard headers, so
|
|
// here we put it somewhere it won't be overwritten.
|
|
headers_to_proxy.insert(HeaderName::from_static("x-voder-proxy-for"), upstream_client_ip.clone());
|
|
headers_to_proxy.insert(HeaderName::from_static("x-gateway-ip"), gateway_ip);
|
|
|
|
// We're going to be changing the request body length, so remove the reported value
|
|
// so Hyper doesn't choke on any mismatch.
|
|
headers_to_proxy.remove(HeaderName::from_static("content-length"));
|
|
|
|
// The proxy host is a Kubernetes load balancer. We're not doing the round robin magic here,
|
|
// so it makes sense to reuse the same value for each retry.
|
|
let load_balancer_hostname = router.get_speaker_host(&external_http_request.speak_request.speaker);
|
|
|
|
loop {
|
|
let internal_speak_request = InternalSpeakRequest {
|
|
speaker: external_http_request.speak_request.speaker.clone(),
|
|
text: external_http_request.speak_request.text.clone(),
|
|
retry_attempt_number: attempt,
|
|
skip_rate_limiter: false,
|
|
};
|
|
|
|
let result = speak_proxy(
|
|
ip_addr,
|
|
internal_speak_request,
|
|
headers_to_proxy.clone(),
|
|
&load_balancer_hostname,
|
|
endpoint).await;
|
|
|
|
match result {
|
|
Ok(good_response) => {
|
|
// All we want to do is add our special header
|
|
return Ok(modify_response(good_response, proxy_hostname, attempt));
|
|
},
|
|
Err(_) => {},
|
|
}
|
|
|
|
attempt += 1;
|
|
|
|
thread::sleep(Duration::from_millis(retry_wait_ms)); // TODO: Needs backoff + jitter
|
|
|
|
if attempt >= max_retries {
|
|
return Err(ErrorResponse {
|
|
error_type: ErrorType::ProxyError,
|
|
error_description: "max internal retry attempts reached".to_string(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Debug)]
|
|
pub struct ResponseError {
|
|
pub reason: String,
|
|
}
|
|
|
|
async fn speak_proxy(
|
|
ip_addr: IpAddr,
|
|
internal_speak_request: InternalSpeakRequest,
|
|
headers: HeaderMap<HeaderValue>,
|
|
load_balancer_hostname: &str,
|
|
endpoint: &'static str) -> Result<Response<Body>, ()>
|
|
{
|
|
info!("Routing {} for {} to {} (attempt # {})", endpoint, &internal_speak_request.speaker,
|
|
&load_balancer_hostname, internal_speak_request.retry_attempt_number);
|
|
|
|
let mut request_builder = Request::builder()
|
|
.method(Method::POST)
|
|
.uri(endpoint);
|
|
|
|
for (k, v) in headers.iter() {
|
|
// NB: Handling move because of builder class
|
|
request_builder = request_builder.header(k, v);
|
|
}
|
|
|
|
let json_speak_request = serde_json::to_string(&internal_speak_request).unwrap();
|
|
|
|
let request = request_builder
|
|
.body(Body::from(json_speak_request))
|
|
.unwrap();
|
|
|
|
match hyper_reverse_proxy::call(ip_addr, &load_balancer_hostname, request).await {
|
|
Ok(result) => Ok(result),
|
|
Err(_result) => Err(()), // TODO: Surface this?
|
|
}
|
|
}
|
|
|
|
fn modify_response(mut response: Response<Body>, proxy_hostname: &str, retry_count: u8)
|
|
-> Response<Body>
|
|
{
|
|
let headers = response.headers_mut();
|
|
|
|
let proxy_hostname = HeaderValue::from_str(proxy_hostname)
|
|
.ok()
|
|
.unwrap_or(HeaderValue::from_static("proxy-unknown"));
|
|
|
|
let retry_count = HeaderValue::from_str(&retry_count.to_string())
|
|
.ok()
|
|
.unwrap_or(HeaderValue::from_static("unknown"));
|
|
|
|
headers.append(HeaderName::from_static("x-proxy-hostname"), proxy_hostname);
|
|
headers.append(HeaderName::from_static("x-retry-count"), retry_count);
|
|
|
|
response
|
|
} |