fix: avoid false partial research failures
This commit is contained in:
+21
-11
@@ -468,22 +468,31 @@ class PolymarketActivityWsService(
|
|||||||
if (shouldThrottleResearchCaptureHealth(status)) {
|
if (shouldThrottleResearchCaptureHealth(status)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
researchSourceHealthProvider.getIfAvailable()?.record(
|
try {
|
||||||
sourceType = LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE,
|
researchSourceHealthProvider.getIfAvailable()?.record(
|
||||||
status = status,
|
sourceType = LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE,
|
||||||
candidateCount = candidateCount,
|
status = status,
|
||||||
errorClass = errorClass,
|
candidateCount = candidateCount,
|
||||||
errorMessage = errorMessage,
|
errorClass = errorClass,
|
||||||
disabledReason = disabledReason,
|
errorMessage = errorMessage,
|
||||||
lastCursor = lastCursor
|
disabledReason = disabledReason,
|
||||||
)
|
lastCursor = lastCursor
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("记录研究全局 activity 来源健康失败: status={}, error={}", status, e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldThrottleResearchCaptureHealth(status: LeaderResearchSourceStatus): Boolean {
|
private fun shouldThrottleResearchCaptureHealth(status: LeaderResearchSourceStatus): Boolean {
|
||||||
val now = System.currentTimeMillis()
|
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 &&
|
status == researchCaptureLastHealthStatus &&
|
||||||
now - researchCaptureLastHealthWriteAt < RESEARCH_CAPTURE_HEALTH_THROTTLE_MS
|
now - researchCaptureLastHealthWriteAt < throttleWindow
|
||||||
if (!throttle) {
|
if (!throttle) {
|
||||||
researchCaptureLastHealthStatus = status
|
researchCaptureLastHealthStatus = status
|
||||||
researchCaptureLastHealthWriteAt = now
|
researchCaptureLastHealthWriteAt = now
|
||||||
@@ -678,5 +687,6 @@ class PolymarketActivityWsService(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val RESEARCH_CAPTURE_HEALTH_THROTTLE_MS = 60_000L
|
private const val RESEARCH_CAPTURE_HEALTH_THROTTLE_MS = 60_000L
|
||||||
|
private const val RESEARCH_CAPTURE_DISABLED_HEALTH_THROTTLE_MS = 60L * 60L * 1000L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -3,6 +3,7 @@ package com.wrbug.polymarketbot.service.copytrading.research
|
|||||||
import com.wrbug.polymarketbot.entity.LeaderResearchRun
|
import com.wrbug.polymarketbot.entity.LeaderResearchRun
|
||||||
import com.wrbug.polymarketbot.enums.LeaderResearchEventType
|
import com.wrbug.polymarketbot.enums.LeaderResearchEventType
|
||||||
import com.wrbug.polymarketbot.enums.LeaderResearchRunStatus
|
import com.wrbug.polymarketbot.enums.LeaderResearchRunStatus
|
||||||
|
import com.wrbug.polymarketbot.enums.LeaderResearchSourceStatus
|
||||||
import com.wrbug.polymarketbot.enums.LeaderResearchState
|
import com.wrbug.polymarketbot.enums.LeaderResearchState
|
||||||
import com.wrbug.polymarketbot.enums.LeaderResearchTriggerType
|
import com.wrbug.polymarketbot.enums.LeaderResearchTriggerType
|
||||||
import com.wrbug.polymarketbot.repository.LeaderActivityEventRepository
|
import com.wrbug.polymarketbot.repository.LeaderActivityEventRepository
|
||||||
@@ -97,7 +98,9 @@ class LeaderResearchJobService(
|
|||||||
}
|
}
|
||||||
val lastEventCursor = activityEventRepository.findTopByOrderByEventTimeDesc()
|
val lastEventCursor = activityEventRepository.findTopByOrderByEventTimeDesc()
|
||||||
?.let { "${it.eventTime}:${it.stableEventKey}" }
|
?.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 = runRepository.save(
|
||||||
run.copy(
|
run.copy(
|
||||||
status = if (hasSourceProblems) LeaderResearchRunStatus.PARTIAL_FAILURE else LeaderResearchRunStatus.SUCCESS,
|
status = if (hasSourceProblems) LeaderResearchRunStatus.PARTIAL_FAILURE else LeaderResearchRunStatus.SUCCESS,
|
||||||
|
|||||||
+14
-7
@@ -25,7 +25,8 @@ data class LeaderResearchSourceRunResult(
|
|||||||
val status: LeaderResearchSourceStatus,
|
val status: LeaderResearchSourceStatus,
|
||||||
val errorClass: String? = null,
|
val errorClass: String? = null,
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val limitation: String? = null
|
val limitation: String? = null,
|
||||||
|
val expectedLimitation: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class SourceDiscovery(
|
private data class SourceDiscovery(
|
||||||
@@ -108,7 +109,8 @@ class LeaderResearchSourceService(
|
|||||||
LeaderResearchSourceType.ACTIVITY_DERIVED,
|
LeaderResearchSourceType.ACTIVITY_DERIVED,
|
||||||
activity,
|
activity,
|
||||||
if (globalCaptureEnabled) LeaderResearchSourceStatus.SUCCESS else LeaderResearchSourceStatus.DEGRADED,
|
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) {
|
if (!globalCaptureEnabled) {
|
||||||
@@ -116,14 +118,16 @@ class LeaderResearchSourceService(
|
|||||||
LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE,
|
LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE,
|
||||||
emptyList(),
|
emptyList(),
|
||||||
LeaderResearchSourceStatus.DISABLED,
|
LeaderResearchSourceStatus.DISABLED,
|
||||||
limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION
|
limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION,
|
||||||
|
expectedLimitation = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
results += LeaderResearchSourceRunResult(
|
results += LeaderResearchSourceRunResult(
|
||||||
LeaderResearchSourceType.PUBLIC_LEADERBOARD,
|
LeaderResearchSourceType.PUBLIC_LEADERBOARD,
|
||||||
emptyList(),
|
emptyList(),
|
||||||
LeaderResearchSourceStatus.DISABLED,
|
LeaderResearchSourceStatus.DISABLED,
|
||||||
limitation = PUBLIC_LEADERBOARD_DISABLED_LIMITATION
|
limitation = PUBLIC_LEADERBOARD_DISABLED_LIMITATION,
|
||||||
|
expectedLimitation = true
|
||||||
)
|
)
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
@@ -406,7 +410,8 @@ class LeaderResearchSourceService(
|
|||||||
)
|
)
|
||||||
return result.copy(
|
return result.copy(
|
||||||
status = LeaderResearchSourceStatus.DEGRADED,
|
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,
|
sourceType = LeaderResearchSourceType.PUBLIC_LEADERBOARD,
|
||||||
candidates = emptyList(),
|
candidates = emptyList(),
|
||||||
status = LeaderResearchSourceStatus.DISABLED,
|
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,
|
sourceType = LeaderResearchSourceType.GLOBAL_ACTIVITY_CAPTURE,
|
||||||
candidates = emptyList(),
|
candidates = emptyList(),
|
||||||
status = LeaderResearchSourceStatus.DISABLED,
|
status = LeaderResearchSourceStatus.DISABLED,
|
||||||
limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION
|
limitation = GLOBAL_CAPTURE_DISABLED_LIMITATION,
|
||||||
|
expectedLimitation = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
@@ -34,6 +34,16 @@ class PolymarketActivityWsResearchCaptureTest {
|
|||||||
assertEquals("Global activity capture is disabled", invocation.arguments[5])
|
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
|
@Test
|
||||||
fun `write cap records degraded source health`() {
|
fun `write cap records degraded source health`() {
|
||||||
val service = service(globalCaptureEnabled = true, maxWritesPerMinute = 0)
|
val service = service(globalCaptureEnabled = true, maxWritesPerMinute = 0)
|
||||||
|
|||||||
+38
@@ -77,6 +77,44 @@ class LeaderResearchJobServiceTest {
|
|||||||
Mockito.verify(paperTradingService).processPaperCandidates(run.id)
|
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
|
@Test
|
||||||
fun `preview run does not score advance or paper trade`() {
|
fun `preview run does not score advance or paper trade`() {
|
||||||
val service = service()
|
val service = service()
|
||||||
|
|||||||
Reference in New Issue
Block a user