刘光辉
4 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package jnpf.flowable.service;
 
import com.baomidou.dynamic.datasource.tx.*;
import com.alibaba.fastjson.JSONObject;
import jnpf.config.ConfigValueUtil;
import jnpf.exception.WorkFlowException;
import jnpf.flowable.mapper.DmsLifecycleEngineRecoveryMapper;
import jnpf.flowable.model.flowable.FlowAbleUrl;
import jnpf.util.UserProvider;
import jnpf.util.wxutil.HttpUtil;
import org.junit.After;
import org.junit.Test;
import org.mybatis.spring.SqlSessionTemplate;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
 
public class DmsLifecycleEngineRecoveryTest {
    @After public void clean(){TransactionContext.remove();TransactionContext.removeSynchronizations();}
    private static ConnectionProxy enlistConnection() {
        String xid=TransactionContext.getXID();
        var existing=ConnectionFactory.getConnection(xid,"master");
        if(existing!=null) return existing;
        var physical=mock(java.sql.Connection.class);
        try {when(physical.getTransactionIsolation()).thenReturn(java.sql.Connection.TRANSACTION_READ_COMMITTED);}
        catch(java.sql.SQLException impossible){throw new AssertionError(impossible);}
        var proxy=new ConnectionProxy(physical,"master");
        ConnectionFactory.putConnection(xid,"master",proxy);return proxy;
    }
    private org.mockito.MockedStatic<org.mybatis.spring.SqlSessionUtils> sessions(SqlSessionTemplate template,DmsLifecycleEngineRecoveryMapper mapper) {
        var factory=mock(org.apache.ibatis.session.SqlSessionFactory.class);
        var session=mock(org.apache.ibatis.session.SqlSession.class);
        when(template.getSqlSessionFactory()).thenReturn(factory);
        when(template.getMapper(DmsLifecycleEngineRecoveryMapper.class)).thenReturn(mapper);
        when(session.getConnection()).thenAnswer(invocation->enlistConnection());
        var utils=mockStatic(org.mybatis.spring.SqlSessionUtils.class);
        utils.when(()->org.mybatis.spring.SqlSessionUtils.getSqlSession(eq(factory),any(),any())).thenReturn(session);
        return utils;
    }
    @Test public void independentRecoveryCommitSurvivesOuterDynamicRollback() throws Throwable {
        var committed=new AtomicInteger();var outerRollback=new AtomicInteger();
        var info=new TransactionalInfo();info.setPropagation(DsPropagation.REQUIRED);info.setRollbackFor(new Class[]{Exception.class});info.setNoRollbackFor(new Class[]{});
        try {new TransactionalTemplate().execute(new TransactionalExecutor(){
            public TransactionalInfo getTransactionInfo(){return info;}
            public Object execute(){
                enlistConnection();
                String xid=TransactionContext.getXID();
                TransactionContext.registerSynchronization(new org.springframework.transaction.support.TransactionSynchronization(){
                    public void afterCompletion(int status){if(status==STATUS_ROLLED_BACK)outerRollback.incrementAndGet();}
                });
                DmsLifecycleEngineRecovery.independent(()->{
                    enlistConnection();
                    assertNotEquals(xid,TransactionContext.getXID());
                    TransactionContext.registerSynchronization(new org.springframework.transaction.support.TransactionSynchronization(){
                        public void afterCompletion(int status){if(status==STATUS_COMMITTED)committed.incrementAndGet();}
                    });return null;
                });assertEquals(xid,TransactionContext.getXID());throw new IllegalStateException("native failure");
            }
        });fail();}catch(IllegalStateException expected){}
        assertEquals(1,committed.get());assertEquals(1,outerRollback.get());
    }
    @Test public void startJournalsBeforeRemoteCallAndRetainsFailureBeyondRollback() throws Throwable {
        var sessions=mock(SqlSessionTemplate.class);var mapper=mock(DmsLifecycleEngineRecoveryMapper.class);
        var engine=mock(FlowAbleUrl.class);
        when(mapper.tryLock(anyString())).thenReturn(true);when(mapper.insert(any())).thenReturn(1);when(mapper.attach(anyString(),eq("engine"))).thenReturn(1);
        when(engine.startInstance(eq("deployment"),anyMap())).thenReturn("engine");
        doThrow(new WorkFlowException("offline")).when(engine).deleteDmsInstanceStrict(eq("engine"),anyString());
        var info=new TransactionalInfo();info.setPropagation(DsPropagation.REQUIRED);info.setRollbackFor(new Class[]{Exception.class});info.setNoRollbackFor(new Class[]{});
        try(var utils=sessions(sessions,mapper)) {
            try {new TransactionalTemplate().execute(new TransactionalExecutor(){
                public TransactionalInfo getTransactionInfo(){return info;}
                public Object execute(){
                    enlistConnection();
                    try {
                        var started=new DmsLifecycleEngineRecovery(sessions,engine,true).start("tenant","task","deployment",java.util.Map.of());
                        var failure=new IllegalStateException("later native failure");started.compensation().compensate(failure);throw failure;
                    } catch(WorkFlowException failure){throw new IllegalStateException(failure);}
                }
            });fail();}catch(IllegalStateException expected){}
        }
        var order=inOrder(mapper,engine);order.verify(mapper).insert(any());order.verify(engine).startInstance(eq("deployment"),argThat(v->v.containsKey(DmsLifecycleEngineRecovery.CORRELATION_VARIABLE)));
        order.verify(mapper).attach(anyString(),eq("engine"));
        verify(engine,times(2)).deleteDmsInstanceStrict(eq("engine"),anyString());
        verify(mapper,never()).finish(anyString(),anyString());
        assertTrue(TransactionContext.getSynchronizations().isEmpty());
    }
    private void recoveryCase(boolean lock,String instance,boolean committed,boolean deletionFails,String expected) throws Exception {
        var sessions=mock(SqlSessionTemplate.class);var mapper=mock(DmsLifecycleEngineRecoveryMapper.class);var engine=mock(FlowAbleUrl.class);
        when(sessions.getMapper(DmsLifecycleEngineRecoveryMapper.class)).thenReturn(mapper);
        when(mapper.tryLock("r")).thenReturn(lock);
        var row=new DmsLifecycleEngineRecovery.Record();row.setId("r");row.setTenantId("t");row.setTaskId("task");row.setInstanceId(instance);row.setStatus("PENDING");
        when(mapper.get("r")).thenReturn(row);when(mapper.committed(row)).thenReturn(committed);
        if(deletionFails)doThrow(new WorkFlowException("no acknowledgement")).when(engine).deleteDmsInstanceStrict("engine","dmsLifecycleRollback:r");
        try(var utils=sessions(sessions,mapper)){new DmsLifecycleEngineRecovery(sessions,engine,true).recover("r");}
        if(!lock){verify(mapper,never()).get(anyString());verifyNoInteractions(engine);}
        else if(deletionFails){verify(mapper).failed(eq("r"),anyString());verify(mapper,never()).finish(anyString(),anyString());}
        else {verify(mapper).finish("r",expected);if(!"DELETED".equals(expected))verifyNoInteractions(engine);}
    }
    @Test public void activeNativeTransactionCannotBeRecovered() throws Exception {recoveryCase(false,"engine",false,false,null);}
    @Test public void committedNativeTaskIsNeverDeleted() throws Exception {recoveryCase(true,"engine",true,false,"KEPT");}
    @Test public void unknownStartResultRequiresEvidence() throws Exception {recoveryCase(true,null,false,false,"NEEDS_REVIEW");}
    @Test public void confirmedDeletionCompletesRecovery() throws Exception {recoveryCase(true,"engine",false,false,"DELETED");}
    @Test public void deletionFailureRetainsPendingRecovery() throws Exception {recoveryCase(true,"engine",false,true,null);}
    @Test public void strictClientRejectsFalseEmptyAndNullAcknowledgements() throws Exception {
        var config=mock(ConfigValueUtil.class);when(config.getFlowDomain()).thenReturn("http://review.invalid");
        var client=new FlowAbleUrl(config);
        try(var context=mockStatic(jnpf.util.context.SpringContext.class);var user=mockStatic(UserProvider.class);var http=mockStatic(HttpUtil.class)){
            user.when(UserProvider::getToken).thenReturn("test-only");
            for(JSONObject response:new JSONObject[]{null,new JSONObject(),JSONObject.parseObject("{\"success\":false}")}){
                http.when(()->HttpUtil.httpRequest(anyString(),eq("DELETE"),isNull(),eq("test-only"))).thenReturn(response);
                http.when(()->HttpUtil.httpRequest(anyString(),eq("GET"),isNull(),eq("test-only"))).thenReturn(new JSONObject());
                try{client.deleteDmsInstanceStrict("engine","reason");fail();}catch(WorkFlowException expected){}
            }
        }
    }
    @Test public void lostDeletionResponseNeedsExactHistoricReceipt() throws Exception {
        var config=mock(ConfigValueUtil.class);when(config.getFlowDomain()).thenReturn("http://review.invalid");var client=new FlowAbleUrl(config);
        try(var context=mockStatic(jnpf.util.context.SpringContext.class);var user=mockStatic(UserProvider.class);var http=mockStatic(HttpUtil.class)){
            user.when(UserProvider::getToken).thenReturn("test-only");
            http.when(()->HttpUtil.httpRequest(anyString(),eq("DELETE"),isNull(),eq("test-only"))).thenReturn(JSONObject.parseObject("{\"success\":false}"));
            for(String reason:new String[]{"different","reason"}){
                var history=JSONObject.parseObject("{\"success\":true,\"data\":{\"instanceId\":\"engine\",\"endTime\":\"2026-10-15T12:00:00\"}}");history.getJSONObject("data").put("deleteReason",reason);
                http.when(()->HttpUtil.httpRequest(anyString(),eq("GET"),isNull(),eq("test-only"))).thenReturn(history);
                if("reason".equals(reason))client.deleteDmsInstanceStrict("engine","reason");
                else try{client.deleteDmsInstanceStrict("engine","reason");fail();}catch(WorkFlowException expected){}
            }
        }
    }
}