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:
zhenyi
2026-06-08 01:21:20 +08:00
parent 5b740eecd7
commit 5c99b27421
22 changed files with 2015 additions and 98 deletions
+112 -5
View File
@@ -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(