package jnpf.flowable.service; import com.baomidou.dynamic.datasource.tx.*; import jnpf.exception.WorkFlowException; import jnpf.flowable.mapper.DmsLifecycleEngineRecoveryMapper; import jnpf.flowable.model.flowable.FlowAbleUrl; import lombok.Data; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.SqlSessionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.function.Supplier; /** Durable DMS-only recovery; native business CRUD and ordinary engine calls are unchanged. */ @Service public class DmsLifecycleEngineRecovery { private static final Logger LOG=LoggerFactory.getLogger(DmsLifecycleEngineRecovery.class); public static final String CORRELATION_VARIABLE="dmsLifecycleRecoveryId"; private final SqlSessionTemplate sessions; private final FlowAbleUrl engine; private final boolean enabled; public DmsLifecycleEngineRecovery(SqlSessionTemplate sessions,FlowAbleUrl engine, @Value("${dms.business-occupancy.enabled:false}") boolean enabled) { this.sessions=sessions;this.engine=engine;this.enabled=enabled; } private DmsLifecycleEngineRecoveryMapper mapper() {return sessions.getMapper(DmsLifecycleEngineRecoveryMapper.class);} /** A log is committed BEFORE starting the engine; the native transaction holds its recovery gate. */ public Started start(String tenant,String task,String deployment,Map variables) throws WorkFlowException { if(!enabled || task==null || task.isBlank()) throw new IllegalStateException("DMS 引擎恢复未启用或任务标识缺失"); if(variables.containsKey(CORRELATION_VARIABLE)) throw new IllegalArgumentException("DMS 引擎恢复变量冲突"); var row=new Record();row.setId(UUID.randomUUID().toString());row.setTenantId(tenant==null || tenant.isBlank()?"0":tenant);row.setTaskId(task); holdNativeGate(row.getId()); independentChecked(()->{if(mapper().insert(row)!=1)throw new IllegalStateException("DMS 引擎恢复记录未保存");return null;}); var actual=new HashMap<>(variables);actual.put(CORRELATION_VARIABLE,row.getId()); String instance=engine.startInstance(deployment,actual); if(instance==null || instance.isBlank()) throw new WorkFlowException("DMS 引擎启动结果不明确,需按恢复记录核对"); row.setInstanceId(instance); var ticket=DmsLifecycleEngineCompensation.register(instance,()->{ independentChecked(()->{ // Even a commit-response failure must not delete an instance already referenced by committed data. if(mapper().committed(row)) {mapper().finish(row.getId(),"KEPT");return null;} try {engine.deleteDmsInstanceStrict(instance,reason(row.getId()));} catch(WorkFlowException failure) {throw new IllegalStateException(failure);} mapper().finish(row.getId(),"DELETED");return null; }); }); try { independentChecked(()->{if(mapper().attach(row.getId(),instance)!=1)throw new IllegalStateException("DMS 引擎实例恢复关联未保存");return null;}); } catch(RuntimeException failure) {ticket.compensate(failure);throw failure;} return new Started(instance,ticket); } private void holdNativeGate(String id) { withVerifiedConnection(()->{ if(!mapper().tryLock(id)) throw new IllegalStateException("DMS 引擎恢复门闩冲突"); return null; }); } private T independentChecked(Supplier action) { return independent(()->withVerifiedConnection(action)); } private T withVerifiedConnection(Supplier action) { String xid=TransactionContext.getXID(); if(xid==null || TransactionSynchronizationManager.isActualTransactionActive()) throw new IllegalStateException("DMS 引擎启动必须属于原生动态事务"); var session=SqlSessionUtils.getSqlSession(sessions.getSqlSessionFactory(),sessions.getExecutorType(),sessions.getPersistenceExceptionTranslator()); try { var connection=session.getConnection(); if(!(connection instanceof ConnectionProxy proxy) || ConnectionFactory.getConnection(xid,proxy.getDs())!=connection || connection.getAutoCommit() || connection.isReadOnly() || connection.getTransactionIsolation()!=java.sql.Connection.TRANSACTION_READ_COMMITTED) throw new IllegalStateException("DMS 引擎恢复门闩未绑定原生事务连接"); return action.get(); } catch(java.sql.SQLException failure) {throw new IllegalStateException(failure);} finally {SqlSessionUtils.closeSqlSession(session,sessions.getSqlSessionFactory());} } /** REQUIRES_NEW suspends the actual dynamic transaction; Spring propagation is not substituted. */ static T independent(Supplier action) { if(TransactionSynchronizationManager.isActualTransactionActive()) throw new IllegalStateException("不能混用DMS恢复事务管理器"); // Version 4.3.1 suspends only XID, not its thread-local synchronization list. // Isolate callbacks here so an independent journal transaction cannot consume native callbacks. String outerXid=TransactionContext.getXID(); var outerCallbacks=TransactionContext.getSynchronizations(); TransactionContext.removeSynchronizations(); var info=new TransactionalInfo();info.setPropagation(DsPropagation.REQUIRES_NEW); info.setRollbackFor(new Class[]{Exception.class});info.setNoRollbackFor(new Class[]{}); try { @SuppressWarnings("unchecked") T result=(T)new TransactionalTemplate().execute(new TransactionalExecutor() { public Object execute() {return action.get();} public TransactionalInfo getTransactionInfo() {return info;} });return result; } catch(RuntimeException | Error failure) {throw failure;} catch(Throwable failure) {throw new IllegalStateException("DMS 独立恢复事务失败",failure);} finally { TransactionContext.removeSynchronizations(); // afterCompletion runs after LocalTxUtil has already removed XID. // Its callback snapshot is still being iterated, but no live transaction can be re-registered. if(outerXid!=null && outerXid.equals(TransactionContext.getXID())) outerCallbacks.forEach(TransactionContext::registerSynchronization); } } @Scheduled(cron="${dms.business-occupancy.engine-recovery-cron:0 * * * * ?}") public void recoverPending() { if(!enabled) return; for(var candidate:mapper().pending(100)) { try {recover(candidate.getId());} catch(RuntimeException failure) {LOG.error("DMS engine recovery scan failed, recoveryId={}",candidate.getId(),failure);} } } public void recover(String id) { independentChecked(()->{ if(!mapper().tryLock(id)) return null; // A live native transaction still owns this instance. var row=mapper().get(id); if(row==null || !"PENDING".equals(row.getStatus())) return null; if(row.getInstanceId()==null || row.getInstanceId().isBlank()) { mapper().finish(id,"NEEDS_REVIEW");return null; // Never guess which remote instance to delete. } if(mapper().committed(row)) {mapper().finish(id,"KEPT");return null;} try { engine.deleteDmsInstanceStrict(row.getInstanceId(),reason(id)); mapper().finish(id,"DELETED"); } catch(Exception failure) { String type=failure.getClass().getName(); mapper().failed(id,type.substring(0,Math.min(200,type.length()))); LOG.warn("DMS engine deletion unconfirmed, recoveryId={}",id); } return null; }); } private static String reason(String id) {return "dmsLifecycleRollback:"+id;} public record Started(String instanceId,DmsLifecycleEngineCompensation compensation) { } @Data public static class Record { private String id; private String tenantId; private String taskId; private String instanceId; private String status; } }