feat:下单操作写入日志
This commit is contained in:
1 parent
0259445866
commit
42e0323080
10 files changed
+957
-48
No files matched your search
@@ -0,0 +1,58 @@
|
||||
# 消费机创建订单分段耗时
|
||||
|
||||
外部接口 `/hard/hard-api/ad/create-order` 对应 hard 服务内部
|
||||
`POST /hard-api/ad/create-order`。本次诊断仅观察该请求,不改变请求协议、
|
||||
事务注解、分布式锁、支付顺序、重试和返回值。
|
||||
|
||||
## 日志策略与开销
|
||||
|
||||
- 通过 `System.nanoTime()` 和请求内存中的阶段表计时,不新增 SQL、Redis 或 Feign 请求。
|
||||
- 正常且小于 1 秒的请求不新增摘要;慢请求、失败及阶段内被业务处理的异常输出一条 warning。
|
||||
- 摘要在过滤器链返回或抛错后统一生成,不在扣款写事务中逐阶段输出日志。
|
||||
- 专用 Logback 异步队列容量为 256,`neverBlock=true`。队列满时丢弃诊断摘要,不等待落盘。
|
||||
- 使用已有 `debug.log` 文件;不写入 DTO、人员资料、金额、令牌、付款码、图片或异常消息。
|
||||
|
||||
计时与内存分配存在少量开销,不能承诺零开销。异步队列优先保障请求返回,
|
||||
日志管道持续拥堵时可能缺少部分诊断记录。
|
||||
|
||||
## 取日志与匹配
|
||||
|
||||
部署包含 Java 代码和 `logback-spring.xml` 改动的 hard 服务后,在 `debug.log`
|
||||
搜索 `|终端支付耗时|`。按 `androidOrderCode` 与消费机的 `|支付耗时|` 摘要、
|
||||
原有订单 SQL 对齐;同号重试可通过不同的 `trace` 区分请求。
|
||||
|
||||
`outcome:failed` 是请求未正常完成,不能单凭这一字段判断钱包是否已经扣款;
|
||||
仍需按原有订单对账逻辑确认。旁路通知或证据登记异常被业务自行处理时,
|
||||
请求可能保持 `outcome:success`,异常类型保存在 `stageErrorTypes` 中。
|
||||
|
||||
## 主要阶段
|
||||
|
||||
| 字段 | 范围 |
|
||||
| --- | --- |
|
||||
| `pre_controller` | 请求监听器开始至 Controller 进入,含期间租户检查、认证、解析及校验 |
|
||||
| `controller` | Controller 参数整理、Service 调用和返回对象创建 |
|
||||
| `redis.lock` / `redis.unlock` | 原有 Redis 加锁、持锁检查及解锁 |
|
||||
| `idempotency.lookup` / `idempotency.validate` | 原订单查询及重复请求一致性校验 |
|
||||
| `prepare` | 下单准备整体,含下面的远程及本地查询 |
|
||||
| `prepare.merchant` / `prepare.hardware` | 商户远程查询、设备本地查询 |
|
||||
| `prepare.user` / `prepare.wallet` / `prepare.cart` | 用户、钱包及商品计价准备 |
|
||||
| `transaction` | 原有代理事务调用,包含事务开始、提交及同步 afterCommit |
|
||||
| `db.cart` / `db.orderInsert` | 购物车和订单写入 |
|
||||
| `wallet.debit` | 原有钱包扣款远程调用 |
|
||||
| `db.orderPaid` / `db.cartPaid` | 已支付状态更新 |
|
||||
| `afterCommit.evidence` | 原有同步人脸证据元数据登记 |
|
||||
| `postCommit.notify` | 原有提交后打印/播报分支 |
|
||||
|
||||
`stagesInclusiveMs` 单位为毫秒,父阶段包含子阶段,不能直接相加。
|
||||
其他支付方式仍按原逻辑执行,日志可出现核销、库存及第三方支付阶段。
|
||||
|
||||
## 时间边界
|
||||
|
||||
监听器在 Servlet 过滤器之前记录起点,专用过滤器在原有租户过滤器之后、
|
||||
安全过滤器之前绑定请求追踪,不调整已有过滤器顺序。
|
||||
若请求在更早的租户过滤器中直接拒绝,尚未进入计时过滤器,则不会产生本摘要。
|
||||
|
||||
`totalMs` 截止过滤器链返回,不是客户端端到端耗时;不覆盖网关和入站网络、
|
||||
容器调用请求监听器前的排队、最后的响应传输,以及客户端本地处理。
|
||||
因此“客户端 4500ms、服务摘要 200ms”只能证明还需排查摘要计时区间之外的等待,
|
||||
不能直接认定为 Flutter 程序卡顿。
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.bsd.pjcloud.hard.config;
|
||||
|
||||
import com.bsd.pjcloud.hard.config.filter.CreateOrderPaymentTimingFilter;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.ServletRequestEvent;
|
||||
import jakarta.servlet.ServletRequestListener;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class PaymentTimingConfiguration {
|
||||
/** 请求监听器先于全部过滤器,避免调整现有 Integer.MIN_VALUE 的租户过滤器顺序。 */
|
||||
@Bean
|
||||
public ServletListenerRegistrationBean<RequestStartListener> paymentTimingRequestStartListener() {
|
||||
return new ServletListenerRegistrationBean<>(new RequestStartListener(System::nanoTime));
|
||||
}
|
||||
|
||||
/** 租户检查之后、安全认证之前建立线程内追踪,耗时仍从监听器记录的时间开始。 */
|
||||
@Bean
|
||||
public FilterRegistrationBean<CreateOrderPaymentTimingFilter> paymentTimingFilter() {
|
||||
FilterRegistrationBean<CreateOrderPaymentTimingFilter> registration =
|
||||
new FilterRegistrationBean<>(new CreateOrderPaymentTimingFilter());
|
||||
registration.setName("createOrderPaymentTimingFilter");
|
||||
registration.addUrlPatterns(CreateOrderPaymentTimingFilter.PATH);
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST);
|
||||
registration.setAsyncSupported(true);
|
||||
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
|
||||
return registration;
|
||||
}
|
||||
|
||||
public static final class RequestStartListener implements ServletRequestListener {
|
||||
private final LongSupplier clock;
|
||||
|
||||
public RequestStartListener(LongSupplier clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestInitialized(ServletRequestEvent event) {
|
||||
try {
|
||||
if (event.getServletRequest() instanceof HttpServletRequest request
|
||||
&& CreateOrderPaymentTimingFilter.matches(request)) {
|
||||
request.setAttribute(CreateOrderPaymentTimingFilter.STARTED_AT_ATTRIBUTE, clock.getAsLong());
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 诊断监听器不参与请求受理;没有时间戳时过滤器从自身入口开始计时。
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.bsd.pjcloud.hard.config.filter;
|
||||
|
||||
import com.bsd.pjcloud.hard.support.PaymentTimingTrace;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.LongFunction;
|
||||
|
||||
/** 只观察同步的终端下单请求,不读取请求体、不更改响应或安全配置。 */
|
||||
public class CreateOrderPaymentTimingFilter extends OncePerRequestFilter {
|
||||
public static final String PATH = "/hard-api/ad/create-order";
|
||||
public static final String STARTED_AT_ATTRIBUTE = CreateOrderPaymentTimingFilter.class.getName() + ".startedAtNanos";
|
||||
private final LongFunction<PaymentTimingTrace.Scope> opener;
|
||||
|
||||
public CreateOrderPaymentTimingFilter() {
|
||||
this(PaymentTimingTrace::open);
|
||||
}
|
||||
|
||||
public CreateOrderPaymentTimingFilter(LongFunction<PaymentTimingTrace.Scope> opener) {
|
||||
this.opener = opener;
|
||||
}
|
||||
|
||||
public static boolean matches(HttpServletRequest request) {
|
||||
String contextPath = request.getContextPath();
|
||||
return "POST".equals(request.getMethod())
|
||||
&& ((contextPath == null ? "" : contextPath) + PATH).equals(request.getRequestURI());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
return !matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
Object start = request.getAttribute(STARTED_AT_ATTRIBUTE);
|
||||
long startedAtNanos = start instanceof Long ? (Long) start : System.nanoTime();
|
||||
try (PaymentTimingTrace.Scope scope = opener.apply(startedAtNanos)) {
|
||||
try {
|
||||
chain.doFilter(request, response);
|
||||
} catch (IOException | ServletException | RuntimeException | Error error) {
|
||||
PaymentTimingTrace.markFailure(error);
|
||||
throw error;
|
||||
} finally {
|
||||
scope.finish(response.getStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-10
@@ -28,6 +28,7 @@ import com.bsd.pjcloud.hard.api.vo.order.HardCreateHardQrCodeScanOrderVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.order.HardOrderByAndroidCodeVo;
|
||||
import com.bsd.pjcloud.hard.service.*;
|
||||
import com.bsd.pjcloud.hard.support.PaymentTimingTrace;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -160,18 +161,28 @@ public class HardApiController {
|
||||
@Operation(summary = "创建订单")
|
||||
// Service 按设备和 androidOrderCode 加自动续期锁并复用已支付订单,旧终端无需新增字段。
|
||||
public R<Long> createOrder(@Valid @RequestBody HardCreateOrderDto dto) {
|
||||
// 处理商品类型
|
||||
if (ObjectUtil.isNotEmpty(dto.getList())) {
|
||||
for (HardCreateCarDto item : dto.getList()) {
|
||||
if (ObjectUtil.isEmpty(item.getProductNature())) continue;
|
||||
if (ObjectUtil.isNotEmpty(item.getProductWeight()) && item.getProductWeight().compareTo(BigDecimal.valueOf(0)) != 0) {
|
||||
item.setProductNature(HardProductNatureEnum.WEIGHT);
|
||||
} else if (ObjectUtil.isNotEmpty(item.getProductCount())) {
|
||||
item.setProductNature(HardProductNatureEnum.DEFAULT);
|
||||
PaymentTimingTrace.markControllerEntered();
|
||||
PaymentTimingTrace.bind(dto.getAndroidOrderCode(), dto.getHardwareId());
|
||||
try {
|
||||
return PaymentTimingTrace.call("controller", () -> {
|
||||
// 处理商品类型
|
||||
if (ObjectUtil.isNotEmpty(dto.getList())) {
|
||||
for (HardCreateCarDto item : dto.getList()) {
|
||||
if (ObjectUtil.isEmpty(item.getProductNature())) continue;
|
||||
if (ObjectUtil.isNotEmpty(item.getProductWeight()) && item.getProductWeight().compareTo(BigDecimal.valueOf(0)) != 0) {
|
||||
item.setProductNature(HardProductNatureEnum.WEIGHT);
|
||||
} else if (ObjectUtil.isNotEmpty(item.getProductCount())) {
|
||||
item.setProductNature(HardProductNatureEnum.DEFAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return R.ok(hardwareOrderService.createOrder(dto));
|
||||
});
|
||||
} catch (RuntimeException | Error failure) {
|
||||
// 业务异常可能被统一异常处理器转换为 HTTP 200,仍保留失败请求的计时摘要。
|
||||
PaymentTimingTrace.markFailure(failure);
|
||||
throw failure;
|
||||
}
|
||||
return R.ok(hardwareOrderService.createOrder(dto));
|
||||
}
|
||||
|
||||
@PostMapping("/create-hard-qr-code-order")
|
||||
|
||||
+62
-38
@@ -73,6 +73,7 @@ import com.bsd.pjcloud.hard.mapper.HardThirdMiOrderMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
|
||||
import com.bsd.pjcloud.hard.service.*;
|
||||
import com.bsd.pjcloud.hard.service.order.HardOrderCartSnapshot;
|
||||
import com.bsd.pjcloud.hard.support.PaymentTimingTrace;
|
||||
import com.bsd.pjcloud.supermarket.api.feign.RemoteMallProductService;
|
||||
import com.bsd.pjcloud.supermarket.api.feign.RemoteShopSkuService;
|
||||
import com.bsd.pjcloud.supermarket.api.feign.RemoteSupermarketSpuService;
|
||||
@@ -282,24 +283,28 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
throw new RuntimeException("安卓订单code不能为空");
|
||||
}
|
||||
RLock lock = redissonClient.getLock(keyStrResolver.extract("hard:order:create:" + dto.getHardwareId() + ":" + dto.getAndroidOrderCode(), ":"));
|
||||
if (!lock.tryLock()) throw new RuntimeException("正在创建订单中,请勿重复操作");
|
||||
if (!PaymentTimingTrace.call("redis.lock", lock::tryLock)) throw new RuntimeException("正在创建订单中,请勿重复操作");
|
||||
Long orderId;
|
||||
CreateOrderContext context;
|
||||
try {
|
||||
HardOrderEntity existing = findTerminalOrder(dto.getAndroidOrderCode(), dto.getHardwareId());
|
||||
HardOrderEntity existing = PaymentTimingTrace.call("idempotency.lookup",
|
||||
() -> findTerminalOrder(dto.getAndroidOrderCode(), dto.getHardwareId()));
|
||||
if (existing != null) {
|
||||
validateReusableOrder(existing, dto);
|
||||
PaymentTimingTrace.run("idempotency.validate", () -> validateReusableOrder(existing, dto));
|
||||
return existing.getId();
|
||||
}
|
||||
// 远程只读查询全部先完成,数据库写事务不等待商户、用户、钱包和商品查询。
|
||||
context = prepareCreateOrder(dto);
|
||||
orderId = getSelf().createOrderInTransaction(dto, context);
|
||||
context = PaymentTimingTrace.call("prepare", () -> prepareCreateOrder(dto));
|
||||
// 包住代理调用,包含原事务的开启、提交及同步 afterCommit;阶段可嵌套,不能直接相加。
|
||||
orderId = PaymentTimingTrace.call("transaction", () -> getSelf().createOrderInTransaction(dto, context));
|
||||
} finally {
|
||||
if (lock.isHeldByCurrentThread()) lock.unlock();
|
||||
PaymentTimingTrace.run("redis.unlock", () -> {
|
||||
if (lock.isHeldByCurrentThread()) lock.unlock();
|
||||
});
|
||||
}
|
||||
// 提交后再生成打印/播报任务;旁路失败不能把已经扣款的订单反向报告为失败。
|
||||
try {
|
||||
getSelf().createOrderAfterSendMqMsg(orderId, context.userDetail);
|
||||
PaymentTimingTrace.run("postCommit.notify", () -> getSelf().createOrderAfterSendMqMsg(orderId, context.userDetail));
|
||||
} catch (Exception exception) {
|
||||
log.warn("|{}|订单已提交,通知失败|orderId:{}|", TAG, orderId, exception);
|
||||
}
|
||||
@@ -310,7 +315,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
* 无写事务的准备阶段,保留 PJ 原有钱包选择、额外餐费和一次商品查询的计价规则。
|
||||
*/
|
||||
private CreateOrderContext prepareCreateOrder(HardCreateOrderDto dto) {
|
||||
MchManageVo mch = validateCreateOrderDto(dto);
|
||||
MchManageVo mch = PaymentTimingTrace.call("prepare.validate", () -> validateCreateOrderDto(dto));
|
||||
if (ObjectUtil.isEmpty(mch)) throw new RuntimeException("机器绑定的商户不存在");
|
||||
// 是否需要钱包支付
|
||||
boolean isWalletPay = isPay(dto.getOrderType(), dto.getOrderPayType());
|
||||
@@ -327,7 +332,8 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
UserInfo userDetail = null;
|
||||
SysUser sysUser = null;
|
||||
if (isWalletPay || isVerify) {
|
||||
userDetail = requireCreateOrderData(userService.getInnerByUserId(dto.getUserId()), "用户不存在");
|
||||
userDetail = PaymentTimingTrace.call("prepare.user",
|
||||
() -> requireCreateOrderData(userService.getInnerByUserId(dto.getUserId()), "用户不存在"));
|
||||
if (ObjectUtil.isEmpty(userDetail.getSysUser())) throw new RuntimeException("用户不存在");
|
||||
sysUser = userDetail.getSysUser();
|
||||
}
|
||||
@@ -337,19 +343,23 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
WalletManageEntity wallet = null;
|
||||
if (isWalletPay) {
|
||||
if (ObjectUtil.isNotEmpty(dto.getWalletId()) && ObjectUtil.equals(dto.getSysType(), HardTypeEnum.SUPERMARKET)) {
|
||||
R<WalletManageVo> walletResult = remoteWalletService.getWalletById(dto.getWalletId());
|
||||
R<WalletManageVo> walletResult = PaymentTimingTrace.call("prepare.wallet",
|
||||
() -> remoteWalletService.getWalletById(dto.getWalletId()));
|
||||
wallet = requireCreateOrderData(walletResult, "用户钱包不存在");
|
||||
} else {
|
||||
if (ObjectUtil.isEmpty(mch.getDefaultWalletTypeId())) {
|
||||
if (dto.getSysType().equals(HardTypeEnum.CANTEEN)) {
|
||||
R<WalletManageEntity> walletResult = remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.EAT);
|
||||
R<WalletManageEntity> walletResult = PaymentTimingTrace.call("prepare.wallet",
|
||||
() -> remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.EAT));
|
||||
wallet = requireCreateOrderData(walletResult, "用户钱包不存在");
|
||||
} else {
|
||||
R<WalletManageEntity> walletResult = remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.PAY);
|
||||
R<WalletManageEntity> walletResult = PaymentTimingTrace.call("prepare.wallet",
|
||||
() -> remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.PAY));
|
||||
wallet = requireCreateOrderData(walletResult, "用户钱包不存在");
|
||||
}
|
||||
} else {
|
||||
R<WalletManageEntity> walletResult = remoteWalletService.getWalletByWalletTypeId(dto.getUserId(), mch.getDefaultWalletTypeId());
|
||||
R<WalletManageEntity> walletResult = PaymentTimingTrace.call("prepare.wallet",
|
||||
() -> remoteWalletService.getWalletByWalletTypeId(dto.getUserId(), mch.getDefaultWalletTypeId()));
|
||||
wallet = requireCreateOrderData(walletResult, "用户钱包不存在");
|
||||
}
|
||||
}
|
||||
@@ -364,15 +374,20 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
}
|
||||
} else if (isVerify) {
|
||||
// 判断该二维码有没有被核销
|
||||
boolean isCancel = validateCodeIsCancel(dto.getOrderType(), dto.getOrderSourceTypeId());
|
||||
boolean isCancel = PaymentTimingTrace.call("prepare.verifyCode",
|
||||
() -> validateCodeIsCancel(dto.getOrderType(), dto.getOrderSourceTypeId()));
|
||||
if (isCancel) throw new RuntimeException("该二维码已被核销");
|
||||
}
|
||||
HardOrderCartSnapshot cart = hardCarService.prepareCardWithCompute(dto.getSysType(), dto.getOrderType(), dto.getList());
|
||||
HardOrderCartSnapshot cart = PaymentTimingTrace.call("prepare.cart",
|
||||
() -> hardCarService.prepareCardWithCompute(dto.getSysType(), dto.getOrderType(), dto.getList()));
|
||||
if (cart == null) {
|
||||
// 非收银业态仍沿用旧计价/摘要规则,所有商品查询同样在写事务外完成。
|
||||
int total = hardCarService.computerCarTotalPrice(dto.getSysType(), dto.getOrderType(), dto.getList());
|
||||
String subject = hardCarService.computerCarSubject(dto.getSysType(), dto.getOrderType(), dto.getList());
|
||||
cart = new HardOrderCartSnapshot(total, subject, hardCarService.prepareCard(dto.getSysType(), dto.getOrderType(), dto.getList()));
|
||||
int total = PaymentTimingTrace.call("prepare.cartTotal",
|
||||
() -> hardCarService.computerCarTotalPrice(dto.getSysType(), dto.getOrderType(), dto.getList()));
|
||||
String subject = PaymentTimingTrace.call("prepare.cartSubject",
|
||||
() -> hardCarService.computerCarSubject(dto.getSysType(), dto.getOrderType(), dto.getList()));
|
||||
cart = new HardOrderCartSnapshot(total, subject, PaymentTimingTrace.call("prepare.cartDetails",
|
||||
() -> hardCarService.prepareCard(dto.getSysType(), dto.getOrderType(), dto.getList())));
|
||||
}
|
||||
int extraPrice = isExtraPrice && dto.getExtraPrice() != null ? dto.getExtraPrice() : 0;
|
||||
int payPrice = Math.addExact(cart.totalPrice(), extraPrice);
|
||||
@@ -399,7 +414,8 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
int totalPrice = context.cart.totalPrice(), extraPrice = context.extraPrice;
|
||||
int payPrice = Math.addExact(totalPrice, extraPrice);
|
||||
String subject = context.cart.subject();
|
||||
Long parentCarId = hardCarService.createCardFromSnapshot(dto.getUserId(), dto.getMerchantId(), dto.getSysType(), context.cart);
|
||||
Long parentCarId = PaymentTimingTrace.call("db.cart",
|
||||
() -> hardCarService.createCardFromSnapshot(dto.getUserId(), dto.getMerchantId(), dto.getSysType(), context.cart));
|
||||
// 添加硬件订单
|
||||
HardOrderEntity orderObj = HardOrderEntity.builder()
|
||||
.androidOrderCode(dto.getAndroidOrderCode())
|
||||
@@ -430,20 +446,20 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
if (isWalletPay) {
|
||||
orderObj.setWalletId(wallet.getId());
|
||||
}
|
||||
baseMapper.insert(orderObj);
|
||||
PaymentTimingTrace.run("db.orderInsert", () -> baseMapper.insert(orderObj));
|
||||
registerEvidenceAfterCommit(orderObj, dto);
|
||||
// 扣减库存
|
||||
getSelf().fastenerInventory(orderObj, YesNoEnum.YES);
|
||||
PaymentTimingTrace.run("stock.reduce", () -> getSelf().fastenerInventory(orderObj, YesNoEnum.YES));
|
||||
// 钱包支付或者核销
|
||||
if (isVerify || isWalletPay) {
|
||||
// 钱包扣款
|
||||
try {
|
||||
reduceOrderBalance(orderObj, false);
|
||||
PaymentTimingTrace.run("payment", () -> reduceOrderBalance(orderObj, false));
|
||||
} catch (InsufficientWalletBalanceException rejected) {
|
||||
// 不透支扣款可能因另一台终端抢先消费而明确失败,此时库存服务已经提交。
|
||||
// 仅在余额不足的确定结果下恢复库存;超时等未知资金结果不能据此做补偿。
|
||||
try {
|
||||
getSelf().fastenerInventory(orderObj, YesNoEnum.NO);
|
||||
PaymentTimingTrace.run("stock.restore", () -> getSelf().fastenerInventory(orderObj, YesNoEnum.NO));
|
||||
} catch (Exception compensationFailure) {
|
||||
rejected.addSuppressed(compensationFailure);
|
||||
log.error("|{}|余额不足后的库存恢复失败,需核对库存|orderId:{}|", TAG, orderObj.getId(), compensationFailure);
|
||||
@@ -451,40 +467,44 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
throw rejected;
|
||||
}
|
||||
// 核销二维码
|
||||
getSelf().cancelCode(orderObj, userDetail);
|
||||
PaymentTimingTrace.run("verifyCode", () -> getSelf().cancelCode(orderObj, userDetail));
|
||||
}
|
||||
// 现金支付
|
||||
if (isOffline) {
|
||||
// 更改为支付成功
|
||||
getSelf().orderSuccess(orderObj, Boolean.FALSE);
|
||||
PaymentTimingTrace.run("payment.offlineSuccess", () -> getSelf().orderSuccess(orderObj, Boolean.FALSE));
|
||||
}
|
||||
// 微信支付
|
||||
if (isWeChatPay) {
|
||||
WechatMicropayDto wechatPayDto = WechatMicropayDto.builder().merId(orderObj.getMerchantId()).orderId(orderObj.getId()).price(payPrice).wechatQrCode(String.valueOf(dto.getOrderSourceTypeId())).build();
|
||||
WechatMicropayVo wechatPayVo = wechatPayService.micropay(wechatPayDto);
|
||||
WechatMicropayVo wechatPayVo = PaymentTimingTrace.call("payment.wechat",
|
||||
() -> wechatPayService.micropay(wechatPayDto));
|
||||
if (ObjectUtil.isEmpty(wechatPayVo)) {
|
||||
throw new RuntimeException("微信支付失败");
|
||||
}
|
||||
boolean wehcantPaySuccess = wechatPayVo.isAllSuccess();
|
||||
if (!wehcantPaySuccess) {
|
||||
// 如果微信支付订单返回不明确(不是支付成功)直接撤销支付
|
||||
wechatPayService.reverse(WechantReverseDto.builder().orderId(orderObj.getId()).build());
|
||||
PaymentTimingTrace.run("payment.wechatReverse",
|
||||
() -> wechatPayService.reverse(WechantReverseDto.builder().orderId(orderObj.getId()).build()));
|
||||
throw new RuntimeException(wechatPayVo.getErr_code_des());
|
||||
} else {
|
||||
getSelf().orderSuccess(orderObj, Boolean.TRUE);
|
||||
PaymentTimingTrace.run("payment.wechatSuccess", () -> getSelf().orderSuccess(orderObj, Boolean.TRUE));
|
||||
}
|
||||
}
|
||||
// 支付宝支付
|
||||
if (isAliPay) {
|
||||
AliMicropayDto wechatPayDto = AliMicropayDto.builder().merId(orderObj.getMerchantId()).orderId(orderObj.getId()).price(payPrice).subject(subject).authCode(String.valueOf(dto.getOrderSourceTypeId())).build();
|
||||
AlipayTradePayResponse micropay = alipayService.micropay(wechatPayDto);
|
||||
AlipayTradePayResponse micropay = PaymentTimingTrace.call("payment.alipay",
|
||||
() -> alipayService.micropay(wechatPayDto));
|
||||
if (ObjectUtil.isEmpty(micropay) || !micropay.isSuccess()) {
|
||||
// 如果支付宝支付订单返回不明确(不是支付成功)直接撤销支付
|
||||
alipayService.reverse(AliReverseDto.builder().orderId(orderObj.getId()).build());
|
||||
PaymentTimingTrace.run("payment.alipayReverse",
|
||||
() -> alipayService.reverse(AliReverseDto.builder().orderId(orderObj.getId()).build()));
|
||||
throw new RuntimeException(micropay == null ? "支付宝支付失败" : micropay.getSubMsg());
|
||||
} else {
|
||||
orderObj.setOrderNo(micropay.getTradeNo());
|
||||
getSelf().orderSuccess(orderObj, Boolean.TRUE);
|
||||
PaymentTimingTrace.run("payment.alipaySuccess", () -> getSelf().orderSuccess(orderObj, Boolean.TRUE));
|
||||
}
|
||||
}
|
||||
return orderObj.getId();
|
||||
@@ -616,7 +636,8 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
}
|
||||
Runnable action = () -> {
|
||||
try {
|
||||
hardOrderEvidenceImageService.registerOrderEvidence(order, dto.getEvidenceMetadataList(), dto.getAppVersion());
|
||||
PaymentTimingTrace.run("afterCommit.evidence",
|
||||
() -> hardOrderEvidenceImageService.registerOrderEvidence(order, dto.getEvidenceMetadataList(), dto.getAppVersion()));
|
||||
} catch (Exception exception) {
|
||||
// 终端本地队列仍会按 androidOrderCode 补交,服务端占位失败不得改变订单支付结果。
|
||||
log.warn("|{}|订单证据占位登记失败|orderId:{}|error:{}|", TAG, order.getId(), exception.getMessage());
|
||||
@@ -1630,7 +1651,8 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
reduceBuild.setForce(force);
|
||||
R<WalletTransactionEntity> reduceRes;
|
||||
try {
|
||||
reduceRes = walletTransactionService.reduceWalletBalance(reduceBuild);
|
||||
reduceRes = PaymentTimingTrace.call("wallet.debit",
|
||||
() -> walletTransactionService.reduceWalletBalance(reduceBuild));
|
||||
} catch (FeignException rejected) {
|
||||
// 钱包的全局异常处理器把余额不足包装成 HTTP 500 + R;Feign 会直接抛异常,
|
||||
// 必须只识别这个明确业务结果,不能把一般 500 或网关超时当作扣款未发生。
|
||||
@@ -1647,7 +1669,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
}
|
||||
// 订单成功
|
||||
if (isWalletPay || isVerify) {
|
||||
getSelf().orderSuccess(order, isWalletPay);
|
||||
PaymentTimingTrace.run("payment.success", () -> getSelf().orderSuccess(order, isWalletPay));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1679,11 +1701,11 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
if (ObjectUtil.isNotEmpty(order.getOrderNo())) {
|
||||
update.setOrderNo(order.getOrderNo());
|
||||
}
|
||||
baseMapper.updateById(update);
|
||||
PaymentTimingTrace.run("db.orderPaid", () -> baseMapper.updateById(update));
|
||||
// 父购物车订单表订单状态
|
||||
if (ObjectUtil.isNotEmpty(order.getParentCarId())) {
|
||||
HardParentCarEntity updateParentCar = HardParentCarEntity.builder().id(order.getParentCarId()).orderStatus(HardOrderStatusEnum.PAY.getType()).build();
|
||||
hardParentCarServiceImpl.updateById(updateParentCar);
|
||||
PaymentTimingTrace.run("db.cartPaid", () -> hardParentCarServiceImpl.updateById(updateParentCar));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2240,9 +2262,11 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
}
|
||||
|
||||
private MchManageVo validateCreateOrderDto(HardCreateOrderDto dto) {
|
||||
MchManageVo mch = requireCreateOrderData(remoteMchService.getById(dto.getMerchantId()), "商户不存在");
|
||||
MchManageVo mch = PaymentTimingTrace.call("prepare.merchant",
|
||||
() -> requireCreateOrderData(remoteMchService.getById(dto.getMerchantId()), "商户不存在"));
|
||||
// 此处只取 deptId,按主键直查即可;getByHardId 还会联查部门和商户两次远程调用
|
||||
HardManagerEntity hard = hardManagerService.getById(dto.getHardwareId());
|
||||
HardManagerEntity hard = PaymentTimingTrace.call("prepare.hardware",
|
||||
() -> hardManagerService.getById(dto.getHardwareId()));
|
||||
if (ObjectUtil.isEmpty(hard)) {
|
||||
throw new RuntimeException("设备不存在");
|
||||
}
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package com.bsd.pjcloud.hard.support;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 终端下单的请求内计时;不修改支付结果,也不记录 DTO、金额、人员或异常消息。 */
|
||||
public final class PaymentTimingTrace {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PaymentTimingTrace.class);
|
||||
private static final ThreadLocal<Scope> CURRENT = new ThreadLocal<>();
|
||||
private static final AtomicLong SEQUENCE = new AtomicLong();
|
||||
private static final Pattern SAFE_IDENTIFIER = Pattern.compile("[A-Za-z0-9_.-]+");
|
||||
private static final long SLOW_NANOS = 1_000_000_000L;
|
||||
|
||||
private PaymentTimingTrace() {
|
||||
}
|
||||
|
||||
/** 时间由 ServletRequestListener 在过滤器之前采集,包含租户与安全过滤器的等待。 */
|
||||
public static Scope open(long startedAtNanos) {
|
||||
return open(startedAtNanos, System::nanoTime, LOG::warn);
|
||||
}
|
||||
|
||||
/** 可注入单调时钟与日志接收器,测试无需休眠或启动真实服务。 */
|
||||
public static Scope open(long startedAtNanos, LongSupplier clock, Consumer<String> log) {
|
||||
Scope scope = new Scope(startedAtNanos, clock, log, CURRENT.get());
|
||||
CURRENT.set(scope);
|
||||
return scope;
|
||||
}
|
||||
|
||||
public static Scope current() {
|
||||
return CURRENT.get();
|
||||
}
|
||||
|
||||
public static void bind(String androidOrderCode, Long hardId) {
|
||||
Scope scope = CURRENT.get();
|
||||
if (scope == null) return;
|
||||
scope.androidOrderCode = safeIdentifier(androidOrderCode, 128, null);
|
||||
scope.hardId = hardId;
|
||||
}
|
||||
|
||||
/** Controller 参数解析及校验完成后调用,之前的时间统一计入 pre_controller。 */
|
||||
public static void markControllerEntered() {
|
||||
Scope scope = CURRENT.get();
|
||||
if (scope == null || scope.controllerEntered) return;
|
||||
scope.controllerEntered = true;
|
||||
scope.record("pre_controller", scope.elapsedNanos(), null);
|
||||
}
|
||||
|
||||
/** 只有请求最终失败时标记;阶段内被业务自行处理的异常不改变最终成功状态。 */
|
||||
public static void markFailure(Throwable error) {
|
||||
Scope scope = CURRENT.get();
|
||||
if (scope == null) return;
|
||||
scope.failed = true;
|
||||
if (error != null) scope.errorTypes.add(errorType(error));
|
||||
}
|
||||
|
||||
public static <T> T call(String stage, Supplier<T> callback) {
|
||||
Scope scope = CURRENT.get();
|
||||
if (scope == null || scope.finished) return callback.get();
|
||||
long started = scope.elapsedNanos();
|
||||
Throwable failure = null;
|
||||
try {
|
||||
return callback.get();
|
||||
} catch (RuntimeException | Error error) {
|
||||
failure = error;
|
||||
throw error;
|
||||
} finally {
|
||||
scope.record(stage, scope.elapsedNanos() - started, failure);
|
||||
}
|
||||
}
|
||||
|
||||
public static void run(String stage, Runnable callback) {
|
||||
call(stage, () -> {
|
||||
callback.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static final class Scope implements AutoCloseable {
|
||||
private final long startedAtNanos;
|
||||
private final LongSupplier clock;
|
||||
private final Consumer<String> log;
|
||||
private final Scope previous;
|
||||
private final String id;
|
||||
private final Map<String, Long> stages = new LinkedHashMap<>();
|
||||
private final Map<String, String> stageErrors = new LinkedHashMap<>();
|
||||
private final Set<String> errorTypes = new LinkedHashSet<>();
|
||||
private String androidOrderCode;
|
||||
private Long hardId;
|
||||
private long lastElapsedNanos;
|
||||
private boolean controllerEntered;
|
||||
private boolean failed;
|
||||
private boolean finished;
|
||||
private boolean closed;
|
||||
|
||||
private Scope(long startedAtNanos, LongSupplier clock, Consumer<String> log, Scope previous) {
|
||||
this.startedAtNanos = startedAtNanos;
|
||||
this.clock = clock;
|
||||
this.log = log;
|
||||
this.previous = previous;
|
||||
this.id = Long.toUnsignedString(startedAtNanos, 36) + "_" + SEQUENCE.incrementAndGet();
|
||||
}
|
||||
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/** 正常快请求不落日志,慢请求、失败及含被处理异常的请求只输出一条汇总。 */
|
||||
public void finish(int httpStatus) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
try {
|
||||
long total = elapsedNanos();
|
||||
boolean requestFailed = failed || httpStatus >= 400;
|
||||
if (total < SLOW_NANOS && !requestFailed && stageErrors.isEmpty()) return;
|
||||
String stageSummary = stages.entrySet().stream()
|
||||
.map(entry -> entry.getKey() + "=" + toMillis(entry.getValue()))
|
||||
.collect(Collectors.joining(","));
|
||||
String stageErrorSummary = stageErrors.entrySet().stream()
|
||||
.map(entry -> entry.getKey() + "=" + entry.getValue())
|
||||
.collect(Collectors.joining(","));
|
||||
log.accept("|终端支付耗时|trace:" + id
|
||||
+ (androidOrderCode == null ? "" : "|androidOrderCode:" + androidOrderCode)
|
||||
+ (hardId == null ? "" : "|hardId:" + hardId)
|
||||
+ "|httpStatus:" + httpStatus + "|outcome:" + (requestFailed ? "failed" : "success")
|
||||
+ "|totalMs:" + toMillis(total) + "|controllerEntered:" + controllerEntered
|
||||
+ "|stagesInclusiveMs:" + stageSummary
|
||||
+ "|stageErrorTypes:" + (stageErrorSummary.isEmpty() ? "-" : stageErrorSummary)
|
||||
+ "|errorTypes:" + (errorTypes.isEmpty() ? "-" : String.join(",", errorTypes)) + "|");
|
||||
} catch (Throwable ignored) {
|
||||
// 诊断输出失败不能覆盖原请求结果或阻止 ThreadLocal 清理。
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
if (previous == null) CURRENT.remove();
|
||||
else CURRENT.set(previous);
|
||||
}
|
||||
|
||||
private long elapsedNanos() {
|
||||
try {
|
||||
long elapsed = clock.getAsLong() - startedAtNanos;
|
||||
lastElapsedNanos = Math.max(lastElapsedNanos, elapsed);
|
||||
} catch (Throwable ignored) {
|
||||
// 测量来源故障时保留最近值,业务继续执行。
|
||||
}
|
||||
return lastElapsedNanos;
|
||||
}
|
||||
|
||||
private void record(String stage, long durationNanos, Throwable failure) {
|
||||
try {
|
||||
if (finished) return;
|
||||
String name = safeIdentifier(stage, 64, "unknown_stage");
|
||||
stages.merge(name, Math.max(0, durationNanos), Long::sum);
|
||||
if (failure != null) {
|
||||
String type = errorType(failure);
|
||||
stageErrors.put(name, type);
|
||||
errorTypes.add(type);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 计时不可改变回调返回值、异常对象或业务异常传播路径。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long toMillis(long nanos) {
|
||||
return nanos / 1_000_000L;
|
||||
}
|
||||
|
||||
private static String errorType(Throwable error) {
|
||||
return safeIdentifier(error.getClass().getSimpleName(), 128, "Throwable");
|
||||
}
|
||||
|
||||
private static String safeIdentifier(String value, int maxLength, String fallback) {
|
||||
return value != null && !value.isEmpty() && value.length() <= maxLength
|
||||
&& SAFE_IDENTIFIER.matcher(value).matches() ? value : fallback;
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,18 @@
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<!-- 支付诊断仅写慢请求/失败摘要;队列满时丢弃诊断日志,禁止请求线程等待磁盘。 -->
|
||||
<appender name="paymentTimingAsync" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<queueSize>256</queueSize>
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<neverBlock>true</neverBlock>
|
||||
<includeCallerData>false</includeCallerData>
|
||||
<appender-ref ref="debug"/>
|
||||
</appender>
|
||||
<logger name="com.bsd.pjcloud.hard.support.PaymentTimingTrace" level="WARN" additivity="false">
|
||||
<appender-ref ref="paymentTimingAsync"/>
|
||||
</logger>
|
||||
|
||||
<logger name="org.activiti.engine.impl.db" level="DEBUG">
|
||||
<appender-ref ref="debug"/>
|
||||
</logger>
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package com.bsd.pjcloud.hard.config.filter;
|
||||
|
||||
import com.bsd.pjcloud.hard.config.PaymentTimingConfiguration;
|
||||
import com.bsd.pjcloud.hard.support.PaymentTimingTrace;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequestEvent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class CreateOrderPaymentTimingFilterTest {
|
||||
@Test
|
||||
void listenerTimeIncludesEarlierTenantFilterWaitAndRequestBodyIsNeverRead() throws Exception {
|
||||
AtomicLong clock = new AtomicLong(10_000_000L);
|
||||
List<String> logs = new ArrayList<>();
|
||||
MockHttpServletRequest request = spy(new MockHttpServletRequest("POST", "/api" + CreateOrderPaymentTimingFilter.PATH));
|
||||
request.setContextPath("/api");
|
||||
var listener = new PaymentTimingConfiguration.RequestStartListener(clock::get);
|
||||
listener.requestInitialized(new ServletRequestEvent(new MockServletContext(), request));
|
||||
clock.set(1_210_000_000L); // 在计时过滤器之前,租户或其他过滤器已经消耗 1200ms。
|
||||
var filter = new CreateOrderPaymentTimingFilter(start -> PaymentTimingTrace.open(start, clock::get, logs::add));
|
||||
filter.doFilter(request, new MockHttpServletResponse(), (req, response) -> {
|
||||
PaymentTimingTrace.markControllerEntered();
|
||||
PaymentTimingTrace.bind("android_order", 77L);
|
||||
PaymentTimingTrace.run("controller", () -> clock.set(1_360_000_000L));
|
||||
});
|
||||
|
||||
assertEquals(1, logs.size());
|
||||
assertTrue(logs.get(0).contains("totalMs:1350"));
|
||||
assertTrue(logs.get(0).contains("pre_controller=1200"));
|
||||
assertTrue(logs.get(0).contains("controller=150"));
|
||||
assertNull(PaymentTimingTrace.current());
|
||||
verify(request, never()).getInputStream();
|
||||
verify(request, never()).getReader();
|
||||
}
|
||||
|
||||
@Test
|
||||
void exactPostRouteOnlyAndContextPathIsSupported() throws Exception {
|
||||
AtomicInteger opened = new AtomicInteger();
|
||||
var filter = new CreateOrderPaymentTimingFilter(start -> {
|
||||
opened.incrementAndGet();
|
||||
return PaymentTimingTrace.open(start);
|
||||
});
|
||||
for (String path : List.of("/other", CreateOrderPaymentTimingFilter.PATH + "/", "/hard" + CreateOrderPaymentTimingFilter.PATH)) {
|
||||
var request = new MockHttpServletRequest("POST", path);
|
||||
filter.doFilter(request, new MockHttpServletResponse(), (req, response) -> assertNull(PaymentTimingTrace.current()));
|
||||
}
|
||||
var get = new MockHttpServletRequest("GET", CreateOrderPaymentTimingFilter.PATH);
|
||||
filter.doFilter(get, new MockHttpServletResponse(), (req, response) -> assertNull(PaymentTimingTrace.current()));
|
||||
assertEquals(0, opened.get());
|
||||
assertTrue(CreateOrderPaymentTimingFilter.matches(new MockHttpServletRequest("POST", CreateOrderPaymentTimingFilter.PATH)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void originalServletExceptionPropagatesAndLoggingFailureCannotLeakThreadLocal() {
|
||||
var request = new MockHttpServletRequest("POST", CreateOrderPaymentTimingFilter.PATH);
|
||||
request.setAttribute(CreateOrderPaymentTimingFilter.STARTED_AT_ATTRIBUTE, 0L);
|
||||
var filter = new CreateOrderPaymentTimingFilter(start -> PaymentTimingTrace.open(start, () -> 0,
|
||||
ignored -> { throw new IllegalStateException("logger failure"); }));
|
||||
ServletException failure = new ServletException("private-request-data");
|
||||
ServletException caught = assertThrows(ServletException.class,
|
||||
() -> filter.doFilter(request, new MockHttpServletResponse(), (req, response) -> { throw failure; }));
|
||||
assertSame(failure, caught);
|
||||
assertNull(PaymentTimingTrace.current());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolverHandledBusinessFailureStillProducesOneFailedSummary() throws Exception {
|
||||
List<String> logs = new ArrayList<>();
|
||||
var request = new MockHttpServletRequest("POST", CreateOrderPaymentTimingFilter.PATH);
|
||||
request.setAttribute(CreateOrderPaymentTimingFilter.STARTED_AT_ATTRIBUTE, 0L);
|
||||
var filter = new CreateOrderPaymentTimingFilter(start -> PaymentTimingTrace.open(start, () -> 0, logs::add));
|
||||
filter.doFilter(request, new MockHttpServletResponse(), (req, response) -> {
|
||||
RuntimeException failure = new RuntimeException("business-secret");
|
||||
PaymentTimingTrace.markFailure(failure);
|
||||
// 模拟 MVC 异常处理器已接住错误,最终仍以 HTTP 200 返回业务错误。
|
||||
});
|
||||
assertEquals(1, logs.size());
|
||||
assertTrue(logs.get(0).contains("httpStatus:200|outcome:failed"));
|
||||
assertFalse(logs.get(0).contains("business-secret"));
|
||||
assertNull(PaymentTimingTrace.current());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listenerIsSelectiveAndConfigurationPreservesExistingFilterOrder() {
|
||||
var listener = new PaymentTimingConfiguration.RequestStartListener(() -> 42L);
|
||||
var skipped = new MockHttpServletRequest("GET", CreateOrderPaymentTimingFilter.PATH);
|
||||
listener.requestInitialized(new ServletRequestEvent(new MockServletContext(), skipped));
|
||||
assertNull(skipped.getAttribute(CreateOrderPaymentTimingFilter.STARTED_AT_ATTRIBUTE));
|
||||
var registration = new PaymentTimingConfiguration().paymentTimingFilter();
|
||||
assertEquals(Ordered.HIGHEST_PRECEDENCE + 1, registration.getOrder());
|
||||
assertEquals(List.of(CreateOrderPaymentTimingFilter.PATH), List.copyOf(registration.getUrlPatterns()));
|
||||
}
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
package com.bsd.pjcloud.hard.service.impl.order;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.bsd.pjcloud.admin.api.dto.UserInfo;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
|
||||
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
|
||||
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.HardCreateOrderDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.order.HardCreateCarDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity;
|
||||
import com.bsd.pjcloud.hard.api.entity.order.HardCarEntity;
|
||||
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
|
||||
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
|
||||
import com.bsd.pjcloud.hard.service.HardCarService;
|
||||
import com.bsd.pjcloud.hard.service.HardManagerService;
|
||||
import com.bsd.pjcloud.hard.service.order.HardOrderCartSnapshot;
|
||||
import com.bsd.pjcloud.hard.support.PaymentTimingTrace;
|
||||
import com.bsd.pjcloud.wallet.api.entity.WalletManageEntity;
|
||||
import com.bsd.pjcloud.wallet.api.entity.WalletTransactionEntity;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletService;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletTransactionService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** Active timing must observe existing payment calls without repeating or reordering them. */
|
||||
class HardOrderPaymentTimingRegressionTest {
|
||||
private HardOrderServiceImpl service;
|
||||
private HardOrderMapper orders;
|
||||
private HardCarService carts;
|
||||
private HardManagerService managers;
|
||||
private RemoteMchService merchants;
|
||||
private RemoteUserService users;
|
||||
private RemoteWalletService wallets;
|
||||
private RemoteWalletTransactionService payments;
|
||||
private RLock lock;
|
||||
private GenericApplicationContext context;
|
||||
private ApplicationContext previousContext;
|
||||
private final List<String> logs = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
service = mock(HardOrderServiceImpl.class, CALLS_REAL_METHODS);
|
||||
orders = mock(HardOrderMapper.class);
|
||||
carts = mock(HardCarService.class);
|
||||
managers = mock(HardManagerService.class);
|
||||
merchants = mock(RemoteMchService.class);
|
||||
users = mock(RemoteUserService.class);
|
||||
wallets = mock(RemoteWalletService.class);
|
||||
payments = mock(RemoteWalletTransactionService.class);
|
||||
RedissonClient redis = mock(RedissonClient.class);
|
||||
KeyStrResolver keys = mock(KeyStrResolver.class);
|
||||
lock = mock(RLock.class);
|
||||
when(keys.extract(anyString(), eq(":"))).thenAnswer(call -> call.getArgument(0));
|
||||
when(redis.getLock(anyString())).thenReturn(lock);
|
||||
when(lock.tryLock()).thenReturn(true);
|
||||
when(lock.isHeldByCurrentThread()).thenReturn(true);
|
||||
ReflectionTestUtils.setField(service, "baseMapper", orders);
|
||||
ReflectionTestUtils.setField(service, "hardCarService", carts);
|
||||
ReflectionTestUtils.setField(service, "hardManagerService", managers);
|
||||
ReflectionTestUtils.setField(service, "remoteMchService", merchants);
|
||||
ReflectionTestUtils.setField(service, "userService", users);
|
||||
ReflectionTestUtils.setField(service, "remoteWalletService", wallets);
|
||||
ReflectionTestUtils.setField(service, "walletTransactionService", payments);
|
||||
ReflectionTestUtils.setField(service, "redissonClient", redis);
|
||||
ReflectionTestUtils.setField(service, "keyStrResolver", keys);
|
||||
// Only resolve the existing getSelf() lookup; no Boot, transaction or remote connection starts.
|
||||
previousContext = SpringUtil.getApplicationContext();
|
||||
context = new GenericApplicationContext();
|
||||
context.registerBean("paymentTimingTestOrderService", HardOrderServiceImpl.class, () -> service);
|
||||
context.refresh();
|
||||
new SpringUtil().setApplicationContext(context);
|
||||
doNothing().when(service).fastenerInventory(any(), any(YesNoEnum.class));
|
||||
doNothing().when(service).orderSuccess(any(), anyBoolean());
|
||||
doNothing().when(service).cancelCode(any(), any());
|
||||
doNothing().when(service).createOrderAfterSendMqMsg(anyLong(), any());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
new SpringUtil().setApplicationContext(previousContext);
|
||||
context.close();
|
||||
assertNull(PaymentTimingTrace.current());
|
||||
}
|
||||
|
||||
@Test
|
||||
void busyLockKeepsRejectionWithoutLookupPaymentOrUnlock() {
|
||||
when(lock.tryLock()).thenReturn(false);
|
||||
|
||||
RuntimeException failure = assertThrows(RuntimeException.class,
|
||||
() -> observed(() -> service.createOrder(request())));
|
||||
|
||||
assertEquals("正在创建订单中,请勿重复操作", failure.getMessage());
|
||||
verify(lock).tryLock();
|
||||
verifyNoMoreInteractions(lock);
|
||||
verifyZeroInteractions(orders, carts, managers, merchants, users, wallets, payments);
|
||||
verify(service, never()).createOrderInTransaction(any(), any());
|
||||
assertTrue(logs.get(0).contains("outcome:failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void paidOrderReuseKeepsOneLocalLookupAndNoRemoteOrPaymentCalls() {
|
||||
HardOrderEntity paid = HardOrderEntity.builder().id(100L).parentCarId(10L)
|
||||
.hardId(3L).merchantId(2L).sysType(HardTypeEnum.CANTEEN.getType())
|
||||
.hourType("1").orderType(HardOrderTypeEnum.BELOW.getType())
|
||||
.payType(HardPayTypeEnum.FACE.getType()).userId(4L)
|
||||
.extraPrice(0).orderStatus("5").build();
|
||||
when(orders.selectOne(any(Wrapper.class))).thenReturn(paid);
|
||||
when(carts.getByParentCarId(10L)).thenReturn(snapshot().items());
|
||||
|
||||
assertEquals(100L, observed(() -> service.createOrder(request())));
|
||||
|
||||
InOrder calls = inOrder(lock, orders, carts);
|
||||
calls.verify(lock).tryLock();
|
||||
calls.verify(orders).selectOne(any(Wrapper.class));
|
||||
calls.verify(carts).getByParentCarId(10L);
|
||||
calls.verify(lock).isHeldByCurrentThread();
|
||||
calls.verify(lock).unlock();
|
||||
calls.verifyNoMoreInteractions();
|
||||
verifyZeroInteractions(managers, merchants, users, wallets, payments);
|
||||
verify(service, never()).createOrderInTransaction(any(), any());
|
||||
verify(service, never()).createOrderAfterSendMqMsg(anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void lookupFailurePropagatesSameExceptionAndStillReleasesHeldLock() {
|
||||
RuntimeException databaseFailure = new RuntimeException("database unavailable");
|
||||
when(orders.selectOne(any(Wrapper.class))).thenThrow(databaseFailure);
|
||||
|
||||
assertSame(databaseFailure, assertThrows(RuntimeException.class,
|
||||
() -> observed(() -> service.createOrder(request()))));
|
||||
|
||||
InOrder calls = inOrder(lock, orders);
|
||||
calls.verify(lock).tryLock();
|
||||
calls.verify(orders).selectOne(any(Wrapper.class));
|
||||
calls.verify(lock).isHeldByCurrentThread();
|
||||
calls.verify(lock).unlock();
|
||||
calls.verifyNoMoreInteractions();
|
||||
verifyZeroInteractions(carts, managers, merchants, users, wallets, payments);
|
||||
verify(service, never()).createOrderInTransaction(any(), any());
|
||||
verify(service, never()).createOrderAfterSendMqMsg(anyLong(), any());
|
||||
assertFalse(logs.get(0).contains("database unavailable"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void successKeepsPreparationWriteDebitUnlockAndNotificationOrder() {
|
||||
stubPreparedOrder();
|
||||
when(orders.insert(any(HardOrderEntity.class))).thenAnswer(call -> {
|
||||
assertTrue(logs.isEmpty(), "timing must not write logs inside order persistence");
|
||||
call.<HardOrderEntity>getArgument(0).setId(100L);
|
||||
return 1;
|
||||
});
|
||||
when(payments.reduceWalletBalance(any())).thenAnswer(call -> {
|
||||
assertTrue(logs.isEmpty(), "timing must not write logs inside wallet debit");
|
||||
return R.ok(new WalletTransactionEntity());
|
||||
});
|
||||
|
||||
assertEquals(100L, observed(() -> service.createOrder(request())));
|
||||
|
||||
InOrder calls = inOrder(lock, orders, merchants, managers, users, wallets, carts, payments, service);
|
||||
calls.verify(lock).tryLock();
|
||||
calls.verify(orders).selectOne(any(Wrapper.class));
|
||||
calls.verify(merchants).getById(2L);
|
||||
calls.verify(managers).getById(3L);
|
||||
calls.verify(users).getInnerByUserId(4L);
|
||||
calls.verify(wallets).getWallet(eq(4L), any());
|
||||
calls.verify(carts).prepareCardWithCompute(any(), any(), anyList());
|
||||
calls.verify(service).createOrderInTransaction(any(), any());
|
||||
calls.verify(carts).createCardFromSnapshot(any(), any(), any(), any());
|
||||
calls.verify(orders).insert(any(HardOrderEntity.class));
|
||||
calls.verify(service).fastenerInventory(any(), eq(YesNoEnum.YES));
|
||||
calls.verify(payments).reduceWalletBalance(argThat(dto ->
|
||||
Boolean.FALSE.equals(dto.getForce()) && dto.getPrice() == 350));
|
||||
calls.verify(service).orderSuccess(any(), eq(true));
|
||||
calls.verify(service).cancelCode(any(), any());
|
||||
calls.verify(lock).isHeldByCurrentThread();
|
||||
calls.verify(lock).unlock();
|
||||
calls.verify(service).createOrderAfterSendMqMsg(eq(100L), any());
|
||||
// Ignore service entry/private internal calls, but prohibit extra database or remote calls.
|
||||
verifyNoMoreInteractions(orders, merchants, managers, users, wallets, carts, payments, lock);
|
||||
verify(service, times(1)).createOrderInTransaction(any(), any());
|
||||
verify(service, times(1)).orderSuccess(any(), anyBoolean());
|
||||
verify(service, times(1)).createOrderAfterSendMqMsg(anyLong(), any());
|
||||
assertTrue(logs.get(0).contains("outcome:success"));
|
||||
}
|
||||
|
||||
private void stubPreparedOrder() {
|
||||
MchManageVo merchant = new MchManageVo();
|
||||
merchant.setId(2L);
|
||||
when(merchants.getById(2L)).thenReturn(R.ok(merchant));
|
||||
when(managers.getById(3L)).thenReturn(new HardManagerEntity());
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(4L);
|
||||
UserInfo info = new UserInfo();
|
||||
info.setSysUser(user);
|
||||
when(users.getInnerByUserId(4L)).thenReturn(R.ok(info));
|
||||
WalletManageEntity wallet = new WalletManageEntity();
|
||||
wallet.setId(5L);
|
||||
wallet.setUserId(4L);
|
||||
wallet.setBalance(500);
|
||||
when(wallets.getWallet(eq(4L), any())).thenReturn(R.ok(wallet));
|
||||
when(carts.prepareCardWithCompute(any(), any(), anyList())).thenReturn(snapshot());
|
||||
when(carts.createCardFromSnapshot(any(), any(), any(), any())).thenReturn(10L);
|
||||
}
|
||||
|
||||
private <T> T observed(Supplier<T> action) {
|
||||
AtomicLong clock = new AtomicLong();
|
||||
try (PaymentTimingTrace.Scope scope = PaymentTimingTrace.open(0, clock::get, logs::add)) {
|
||||
PaymentTimingTrace.bind("legacy-code", 3L);
|
||||
PaymentTimingTrace.markControllerEntered();
|
||||
int status = 200;
|
||||
try {
|
||||
T result = action.get();
|
||||
assertTrue(logs.isEmpty(), "stages must buffer until the request is complete");
|
||||
return result;
|
||||
} catch (RuntimeException | Error failure) {
|
||||
status = 500;
|
||||
PaymentTimingTrace.markFailure(failure);
|
||||
throw failure;
|
||||
} finally {
|
||||
clock.set(2_000_000_000L);
|
||||
scope.finish(status);
|
||||
assertEquals(1, logs.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HardCreateOrderDto request() {
|
||||
HardCreateCarDto item = new HardCreateCarDto();
|
||||
item.setProductId(6L);
|
||||
item.setProductCount(1);
|
||||
item.setProductWeight(BigDecimal.ZERO);
|
||||
return HardCreateOrderDto.builder().androidOrderCode("legacy-code").hardwareId(3L).merchantId(2L)
|
||||
.sysType(HardTypeEnum.CANTEEN).hourType(1).userId(4L).orderType(HardOrderTypeEnum.BELOW)
|
||||
.orderPayType(HardPayTypeEnum.FACE).list(List.of(item)).build();
|
||||
}
|
||||
|
||||
private HardOrderCartSnapshot snapshot() {
|
||||
HardCarEntity item = HardCarEntity.builder().productId(6L).productCount(1).productWeight(BigDecimal.ZERO)
|
||||
.productPrice(350).totalPrice(350L).isDiscountsProduct(YesNoEnum.NO.getCode()).build();
|
||||
return new HardOrderCartSnapshot(350, "饭菜x1;", List.of(item));
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package com.bsd.pjcloud.hard.support;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class PaymentTimingTraceTest {
|
||||
@Test
|
||||
void fastSuccessfulRequestDoesNotLogAndAlwaysClearsThreadLocal() {
|
||||
AtomicLong clock = new AtomicLong();
|
||||
List<String> logs = new ArrayList<>();
|
||||
try (PaymentTimingTrace.Scope scope = PaymentTimingTrace.open(0, clock::get, logs::add)) {
|
||||
assertSame(scope, PaymentTimingTrace.current());
|
||||
assertEquals(7, PaymentTimingTrace.call("local_sql", () -> {
|
||||
clock.set(20_000_000L);
|
||||
return 7;
|
||||
}));
|
||||
scope.finish(200);
|
||||
scope.finish(500);
|
||||
}
|
||||
assertTrue(logs.isEmpty());
|
||||
assertNull(PaymentTimingTrace.current());
|
||||
assertEquals(9, PaymentTimingTrace.call("inactive", () -> 9));
|
||||
}
|
||||
|
||||
@Test
|
||||
void summaryIncludesPreControllerAndInclusiveNestedStagesOnce() {
|
||||
AtomicLong clock = new AtomicLong();
|
||||
List<String> logs = new ArrayList<>();
|
||||
try (PaymentTimingTrace.Scope scope = PaymentTimingTrace.open(0, clock::get, logs::add)) {
|
||||
PaymentTimingTrace.bind("android_123_456", 123L);
|
||||
clock.set(1_200_000_000L);
|
||||
PaymentTimingTrace.markControllerEntered();
|
||||
PaymentTimingTrace.markControllerEntered();
|
||||
PaymentTimingTrace.run("controller", () -> {
|
||||
clock.set(1_300_000_000L);
|
||||
PaymentTimingTrace.run("wallet_remote", () -> clock.set(1_500_000_000L));
|
||||
clock.set(1_600_000_000L);
|
||||
});
|
||||
scope.finish(200);
|
||||
scope.finish(200);
|
||||
}
|
||||
assertEquals(1, logs.size());
|
||||
String summary = logs.get(0);
|
||||
assertTrue(summary.contains("androidOrderCode:android_123_456|hardId:123"));
|
||||
assertTrue(summary.contains("outcome:success|totalMs:1600|controllerEntered:true"));
|
||||
assertTrue(summary.contains("pre_controller=1200"));
|
||||
assertTrue(summary.contains("controller=400"));
|
||||
assertTrue(summary.contains("wallet_remote=200"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handledStageErrorDoesNotReclassifySuccessfulPaymentOrExposePayload() {
|
||||
AtomicLong clock = new AtomicLong();
|
||||
List<String> logs = new ArrayList<>();
|
||||
IllegalStateException failure = new IllegalStateException("secret-token-and-user-body");
|
||||
try (PaymentTimingTrace.Scope scope = PaymentTimingTrace.open(0, clock::get, logs::add)) {
|
||||
IllegalStateException caught = assertThrows(IllegalStateException.class,
|
||||
() -> PaymentTimingTrace.run("notify", () -> { throw failure; }));
|
||||
assertSame(failure, caught);
|
||||
scope.finish(200);
|
||||
}
|
||||
assertEquals(1, logs.size());
|
||||
assertTrue(logs.get(0).contains("outcome:success"));
|
||||
assertTrue(logs.get(0).contains("stageErrorTypes:notify=IllegalStateException"));
|
||||
assertFalse(logs.get(0).contains("secret"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitBusinessFailureIsLoggedEvenWhenExceptionResolverReturnsHttp200() {
|
||||
List<String> logs = new ArrayList<>();
|
||||
try (PaymentTimingTrace.Scope scope = PaymentTimingTrace.open(0, () -> 0, logs::add)) {
|
||||
PaymentTimingTrace.bind("unsafe|payload=secret", 123L);
|
||||
PaymentTimingTrace.markFailure(new IllegalArgumentException("secret"));
|
||||
scope.finish(200);
|
||||
}
|
||||
assertEquals(1, logs.size());
|
||||
assertTrue(logs.get(0).contains("httpStatus:200|outcome:failed"));
|
||||
assertTrue(logs.get(0).contains("errorTypes:IllegalArgumentException"));
|
||||
assertFalse(logs.get(0).contains("androidOrderCode"));
|
||||
assertFalse(logs.get(0).contains("secret"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sinkAndClockFailuresDoNotReplaceResultAndNestedScopeRestoresPreviousTrace() {
|
||||
try (PaymentTimingTrace.Scope outer = PaymentTimingTrace.open(0, () -> 0, ignored -> {})) {
|
||||
try (PaymentTimingTrace.Scope inner = PaymentTimingTrace.open(0,
|
||||
() -> { throw new IllegalStateException("clock"); },
|
||||
ignored -> { throw new IllegalStateException("sink"); })) {
|
||||
assertEquals(8, PaymentTimingTrace.call("paid", () -> 8));
|
||||
assertDoesNotThrow(() -> inner.finish(500));
|
||||
}
|
||||
assertSame(outer, PaymentTimingTrace.current());
|
||||
}
|
||||
assertNull(PaymentTimingTrace.current());
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentRequestsKeepSeparateThreadLocals() throws Exception {
|
||||
var executor = Executors.newFixedThreadPool(2);
|
||||
CountDownLatch opened = new CountDownLatch(2);
|
||||
try {
|
||||
var first = executor.submit(() -> isolatedRequest("first_order", opened));
|
||||
var second = executor.submit(() -> isolatedRequest("second_order", opened));
|
||||
assertTrue(first.get(5, TimeUnit.SECONDS).contains("androidOrderCode:first_order"));
|
||||
assertTrue(second.get(5, TimeUnit.SECONDS).contains("androidOrderCode:second_order"));
|
||||
assertNull(PaymentTimingTrace.current());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private String isolatedRequest(String order, CountDownLatch opened) throws InterruptedException {
|
||||
List<String> logs = new ArrayList<>();
|
||||
try (PaymentTimingTrace.Scope scope = PaymentTimingTrace.open(0, () -> 0, logs::add)) {
|
||||
PaymentTimingTrace.bind(order, 123L);
|
||||
opened.countDown();
|
||||
assertTrue(opened.await(5, TimeUnit.SECONDS));
|
||||
assertSame(scope, PaymentTimingTrace.current());
|
||||
scope.finish(500);
|
||||
}
|
||||
assertNull(PaymentTimingTrace.current());
|
||||
return logs.get(0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user