diff --git a/src/main/java/com/ffii/fpsms/modules/monitoring/service/ClientPresenceService.kt b/src/main/java/com/ffii/fpsms/modules/monitoring/service/ClientPresenceService.kt index 33d36ec..f69cb06 100644 --- a/src/main/java/com/ffii/fpsms/modules/monitoring/service/ClientPresenceService.kt +++ b/src/main/java/com/ffii/fpsms/modules/monitoring/service/ClientPresenceService.kt @@ -24,6 +24,10 @@ open class ClientPresenceService( @Value("\${fpsms.client-presence.history-max-range-days:30}") private var historyMaxRangeDays: Long = 30 + /** Omit from live monitor and purge rows with no heartbeat for longer than this. */ + @Value("\${fpsms.client-presence.live-presence-retention-days:7}") + private var livePresenceRetentionDays: Long = 7 + data class HeartbeatRequest( val deviceId: String, val displayName: String? = null, @@ -201,13 +205,16 @@ open class ClientPresenceService( mapOf("cutoff" to cutoff), ) for (row in rows) { - maybeRecordEvent(row, "offline", deriveStatus(row, now), now) + val lastHb = parseDateTime(row["lastHeartbeat"]) + val statusBeforeOffline = if (lastHb != null) deriveStatus(row, lastHb) else null + maybeRecordEvent(row, "offline", statusBeforeOffline, now) } } @Transactional(readOnly = true) open fun listActive(clientTypeFilter: String?): List> { val type = clientTypeFilter?.trim()?.lowercase()?.takeIf { it.isNotBlank() && it != "all" } + val minHeartbeat = LocalDateTime.now().minusDays(livePresenceRetentionDays.coerceAtLeast(1)) val sql = buildString { append( """ @@ -226,7 +233,7 @@ open class ClientPresenceService( last_heartbeat AS lastHeartbeat, last_activity AS lastActivity FROM client_presence - WHERE 1=1 + WHERE last_heartbeat >= :minHeartbeat """.trimIndent(), ) if (type != null) { @@ -234,12 +241,28 @@ open class ClientPresenceService( } append(" ORDER BY last_heartbeat DESC") } - val args = if (type != null) mapOf("clientType" to type) else emptyMap() + val args = mutableMapOf("minHeartbeat" to minHeartbeat) + if (type != null) { + args["clientType"] = type + } val rows = jdbcDao.queryForList(sql, args) val now = LocalDateTime.now() return rows.map { row -> enrichWithStatus(row, now) } } + @Transactional + open fun purgeStalePresence(): Int { + val days = livePresenceRetentionDays.coerceAtLeast(1) + val cutoff = LocalDateTime.now().minusDays(days) + return jdbcDao.executeUpdate( + """ + DELETE FROM client_presence + WHERE last_heartbeat < :cutoff + """.trimIndent(), + mapOf("cutoff" to cutoff), + ) + } + @Transactional(readOnly = true) open fun listHistory( from: LocalDateTime, diff --git a/src/main/java/com/ffii/fpsms/modules/monitoring/service/MonitoringRetentionService.kt b/src/main/java/com/ffii/fpsms/modules/monitoring/service/MonitoringRetentionService.kt index ed34851..cadfc01 100644 --- a/src/main/java/com/ffii/fpsms/modules/monitoring/service/MonitoringRetentionService.kt +++ b/src/main/java/com/ffii/fpsms/modules/monitoring/service/MonitoringRetentionService.kt @@ -10,6 +10,7 @@ import java.time.LocalDateTime @Service open class MonitoringRetentionService( private val jdbcDao: JdbcDao, + private val clientPresenceService: ClientPresenceService, ) { private val log = LoggerFactory.getLogger(MonitoringRetentionService::class.java) @@ -36,19 +37,23 @@ open class MonitoringRetentionService( mapOf("cutoff" to cutoff), ) - if (deviceDeleted > 0 || printerDeleted > 0) { + val stalePresenceDeleted = clientPresenceService.purgeStalePresence() + + if (deviceDeleted > 0 || printerDeleted > 0 || stalePresenceDeleted > 0) { log.info( - "Monitoring retention: deleted {} device event(s) and {} printer event(s) older than {} day(s) (before {})", + "Monitoring retention: deleted {} device event(s), {} printer event(s) older than {} day(s) (before {}), and {} stale presence row(s)", deviceDeleted, printerDeleted, days, cutoff, + stalePresenceDeleted, ) } return mapOf( "deviceEventsDeleted" to deviceDeleted, "printerEventsDeleted" to printerDeleted, + "stalePresenceDeleted" to stalePresenceDeleted, "retentionDays" to days.toInt(), ) } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 81f0b94..74ce2bc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -81,6 +81,8 @@ fpsms: idle-threshold-sec: 300 history-sample-sec: 60 history-max-range-days: 30 + # Live monitor omits devices with no heartbeat longer than this; stale rows are purged daily. + live-presence-retention-days: 7 offline-scan-interval-ms: 60000 printer-monitor: scan-interval-ms: 120000