leader discovery fix and code cleaning

This commit is contained in:
Pierre
2026-02-01 13:39:36 +01:00
parent 2c85b409b9
commit 9b4a802ff3
8 changed files with 928 additions and 415 deletions
+70 -48
View File
@@ -2,16 +2,17 @@
* KV Manager for meta-fuse (FOLLOWER-only)
*
* Simplified KV manager that:
* 1. Discovers the leader via lock file
* 1. Discovers the leader via LeaderClient (reads kv-leader.info)
* 2. Creates Redis client connection
* 3. Handles reconnection on leader failure
*
* Unlike meta-sort's KVManager, this version never spawns Redis -
* it only connects as a client.
* it only connects as a client. Leader election is handled by meta-core.
*/
import { hostname } from 'os';
import { Logger } from 'tslog';
import { LeaderDiscovery } from './LeaderDiscovery.js';
import { LeaderClient } from './LeaderClient.js';
import { ServiceDiscovery } from './ServiceDiscovery.js';
import { RedisClient } from './RedisClient.js';
import type { LeaderLockInfo } from './IKVClient.js';
@@ -28,9 +29,6 @@ interface KVManagerConfig {
/** Service name (for logging) */
serviceName?: string;
/** Service version */
version?: string;
/** HTTP API port */
apiPort?: number;
@@ -43,13 +41,13 @@ interface KVManagerConfig {
/** Base URL for service discovery (overrides auto-detected URL) */
baseUrl?: string;
/** Service capabilities */
capabilities?: string[];
/** meta-core API URL (e.g., http://localhost:9000) */
metaCoreUrl?: string;
}
export class KVManager {
private config: Required<KVManagerConfig>;
private leaderDiscovery: LeaderDiscovery | null = null;
private leaderClient: LeaderClient | null = null;
private serviceDiscovery: ServiceDiscovery | null = null;
private redisClient: RedisClient | null = null;
private isStarted = false;
@@ -62,12 +60,11 @@ export class KVManager {
constructor(config: KVManagerConfig) {
this.config = {
serviceName: 'meta-fuse',
version: '1.0.0',
apiPort: 3000,
redisUrl: '',
redisPrefix: 'meta-sort:',
baseUrl: '',
capabilities: ['read', 'vfs'],
metaCoreUrl: '',
...config
};
}
@@ -83,24 +80,15 @@ export class KVManager {
logger.info(`Starting KVManager for ${this.config.serviceName}...`);
// Initialize service discovery
const apiUrl = this.config.baseUrl || `http://localhost:${this.config.apiPort}`;
// Initialize service discovery (meta-core handles actual registration)
// We keep this for service discovery (reading other services)
const apiUrl = this.config.baseUrl || `http://${hostname()}:${this.config.apiPort}`;
this.serviceDiscovery = new ServiceDiscovery({
metaCorePath: this.config.metaCorePath,
serviceName: this.config.serviceName,
version: this.config.version,
apiUrl,
baseUrl: this.config.baseUrl || undefined,
capabilities: this.config.capabilities,
endpoints: {
health: '/api/fuse/health',
dashboard: '/',
stats: '/api/fuse/stats',
}
apiUrl
});
// Start service discovery
await this.serviceDiscovery.start();
// Note: We don't start ServiceDiscovery - meta-core handles registration
// If direct Redis URL provided, skip leader discovery
if (this.config.redisUrl) {
@@ -110,29 +98,56 @@ export class KVManager {
return;
}
// Use leader discovery
this.leaderDiscovery = new LeaderDiscovery({
metaCorePath: this.config.metaCorePath
// Use leader client
this.leaderClient = new LeaderClient({
metaCorePath: this.config.metaCorePath,
metaCoreUrl: this.config.metaCoreUrl || undefined
});
// Set up leader discovery callbacks
this.leaderDiscovery.onLeaderFound(async (info: LeaderLockInfo) => {
logger.info(`Connecting to leader at ${info.api}...`);
await this.connectToRedis(info.api);
// Set up leader change callback
this.leaderClient.onChange(async () => {
logger.info('Leader changed, reconnecting...');
await this.reconnect();
});
this.leaderDiscovery.onLeaderLost(async () => {
logger.warn('Leader lost, disconnecting...');
await this.disconnectRedis();
this.notifyDisconnect();
});
// Wait for leader and connect
try {
const leaderInfo = await this.leaderClient.waitForLeader(30000);
logger.info(`Connecting to Redis at ${leaderInfo.redisUrl}...`);
await this.connectToRedis(leaderInfo.redisUrl);
} catch (error: any) {
logger.error(`Failed to connect to leader: ${error.message}`);
throw error;
}
// Start leader discovery
await this.leaderDiscovery.start();
// Start watching for leader changes
this.leaderClient.startWatching();
this.isStarted = true;
}
/**
* Reconnect to Redis after leader change
*/
private async reconnect(): Promise<void> {
if (this.isShuttingDown || !this.leaderClient) {
return;
}
// Disconnect existing
await this.disconnectRedis();
this.notifyDisconnect();
// Wait for new leader and connect
try {
const leaderInfo = await this.leaderClient.waitForLeader(30000);
logger.info(`Reconnecting to Redis at ${leaderInfo.redisUrl}...`);
await this.connectToRedis(leaderInfo.redisUrl);
} catch (error: any) {
logger.error(`Failed to reconnect: ${error.message}`);
}
}
/**
* Stop the KV manager
*/
@@ -142,18 +157,18 @@ export class KVManager {
logger.info('Stopping KVManager...');
this.isShuttingDown = true;
// Stop service discovery
// Stop leader client
if (this.leaderClient) {
this.leaderClient.close();
this.leaderClient = null;
}
// Stop service discovery (if it was started)
if (this.serviceDiscovery) {
await this.serviceDiscovery.stop();
this.serviceDiscovery = null;
}
// Stop leader discovery
if (this.leaderDiscovery) {
await this.leaderDiscovery.stop();
this.leaderDiscovery = null;
}
// Disconnect Redis
await this.disconnectRedis();
@@ -313,10 +328,17 @@ export class KVManager {
}
/**
* Get leader info (if using leader discovery)
* Get leader info
*/
getLeaderInfo(): LeaderLockInfo | null {
return this.leaderDiscovery?.getLeaderInfo() ?? null;
return this.leaderClient?.getCachedLeaderInfo() ?? null;
}
/**
* Get leader client
*/
getLeaderClient(): LeaderClient | null {
return this.leaderClient;
}
/**