调用AI接口增加guava Retrying重试机制

This commit is contained in:
yunpeng.zhang 2023-08-01 16:23:24 +08:00
parent 8865981c04
commit b98819cc74
9 changed files with 194 additions and 14 deletions

View File

@ -139,13 +139,14 @@ yarn run dev
## 后续计划
- [x] 使用死信队列处理异常情况,将图表生成任务置为失败
- [x] 引入Guava RateLimiter(单机) 和 Redisson RateLimiter(分布式) 两种限流机制
- [x] 引入Guava RateLimiter(单机) 和 Redisson RateLimiter(分布式) 两种限流机制 (在请求方法上添加注解即可限流,方便快捷)
- [x] 支持用户对失败的图表进行手动重试
- [x] 引入redis缓存提高加载速度
- [x] 引入jasypt encryption 对配置文件加密、解密
- [ ] 图表数据分表存储,提高查询灵活性和性能
- [ ] 给任务执行增加 guava Retrying重试机制保证系统可靠性
- [x] 给任务执行增加 guava Retrying重试机制保证系统可靠性
(guava Retrying 要使用 AttemptTimeLimiters.fixedTimeLimit()设置固定时间的超时限制 时需要 保证 guava版本在22或22以下)
- [ ] 定时任务把失败状态的图表放到队列中(补偿机制)
- [ ] 给任务的执行增加超时时间,超时自动标记为失败(超时控制)
- [ ] 图表数据分表存储,提高查询灵活性和性能
- [ ] 任务执行结果通过websocket实时通知给用户
- [ ] 我的图表管理页增加一个刷新、定时刷新的按钮,保证获取到图表的最新状态(前端轮询)

29
pom.xml
View File

@ -55,12 +55,24 @@
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.4.0</version>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- https://doc.xiaominfo.com/knife4j/documentation/get_start.html-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- https://cloud.tencent.com/document/product/436/10199-->
<dependency>
@ -77,7 +89,7 @@
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.1</version>
<version>2.9.0</version>
</dependency>
<!-- https://github.com/alibaba/easyexcel -->
<dependency>
@ -89,7 +101,7 @@
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
<version>5.8.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -130,7 +142,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
<version>22.0</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
@ -143,6 +155,17 @@
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>

View File

@ -72,11 +72,10 @@ public class BiMessageConsumer {
chartService.updateChartStatus(chart.getId(), BiTaskStatusEnum.FAILED.getValue(), "更新图表执行中状态失败");
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "图表为空");
}
//调用AI
String aiResult = aiManager.doChat(BiConstant.BI_MODEL_ID, BiUtils.buildUserInputForAi(chart));
BiResponse biResponse;
try {
//调用AI
String aiResult = aiManager.doChat(BiConstant.BI_MODEL_ID, BiUtils.buildUserInputForAi(chart));
biResponse = aiManager.aiAnsToBiResp(aiResult);
} catch (BusinessException e) {
channel.basicNack(deliveryTag, false, false);

View File

@ -15,6 +15,7 @@ public enum ErrorCode {
NOT_FOUND_ERROR(40400, "请求数据不存在"),
TOO_MANY_REQUEST(42900,"请求过于频繁"),
FORBIDDEN_ERROR(40300, "禁止访问"),
REQUEST_TIME_OUT(40300, "请求超时"),
SYSTEM_ERROR(50000, "系统内部异常"),
OPERATION_ERROR(50001, "操作失败");

View File

@ -283,10 +283,12 @@ public class ChartController {
boolean saveResult = chartService.save(chart);
ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "图表保存失败");
//创建线程任务
CompletableFuture.runAsync(() -> {
//先修改图表任务状态为执行中;
chartService.updateChartStatus(chart.getId(),BiTaskStatusEnum.RUNNING.getValue(), "");
chartService.updateChartStatus(chart.getId(),BiTaskStatusEnum.RUNNING.getValue(), null);
//调用AI
String aiResult = aiManager.doChat(BiConstant.BI_MODEL_ID, userInput);
@ -295,13 +297,17 @@ public class ChartController {
biResponse = aiManager.aiAnsToBiResp(aiResult);
} catch (BusinessException e) {
//执行失败状态修改为失败,记录任务失败信息
chartService.updateChartStatus(chart.getId(),BiTaskStatusEnum.FAILED.getValue(), e.getMessage());
chartService.updateChartStatus(chart.getId(), BiTaskStatusEnum.FAILED.getValue(), "AI生成错误");
throw e;
}
//执行成功后修改为已完成保存执行结果
biResponse.setChartId(chart.getId());
chartService.updateChartSucceedResult(biResponse);
}, threadPoolExecutor);
}, threadPoolExecutor).exceptionally((e) -> {
log.error("AI生成错误 chartId = {} userId = {} error = {}", chart.getUserId(), chart.getUserId(), e.getMessage());
chartService.updateChartStatus(chart.getId(), BiTaskStatusEnum.FAILED.getValue(), "AI生成错误");
return null;
});
BiResponse biResponse = new BiResponse();
biResponse.setChartId(chart.getId());

View File

