yuan
9 天以前 873adcf1933989d46ffa199f24b59a8f5aa476d3
fix: 添加对 Pinecone 启动期连接的重试机制,处理瞬时网络异常
已修改1个文件
74 ■■■■■ 文件已修改
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
@@ -7,15 +7,26 @@
import dev.langchain4j.store.embedding.pinecone.PineconeServerlessIndexConfig;
import io.pinecone.clients.Index;
import io.pinecone.clients.Pinecone;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.util.function.Supplier;
/**
 * 向量存储配置
 */
@Slf4j
@Configuration
public class EmbeddingStoreConfig {
    /** Pinecone 连接重试次数:构造期会同步发起 HTTP/2 握手,偶发 "Required SETTINGS preface not received" */
    private static final int CONNECT_ATTEMPTS = 3;
    /** 重试间隔基数(毫秒),按次数线性退避 */
    private static final long RETRY_BACKOFF_MILLIS = 1500L;
    @Value("${pinecone.api-key:pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9}")
    private String pineconeApiKey;
@@ -33,12 +44,12 @@
    @Bean
    public Index pineconeIndex(Pinecone pinecone) {
        return pinecone.getIndexConnection(indexName);
        return retry("获取 Pinecone 索引连接", () -> pinecone.getIndexConnection(indexName));
    }
    @Bean
    public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) {
        return PineconeEmbeddingStore.builder()
        return retry("初始化 Pinecone 向量存储", () -> PineconeEmbeddingStore.builder()
                .apiKey(pineconeApiKey)
                .index(indexName)
                .nameSpace(namespace)
@@ -47,6 +58,63 @@
                        .region("us-east-1")
                        .dimension(embeddingModel.dimension())
                        .build())
                .build();
                .build());
    }
    /**
     * 对 Pinecone 启动期连接做有限次重试。
     * <p>
     * Pinecone 客户端固定走 HTTP/2,构造 EmbeddingStore / 获取索引连接时会同步调用控制面接口,
     * 偶发握手失败(java.io.IOException: Required SETTINGS preface not received)会以运行时异常抛出,
     * 直接导致整个 ApplicationContext 加载失败。这里仅对这类瞬时网络异常重试,其它异常立即抛出。
     */
    private <T> T retry(String action, Supplier<T> supplier) {
        RuntimeException last = null;
        for (int attempt = 1; attempt <= CONNECT_ATTEMPTS; attempt++) {
            try {
                return supplier.get();
            } catch (RuntimeException e) {
                if (!isTransientConnectFailure(e) || attempt == CONNECT_ATTEMPTS) {
                    throw e;
                }
                last = e;
                log.warn("{}失败(第 {}/{} 次),{}ms 后重试:{}",
                        action, attempt, CONNECT_ATTEMPTS, RETRY_BACKOFF_MILLIS * attempt, rootMessage(e));
                sleep(RETRY_BACKOFF_MILLIS * attempt);
            }
        }
        throw last;
    }
    /** 沿 cause 链判断是否为 HTTP/2 握手类的瞬时连接失败 */
    private boolean isTransientConnectFailure(Throwable e) {
        for (Throwable t = e; t != null; t = t.getCause()) {
            if (t instanceof IOException) {
                return true;
            }
            String message = t.getMessage();
            if (message != null && (message.contains("SETTINGS preface")
                    || message.contains("Connection reset")
                    || message.contains("timed out"))) {
                return true;
            }
        }
        return false;
    }
    private String rootMessage(Throwable e) {
        Throwable root = e;
        while (root.getCause() != null && root.getCause() != root) {
            root = root.getCause();
        }
        return root.getMessage();
    }
    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}