#![cfg_attr(
target_family = "unix",
expect(
clippy::expect_used,
reason = "examples: panic-on-error is the standard pattern"
)
)]
#[cfg(target_family = "unix")]
mod unix_impl {
use std::{path::PathBuf, sync::Arc, time::Duration};
use rama::{
Service,
error::BoxError,
gateway::fastcgi::{FastCgiClientRequest, FastCgiHttpClient, FastCgiUnixConnector},
graceful::Shutdown,
http::{
Request, Response, StatusCode,
layer::{error_handling::ErrorHandlerLayer, trace::TraceLayer},
server::HttpServer,
service::web::{
Router,
response::{IntoResponse, Json},
},
},
layer::{ArcLayer, Layer},
net::{address::SocketAddress, client::EstablishedClientConnection},
rt::Executor,
tcp::server::TcpListener,
telemetry::tracing::{
self,
level_filters::LevelFilter,
subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt},
},
unix::UnixStream,
};
use serde_json::json;
pub(super) async fn run() {
tracing::subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();
let listen: SocketAddress = std::env::var("RAMA_FASTCGI_PHP_LISTEN")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| SocketAddress::local_ipv4(62081));
let backend_socket = PathBuf::from(
std::env::var("RAMA_FASTCGI_PHP_BACKEND_SOCKET")
.expect("RAMA_FASTCGI_PHP_BACKEND_SOCKET must point to the php-fpm Unix socket"),
);
let script_filename = std::env::var("RAMA_FASTCGI_PHP_SCRIPT_FILENAME")
.expect("RAMA_FASTCGI_PHP_SCRIPT_FILENAME must point to the PHP front controller");
let document_root = std::env::var("RAMA_FASTCGI_PHP_DOCUMENT_ROOT").unwrap_or_else(|_| {
std::path::Path::new(&script_filename)
.parent()
.and_then(|p| p.to_str())
.unwrap_or(".")
.to_owned()
});
let shutdown = Shutdown::default();
let exec = Executor::graceful(shutdown.guard());
let fastcgi_fallback = Arc::new(FastCgiHttpClient::new(
FastCgiUnixConnector::php_fpm(backend_socket, &script_filename)
.with_document_root(&document_root),
));
let router = Router::new()
.with_get("/api/health", async || {
Json(json!({ "status": "ok", "source": "rust" }))
})
.with_get("/api/version", async || {
Json(json!({ "version": env!("CARGO_PKG_VERSION"), "source": "rust" }))
})
.with_not_found(php_fallback_service(fastcgi_fallback));
let http_server = HttpServer::auto(exec.clone()).service(
(
ArcLayer::new(),
TraceLayer::new_for_http(),
ErrorHandlerLayer::new(),
)
.into_layer(router),
);
let tcp = TcpListener::bind_address(listen, exec.clone())
.await
.expect("bind http listener");
tracing::info!("rama-fastcgi-php migration listening on http://{listen}");
shutdown.spawn_task(tcp.serve(http_server));
shutdown
.shutdown_with_limit(Duration::from_secs(30))
.await
.expect("graceful shutdown");
}
fn php_fallback_service<S>(client: Arc<FastCgiHttpClient<S>>) -> PhpFallback<S> {
PhpFallback(client)
}
struct PhpFallback<S>(Arc<FastCgiHttpClient<S>>);
impl<S> Clone for PhpFallback<S> {
#[inline(always)]
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<S> Service<Request> for PhpFallback<S>
where
S: Service<
FastCgiClientRequest,
Output = EstablishedClientConnection<UnixStream, FastCgiClientRequest>,
Error: Into<BoxError>,
>,
{
type Output = Response;
type Error = std::convert::Infallible;
async fn serve(&self, req: Request) -> Result<Response, std::convert::Infallible> {
match self.0.serve(req).await {
Ok(resp) => Ok(resp),
Err(err) => {
tracing::error!(?err, "fastcgi backend error");
Ok((
StatusCode::BAD_GATEWAY,
Json(json!({
"error": "FastCGI backend unreachable",
"source": "rust",
})),
)
.into_response())
}
}
}
}
}
#[cfg(target_family = "unix")]
use unix_impl::run;
#[cfg(not(target_family = "unix"))]
async fn run() -> Result<(), rama::error::BoxError> {
use rama::error::BoxErrorExt;
Err(rama::error::BoxError::from_static_str(
"fastcgi_php_migration: this example uses Unix sockets and requires a \
Unix-family target (Linux, macOS, BSD).",
))
}
#[tokio::main]
async fn main() -> Result<(), rama::error::extra::OpaqueError> {
_ = run().await;
Ok(())
}