feat(cluster): implement distributed clustering with etcd coordination

- Integrate etcd-client for distributed coordination and leader election
- Add remote client macros with proper formatting for all services
- Implement RequestMetrics for tracking RPC performance and errors
- Add rate limiting mechanism across all service endpoints
- Create ElectionRequest and ElectionResult message types for leader election
- Add role management with primary/replica switching capabilities
- Implement health checker with automatic failover detection
- Add repository count metrics for cluster monitoring
- Update Cargo.toml with etcd-client and dashmap dependencies
- Modify RepoEntry to include read_only flag for replica handling
- Implement should_accept_election logic to prevent duplicate elections
- Add RoleChangedEvent handling for cluster role updates
This commit is contained in:
zhenyi
2026-06-08 14:31:29 +08:00
parent d243dce027
commit 8f472a0443
37 changed files with 4691 additions and 83 deletions
+157
View File
@@ -0,0 +1,157 @@
//! Tests for DiskCache and PackCache.
use std::path::PathBuf;
use std::time::Duration;
use gitks::disk_cache::DiskCache;
fn temp_dir() -> PathBuf {
tempfile::tempdir().unwrap().path().to_path_buf()
}
#[test]
fn test_disk_cache_basic_operations() {
let dir = temp_dir();
let cache = DiskCache::new(dir.clone(), "test-version".to_string(), 300, true);
// Ensure state creates latest file
let state = cache.ensure_state("test_repo.git").unwrap();
assert!(!state.is_empty());
// Same state on second call
let state2 = cache.ensure_state("test_repo.git").unwrap();
assert_eq!(state, state2);
}
#[test]
fn test_disk_cache_insert_and_lookup() {
let dir = temp_dir();
let cache = DiskCache::new(dir.clone(), "test-version".to_string(), 300, true);
let digest = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
let data = b"test cache data";
// Insert
cache.insert("+gitks-cache/cache", digest, data).unwrap();
// Lookup
let result = cache.lookup("+gitks-cache/cache", digest).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), data.to_vec());
// Non-existent key
let result = cache.lookup("+gitks-cache/cache", "nonexistent").unwrap();
assert!(result.is_none());
}
#[test]
fn test_disk_cache_invalidation() {
let dir = temp_dir();
let cache = DiskCache::new(dir.clone(), "test-version".to_string(), 300, true);
let state1 = cache.ensure_state("test_repo.git").unwrap();
// Invalidate
cache.invalidate_repo("test_repo.git");
// State should change
let state2 = cache.ensure_state("test_repo.git").unwrap();
assert_ne!(state1, state2);
}
#[test]
fn test_disk_cache_disabled() {
let dir = temp_dir();
let cache = DiskCache::new(dir.clone(), "test-version".to_string(), 300, false);
// All operations should succeed but do nothing
let state = cache.ensure_state("test_repo.git").unwrap();
assert!(!state.is_empty()); // Returns random value
let result = cache.lookup("+gitks-cache/cache", "anykey").unwrap();
assert!(result.is_none()); // Disabled → always None
}
#[test]
fn test_disk_cache_lease_guard() {
let dir = temp_dir();
let cache = DiskCache::new(dir.clone(), "test-version".to_string(), 300, true);
let state1 = cache.ensure_state("test_repo.git").unwrap();
let mut lease = cache.create_lease("test_repo.git").unwrap();
// Lease exists
let pending_dir = dir.join("+gitks-cache/state/test_repo.git/pending");
assert!(pending_dir.exists());
// Commit lease (updates latest)
lease.commit();
// Pending should be cleaned up
// (may still have dir but no files)
let state2 = cache.ensure_state("test_repo.git").unwrap();
assert_ne!(state1, state2);
}
#[test]
fn test_sha256_digest_determinism() {
let dir = temp_dir();
let cache = DiskCache::new(dir, "v1".to_string(), 300, true);
cache.ensure_state("repo.git").unwrap();
let key1 = cache
.compute_info_refs_key("repo.git", "upload-pack")
.unwrap();
let key2 = cache
.compute_info_refs_key("repo.git", "upload-pack")
.unwrap();
assert_eq!(key1, key2);
// Different protocol should produce different key
let key3 = cache
.compute_info_refs_key("repo.git", "receive-pack")
.unwrap();
assert_ne!(key1, key3);
}
#[test]
fn test_cleanup_expired() {
let dir = temp_dir();
// Very short max age for testing
let cache = DiskCache::new(dir.clone(), "test-version".to_string(), 1, true);
let digest = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
cache.insert("+gitks-cache/cache", digest, b"data").unwrap();
// Should be available immediately
let result = cache.lookup("+gitks-cache/cache", digest).unwrap();
assert!(result.is_some());
// Wait for expiration
std::thread::sleep(Duration::from_secs(2));
// Should be expired now
let result = cache.lookup("+gitks-cache/cache", digest).unwrap();
assert!(result.is_none());
}
#[test]
fn test_startup_cleanup() {
let dir = temp_dir();
let cache = DiskCache::new(dir.clone(), "test-version".to_string(), 300, true);
// Write some cache data
cache
.insert("+gitks-cache/cache", "abc123", b"test")
.unwrap();
assert!(
dir.join("+gitks-cache/cache")
.join("ab")
.join("c123")
.exists()
);
// Startup cleanup removes all cache dirs
cache.cleanup_on_startup().unwrap();
assert!(!dir.join("+gitks-cache/cache").exists());
}