From a171f2ac0e24ff2e8dc1621e02c8d38278e98c9f Mon Sep 17 00:00:00 2001 From: matevip Date: Sun, 19 Apr 2026 19:37:50 +0800 Subject: [PATCH] fix(wiki): recover raw materials stuck in processing on server restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: recoverOnStartup() only reset mate_wiki_processing_job rows, not mate_wiki_raw_material. claimForProcessing() only accepts pending, so restart-orphaned processing rows were permanently stuck — frontend showed "preparing..." forever. Fix: - Add WikiRawMaterialService.recoverStuckRawMaterialsOnStartup(): resets processing→pending, clears progress fields, fires WikiProcessingEvent when autoProcessOnUpload is enabled - WikiAutoConfiguration: call raw recovery after job recovery - Execution order: job table first (queued), then raw table (pending) Test: WikiRawMaterialRecoveryTest — 4 cases: reset + events, reset without events (autoProcess=false), noop on empty. --- .../vip/mate/wiki/WikiAutoConfiguration.java | 10 +- .../wiki/service/WikiRawMaterialService.java | 30 +++++ .../service/WikiRawMaterialRecoveryTest.java | 110 ++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 mateclaw-server/src/test/java/vip/mate/wiki/service/WikiRawMaterialRecoveryTest.java diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/WikiAutoConfiguration.java b/mateclaw-server/src/main/java/vip/mate/wiki/WikiAutoConfiguration.java index 95ea616e..a01510db 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/WikiAutoConfiguration.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/WikiAutoConfiguration.java @@ -7,6 +7,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; import vip.mate.wiki.job.WikiProcessingJobService; +import vip.mate.wiki.service.WikiRawMaterialService; /** * Wiki module auto-configuration @@ -20,12 +21,19 @@ import vip.mate.wiki.job.WikiProcessingJobService; public class WikiAutoConfiguration { private final WikiProcessingJobService wikiProcessingJobService; + private final WikiRawMaterialService wikiRawMaterialService; /** - * RFC-030: Recover stuck wiki processing jobs on startup. + * Recover stuck wiki state on startup: + * 1. Job table: routing/*_running → queued (RFC-030) + * 2. Raw material table: processing → pending (avoids forever-spinning progress bars) */ @EventListener(ApplicationReadyEvent.class) public void recoverWikiJobs(ApplicationReadyEvent event) { wikiProcessingJobService.recoverOnStartup(); + int recovered = wikiRawMaterialService.recoverStuckRawMaterialsOnStartup(); + if (recovered > 0) { + log.info("[Wiki] Recovered {} stuck raw materials on startup", recovered); + } } } diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiRawMaterialService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiRawMaterialService.java index 66d32080..09496bc3 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiRawMaterialService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiRawMaterialService.java @@ -332,6 +332,36 @@ public class WikiRawMaterialService { return entity.getOriginalContent(); } + /** + * Recover raw materials stuck in 'processing' status after a server restart. + * Resets them to 'pending', clears stale progress fields, and optionally + * fires processing events so they get picked up automatically. + * + * @return number of recovered rows + */ + @Transactional + public int recoverStuckRawMaterialsOnStartup() { + List stuck = rawMapper.selectList( + new LambdaQueryWrapper() + .eq(WikiRawMaterialEntity::getProcessingStatus, "processing")); + if (stuck.isEmpty()) return 0; + + for (WikiRawMaterialEntity raw : stuck) { + raw.setProcessingStatus("pending"); + raw.setProgressPhase(null); + raw.setProgressTotal(0); + raw.setProgressDone(0); + raw.setErrorMessage(null); + rawMapper.updateById(raw); + + if (properties.isAutoProcessOnUpload()) { + eventPublisher.publishEvent(new WikiProcessingEvent(this, raw.getId(), raw.getKbId())); + } + log.info("[Wiki] Recovered stuck processing raw material: id={}, kbId={}", raw.getId(), raw.getKbId()); + } + return stuck.size(); + } + /** * Handle a duplicate upload: decide what to do based on the existing row's status. * - completed → return as-is (no reprocessing needed) diff --git a/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiRawMaterialRecoveryTest.java b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiRawMaterialRecoveryTest.java new file mode 100644 index 00000000..a49c977b --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiRawMaterialRecoveryTest.java @@ -0,0 +1,110 @@ +package vip.mate.wiki.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.ApplicationEventPublisher; +import vip.mate.tool.builtin.DocumentExtractTool; +import vip.mate.wiki.WikiProperties; +import vip.mate.wiki.event.WikiProcessingEvent; +import vip.mate.wiki.model.WikiRawMaterialEntity; +import vip.mate.wiki.repository.WikiRawMaterialMapper; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Verifies that raw materials stuck in 'processing' status after a server + * restart are recovered to 'pending' with progress fields cleared. + */ +class WikiRawMaterialRecoveryTest { + + private WikiRawMaterialMapper rawMapper; + private ApplicationEventPublisher eventPublisher; + private WikiRawMaterialService service; + private WikiProperties props; + + @BeforeEach + void setUp() { + rawMapper = mock(WikiRawMaterialMapper.class); + WikiKnowledgeBaseService kbService = mock(WikiKnowledgeBaseService.class); + eventPublisher = mock(ApplicationEventPublisher.class); + WikiChunkService chunkService = mock(WikiChunkService.class); + DocumentExtractTool docTool = mock(DocumentExtractTool.class); + props = new WikiProperties(); + props.setAutoProcessOnUpload(true); + service = new WikiRawMaterialService(rawMapper, kbService, props, eventPublisher, docTool, chunkService); + } + + private WikiRawMaterialEntity stuckRow(Long id, Long kbId) { + WikiRawMaterialEntity e = new WikiRawMaterialEntity(); + e.setId(id); + e.setKbId(kbId); + e.setProcessingStatus("processing"); + e.setProgressPhase("route"); + e.setProgressTotal(5); + e.setProgressDone(2); + return e; + } + + @Test + @DisplayName("Stuck 'processing' rows are reset to 'pending' with cleared progress") + void recoverStuck_resetsToPending() { + WikiRawMaterialEntity row1 = stuckRow(10L, 1L); + WikiRawMaterialEntity row2 = stuckRow(20L, 2L); + when(rawMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(row1, row2)); + + int count = service.recoverStuckRawMaterialsOnStartup(); + + assertEquals(2, count); + assertEquals("pending", row1.getProcessingStatus()); + assertNull(row1.getProgressPhase()); + assertEquals(0, row1.getProgressTotal()); + assertEquals(0, row1.getProgressDone()); + assertEquals("pending", row2.getProcessingStatus()); + verify(rawMapper, times(2)).updateById(any(WikiRawMaterialEntity.class)); + } + + @Test + @DisplayName("Recovery fires WikiProcessingEvent when autoProcessOnUpload is true") + void recoverStuck_firesEvents() { + WikiRawMaterialEntity row = stuckRow(10L, 1L); + when(rawMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(row)); + + service.recoverStuckRawMaterialsOnStartup(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(WikiProcessingEvent.class); + verify(eventPublisher).publishEvent(captor.capture()); + assertEquals(10L, captor.getValue().getRawMaterialId()); + } + + @Test + @DisplayName("Recovery does NOT fire events when autoProcessOnUpload is false") + void recoverStuck_noEventsWhenDisabled() { + props.setAutoProcessOnUpload(false); + WikiRawMaterialEntity row = stuckRow(10L, 1L); + when(rawMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(row)); + + service.recoverStuckRawMaterialsOnStartup(); + + assertEquals("pending", row.getProcessingStatus()); + verify(eventPublisher, never()).publishEvent(any()); + } + + @Test + @DisplayName("No stuck rows means zero recovery, no events") + void recoverStuck_emptyIsNoop() { + when(rawMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of()); + + int count = service.recoverStuckRawMaterialsOnStartup(); + + assertEquals(0, count); + verify(rawMapper, never()).updateById(any(WikiRawMaterialEntity.class)); + verify(eventPublisher, never()).publishEvent(any()); + } +}