+208
@@ -0,0 +1,208 @@
|
||||
package com.nanri.aiimage.modules.task.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskScopeStateMapper;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.support.SimpleTransactionStatus;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class TaskScopePayloadStorageServiceTest {
|
||||
|
||||
@BeforeAll
|
||||
static void initializeMybatisMetadata() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||
TaskScopeStateEntity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadsBeforeOpeningTransactionAndCleansReplacedPayloadAfterCommit() {
|
||||
TaskScopeStateMapper mapper = mock(TaskScopeStateMapper.class);
|
||||
TransientPayloadStorageService payloadStorage = mock(TransientPayloadStorageService.class);
|
||||
PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class);
|
||||
TaskScopePayloadStorageService service = new TaskScopePayloadStorageService(
|
||||
mapper, new ObjectMapper(), payloadStorage, transactionManager);
|
||||
List<String> events = new ArrayList<>();
|
||||
AtomicBoolean transactionActive = new AtomicBoolean();
|
||||
TaskScopeStateEntity existing = new TaskScopeStateEntity();
|
||||
existing.setId(9L);
|
||||
existing.setTaskId(7L);
|
||||
existing.setModuleType("PRICE_TRACK");
|
||||
existing.setScopeHash("scope-hash");
|
||||
existing.setStateJson("rustfs:old.json");
|
||||
|
||||
when(payloadStorage.storeScopePayloadVersioned(
|
||||
eq("PRICE_TRACK"), eq(7L), any(), any(), eq(true)))
|
||||
.thenAnswer(invocation -> {
|
||||
assertFalse(transactionActive.get());
|
||||
events.add("upload");
|
||||
return "rustfs:new.json";
|
||||
});
|
||||
when(payloadStorage.extractPointer("rustfs:new.json")).thenReturn("rustfs:new.json");
|
||||
when(payloadStorage.extractPointer("rustfs:old.json")).thenReturn("rustfs:old.json");
|
||||
when(transactionManager.getTransaction(any(TransactionDefinition.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
events.add("begin");
|
||||
transactionActive.set(true);
|
||||
return new SimpleTransactionStatus();
|
||||
});
|
||||
when(mapper.selectOne(any())).thenAnswer(invocation -> {
|
||||
LambdaQueryWrapper<?> query = invocation.getArgument(0);
|
||||
assertTrue(query.getSqlSegment().endsWith("limit 1 FOR UPDATE"));
|
||||
return existing;
|
||||
});
|
||||
when(mapper.update(any(), any())).thenReturn(1);
|
||||
doAnswer(invocation -> {
|
||||
events.add("commit");
|
||||
transactionActive.set(false);
|
||||
return null;
|
||||
}).when(transactionManager).commit(any(TransactionStatus.class));
|
||||
doAnswer(invocation -> {
|
||||
assertFalse(transactionActive.get());
|
||||
events.add("cleanup");
|
||||
return null;
|
||||
}).when(payloadStorage).deleteReplacedPayloadIfNeeded("rustfs:old.json", "rustfs:new.json");
|
||||
|
||||
service.saveScopePayload(7L, "PRICE_TRACK", "shop-a", new Payload("ok"));
|
||||
|
||||
assertEquals(List.of("upload", "begin", "commit", "cleanup"), events);
|
||||
verify(mapper).update(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletesUploadedPayloadAfterRollbackWhenDatabaseConfirmsItIsUnreferenced() {
|
||||
TaskScopeStateMapper mapper = mock(TaskScopeStateMapper.class);
|
||||
TransientPayloadStorageService payloadStorage = mock(TransientPayloadStorageService.class);
|
||||
PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class);
|
||||
TaskScopePayloadStorageService service = new TaskScopePayloadStorageService(
|
||||
mapper, new ObjectMapper(), payloadStorage, transactionManager);
|
||||
List<String> events = new ArrayList<>();
|
||||
AtomicBoolean transactionActive = new AtomicBoolean();
|
||||
TaskScopeStateEntity existing = new TaskScopeStateEntity();
|
||||
existing.setId(9L);
|
||||
existing.setTaskId(7L);
|
||||
existing.setModuleType("PRICE_TRACK");
|
||||
existing.setScopeHash("scope-hash");
|
||||
existing.setStateJson("rustfs:old.json");
|
||||
|
||||
when(payloadStorage.storeScopePayloadVersioned(
|
||||
eq("PRICE_TRACK"), eq(7L), any(), any(), eq(true)))
|
||||
.thenAnswer(invocation -> {
|
||||
assertFalse(transactionActive.get());
|
||||
events.add("upload");
|
||||
return "rustfs:new.json";
|
||||
});
|
||||
when(payloadStorage.extractPointer("rustfs:new.json")).thenReturn("rustfs:new.json");
|
||||
when(payloadStorage.extractPointer("rustfs:old.json")).thenReturn("rustfs:old.json");
|
||||
when(transactionManager.getTransaction(any(TransactionDefinition.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
events.add("begin");
|
||||
transactionActive.set(true);
|
||||
return new SimpleTransactionStatus();
|
||||
});
|
||||
when(mapper.selectOne(any())).thenAnswer(invocation -> {
|
||||
events.add(transactionActive.get() ? "transaction-read" : "cleanup-read");
|
||||
return existing;
|
||||
});
|
||||
when(mapper.update(any(), any())).thenThrow(new IllegalStateException("database write failed"));
|
||||
doAnswer(invocation -> {
|
||||
events.add("rollback");
|
||||
transactionActive.set(false);
|
||||
return null;
|
||||
}).when(transactionManager).rollback(any(TransactionStatus.class));
|
||||
doAnswer(invocation -> {
|
||||
assertFalse(transactionActive.get());
|
||||
events.add("delete");
|
||||
return null;
|
||||
}).when(payloadStorage).deletePayloadIfPresent("rustfs:new.json");
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> service.saveScopePayload(7L, "PRICE_TRACK", "shop-a", new Payload("ok")));
|
||||
|
||||
assertEquals(List.of("upload", "begin", "transaction-read", "rollback", "cleanup-read", "delete"), events);
|
||||
verify(payloadStorage).deletePayloadIfPresent("rustfs:new.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsUploadedPayloadWhenCommitThrowsButDatabaseReferencesIt() {
|
||||
TaskScopeStateMapper mapper = mock(TaskScopeStateMapper.class);
|
||||
TransientPayloadStorageService payloadStorage = mock(TransientPayloadStorageService.class);
|
||||
PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class);
|
||||
TaskScopePayloadStorageService service = new TaskScopePayloadStorageService(
|
||||
mapper, new ObjectMapper(), payloadStorage, transactionManager);
|
||||
List<String> events = new ArrayList<>();
|
||||
AtomicBoolean transactionActive = new AtomicBoolean();
|
||||
TaskScopeStateEntity previous = new TaskScopeStateEntity();
|
||||
previous.setId(9L);
|
||||
previous.setTaskId(7L);
|
||||
previous.setModuleType("PRICE_TRACK");
|
||||
previous.setScopeHash("scope-hash");
|
||||
previous.setStateJson("rustfs:old.json");
|
||||
TaskScopeStateEntity committed = new TaskScopeStateEntity();
|
||||
committed.setId(9L);
|
||||
committed.setTaskId(7L);
|
||||
committed.setModuleType("PRICE_TRACK");
|
||||
committed.setScopeHash("scope-hash");
|
||||
committed.setStateJson("rustfs:new.json");
|
||||
|
||||
when(payloadStorage.storeScopePayloadVersioned(
|
||||
eq("PRICE_TRACK"), eq(7L), any(), any(), eq(true)))
|
||||
.thenAnswer(invocation -> {
|
||||
assertFalse(transactionActive.get());
|
||||
events.add("upload");
|
||||
return "rustfs:new.json";
|
||||
});
|
||||
when(payloadStorage.extractPointer("rustfs:new.json")).thenReturn("rustfs:new.json");
|
||||
when(payloadStorage.extractPointer("rustfs:old.json")).thenReturn("rustfs:old.json");
|
||||
when(transactionManager.getTransaction(any(TransactionDefinition.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
events.add("begin");
|
||||
transactionActive.set(true);
|
||||
return new SimpleTransactionStatus();
|
||||
});
|
||||
when(mapper.selectOne(any())).thenAnswer(invocation -> {
|
||||
events.add(transactionActive.get() ? "transaction-read" : "cleanup-read");
|
||||
return transactionActive.get() ? previous : committed;
|
||||
});
|
||||
when(mapper.update(any(), any())).thenReturn(1);
|
||||
doAnswer(invocation -> {
|
||||
events.add("commit");
|
||||
transactionActive.set(false);
|
||||
throw new IllegalStateException("commit outcome unknown");
|
||||
}).when(transactionManager).commit(any(TransactionStatus.class));
|
||||
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> service.saveScopePayload(7L, "PRICE_TRACK", "shop-a", new Payload("ok")));
|
||||
|
||||
assertEquals("commit outcome unknown", error.getMessage());
|
||||
assertEquals(List.of("upload", "begin", "transaction-read", "commit", "cleanup-read"), events);
|
||||
verify(payloadStorage, never()).deletePayloadIfPresent("rustfs:new.json");
|
||||
verify(payloadStorage, never()).deleteReplacedPayloadIfNeeded(any(), any());
|
||||
}
|
||||
|
||||
private record Payload(String value) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user