feat(gateway): implement remote service forwarding for distributed git operations
- Add remote client functions for archive, blame, and branch services - Implement fallback logic to forward requests to remote storage nodes - Add logging for forwarding operations with route details - Update Cargo.lock with new dependencies including ractor cluster libraries - Extend .gitignore with IDE and build system files - Remove outdated comments from bare repository implementation
This commit is contained in:
+112
-5
@@ -11,11 +11,14 @@ mod repository_maint;
|
||||
mod tag;
|
||||
mod tree;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use gix::discover::is_git;
|
||||
use ractor::{ActorCell, ActorRef};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
use crate::actor::message::{GitNodeMessage, RouteDecision};
|
||||
use crate::bare::GitBare;
|
||||
use crate::error::GitError;
|
||||
use crate::error::{GitError, GitResult};
|
||||
use crate::pb::{
|
||||
archive_service_server, blame_service_server, branch_service_server, commit_service_server,
|
||||
diff_service_server, merge_service_server, pack_service_server, repository_service_server,
|
||||
@@ -24,11 +27,52 @@ use crate::pb::{
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitksService {
|
||||
/// Root prefix path for all repositories
|
||||
pub repo_prefix: PathBuf,
|
||||
pub node_actor: Option<ActorRef<GitNodeMessage>>,
|
||||
}
|
||||
|
||||
impl GitksService {
|
||||
pub fn new(repo_prefix: PathBuf) -> Self {
|
||||
Self { repo_prefix, node_actor: None }
|
||||
}
|
||||
|
||||
pub fn with_actor(mut self, node_actor: ActorRef<GitNodeMessage>) -> Self {
|
||||
self.node_actor = Some(node_actor);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scan_all_repo(&self) -> GitResult<Vec<String>> {
|
||||
let root = self.repo_prefix.as_ref();
|
||||
let mut repos = Vec::new();
|
||||
if is_bare_git_repo(root) {
|
||||
repos.push(root.to_path_buf());
|
||||
} else {
|
||||
scan_bare_repos_recursively(root, &mut repos)?;
|
||||
}
|
||||
Ok(repos
|
||||
.into_iter()
|
||||
.filter_map(|path| path.to_str().map(str::to_owned))
|
||||
.collect())
|
||||
}
|
||||
pub async fn route_repository(
|
||||
&self,
|
||||
header: &crate::pb::RepositoryHeader,
|
||||
) -> Result<Option<RouteDecision>, tonic::Status> {
|
||||
let members = ractor::pg::get_members(&"gitks_nodes".to_string());
|
||||
let local = self.node_actor.as_ref().map(|actor| actor.get_cell());
|
||||
for member in members {
|
||||
if local.as_ref().is_some_and(|actor| actor == &member) {
|
||||
continue;
|
||||
}
|
||||
if let Some(decision) = query_route(member, header.clone()).await? {
|
||||
if decision.found && !decision.grpc_addr.is_empty() {
|
||||
return Ok(Some(decision));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn repo_label(&self, header: Option<&crate::pb::RepositoryHeader>) -> String {
|
||||
header
|
||||
.and_then(|h| {
|
||||
@@ -93,6 +137,70 @@ impl GitksService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub(super) async fn remote_endpoint(addr: &str) -> Result<tonic::transport::Endpoint, tonic::Status> {
|
||||
let uri: tonic::codegen::http::Uri = addr
|
||||
.parse()
|
||||
.map_err(|e| tonic::Status::invalid_argument(format!("invalid URI: {e}")))?;
|
||||
tonic::transport::Endpoint::new(uri)
|
||||
.map_err(|e| tonic::Status::internal(e.to_string()))
|
||||
}
|
||||
|
||||
pub(super) fn bridge_server_stream<T: Send + 'static>(
|
||||
mut remote: tonic::Streaming<T>,
|
||||
) -> tokio_stream::wrappers::ReceiverStream<Result<T, tonic::Status>> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||
tokio::spawn(async move {
|
||||
use tokio_stream::StreamExt;
|
||||
while let Some(item) = remote.next().await {
|
||||
if tx.send(item).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
tokio_stream::wrappers::ReceiverStream::new(rx)
|
||||
}
|
||||
|
||||
async fn query_route(
|
||||
member: ActorCell,
|
||||
header: crate::pb::RepositoryHeader,
|
||||
) -> Result<Option<RouteDecision>, tonic::Status> {
|
||||
let actor_ref: ActorRef<GitNodeMessage> = member.into();
|
||||
match ractor::call_t!(actor_ref, GitNodeMessage::RouteRepository, 500, header) {
|
||||
Ok(decision) => Ok(Some(decision)),
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "repository route query failed");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_bare_repos_recursively(dir: &Path, repos: &mut Vec<PathBuf>) -> GitResult<()> {
|
||||
for entry in std::fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if is_bare_git_repo(&path) {
|
||||
repos.push(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
scan_bare_repos_recursively(&path, repos)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_bare_git_repo(path: &Path) -> bool {
|
||||
match is_git(path) {
|
||||
Ok(repo) => repo.is_bare(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_status(e: GitError) -> tonic::Status {
|
||||
match &e {
|
||||
GitError::NotFound(_)
|
||||
@@ -160,11 +268,10 @@ pub(crate) fn git_cmd(gb: &GitBare, args: &[&str]) -> Result<std::process::Outpu
|
||||
|
||||
pub async fn serve(
|
||||
addr: std::net::SocketAddr,
|
||||
repo_prefix: PathBuf,
|
||||
svc: GitksService,
|
||||
) -> Result<(), tonic::transport::Error> {
|
||||
let span = tracing::info_span!("gitks.server", %addr);
|
||||
let _enter = span.enter();
|
||||
let svc = GitksService { repo_prefix };
|
||||
tracing::info!("registering gRPC services");
|
||||
let server = tonic::transport::Server::builder()
|
||||
.add_service(repository_service_server::RepositoryServiceServer::new(
|
||||
|
||||
Reference in New Issue
Block a user