chore(lastleg): instrument appointment latency from restored 662dead baseline
This commit is contained in:
+16
-10
@@ -4,6 +4,7 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkPolicy.*;
|
||||
@@ -60,14 +61,14 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
|
||||
var requests=remote.subList(start,Math.min(start+200,remote.size())); timing.stage="FIRSTLEG"; requireToken();
|
||||
try { var response=timing.measure(timing.firstleg,requests.size(),()->firstleg.facts(token,requests));
|
||||
if(response==null || !response.successful()) throw unavailable("提柜及还空事实读取失败");
|
||||
append(result,requests,response.data());
|
||||
run(Stage.FACTS_VALIDATE,()->append(result,requests,response.data()));
|
||||
} catch(RuntimeException error) { throw unavailable("Firstleg工作事实不可用",error); }
|
||||
}
|
||||
groups.forEach((provider,requests)->{
|
||||
for(int start=0; start<requests.size(); start+=200) {
|
||||
var batch=requests.subList(start,Math.min(start+200,requests.size()));
|
||||
var statistics=timing.provider(provider);
|
||||
try { append(result,batch,timing.measure(statistics,batch.size(),()->provider.read(batch))); }
|
||||
try { var response=timing.measure(statistics,batch.size(),()->provider.read(batch));run(Stage.FACTS_VALIDATE,()->append(result,batch,response)); }
|
||||
catch(RuntimeException error) { throw unavailable("预约/收货/运输事实不可用",error); }
|
||||
}
|
||||
});
|
||||
@@ -76,6 +77,7 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
|
||||
}
|
||||
/** 一次 facts 调用的计时,不保存原来源或返回值,不跨调用缓存。 */
|
||||
private static final class FactTiming {
|
||||
private final Span prepare=span(Stage.FACTS_PREPARE);
|
||||
private final long started=System.nanoTime();
|
||||
private final CallStatistics firstleg=new CallStatistics();
|
||||
private final Map<String,CallStatistics> providers=new LinkedHashMap<>();
|
||||
@@ -92,20 +94,24 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
|
||||
}
|
||||
/** 只统计真正尝试的提供端调用;校验及组装耗时保留在 totalMs 中。 */
|
||||
private <T> T measure(CallStatistics statistics,int requests,java.util.function.Supplier<T> operation) {
|
||||
statistics.calls++;statistics.requests+=requests;long begin=System.nanoTime();
|
||||
try { return operation.get(); }
|
||||
finally { statistics.elapsed+=System.nanoTime()-begin; }
|
||||
prepare.close();statistics.calls++;statistics.requests+=requests;long begin=System.nanoTime();
|
||||
try(var span=span(switch(stage){case "FIRSTLEG"->Stage.FACTS_FIRSTLEG;case "AppointmentWorkReceivingProvider"->Stage.FACTS_RECEIVING;
|
||||
case "AppointmentWorkDeliveryProvider"->Stage.FACTS_DELIVERY;case "AppointmentWorkTransportProvider"->Stage.FACTS_TRANSPORT;
|
||||
case "AppointmentWorkDirectTransportProvider"->Stage.FACTS_DIRECT_TRANSPORT;default->Stage.FACTS_OTHER;})){
|
||||
try{return operation.get();}catch(RuntimeException|Error error){span.failed();throw error;}
|
||||
}finally{statistics.elapsed+=System.nanoTime()-begin;}
|
||||
}
|
||||
/** 成功慢调用或任意失败只输出一次汇总,日志故障不得遮蔽原异常。 */
|
||||
private void finish(boolean success) {
|
||||
if(!success)prepare.failed();prepare.close();
|
||||
long totalMs=java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-started);
|
||||
if(success && totalMs<250) return;
|
||||
try {
|
||||
var summaries=new LinkedHashMap<String,String>();
|
||||
providers.forEach((name,value)->summaries.put(name,value.summary()));
|
||||
LOG.warn("Appointment work facts diagnostic: totalMs={}, firstlegCalls={}, firstlegRequests={}, firstlegMs={}, providers={}, failedStage={}",
|
||||
totalMs,firstleg.calls,firstleg.requests,firstleg.millis(),summaries,success?"NONE":stage);
|
||||
} catch(RuntimeException diagnosticFailure) { /* 仅忽略诊断故障,原事实错误仍沿原路径抛出。 */ }
|
||||
LOG.warn("Appointment work facts diagnostic: totalMs={}, firstlegCalls={}, firstlegRequests={}, firstlegMs={}, providers={}, failedStage={}, requestId={}, traceId={}, scopeId={}",
|
||||
totalMs,firstleg.calls,firstleg.requests,firstleg.millis(),summaries,success?"NONE":stage,org.slf4j.MDC.get("requestId"),org.slf4j.MDC.get("traceId"),currentScopeId());
|
||||
} catch(Throwable diagnosticFailure) { /* 仅忽略诊断故障,原事实错误仍沿原路径抛出。 */ }
|
||||
}
|
||||
}
|
||||
/** 仅包含数量和单调时钟耗时的累加器。 */
|
||||
@@ -117,7 +123,7 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
|
||||
private String summary() { return "calls="+calls+",requests="+requests+",ms="+millis(); }
|
||||
}
|
||||
/** 每次生成建立独立读取器;没有实际需求读取时不引入原业务依赖。 */
|
||||
public RequirementReader openRequirements(List<RequirementInput> inputs) {
|
||||
public RequirementReader openRequirements() {
|
||||
return new RequirementReader() {
|
||||
private RequirementReader reader;
|
||||
/** 首次实际读取才取得原域作用域,保留每份需求的完整性校验。 */
|
||||
@@ -125,7 +131,7 @@ public class AppointmentWorkFactAdapter implements AppointmentWorkFactGateway {
|
||||
if(reader==null) {
|
||||
var provider=requirements.getIfAvailable();
|
||||
if(provider==null) throw unavailable("原预约需求与批次清单适配尚未接入,不能生成不完整工作清单");
|
||||
reader=provider.openRequirements(inputs);
|
||||
reader=provider.openRequirements();
|
||||
}
|
||||
var result=reader.read(source,effectiveFlow);
|
||||
if(result==null || result.isEmpty()) throw unavailable("原预约需求清单缺失,不能以空清单完成路线生成");
|
||||
|
||||
+2
-3
@@ -28,7 +28,7 @@ public interface AppointmentWorkFactGateway {
|
||||
SourcePage discover(String after, int size);
|
||||
ContainerSource container(String containerBookingId);
|
||||
/** 建立本次原预约需求读取作用域,保持完整批次及明确替代关系。 */
|
||||
RequirementReader openRequirements(List<RequirementInput> inputs);
|
||||
RequirementReader openRequirements();
|
||||
Map<String, Fact> facts(List<AppointmentWorkState> states);
|
||||
Owner owner(String id, Actor actor);
|
||||
Responsibility supplier(String id);
|
||||
@@ -43,6 +43,5 @@ public interface AppointmentWorkFactGateway {
|
||||
interface SupplierDirectory { Responsibility activeSupplier(String id); }
|
||||
/** 单次生成显式复用目录,不跨请求保存来源事实。 */
|
||||
interface RequirementReader { List<Requirement> read(ContainerSource source,Flow effectiveFlow); }
|
||||
record RequirementInput(ContainerSource source,Flow flow) { }
|
||||
interface RequirementProvider { RequirementReader openRequirements(List<RequirementInput> inputs); }
|
||||
interface RequirementProvider { RequirementReader openRequirements(); }
|
||||
}
|
||||
|
||||
+32
-6
@@ -9,6 +9,7 @@ import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkPolicy.*;
|
||||
@@ -26,10 +27,16 @@ public class AppointmentWorkQueryService {
|
||||
Snapshot snapshot(Actor actor) {
|
||||
return snapshot(actor,s->true,true);
|
||||
}
|
||||
/** 按原顺序构造完整快照,计时只分离读取、筛选、事实及投影。 */
|
||||
private Snapshot snapshot(Actor actor,Predicate<AppointmentWorkState> selected,boolean includeNext) {
|
||||
observeTransaction();var timing=span(Stage.QUERY_GUARD);
|
||||
try {
|
||||
String scopeVersion=store.requireInitialized();
|
||||
timing.close();timing=span(Stage.QUERY_DISCOVER);
|
||||
if(!Objects.equals(scopeVersion,facts.discover(null,1).scopeVersion())) throw conflict("已开船工作范围变化,请先完成服务端同步");
|
||||
timing.close();timing=span(Stage.QUERY_STORE);
|
||||
var all=store.all();
|
||||
timing.close();timing=span(Stage.QUERY_SELECT);
|
||||
var stateByKey=all.stream().collect(Collectors.toMap(AppointmentWorkState::getWorkItemKey,Function.identity()));
|
||||
var selectedKeys=all.stream().filter(s->!s.isRetired() && selected.test(s)).map(AppointmentWorkState::getWorkItemKey).collect(Collectors.toSet());
|
||||
var needed=new HashSet<>(selectedKeys);
|
||||
@@ -41,12 +48,16 @@ public class AppointmentWorkQueryService {
|
||||
}
|
||||
}
|
||||
var states=all.stream().filter(s->!s.isRetired() && needed.contains(s.getWorkItemKey())).toList();
|
||||
timing.close();timing=span(Stage.QUERY_FACTS);
|
||||
var current=facts.facts(states);
|
||||
timing.close();timing=span(Stage.QUERY_PROJECT);
|
||||
var visible=states.stream().filter(s->canRead(s,current.get(s.getWorkItemKey()),actor)).toList();
|
||||
var items=visible.stream().map(s->project(s,current.get(s.getWorkItemKey()),actor,clock)).toList();
|
||||
var byKey=items.stream().collect(Collectors.toMap(WorkItem::workItemKey,Function.identity()));
|
||||
timing.close();timing=span(Stage.QUERY_NEXT);
|
||||
return new Snapshot(all,current,items.stream().filter(i->selectedKeys.contains(i.workItemKey()))
|
||||
.map(i->includeNext?withNext(i,next(i,byKey,stateByKey,new HashSet<>())):i).toList());
|
||||
} catch(RuntimeException|Error error){timing.failed();throw error;} finally{timing.close();}
|
||||
}
|
||||
private Action next(WorkItem item,Map<String,WorkItem> items,Map<String,AppointmentWorkState> states,Set<String> visited) {
|
||||
if(!visited.add(item.workItemKey()) || item.reviewRequired()) return item.nextAction();
|
||||
@@ -73,23 +84,34 @@ public class AppointmentWorkQueryService {
|
||||
}
|
||||
return item.nextAction();
|
||||
}
|
||||
/** 完整快照之后单独统计汇总耗时,不改变待办筛选语义。 */
|
||||
public Summary summary(Actor actor) {
|
||||
var items=snapshot(actor,s->true,false).items().stream().filter(i->i.executionStatus()!=ExecutionStatus.COMPLETED).toList();
|
||||
var snapshotItems=snapshot(actor,s->true,false).items();
|
||||
return measure(Stage.QUERY_FINAL,()->{
|
||||
var items=snapshotItems.stream().filter(i->i.executionStatus()!=ExecutionStatus.COMPLETED).toList();
|
||||
return new Summary(items.size(),items.stream().filter(i->followToday(i,clock)).count(),items.stream().filter(i->!i.overdueReasons().isEmpty()).count(),items.stream().filter(i->i.calendarDate()==null).count(),items.stream().filter(WorkItem::reviewRequired).count());
|
||||
});
|
||||
}
|
||||
/** 日历最终分组保持原顺序,单独记录快照后处理。 */
|
||||
public CalendarResult calendar(LocalDate from,LocalDate to,Flow flow,Set<Type> types,Actor actor) {
|
||||
if(from==null || to==null || to.isBefore(from) || ChronoUnit.DAYS.between(from,to)>=62) throw bad("日历范围须为1至62天");
|
||||
if(types!=null && types.contains(Type.ROUTE)) throw bad("路线待确认需求请从全局待办查看");
|
||||
var items=snapshot(actor,s->s.getType()!=Type.ROUTE && matches(s,flow,types,null),false).items().stream().filter(i->i.type()!=Type.ROUTE && i.calendarDate()!=null && !i.calendarDate().isBefore(from) && !i.calendarDate().isAfter(to))
|
||||
var snapshotItems=snapshot(actor,s->s.getType()!=Type.ROUTE && matches(s,flow,types,null),false).items();
|
||||
return measure(Stage.QUERY_FINAL,()->{
|
||||
var items=snapshotItems.stream().filter(i->i.type()!=Type.ROUTE && i.calendarDate()!=null && !i.calendarDate().isBefore(from) && !i.calendarDate().isAfter(to))
|
||||
.filter(i->flow==null || i.flow()==flow).filter(i->types==null || types.contains(i.type())).toList();
|
||||
var groups=items.stream().collect(Collectors.groupingBy(WorkItem::calendarDate,TreeMap::new,Collectors.toList()));
|
||||
var days=groups.entrySet().stream().map(e->new DayAggregate(e.getKey(),e.getValue().size(),e.getValue().stream().collect(Collectors.groupingBy(WorkItem::type,()->new EnumMap<>(Type.class),Collectors.counting())),counts(e.getValue()))).toList();
|
||||
return new CalendarResult(from,to,items.size(),counts(items),days);
|
||||
});
|
||||
}
|
||||
/** 保留事实核对之后的最终过滤排序分页,仅拆出其耗时。 */
|
||||
public PageResult<WorkItem> items(View view,int page,int size,LocalDate date,Flow flow,Set<Type> types,String containerId,Bucket bucket,String keyword,Actor actor) {
|
||||
if(page<1 || size<1 || size>100 || keyword!=null && keyword.length()>128) throw bad("分页或搜索条件无效");
|
||||
var word=keyword==null?"":keyword.trim().toLowerCase(Locale.ROOT);
|
||||
var items=snapshot(actor,s->matches(s,flow,types,containerId) && candidate(s,view),true).items().stream()
|
||||
var snapshotItems=snapshot(actor,s->matches(s,flow,types,containerId) && candidate(s,view),true).items();
|
||||
return measure(Stage.QUERY_FINAL,()->{
|
||||
var items=snapshotItems.stream()
|
||||
.filter(i->date!=null || i.executionStatus()!=ExecutionStatus.COMPLETED)
|
||||
.filter(i->date==null || date.equals(i.calendarDate()))
|
||||
.filter(i->flow==null || flow==i.flow()).filter(i->types==null || types.contains(i.type()))
|
||||
@@ -100,6 +122,7 @@ public class AppointmentWorkQueryService {
|
||||
.sorted(Comparator.comparing(WorkItem::followupDate,Comparator.nullsLast(Comparator.naturalOrder())).thenComparing(WorkItem::workItemKey)).toList();
|
||||
long offset=(long)(page-1)*size; int start=(int)Math.min(offset,items.size());
|
||||
return new PageResult<>(items.subList(start,Math.min(start+size,items.size())),page,size,items.size(),(items.size()+size-1L)/size);
|
||||
});
|
||||
}
|
||||
public BatchWorkResolution resolveBatch(String batchKey,String customerId,Actor actor) {
|
||||
if(batchKey==null || !batchKey.matches("(?:FBA_APPOINTMENT|DISPATCH):[1-9][0-9]*")) throw bad("请提供原业务返回的完整批次键");
|
||||
@@ -117,10 +140,12 @@ public class AppointmentWorkQueryService {
|
||||
var selected=candidates.get(0);
|
||||
return new BatchWorkResolution(batchKey,selected.customerId(),detail(selected.workItemKey(),actor),candidates);
|
||||
}
|
||||
/** 保留详情完整快照与退休事实读取,历史查询单独观察。 */
|
||||
public WorkDetail detail(String key,Actor actor) {
|
||||
var snapshot=snapshot(actor); var state=store.get(key);
|
||||
var snapshot=snapshot(actor); var state=measure(Stage.DETAIL_STORE,()->store.get(key));
|
||||
var fact=snapshot.facts().get(key);
|
||||
if(state.isRetired() && state.getType()!=Type.ROUTE) fact=facts.facts(List.of(state)).get(key);
|
||||
if(state.isRetired() && state.getType()!=Type.ROUTE) fact=measure(Stage.DETAIL_FACTS,()->facts.facts(List.of(state))).get(key);
|
||||
try(var timing=span(Stage.DETAIL_PROJECT)) {
|
||||
if(!canRead(state,fact,actor)) throw forbidden("无权读取关联来源");
|
||||
var item=state.isRetired()?project(state,fact,actor,clock):snapshot.items().stream().filter(i->i.workItemKey().equals(key)).findFirst().orElseThrow(()->missing("事项不存在"));
|
||||
var relatedKeys=new HashSet<>(state.getPrerequisites()); relatedKeys.addAll(state.getReplacementKeys());
|
||||
@@ -128,9 +153,10 @@ public class AppointmentWorkQueryService {
|
||||
relatedKeys.addAll(effectiveKeys(relatedKeys,statesByKey));
|
||||
snapshot.states().stream().filter(s->effectiveKeys(s.getPrerequisites(),statesByKey).contains(key)).map(AppointmentWorkState::getWorkItemKey).forEach(relatedKeys::add);
|
||||
var refs=new ArrayList<>(state.getReferences()); if(fact!=null) refs.addAll(fact.references());
|
||||
return new WorkDetail(item,snapshot.items().stream().filter(i->relatedKeys.contains(i.workItemKey())).toList(),refs.stream().distinct().toList(),store.history(key),
|
||||
return new WorkDetail(item,snapshot.items().stream().filter(i->relatedKeys.contains(i.workItemKey())).toList(),refs.stream().distinct().toList(),measure(Stage.DETAIL_HISTORY,()->store.history(key)),
|
||||
state.isReviewRequired()?new Review(state.getReviewVersion(),List.of(Objects.toString(state.getReviewReason(),"关联安排变化"))):null,
|
||||
requirementOwned(state,fact)?new RequirementDetails(state.isBookingRequired(),Objects.toString(state.getReceivingRequirements(),""),state.getRequirementReason()):null,state.getReturnHandoff());
|
||||
}
|
||||
}
|
||||
WorkItem current(AppointmentWorkState state,Fact fact,Actor actor) { return project(state,fact,actor,clock); }
|
||||
private boolean matches(AppointmentWorkState state,Flow flow,Set<Type> types,String containerId) {
|
||||
|
||||
+12
-7
@@ -7,6 +7,7 @@ import org.slf4j.MDC;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkPolicy.*;
|
||||
|
||||
@@ -21,13 +22,14 @@ public class AppointmentWorkStore {
|
||||
public void invalidateRequirements() {
|
||||
long started = System.nanoTime();
|
||||
try {
|
||||
if (mapper.invalidateRequirements() != 1) throw unavailable("工作事项同步守卫未初始化");
|
||||
if (measure(Stage.GUARD_UPDATE,mapper::invalidateRequirements) != 1) throw unavailable("工作事项同步守卫未初始化");
|
||||
} finally {
|
||||
long elapsedMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
|
||||
if (elapsedMs >= 250) {
|
||||
// 语句耗时可能包含等锁或连接等待,不能仅凭日志认定具体锁根因;不记录请求业务载荷。
|
||||
LOG.warn("Appointment work guard update slow: elapsedMs={}, requestId={}, traceId={}",
|
||||
elapsedMs, MDC.get("requestId"), MDC.get("traceId"));
|
||||
try { LOG.warn("Appointment work guard update slow: elapsedMs={}, requestId={}, traceId={}, scopeId={}",
|
||||
elapsedMs, MDC.get("requestId"), MDC.get("traceId"), currentScopeId());
|
||||
} catch(Throwable ignored) { /* 诊断输出失败不得覆盖原守卫结果或异常。 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,20 +37,23 @@ public class AppointmentWorkStore {
|
||||
public AppointmentWorkMapper.Guard lock() {
|
||||
long started = System.nanoTime();
|
||||
try {
|
||||
var guard = mapper.lockGuard();
|
||||
observeTransaction();
|
||||
var guard = measure(Stage.GUARD_LOCK,mapper::lockGuard);
|
||||
if (guard == null) throw unavailable("工作事项DDL尚未集成");
|
||||
return guard;
|
||||
} finally {
|
||||
long elapsedMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
|
||||
if (elapsedMs >= 250) {
|
||||
// 耗时包含连接及语句执行,单条日志不能区分具体等待原因,不记录业务载荷。
|
||||
LOG.warn("Appointment work guard lock slow: elapsedMs={}, requestId={}, traceId={}",
|
||||
elapsedMs, MDC.get("requestId"), MDC.get("traceId"));
|
||||
try { LOG.warn("Appointment work guard lock slow: elapsedMs={}, requestId={}, traceId={}, scopeId={}",
|
||||
elapsedMs, MDC.get("requestId"), MDC.get("traceId"), currentScopeId());
|
||||
} catch(Throwable ignored) { /* 诊断输出失败不得覆盖原守卫结果或异常。 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 完整性守卫保持原语义,读取耗时与事务完成只作诊断。 */
|
||||
public String requireInitialized() {
|
||||
var guard=mapper.guard();
|
||||
observeTransaction();var guard=measure(Stage.GUARD_READ,mapper::guard);
|
||||
if(guard==null || !guard.isInitialized()) throw unavailable("完整工作范围尚未完成同步,不能显示零项");
|
||||
if(guard.getRequirementVersion()!=guard.getSynchronizedRequirementVersion()) throw conflict("预约批次或运输关联已变化,服务端正在协调完整工作清单");
|
||||
return guard.getScopeVersion();
|
||||
|
||||
+14
-15
@@ -8,6 +8,7 @@ import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkPolicy.*;
|
||||
@@ -23,7 +24,7 @@ public class AppointmentWorkSyncService {
|
||||
@Transactional
|
||||
public SyncResult synchronize(SyncRequest request) {
|
||||
if(request==null || request.size()<1 || request.size()>100) throw bad("同步批次须为1至100柜");
|
||||
var timing=new SyncTiming();boolean success=false;
|
||||
observeTransaction();var timing=new SyncTiming();boolean success=false;
|
||||
try {
|
||||
var guard=store.lock();
|
||||
if(request.afterContainerBookingId()!=null && !Objects.equals(guard.getScanCursor(),request.afterContainerBookingId())) throw conflict("来源同步游标已变化,请从首批重新核对");
|
||||
@@ -36,16 +37,7 @@ public class AppointmentWorkSyncService {
|
||||
timing.next(SyncPhase.GENERATE);
|
||||
// 保留全部历史与稳定键顺序;逐柜更新立即进入本次工作集,不跨事务缓存。
|
||||
var known=workingSet();
|
||||
var requirementInputs=page.containers().stream().filter(source->{
|
||||
// 坏来源仍由下面原顺序校验并报错,不能在后页原料提示阶段抢先覆盖前柜错误。
|
||||
try { validateSource(source);return true; } catch(RuntimeException invalid) { return false; }
|
||||
}).map(source->{
|
||||
var flow=known.values().stream().filter(s->source.containerBookingId().equals(s.getContainerBookingId()) || s.getContainers().stream().anyMatch(c->source.containerBookingId().equals(c.containerBookingId())))
|
||||
.filter(s->s.getType()!=Type.ROUTE && !s.isRetired()).map(AppointmentWorkState::getFlow).findFirst().orElse(source.establishedFlow());
|
||||
return new RequirementInput(source,flow);
|
||||
}).filter(input->input.flow()!=Flow.UNKNOWN).toList();
|
||||
// 提示仅用于原料批量读取,下面逐柜 existing/activeFlow 仍根据最新工作集决定。
|
||||
var requirementReader=gateway.openRequirements(requirementInputs);
|
||||
var requirementReader=gateway.openRequirements();
|
||||
int created=0; var ids=new HashSet<String>();
|
||||
for(var source:page.containers()) {
|
||||
validateSource(source);
|
||||
@@ -97,6 +89,8 @@ public class AppointmentWorkSyncService {
|
||||
private enum SyncPhase { GUARD, DISCOVER, GENERATE, TRANSPORT_FACTS, RECONCILE, ACTIVE_STORE, ACTIVE_FACTS, OBSERVE_CHECKPOINT }
|
||||
/** 仅统计本次方法执行,不含事务代理提交,不能当作精确锁持有时间。 */
|
||||
private static final class SyncTiming {
|
||||
private final Span body=span(Stage.SYNC_BODY);
|
||||
private Span diagnosticPhase=span(Stage.SYNC_GUARD);
|
||||
private final long started=System.nanoTime();
|
||||
private final long[] elapsed=new long[SyncPhase.values().length];
|
||||
private long phaseStarted=started;
|
||||
@@ -106,19 +100,24 @@ public class AppointmentWorkSyncService {
|
||||
/** 完成前一阶段后开始下一阶段;事实阶段包含紧邻的数据库回读。 */
|
||||
private void next(SyncPhase next) {
|
||||
long now=System.nanoTime();elapsed[phase.ordinal()]+=now-phaseStarted;phaseStarted=now;phase=next;
|
||||
diagnosticPhase.close();diagnosticPhase=span(switch(next){
|
||||
case GUARD->Stage.SYNC_GUARD;case DISCOVER->Stage.SYNC_DISCOVER;case GENERATE->Stage.SYNC_GENERATE;
|
||||
case TRANSPORT_FACTS->Stage.SYNC_TRANSPORT_FACTS;case RECONCILE->Stage.SYNC_RECONCILE;
|
||||
case ACTIVE_STORE->Stage.SYNC_ACTIVE_STORE;case ACTIVE_FACTS->Stage.SYNC_ACTIVE_FACTS;case OBSERVE_CHECKPOINT->Stage.SYNC_CHECKPOINT;});
|
||||
}
|
||||
/** 成功和失败共用一条慢调用摘要,未到达的计数保持为负一。 */
|
||||
private void finish(boolean success) {
|
||||
long now=System.nanoTime();elapsed[phase.ordinal()]+=now-phaseStarted;
|
||||
if(!success){diagnosticPhase.failed();body.failed();}diagnosticPhase.close();body.close();
|
||||
long totalMs=java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(now-started);
|
||||
if(totalMs<250) return;
|
||||
try {
|
||||
// 旧 activeFactsMs 仍包含工作表读取、解析及适配;新增明细不能改变旧指标口径。
|
||||
long activeMs=java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(elapsed[SyncPhase.ACTIVE_STORE.ordinal()]+elapsed[SyncPhase.ACTIVE_FACTS.ordinal()]);
|
||||
LOG.warn("Appointment work sync slow: totalMs={}, guardMs={}, discoverMs={}, generateMs={}, transportFactsMs={}, reconcileMs={}, activeFactsMs={}, activeStoreMs={}, activeAdapterMs={}, observeCheckpointMs={}, pageContainers={}, transportFactsCount={}, activeFactsCount={}, complete={}, failedStage={}",
|
||||
LOG.warn("Appointment work sync slow: totalMs={}, guardMs={}, discoverMs={}, generateMs={}, transportFactsMs={}, reconcileMs={}, activeFactsMs={}, activeStoreMs={}, activeAdapterMs={}, observeCheckpointMs={}, pageContainers={}, transportFactsCount={}, activeFactsCount={}, complete={}, failedStage={}, requestId={}, traceId={}, scopeId={}",
|
||||
totalMs,millis(SyncPhase.GUARD),millis(SyncPhase.DISCOVER),millis(SyncPhase.GENERATE),millis(SyncPhase.TRANSPORT_FACTS),millis(SyncPhase.RECONCILE),activeMs,millis(SyncPhase.ACTIVE_STORE),millis(SyncPhase.ACTIVE_FACTS),millis(SyncPhase.OBSERVE_CHECKPOINT),
|
||||
pageContainers,transportFactsCount,activeFactsCount,complete,success?"NONE":phase.name());
|
||||
} catch(RuntimeException diagnosticFailure) { /* 诊断失败不能替换原业务异常或阻断提交。 */ }
|
||||
pageContainers,transportFactsCount,activeFactsCount,complete,success?"NONE":phase.name(),org.slf4j.MDC.get("requestId"),org.slf4j.MDC.get("traceId"),currentScopeId());
|
||||
} catch(Throwable diagnosticFailure) { /* 诊断失败不能替换原业务异常或阻断提交。 */ }
|
||||
}
|
||||
/** 阶段仅输出整数毫秒,不携带业务标识。 */
|
||||
private long millis(SyncPhase value) { return java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(elapsed[value.ordinal()]); }
|
||||
@@ -140,7 +139,7 @@ public class AppointmentWorkSyncService {
|
||||
}
|
||||
/** 命令入口仍在调用方守卫事务中即时加载工作集。 */
|
||||
List<String> generate(ContainerSource source,Flow flow,List<AppointmentWorkState> existing,Actor actor) {
|
||||
return generate(source,flow,existing,actor,workingSet(),gateway.openRequirements(List.of(new RequirementInput(source,flow))));
|
||||
return generate(source,flow,existing,actor,workingSet(),gateway.openRequirements());
|
||||
}
|
||||
/** 与持久层全量读取保持稳定键顺序,并保留退役事项及原读取上限。 */
|
||||
private Map<String,AppointmentWorkState> workingSet() {
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.lastleg.appointmentwork.diagnostics;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import javax.sql.DataSource;
|
||||
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
|
||||
|
||||
/** 仅观察原Hikari getConnection;同池同连接,unwrap保持容器bean self契约而非隐藏target身份。 */
|
||||
final class AppointmentWorkDataSourcePostProcessor implements BeanPostProcessor {
|
||||
/** 不识别或已有代理的数据源保持原对象,明确记录未覆盖而不主动探测连接。 */
|
||||
@Override public Object postProcessAfterInitialization(Object bean,String name) {
|
||||
if(!(bean instanceof DataSource))return bean;
|
||||
if(!(bean instanceof HikariDataSource) || AopUtils.isAopProxy(bean)) {
|
||||
warnUnavailable();return bean;
|
||||
}
|
||||
try {
|
||||
var factory=new ProxyFactory(bean);factory.setProxyTargetClass(true);
|
||||
factory.addAdvice((MethodInterceptor)invocation->{
|
||||
if(!invocation.getMethod().getName().equals("getConnection") || !active())return invocation.proceed();
|
||||
try(var span=span(Stage.CONNECTION_ACQUIRE)) {
|
||||
try{return invocation.proceed();}catch(Throwable error){span.failed();throw error;}
|
||||
}
|
||||
});
|
||||
return factory.getProxy(bean.getClass().getClassLoader());
|
||||
} catch(RuntimeException failure) { warnUnavailable();return bean; }
|
||||
}
|
||||
/** 不输出 Bean 名、数据源属性或异常内容。 */
|
||||
private static void warnUnavailable() {
|
||||
try{org.slf4j.LoggerFactory.getLogger(AppointmentWorkDataSourcePostProcessor.class).warn("Appointment connection diagnostic unavailable: unsupported or already proxied datasource");}
|
||||
catch(Throwable ignored){ /* 日志故障不改变 Bean 注册。 */ }
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.baiyue.lastleg.appointmentwork.diagnostics;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.*;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import java.io.IOException;
|
||||
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
|
||||
|
||||
/** 仅测量服务端同步 filter dispatch,不代表客户端总耗时或异步响应完成时间。 */
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE+10)
|
||||
public class AppointmentWorkDiagnosticFilter extends OncePerRequestFilter {
|
||||
private static final String BASE="/api/platform/v1/last-leg/appointment-work";
|
||||
/** 仅识别固定模板;路径参数、query string和载荷不进入上下文。 */
|
||||
static Route route(String method,String path){
|
||||
if("GET".equals(method)){
|
||||
if((BASE+"/summary").equals(path))return Route.SUMMARY;
|
||||
if((BASE+"/calendar").equals(path))return Route.CALENDAR;
|
||||
if((BASE+"/items").equals(path))return Route.ITEMS;
|
||||
if((BASE+"/items/resolve").equals(path))return Route.RESOLVE;
|
||||
if(path!=null && path.matches(BASE+"/items/[^/]+"))return Route.DETAIL;
|
||||
}
|
||||
if("POST".equals(method) && path!=null && path.matches(BASE+"/items/[^/]+/commands"))return Route.COMMAND;
|
||||
return null;
|
||||
}
|
||||
/** 不采集其他端点,也不为诊断增加权限判断。 */
|
||||
@Override protected boolean shouldNotFilter(HttpServletRequest request){return route(request.getMethod(),request.getRequestURI())==null;}
|
||||
/** 创建及清理线程作用域;不覆写上游相关标识、状态、响应或异常。 */
|
||||
@Override protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throws ServletException,IOException{
|
||||
Scope scope=null;
|
||||
try{scope=open(Origin.HTTP,route(request.getMethod(),request.getRequestURI()));}catch(RuntimeException ignored){ /* 无诊断仍执行原请求。 */ }
|
||||
try{chain.doFilter(request,response);}
|
||||
catch(ServletException|IOException|RuntimeException|Error error){if(scope!=null)scope.failed();throw error;}
|
||||
finally{if(scope!=null){try{scope.status(response.getStatus());}catch(Throwable ignored){ /* 响应诊断读取失败不替换原异常。 */ }finally{scope.close();}}}
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package com.baiyue.lastleg.appointmentwork.diagnostics;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.MDC;
|
||||
import java.util.*;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** 仅在当前线程的预约调用内累加固定计时桶,不保存业务参数或跨请求状态。 */
|
||||
public final class AppointmentWorkDiagnostics {
|
||||
public enum Origin { HTTP, QUERY, COMMAND, SYNC }
|
||||
public enum Route { INTERNAL, SUMMARY, CALENDAR, ITEMS, RESOLVE, DETAIL, COMMAND }
|
||||
public enum Stage {
|
||||
TRANSACTION_ADVISOR, QUERY_GUARD, QUERY_DISCOVER, QUERY_STORE, QUERY_SELECT, QUERY_FACTS,
|
||||
QUERY_PROJECT, QUERY_NEXT, QUERY_FINAL, DETAIL_STORE, DETAIL_FACTS, DETAIL_PROJECT, DETAIL_HISTORY,
|
||||
GUARD_LOCK, GUARD_READ, GUARD_UPDATE, FACTS_PREPARE, FACTS_FIRSTLEG, FACTS_RECEIVING,
|
||||
FACTS_DELIVERY, FACTS_TRANSPORT, FACTS_DIRECT_TRANSPORT, FACTS_OTHER, FACTS_VALIDATE,
|
||||
DELIVERY_SOURCE_MAP, DELIVERY_CARGO, DELIVERY_SNAPSHOTS, DELIVERY_ROWS, DELIVERY_RECEIVING,
|
||||
DELIVERY_IMPACTS, DELIVERY_ASSEMBLY, SYNC_BODY, SYNC_GUARD, SYNC_DISCOVER, SYNC_GENERATE,
|
||||
SYNC_TRANSPORT_FACTS, SYNC_RECONCILE, SYNC_ACTIVE_STORE, SYNC_ACTIVE_FACTS, SYNC_CHECKPOINT, CONNECTION_ACQUIRE,
|
||||
DB_EXECUTOR_QUERY, DB_EXECUTOR_UPDATE, DB_PREPARE, DB_STATEMENT_QUERY, DB_STATEMENT_UPDATE, DB_RESULT_MAPPING
|
||||
}
|
||||
public enum StatementBucket { NONE, GUARD_LOCK, GUARD_READ, GUARD_UPDATE, WORK_STATE, WORK_HISTORY, DELIVERY, RECEIVING, OTHER }
|
||||
public record Metric(long calls,long inclusiveNanos,long selfNanos,long failures,long firstOffsetNanos,long lastOffsetNanos) { }
|
||||
public record Snapshot(String scopeId,String parentScopeId,Origin origin,Route route,String requestId,String traceId,
|
||||
long elapsedNanos,boolean failed,int status,Map<String,Metric> metrics,boolean transactionObserved,int completionStatus,long completionOffsetNanos,long startedNanos) { }
|
||||
private static final org.slf4j.Logger LOG=LoggerFactory.getLogger(AppointmentWorkDiagnostics.class);
|
||||
private static final ThreadLocal<Scope> CURRENT=new ThreadLocal<>();
|
||||
private static final Span NO_SPAN=new Span(null,null,null,0);
|
||||
/** 诊断工具无需实例化。 */
|
||||
private AppointmentWorkDiagnostics() { }
|
||||
|
||||
/** 新建独立作用域;关闭时恢复调用方原作用域,不改变 MDC。 */
|
||||
public static Scope open(Origin origin,Route route) { return new Scope(origin,route,System::nanoTime); }
|
||||
/** 返回纯诊断标识,缺少作用域时明确为空,不生成业务关联键。 */
|
||||
public static String currentScopeId() { var scope=CURRENT.get();return scope==null?null:scope.id; }
|
||||
/** 无作用域时所有阶段计时均为空操作。 */
|
||||
public static boolean active() { return CURRENT.get()!=null; }
|
||||
/** 只监听既有事务完成,不开启事务或改变其同步、隔离级别及连接属性。 */
|
||||
public static void observeTransaction() {
|
||||
try {
|
||||
var scope=CURRENT.get();
|
||||
if(scope==null || scope.transactionObserved || !org.springframework.transaction.support.TransactionSynchronizationManager.isSynchronizationActive()
|
||||
|| !org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive())return;
|
||||
org.springframework.transaction.support.TransactionSynchronizationManager.registerSynchronization(new org.springframework.transaction.support.TransactionSynchronization(){
|
||||
/** 回调发生在原事务完成后,仅记录相对时刻;不声称这是数据库精确释放锁时刻。 */
|
||||
@Override public void afterCompletion(int status){try{scope.completionStatus=status;scope.completionOffsetNanos=Math.max(0,scope.clock.getAsLong()-scope.started);}catch(Throwable ignored){}}
|
||||
});
|
||||
scope.transactionObserved=true;
|
||||
}catch(Throwable ignored){ /* 诊断监听失败不能改变事务。 */ }
|
||||
}
|
||||
/** 将可信相关标识限制为诊断字符集;任意文本不进入日志。 */
|
||||
private static String correlation(String value) { return value!=null && value.matches("[A-Za-z0-9_-]{1,80}")?value:null; }
|
||||
/** 固定阶段开始计时;最多保留64层,避免异常递归造成诊断状态膨胀。 */
|
||||
public static Span span(Stage stage) { return span(stage,StatementBucket.NONE); }
|
||||
/** 数据库阶段只接受固定语句桶,不记录 SQL、参数或动态 Mapper 名称。 */
|
||||
public static Span span(Stage stage,StatementBucket bucket) {
|
||||
try {
|
||||
var scope=CURRENT.get();if(scope==null || scope.stack.size()>=64)return NO_SPAN;
|
||||
var span=new Span(scope,stage,bucket,scope.clock.getAsLong());scope.stack.push(span);return span;
|
||||
} catch(RuntimeException ignored) { return NO_SPAN; }
|
||||
}
|
||||
/** 保留返回对象和原异常;测量只包围原有调用一次。 */
|
||||
public static <T> T measure(Stage stage,Supplier<T> operation) {
|
||||
try(var span=span(stage)) { try{return operation.get();}catch(RuntimeException|Error error){span.failed();throw error;} }
|
||||
}
|
||||
/** 无返回值调用沿相同路径执行,不增加重试或业务判断。 */
|
||||
public static void run(Stage stage,Runnable operation) { measure(stage,()->{operation.run();return null;}); }
|
||||
/** 读取当前统计仅用于固定诊断摘要和隔离验证。 */
|
||||
static Snapshot currentSnapshot() { var scope=CURRENT.get();return scope==null?null:scope.snapshot(); }
|
||||
|
||||
public static final class Scope implements AutoCloseable {
|
||||
private final Scope previous;
|
||||
private final String id=UUID.randomUUID().toString();
|
||||
private final Origin origin;private final Route route;private final LongSupplier clock;
|
||||
private final String requestId,traceId;private final long started;
|
||||
private final Deque<Span> stack=new ArrayDeque<>();
|
||||
private final Map<String,long[]> totals=new LinkedHashMap<>();
|
||||
private boolean failed,closed,transactionObserved;private int status,completionStatus=-1;private long completionOffsetNanos=-1;
|
||||
/** 包内时钟注入仅供确定性验证,不修改全局时钟。 */
|
||||
Scope(Origin origin,Route route,LongSupplier clock) {
|
||||
this.previous=CURRENT.get();this.origin=origin;this.route=route;this.clock=clock;this.started=clock.getAsLong();
|
||||
this.requestId=correlation(MDC.get("requestId"));this.traceId=correlation(MDC.get("traceId"));CURRENT.set(this);
|
||||
}
|
||||
/** 只标记失败,不保存异常类型以外的业务内容。 */
|
||||
public void failed() { failed=true; }
|
||||
/** HTTP状态由容器响应读取,业务code不从载荷解析。 */
|
||||
public void status(int status) { this.status=status; }
|
||||
/** 快照为累计已结束阶段;self仅扣同scope直接子段,独立子scope由parentScopeId关联且不可叠加。 */
|
||||
public Snapshot snapshot() {
|
||||
var metrics=new LinkedHashMap<String,Metric>();totals.forEach((key,v)->metrics.put(key,new Metric(v[0],v[1],v[2],v[3],v[4],v[5])));
|
||||
return new Snapshot(id,previous==null?null:previous.id,origin,route,requestId,traceId,Math.max(0,clock.getAsLong()-started),failed,status,Collections.unmodifiableMap(metrics),transactionObserved,completionStatus,completionOffsetNanos,started);
|
||||
}
|
||||
/** 只有慢调用或失败输出汇总;清理优先,日志异常不得覆盖原业务异常。 */
|
||||
@Override public void close() {
|
||||
if(closed)return;closed=true;
|
||||
try {
|
||||
if(CURRENT.get()==this){if(previous==null)CURRENT.remove();else CURRENT.set(previous);}
|
||||
var s=snapshot();if(!s.failed() && s.status()<500 && s.elapsedNanos()<250_000_000L)return;
|
||||
// startedNanos及offset只可在同一JVM进程比较,不是数据库锁持有者或跨进程时钟。
|
||||
LOG.warn("Appointment attribution: scopeId={}, parentScopeId={}, origin={}, route={}, requestId={}, traceId={}, startedNanos={}, elapsedNs={}, failed={}, httpStatus={}, connectionCoverage={}, transactionObserved={}, completionStatus={}, completionOffsetNs={}, boundary=SERVER_DISPATCH_OR_ADVISOR, stages={}",
|
||||
s.scopeId(),s.parentScopeId(),s.origin(),s.route(),s.requestId(),s.traceId(),s.startedNanos(),s.elapsedNanos(),s.failed(),s.status(),
|
||||
s.metrics().containsKey(Stage.CONNECTION_ACQUIRE.name())?"OBSERVED":"NOT_OBSERVED",s.transactionObserved(),s.completionStatus(),s.completionOffsetNanos(),s.metrics());
|
||||
} catch(Throwable ignored) { /* 诊断设施故障不影响返回、原异常或线程清理。 */ }
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Span implements AutoCloseable {
|
||||
private final Scope scope;private final Stage stage;private final StatementBucket bucket;private final long started;
|
||||
private long children;private boolean failed,closed;
|
||||
/** 保存常量阶段,不引用任何业务对象。 */
|
||||
private Span(Scope scope,Stage stage,StatementBucket bucket,long started){this.scope=scope;this.stage=stage;this.bucket=bucket;this.started=started;}
|
||||
/** 失败计数只作为耗时解释,不改变原异常。 */
|
||||
public void failed(){if(scope!=null){failed=true;scope.failed=true;}}
|
||||
/** 每个阶段只结束一次;父段self扣除子段inclusive,不能把两种口径相加。 */
|
||||
@Override public void close(){
|
||||
if(scope==null || closed)return;closed=true;
|
||||
try{
|
||||
long elapsed=Math.max(0,scope.clock.getAsLong()-started);
|
||||
if(scope.stack.peek()!=this)return;
|
||||
scope.stack.pop();if(!scope.stack.isEmpty())scope.stack.peek().children+=elapsed;
|
||||
var key=stage.name()+(bucket==StatementBucket.NONE?"":":"+bucket.name());
|
||||
var v=scope.totals.computeIfAbsent(key,ignored->new long[]{0,0,0,0,Math.max(0,started-scope.started),0});v[0]++;v[1]+=elapsed;v[2]+=Math.max(0,elapsed-children);if(failed)v[3]++;
|
||||
v[4]=Math.min(v[4],Math.max(0,started-scope.started));v[5]=Math.max(v[5],Math.max(0,started+elapsed-scope.started));
|
||||
}catch(Throwable ignored){ /* 诊断计数故障不能替换原结果。 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.baiyue.lastleg.appointmentwork.diagnostics;
|
||||
|
||||
import com.baiyue.lastleg.appointmentwork.*;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
import org.springframework.aop.support.StaticMethodMatcherPointcut;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.Ordered;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
|
||||
|
||||
/** 使用现有 Spring 代理设施观察事务边界,不新增 AspectJ 或改变事务属性。 */
|
||||
@Configuration(proxyBeanMethods=false)
|
||||
public class AppointmentWorkDiagnosticsConfiguration {
|
||||
/** 基础设施角色确保事务自动代理器纳入此 Advisor;顺序早于默认事务 Advisor。 */
|
||||
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public Advisor appointmentWorkDiagnosticAdvisor() {
|
||||
var pointcut=new StaticMethodMatcherPointcut() {
|
||||
/** 仅匹配预约直接入口,不拦截其他模块或内部辅助方法。 */
|
||||
@Override public boolean matches(Method method,Class<?> targetClass) {
|
||||
return AppointmentWorkSyncService.class.isAssignableFrom(targetClass) && method.getName().equals("synchronize")
|
||||
|| AppointmentWorkCommandService.class.isAssignableFrom(targetClass) && method.getName().equals("execute")
|
||||
|| AppointmentWorkQueryService.class.isAssignableFrom(targetClass) && Set.of("summary","calendar","items","resolveBatch","detail").contains(method.getName());
|
||||
}
|
||||
};
|
||||
MethodInterceptor interceptor=invocation->{
|
||||
boolean sync=invocation.getThis() instanceof AppointmentWorkSyncService;
|
||||
Origin origin=sync?Origin.SYNC:invocation.getThis() instanceof AppointmentWorkCommandService?Origin.COMMAND:Origin.QUERY;
|
||||
Scope owned=null;
|
||||
try { if(sync || !active())owned=open(origin,Route.INTERNAL); } catch(RuntimeException ignored) { /* 无法计时仍执行原方法。 */ }
|
||||
try(var span=span(Stage.TRANSACTION_ADVISOR)) {
|
||||
try{return invocation.proceed();}catch(Throwable error){span.failed();if(owned!=null)owned.failed();throw error;}
|
||||
} finally { if(owned!=null)owned.close(); }
|
||||
};
|
||||
var advisor=new DefaultPointcutAdvisor(pointcut,interceptor);advisor.setOrder(Ordered.HIGHEST_PRECEDENCE+100);return advisor;
|
||||
}
|
||||
/** 静态 Bean 避免提早实例化业务依赖,只对既有 Hikari 实例增加委托代理。 */
|
||||
@Bean public static BeanPostProcessor appointmentWorkDataSourceDiagnostics() { return new AppointmentWorkDataSourcePostProcessor(); }
|
||||
/** MyBatis 按现有插件入口注册,不注册 Mapper、执行 SQL 或改变缓存配置。 */
|
||||
@Bean public AppointmentWorkMyBatisDiagnostics appointmentWorkMyBatisDiagnostics() { return new AppointmentWorkMyBatisDiagnostics(); }
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.baiyue.lastleg.appointmentwork.diagnostics;
|
||||
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.executor.resultset.ResultSetHandler;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.cache.CacheKey;
|
||||
import org.apache.ibatis.plugin.*;
|
||||
import org.apache.ibatis.session.*;
|
||||
import java.sql.*;
|
||||
import static com.baiyue.lastleg.appointmentwork.diagnostics.AppointmentWorkDiagnostics.*;
|
||||
|
||||
/** query 为含映射的 inclusive 调用;结果读取另记子段,不推断为纯服务器锁等待。 */
|
||||
@Intercepts({
|
||||
@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}),
|
||||
@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class,CacheKey.class,BoundSql.class}),
|
||||
@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class}),
|
||||
@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class}),
|
||||
@Signature(type=StatementHandler.class,method="query",args={Statement.class,ResultHandler.class}),
|
||||
@Signature(type=StatementHandler.class,method="update",args={Statement.class}),
|
||||
@Signature(type=ResultSetHandler.class,method="handleResultSets",args={Statement.class})
|
||||
})
|
||||
public final class AppointmentWorkMyBatisDiagnostics implements Interceptor {
|
||||
private static final ThreadLocal<StatementBucket> BUCKET=new ThreadLocal<>();
|
||||
/** 所有调用只 proceed 一次,异常原样返回;固定桶上下文在 finally 恢复。 */
|
||||
@Override public Object intercept(Invocation invocation)throws Throwable {
|
||||
if(!active())return invocation.proceed();
|
||||
var previous=BUCKET.get();boolean executor=invocation.getTarget() instanceof Executor;
|
||||
var bucket=previous==null?StatementBucket.OTHER:previous;
|
||||
if(executor){bucket=bucket(((MappedStatement)invocation.getArgs()[0]).getId());BUCKET.set(bucket);}
|
||||
Stage stage=executor?(invocation.getMethod().getName().equals("query")?Stage.DB_EXECUTOR_QUERY:Stage.DB_EXECUTOR_UPDATE)
|
||||
:invocation.getTarget() instanceof ResultSetHandler?Stage.DB_RESULT_MAPPING
|
||||
:switch(invocation.getMethod().getName()){case "prepare"->Stage.DB_PREPARE;case "query"->Stage.DB_STATEMENT_QUERY;default->Stage.DB_STATEMENT_UPDATE;};
|
||||
try(var span=span(stage,bucket)){
|
||||
try{return invocation.proceed();}catch(Throwable error){span.failed();throw error;}
|
||||
}finally{if(executor){if(previous==null)BUCKET.remove();else BUCKET.set(previous);}}
|
||||
}
|
||||
/** 只有明确白名单语句有独立桶,其他名字一律合入 OTHER,绝不打印原 ID。 */
|
||||
private static StatementBucket bucket(String id){
|
||||
return switch(id){
|
||||
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.lockGuard"->StatementBucket.GUARD_LOCK;
|
||||
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.guard"->StatementBucket.GUARD_READ;
|
||||
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.invalidateRequirements","com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.saveGuard"->StatementBucket.GUARD_UPDATE;
|
||||
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.all","com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.get"->StatementBucket.WORK_STATE;
|
||||
case "com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.selectHistory"->StatementBucket.WORK_HISTORY;
|
||||
case "com.baiyue.lastleg.inboundplan.work.AppointmentWorkDeliveryMapper.batches","com.baiyue.lastleg.inboundplan.work.AppointmentWorkDeliveryMapper.customer","com.baiyue.lastleg.inboundplan.work.AppointmentWorkDeliveryMapper.privateBatch"->StatementBucket.DELIVERY;
|
||||
default->StatementBucket.OTHER;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user