From 1ce2f5d273ae6522f932b7bd081cd39b764c4e7d Mon Sep 17 00:00:00 2001 From: codychen123 Date: Sun, 10 May 2026 15:48:52 +0800 Subject: [PATCH] fix: avoid false partial research failures --- .../monitor/PolymarketActivityWsService.kt | 32 ++++++++++------ .../research/LeaderResearchJobService.kt | 5 ++- .../research/LeaderResearchSourceService.kt | 21 ++++++---- ...PolymarketActivityWsResearchCaptureTest.kt | 10 +++++ .../research/LeaderResearchJobServiceTest.kt | 38 +++++++++++++++++++ 5 files changed, 87 insertions(+), 19 deletions(-) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/PolymarketActivityWsService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/PolymarketActivityWsService.kt index 4a43f99..881cde9 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/PolymarketActivityWsService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/PolymarketActivityWsService.kt @@ -468,22 +468,31 @@ class PolymarketActivityWsService( if (shouldThrottleResearchCaptureHealth(status)) { return } - researchSourceHealthProvider.getIfAvailable()?.record( - sourceType = LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE, - status = status, - candidateCount = candidateCount, - errorClass = errorClass, - errorMessage = errorMessage, - disabledReason = disabledReason, - lastCursor = lastCursor - ) + try { + researchSourceHealthProvider.getIfAvailable()?.record( + sourceType = LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE, + status = status, + candidateCount = candidateCount, + errorClass = errorClass, + errorMessage = errorMessage, + disabledReason = disabledReason, + lastCursor = lastCursor + ) + } catch (e: Exception) { + logger.warn("记录研究全局 activity 来源健康失败: status={}, error={}", status, e.message) + } } private fun shouldThrottleResearchCaptureHealth(status: LeaderResearchSourceStatus): Boolean { val now = System.currentTimeMillis() - val throttle = status != LeaderResearchSourceStatus.SUCCESS && + val throttleWindow = when (status) { + LeaderResearchSourceStatus.DISABLED -> RESEARCH_CAPTURE_DISABLED_HEALTH_THROTTLE_MS + LeaderResearchSourceStatus.SUCCESS -> 0L + else -> RESEARCH_CAPTURE_HEALTH_THROTTLE_MS + } + val throttle = throttleWindow > 0 && status == researchCaptureLastHealthStatus && - now - researchCaptureLastHealthWriteAt < RESEARCH_CAPTURE_HEALTH_THROTTLE_MS + now - researchCaptureLastHealthWriteAt < throttleWindow if (!throttle) { researchCaptureLastHealthStatus = status researchCaptureLastHealthWriteAt = now @@ -678,5 +687,6 @@ class PolymarketActivityWsService( companion object { private const val RESEARCH_CAPTURE_HEALTH_THROTTLE_MS = 60_000L + private const val RESEARCH_CAPTURE_DISABLED_HEALTH_THROTTLE_MS = 60L * 60L * 1000L } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchJobService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchJobService.kt index 38a9cef..25b4406 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchJobService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchJobService.kt @@ -3,6 +3,7 @@ package com.wrbug.polymarketbot.service.copytrading.research import com.wrbug.polymarketbot.entity.LeaderResearchRun import com.wrbug.polymarketbot.enums.LeaderResearchEventType import com.wrbug.polymarketbot.enums.LeaderResearchRunStatus +import com.wrbug.polymarketbot.enums.LeaderResearchSourceStatus import com.wrbug.polymarketbot.enums.LeaderResearchState import com.wrbug.polymarketbot.enums.LeaderResearchTriggerType import com.wrbug.polymarketbot.repository.LeaderActivityEventRepository @@ -97,7 +98,9 @@ class LeaderResearchJobService( } val lastEventCursor = activityEventRepository.findTopByOrderByEventTimeDesc() ?.let { "${it.eventTime}:${it.stableEventKey}" } - val hasSourceProblems = sourceResults.any { it.status.name == "FAILURE" || it.status.name == "DEGRADED" } + val hasSourceProblems = sourceResults.any { + !it.expectedLimitation && (it.status == LeaderResearchSourceStatus.FAILURE || it.status == LeaderResearchSourceStatus.DEGRADED) + } run = runRepository.save( run.copy( status = if (hasSourceProblems) LeaderResearchRunStatus.PARTIAL_FAILURE else LeaderResearchRunStatus.SUCCESS, diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchSourceService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchSourceService.kt index 06936e0..e84b926 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchSourceService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchSourceService.kt @@ -25,7 +25,8 @@ data class LeaderResearchSourceRunResult( val status: LeaderResearchSourceStatus, val errorClass: String? = null, val errorMessage: String? = null, - val limitation: String? = null + val limitation: String? = null, + val expectedLimitation: Boolean = false ) private data class SourceDiscovery( @@ -108,7 +109,8 @@ class LeaderResearchSourceService( LeaderResearchSourceType.ACTIVITY_DERIVED, activity, if (globalCaptureEnabled) LeaderResearchSourceStatus.SUCCESS else LeaderResearchSourceStatus.DEGRADED, - limitation = if (globalCaptureEnabled) null else GLOBAL_CAPTURE_DISABLED_LIMITATION + limitation = if (globalCaptureEnabled) null else GLOBAL_CAPTURE_DISABLED_LIMITATION, + expectedLimitation = !globalCaptureEnabled ) ) if (!globalCaptureEnabled) { @@ -116,14 +118,16 @@ class LeaderResearchSourceService( LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE, emptyList(), LeaderResearchSourceStatus.DISABLED, - limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION + limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION, + expectedLimitation = true ) } results += LeaderResearchSourceRunResult( LeaderResearchSourceType.PUBLIC_LEADERBOARD, emptyList(), LeaderResearchSourceStatus.DISABLED, - limitation = PUBLIC_LEADERBOARD_DISABLED_LIMITATION + limitation = PUBLIC_LEADERBOARD_DISABLED_LIMITATION, + expectedLimitation = true ) return results } @@ -406,7 +410,8 @@ class LeaderResearchSourceService( ) return result.copy( status = LeaderResearchSourceStatus.DEGRADED, - limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION + limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION, + expectedLimitation = true ) } @@ -429,7 +434,8 @@ class LeaderResearchSourceService( sourceType = LeaderResearchSourceType.PUBLIC_LEADERBOARD, candidates = emptyList(), status = LeaderResearchSourceStatus.DISABLED, - limitation = PUBLIC_LEADERBOARD_DISABLED_LIMITATION + limitation = PUBLIC_LEADERBOARD_DISABLED_LIMITATION, + expectedLimitation = true ) } @@ -452,7 +458,8 @@ class LeaderResearchSourceService( sourceType = LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE, candidates = emptyList(), status = LeaderResearchSourceStatus.DISABLED, - limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION + limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION, + expectedLimitation = true ) } diff --git a/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/PolymarketActivityWsResearchCaptureTest.kt b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/PolymarketActivityWsResearchCaptureTest.kt index 59690a3..cca04b2 100644 --- a/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/PolymarketActivityWsResearchCaptureTest.kt +++ b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/PolymarketActivityWsResearchCaptureTest.kt @@ -34,6 +34,16 @@ class PolymarketActivityWsResearchCaptureTest { assertEquals("Global activity capture is disabled", invocation.arguments[5]) } + @Test + fun `disabled global capture health is written once to avoid source state lock churn`() { + val service = service(globalCaptureEnabled = false) + + invokeHandleMessage(service, "not-json") + invokeHandleMessage(service, "still-not-json") + + assertEquals(1, Mockito.mockingDetails(healthService).invocations.size) + } + @Test fun `write cap records degraded source health`() { val service = service(globalCaptureEnabled = true, maxWritesPerMinute = 0) diff --git a/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchJobServiceTest.kt b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchJobServiceTest.kt index 159c6ab..d218cbe 100644 --- a/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchJobServiceTest.kt +++ b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/research/LeaderResearchJobServiceTest.kt @@ -77,6 +77,44 @@ class LeaderResearchJobServiceTest { Mockito.verify(paperTradingService).processPaperCandidates(run.id) } + @Test + fun `expected disabled sources do not mark run partial failure`() { + val service = service() + stubRunSaves() + Mockito.`when`(sourceService.discoverCandidates(1L)).thenReturn( + listOf( + LeaderResearchSourceRunResult(LeaderResearchSourceType.WATCHLIST, emptyList(), LeaderResearchSourceStatus.SUCCESS), + LeaderResearchSourceRunResult( + LeaderResearchSourceType.ACTIVITY_DERIVED, + emptyList(), + LeaderResearchSourceStatus.DEGRADED, + limitation = "Global activity capture is disabled", + expectedLimitation = true + ), + LeaderResearchSourceRunResult( + LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE, + emptyList(), + LeaderResearchSourceStatus.DISABLED, + limitation = "Global activity capture is disabled", + expectedLimitation = true + ), + LeaderResearchSourceRunResult( + LeaderResearchSourceType.PUBLIC_LEADERBOARD, + emptyList(), + LeaderResearchSourceStatus.DISABLED, + limitation = "Public leaderboard source is intentionally disabled", + expectedLimitation = true + ) + ) + ) + + val run = service.runOnce(dryRun = false, triggerType = LeaderResearchTriggerType.MANUAL) + + assertEquals(LeaderResearchRunStatus.SUCCESS, run.status) + assertFalse(run.partialFailure) + Mockito.verify(paperTradingService).processPaperCandidates(run.id) + } + @Test fun `preview run does not score advance or paper trade`() { val service = service()