@ -0,0 +1,44 @@
/*
* @(#)RetryLogListener.java
*
* Copyright © 2023 YunPeng Corporation.
*/
package top.peng.answerbi.listener;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryListener;
import lombok.extern.slf4j.Slf4j;
/**
* RetryLogListener 重试监听器
*
* @author yunpeng
* @version 1.0 2023/7/31
*/
@Slf4j
public class RetryLogListener implements RetryListener {
@Override
public <V> void onRetry(Attempt<V> attempt) {
// 第几次重试,(注意:第一次重试其实是第一次调用)
log.info("===== get ai response retry time : [{}] =====", attempt.getAttemptNumber());
// 距离第一次重试的延迟
log.info("retry delay : [{}]", attempt.getDelaySinceFirstAttempt());
// 重试结果: 是异常终止, 还是正常返回
log.info("hasException={}", attempt.hasException());
log.info("hasResult={}", attempt.hasResult());
// 是什么原因导致异常
if (attempt.hasException()) {
log.info("causeBy={}" , attempt.getExceptionCause().toString());
} else {
// 正常返回时的结果
log.info("result={}" , attempt.getResult());
}
log.info("===== log listen over. =====");
}
}

View File

@ -5,11 +5,15 @@
*/
package top.peng.answerbi.manager;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.yupi.yucongming.dev.client.YuCongMingClient;
import com.yupi.yucongming.dev.common.BaseResponse;
import com.yupi.yucongming.dev.model.DevChatRequest;
import com.yupi.yucongming.dev.model.DevChatResponse;
import java.util.concurrent.ExecutionException;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import top.peng.answerbi.common.ErrorCode;
import top.peng.answerbi.constant.BiConstant;
@ -24,11 +28,15 @@ import top.peng.answerbi.model.vo.BiResponse;
* @version 1.0 2023/7/14
*/
@Service
@Slf4j
public class AiManager {
@Resource
private YuCongMingClient yuCongMingClient;
@Resource
private AiRetryerBuilder aiRetryerBuilder;
/**
* AI 对话
*
@ -41,9 +49,15 @@ public class AiManager {
devChatRequest.setModelId(modelId);
devChatRequest.setMessage(message);
BaseResponse<DevChatResponse> response = yuCongMingClient.doChat(devChatRequest);
Retryer<BaseResponse<DevChatResponse>> retryer = aiRetryerBuilder.build();
BaseResponse<DevChatResponse> response = null;
try {
response = retryer.call(() -> yuCongMingClient.doChat(devChatRequest));
} catch (ExecutionException | RetryException e) {
log.error("调用AI重试 错误: {}", e.getMessage());
}
ThrowUtils.throwIf(response == null, ErrorCode.SYSTEM_ERROR,"AI响应错误");
ThrowUtils.throwIf(response == null || response.getData() == null , ErrorCode.SYSTEM_ERROR,"AI响应错误");
return response.getData().getContent();
}

View File

@ -0,0 +1,50 @@
/*
* @(#)GuavaRetryingConfig.java
*
* Copyright © 2023 YunPeng Corporation.
*/
package top.peng.answerbi.manager;
import com.github.rholder.retry.AttemptTimeLimiters;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.base.Predicates;
import com.yupi.yucongming.dev.common.BaseResponse;
import com.yupi.yucongming.dev.model.DevChatResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Component;
import top.peng.answerbi.listener.RetryLogListener;
import top.peng.answerbi.utils.SpinBlockStrategy;
/**
* BiRetryerBuilder Bi智能分析业务重试机制
*
* @author yunpeng
* @version 1.0 2023/7/31
*/
@Component
public class AiRetryerBuilder {
public Retryer<BaseResponse<DevChatResponse>> build(){
return RetryerBuilder.<BaseResponse<DevChatResponse>>newBuilder()
.retryIfResult(Predicates.isNull())
//发生IO异常时重试
.retryIfExceptionOfType(IOException.class)
//发生运行时异常时重试
.retryIfRuntimeException()
//重试策略 递增等待时长策略(降频重试) 依次在失败后的第5s15s进行降频重试
.withWaitStrategy(WaitStrategies.incrementingWait(5, TimeUnit.SECONDS,5,TimeUnit.SECONDS))
//最多执行3次首次执行+最多重试2次
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//超时限制 超时则中断执行继续重试
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(90,TimeUnit.SECONDS))
//自定义阻塞策略自旋锁
.withBlockStrategy(new SpinBlockStrategy())
//重试监听器
.withRetryListener(new RetryLogListener())
.build();
}
}

View File

@ -0,0 +1,42 @@
/*
* @(#)SpinBlockStrategy.java
*
* Copyright © 2023 YunPeng Corporation.
*/
package top.peng.answerbi.utils;
import com.github.rholder.retry.BlockStrategy;
import java.time.Duration;
import java.time.LocalDateTime;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* SpinBlockStrategy 自旋锁的实现, 不响应线程中断
*
* @author yunpeng
* @version 1.0 2023/7/31
*/
@Slf4j
@NoArgsConstructor
public class SpinBlockStrategy implements BlockStrategy {
@Override
public void block(long sleepTime) {
LocalDateTime startTime = LocalDateTime.now();
long start = System.currentTimeMillis();
long end = start;
log.info("[SpinBlockStrategy]...begin wait.");
while (end - start <= sleepTime) {
end = System.currentTimeMillis();
}
//使用Java8新增的Duration计算时间间隔
Duration duration = Duration.between(startTime, LocalDateTime.now());
log.info("[SpinBlockStrategy]...end wait.duration={}", duration.toMillis());
}
}