From 4b494446b6ba8a01c0eca0f3d8dfef0c0161b03f Mon Sep 17 00:00:00 2001 From: Lucas Burson Date: Fri, 12 Jan 2024 01:52:32 -0600 Subject: [PATCH] Enhance Country Leaderboard Availability During Refresh The initial approach to refreshing the country leaderboard, which involved deleting all entries at once, led to data unavailability and HTTP 502 errors for users during updates. We've revised the process to sequentially delete and rebuild entries for each country and time duration. This adjustment ensures that leaderboard data remains accessible to users while updates for other countries are in progress. --- app/org/maproulette/jobs/SchedulerActor.scala | 174 +++++++++++------- 1 file changed, 111 insertions(+), 63 deletions(-) diff --git a/app/org/maproulette/jobs/SchedulerActor.scala b/app/org/maproulette/jobs/SchedulerActor.scala index 87ab0a0d..d2422045 100644 --- a/app/org/maproulette/jobs/SchedulerActor.scala +++ b/app/org/maproulette/jobs/SchedulerActor.scala @@ -486,76 +486,116 @@ class SchedulerActor @Inject() ( val start = System.currentTimeMillis logger.info(s"Scheduled Task '$action': Starting run") - db.withConnection { implicit c => - // Clear TABLEs - SQL("DELETE FROM user_leaderboard WHERE country_code IS NOT NULL").executeUpdate() - SQL("DELETE FROM user_top_challenges WHERE country_code IS NOT NULL").executeUpdate() - - val countryCodeMap = boundingBoxFinder.boundingBoxforAll() - for ((countryCode, boundingBox) <- countryCodeMap) { - SQL( - LeaderboardHelper.rebuildChallengesLeaderboardSQLCountry( - SchedulerActor.ONE_MONTH, - countryCode, - boundingBox, - config - ) - ).executeUpdate() - SQL( - LeaderboardHelper.rebuildChallengesLeaderboardSQLCountry( - SchedulerActor.THREE_MONTHS, - countryCode, - boundingBox, - config - ) - ).executeUpdate() - SQL( - LeaderboardHelper.rebuildChallengesLeaderboardSQLCountry( - SchedulerActor.SIX_MONTHS, - countryCode, - boundingBox, - config - ) - ).executeUpdate() - SQL( - LeaderboardHelper.rebuildChallengesLeaderboardSQLCountry( - SchedulerActor.TWELVE_MONTHS, - countryCode, - boundingBox, - config - ) - ).executeUpdate() + def deleteAndUpdateCountryLeaderboardForTimePeriod( + monthDuration: Int, + countryCode: String, + boundingBox: String + ): Unit = { + logger.info( + s"Scheduled Task '$action': updating user_leaderboard monthDuration=$monthDuration countryCode=$countryCode" + ) + db.withConnection { implicit c => + // Delete the existing entries for the country and time period SQL( - LeaderboardHelper.rebuildChallengesLeaderboardSQLCountry( - SchedulerActor.ALL_TIME, - countryCode, - boundingBox, - config - ) - ).executeUpdate() + s"DELETE FROM user_leaderboard WHERE country_code = {countryCode} AND month_duration = {monthDuration}" + ).on(Symbol("countryCode") -> countryCode, Symbol("monthDuration") -> monthDuration) + .executeUpdate() + // Insert the new entries for the country and time period SQL( - LeaderboardHelper.rebuildTopChallengesSQLCountry( - SchedulerActor.TWELVE_MONTHS, - countryCode, - boundingBox, - config - ) - ).executeUpdate() - SQL( - LeaderboardHelper.rebuildTopChallengesSQLCountry( - SchedulerActor.ALL_TIME, - countryCode, - boundingBox, - config - ) + LeaderboardHelper + .rebuildChallengesLeaderboardSQLCountry(monthDuration, countryCode, boundingBox, config) ).executeUpdate() } - - val totalTime = System.currentTimeMillis - start logger.info( - s"Scheduled Task '$action': Finished run. Time spent: ${String.format("%1d", totalTime)}ms" + s"Scheduled Task '$action': finished updating user_leaderboard monthDuration=$monthDuration countryCode=$countryCode" ) } + + // TODO(ljdelight): If the loop order is inverted where each country loops over the monthDuration, will this be faster? + // The database may be able to more effectively cache the per-country results vs the time-based outer loop. + SchedulerActor.MONTH_DURATIONS.foreach(monthDuration => { + val countryCodeMap = boundingBoxFinder.boundingBoxforAll() + for ((countryCode, boundingBox) <- countryCodeMap) { + try { + deleteAndUpdateCountryLeaderboardForTimePeriod(monthDuration, countryCode, boundingBox) + } catch { + // If an exception occurs, log it and continue to the next country + case e: Exception => + logger.error( + s"Scheduled Task '$action': Failed to update user_leaderboard monthDuration=$monthDuration countryCode=$countryCode", + e + ) + } + } + }) + + db.withConnection { implicit c => + val countryCodeMap = boundingBoxFinder.boundingBoxforAll() + for ((countryCode, boundingBox) <- countryCodeMap) { + try { + logger.info( + s"Scheduled Task '$action': updating user_top_challenges monthDuration=12 countryCode=$countryCode" + ) + // Delete the existing entries for the country and time period + SQL( + "DELETE FROM user_top_challenges WHERE country_code = {countryCode} AND month_duration = {monthDuration}" + ).on( + Symbol("countryCode") -> countryCode, + Symbol("monthDuration") -> 12 + ) + .executeUpdate() + + SQL( + LeaderboardHelper.rebuildTopChallengesSQLCountry( + SchedulerActor.TWELVE_MONTHS, + countryCode, + boundingBox, + config + ) + ).executeUpdate() + } catch { + case e: Exception => + logger.error( + s"Scheduled Task '$action': Error updating user_top_challenges for monthDuration=12 countryCode=$countryCode", + e + ) + } + + try { + logger.info( + s"Scheduled Task '$action': updating user_top_challenges monthDuration=-1 countryCode=$countryCode" + ) + // Delete the existing entries for the country and time period + SQL( + "DELETE FROM user_top_challenges WHERE country_code = {countryCode} AND month_duration = {monthDuration}" + ).on( + Symbol("countryCode") -> countryCode, + Symbol("monthDuration") -> -1 + ) + .executeUpdate() + + SQL( + LeaderboardHelper.rebuildTopChallengesSQLCountry( + SchedulerActor.ALL_TIME, + countryCode, + boundingBox, + config + ) + ).executeUpdate() + } catch { + case e: Exception => + logger.error( + s"Scheduled Task '$action': Error updating user_top_challenges for monthDuration=12 countryCode=$countryCode", + e + ) + } + } + } + + val totalTime = System.currentTimeMillis - start + logger.info( + s"Scheduled Task '$action': Finished run. Time spent: ${String.format("%1d", totalTime)}ms" + ) } def sendImmediateNotificationEmails(action: String) = { @@ -846,6 +886,14 @@ object SchedulerActor { private val TWELVE_MONTHS = 12 private val ALL_TIME = -1 + private val MONTH_DURATIONS = List( + SchedulerActor.ONE_MONTH, + SchedulerActor.THREE_MONTHS, + SchedulerActor.SIX_MONTHS, + SchedulerActor.TWELVE_MONTHS, + SchedulerActor.ALL_TIME + ).sorted + def props = Props[SchedulerActor]() case class RunJob(name: String, action: String = "")