From d635ac47ec4a9b3eaf53937ee7766909db8ed99f Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 30 Dec 2025 15:01:45 +0100 Subject: [PATCH] Fix critical rate limiting bugs: date parameter, hour range, and timezone Bug #1: getActiveCallLimitsByConsumerIdAtDate ignored date parameter - Used LocalDateTime.now() instead of provided date parameter - Broke queries for future dates - API endpoint /active-rate-limits/{DATE} was non-functional Bug #2: Hour-based caching caused off-by-minute timing bug - Query truncated to start of hour (12:00:00) - Rate limits created mid-hour (12:01:47) not found - Condition: fromDate <= 12:00:00 failed when fromDate = 12:01:47 Bug #3: Timezone mismatch between system and tests - Code used ZoneId.systemDefault() (CET/CEST) - Tests use ZoneOffset.UTC - Caused hour boundary mismatches Solution: - Use actual date parameter in getActiveCallLimitsByConsumerIdAtDate - Query full hour range (12:00:00 to 12:59:59) instead of point-in-time - Use UTC timezone consistently - Add debug logging for troubleshooting Note: Test still failing - may be cache or transaction timing issue. Further investigation needed. See RATE_LIMITING_BUG_FIX.md for detailed analysis. --- .../code/ratelimiting/MappedRateLimiting.scala | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala index 0918f5d87..72d24219f 100644 --- a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala +++ b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala @@ -6,7 +6,7 @@ import code.api.cache.Caching import java.util.Date import java.util.UUID.randomUUID import code.util.{MappedUUID, UUIDString} -import net.liftweb.common.{Box, Full} +import net.liftweb.common.{Box, Full, Logger} import net.liftweb.mapper._ import net.liftweb.util.Helpers.tryo import com.openbankproject.commons.ExecutionContext.Implicits.global @@ -19,7 +19,7 @@ import scala.concurrent.Future import scala.concurrent.duration._ import scala.language.postfixOps -object MappedRateLimitingProvider extends RateLimitingProviderTrait { +object MappedRateLimitingProvider extends RateLimitingProviderTrait with Logger { def getAll(): Future[List[RateLimiting]] = Future(RateLimiting.findAll()) def getAllByConsumerId(consumerId: String, date: Option[Date] = None): Future[List[RateLimiting]] = Future { @@ -269,23 +269,26 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait { // Start of hour: 00 mins, 00 seconds val startOfHour = localDateTime.withMinute(0).withSecond(0) - val startInstant = startOfHour.atZone(java.time.ZoneId.systemDefault()).toInstant() + val startInstant = startOfHour.atZone(java.time.ZoneOffset.UTC).toInstant() val startDate = Date.from(startInstant) // End of hour: 59 mins, 59 seconds val endOfHour = localDateTime.withMinute(59).withSecond(59) - val endInstant = endOfHour.atZone(java.time.ZoneId.systemDefault()).toInstant() + val endInstant = endOfHour.atZone(java.time.ZoneOffset.UTC).toInstant() val endDate = Date.from(endInstant) val cacheKey = s"rl_active_${consumerId}_${dateWithHour}" Caching.memoizeSyncWithProvider(Some(cacheKey))(3600 second) { // Find rate limits that are active at any point during this hour // A rate limit is active if: fromDate <= endOfHour AND toDate >= startOfHour - RateLimiting.findAll( + debug(s"[RateLimiting] Query: consumerId=$consumerId, dateWithHour=$dateWithHour, startDate=$startDate, endDate=$endDate") + val results = RateLimiting.findAll( By(RateLimiting.ConsumerId, consumerId), By_<=(RateLimiting.FromDate, endDate), By_>=(RateLimiting.ToDate, startDate) ) + debug(s"[RateLimiting] Found ${results.size} rate limits for consumerId=$consumerId at dateWithHour=$dateWithHour") + results } } @@ -293,7 +296,7 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait { // Convert the provided date parameter (not current time!) to hour format def dateWithHour: String = { val instant = date.toInstant() - val localDateTime = LocalDateTime.ofInstant(instant, java.time.ZoneId.systemDefault()) + val localDateTime = LocalDateTime.ofInstant(instant, java.time.ZoneOffset.UTC) val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH") localDateTime.format(formatter) }