fix(service-party-app): joinSession 添加重试逻辑处理乐观锁冲突
问题: - 多个参与方同时加入会话时会触发乐观锁冲突 - server-party 有重试逻辑可以成功重试 - service-party-app (Electron) 没有重试逻辑,直接失败 - 导致外部参与方无法成功加入 co_managed_keygen 会话 修复: - joinSession 方法添加最多 3 次重试 - 支持重试的错误类型:optimistic lock、UNAVAILABLE、DEADLINE_EXCEEDED - 使用指数退避 + 随机抖动避免重试风暴 - 抽取 doJoinSession 内部方法和 sleep 辅助方法 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6de545fcb9
commit
8c3a299714
|
|
@ -436,13 +436,54 @@ export class GrpcClient extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加入会话
|
* 加入会话(带重试逻辑)
|
||||||
|
*
|
||||||
|
* 支持重试以处理乐观锁冲突等瞬态错误
|
||||||
*/
|
*/
|
||||||
async joinSession(sessionId: string, partyId: string, joinToken: string): Promise<JoinSessionResponse> {
|
async joinSession(sessionId: string, partyId: string, joinToken: string): Promise<JoinSessionResponse> {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
throw new Error('Not connected');
|
throw new Error('Not connected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxRetries = 3;
|
||||||
|
const baseDelayMs = 500;
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const result = await this.doJoinSession(sessionId, partyId, joinToken);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err as Error;
|
||||||
|
const errorMessage = lastError.message || '';
|
||||||
|
|
||||||
|
// 判断是否为可重试的错误
|
||||||
|
const isRetryable =
|
||||||
|
errorMessage.includes('optimistic lock') ||
|
||||||
|
errorMessage.includes('modified by another transaction') ||
|
||||||
|
errorMessage.includes('UNAVAILABLE') ||
|
||||||
|
errorMessage.includes('DEADLINE_EXCEEDED');
|
||||||
|
|
||||||
|
if (!isRetryable || attempt >= maxRetries) {
|
||||||
|
console.error(`[gRPC] joinSession failed after ${attempt} attempt(s):`, errorMessage);
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算退避延迟(指数退避 + 随机抖动)
|
||||||
|
const delay = baseDelayMs * Math.pow(2, attempt - 1) + Math.random() * 100;
|
||||||
|
console.log(`[gRPC] joinSession attempt ${attempt}/${maxRetries} failed (${errorMessage}), retrying in ${Math.round(delay)}ms...`);
|
||||||
|
|
||||||
|
await this.sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError || new Error('joinSession failed after retries');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部方法:执行单次 joinSession 调用
|
||||||
|
*/
|
||||||
|
private doJoinSession(sessionId: string, partyId: string, joinToken: string): Promise<JoinSessionResponse> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
(this.client as grpc.Client & { joinSession: (req: unknown, callback: (err: Error | null, res: JoinSessionResponse) => void) => void })
|
(this.client as grpc.Client & { joinSession: (req: unknown, callback: (err: Error | null, res: JoinSessionResponse) => void) => void })
|
||||||
.joinSession(
|
.joinSession(
|
||||||
|
|
@ -462,6 +503,13 @@ export class GrpcClient extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助方法:异步等待
|
||||||
|
*/
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅会话事件(带自动重连)
|
* 订阅会话事件(带自动重连)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue