fix: avoid false partial research failures

This commit is contained in:
codychen123
2026-05-10 15:48:52 +08:00
parent 4057b3f611
commit 1ce2f5d273
5 changed files with 87 additions and 19 deletions
@@ -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
}
}
@@ -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,
@@ -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
)
}
@@ -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)
@@ -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()