From 284743da160e94dc2db1599f7a7b64490f0adad6 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 30 Dec 2025 19:17:02 +0100 Subject: [PATCH] Using UTC and per hour for Rate Limiting --- .../docs/introductory_system_documentation.md | 4 ++-- obp-api/src/main/scala/code/api/util/Glossary.scala | 4 ++-- .../src/main/scala/code/api/v6_0_0/APIMethods600.scala | 6 ++++-- .../scala/code/ratelimiting/MappedRateLimiting.scala | 10 ++++++---- .../main/scala/code/ratelimiting/RateLimiting.scala | 2 +- .../test/scala/code/api/v6_0_0/RateLimitsTest.scala | 2 +- test-results/warning_analysis.tmp | 0 7 files changed, 16 insertions(+), 12 deletions(-) delete mode 100644 test-results/warning_analysis.tmp diff --git a/obp-api/src/main/resources/docs/introductory_system_documentation.md b/obp-api/src/main/resources/docs/introductory_system_documentation.md index bb9e3566b..e48843119 100644 --- a/obp-api/src/main/resources/docs/introductory_system_documentation.md +++ b/obp-api/src/main/resources/docs/introductory_system_documentation.md @@ -2848,9 +2848,9 @@ Query active rate limits for a specific hour: GET /obp/v6.0.0/management/consumers/CONSUMER_ID/active-rate-limits/DATE_WITH_HOUR ``` -Where `DATE_WITH_HOUR` is in format `YYYY-MM-DD-HH` (e.g., `2025-12-31-13` for hour 13:00-13:59 on Dec 31, 2025). +Where `DATE_WITH_HOUR` is in format `YYYY-MM-DD-HH` in **UTC timezone** (e.g., `2025-12-31-13` for hour 13:00-13:59 UTC on Dec 31, 2025). -Rate limits are cached and queried at hour-level granularity for performance. +Rate limits are cached and queried at hour-level granularity for performance. All hours are interpreted in UTC for consistency. **Rate Limit Headers:** diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index 0975a3bba..cde7dd1dd 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -303,11 +303,11 @@ object Glossary extends MdcLoggable { |GET /obp/v6.0.0/management/consumers/{CONSUMER_ID}/active-rate-limits/{DATE_WITH_HOUR} |``` | - |Where `DATE_WITH_HOUR` is in format `YYYY-MM-DD-HH` (e.g., `2025-12-31-13` for hour 13:00-13:59 on Dec 31, 2025). + |Where `DATE_WITH_HOUR` is in format `YYYY-MM-DD-HH` in **UTC timezone** (e.g., `2025-12-31-13` for hour 13:00-13:59 UTC on Dec 31, 2025). | |Returns the aggregated active rate limits for the specified hour, including which rate limit records contributed to the totals. | - |Rate limits are cached and queried at hour-level granularity for performance. + |Rate limits are cached and queried at hour-level granularity for performance. All hours are interpreted in UTC for consistency across all servers. | |### System Defaults | diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index b1346dbda..c557aa8f5 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -469,7 +469,9 @@ trait APIMethods600 { | |See ${Glossary.getGlossaryItemLink("Rate Limiting")} for more details on how rate limiting works. | - |Date format: YYYY-MM-DD-HH (e.g. 2025-12-31-13 for hour 13:00-13:59 on Dec 31, 2025) + |Date format: YYYY-MM-DD-HH in UTC timezone (e.g. 2025-12-31-13 for hour 13:00-13:59 UTC on Dec 31, 2025) + | + |Note: The hour is always interpreted in UTC for consistency across all servers. | |${userAuthenticationMessage(true)} | @@ -496,7 +498,7 @@ trait APIMethods600 { (Full(u), callContext) <- authenticatedAccess(cc) _ <- NewStyle.function.hasEntitlement("", u.userId, canGetRateLimits, callContext) _ <- NewStyle.function.getConsumerByConsumerId(consumerId, callContext) - date <- NewStyle.function.tryons(s"$InvalidDateFormat Current date format is: $dateWithHourString. Please use this format: YYYY-MM-DD-HH (e.g. 2025-12-31-13 for hour 13 on Dec 31, 2025)", 400, callContext) { + date <- NewStyle.function.tryons(s"$InvalidDateFormat Current date format is: $dateWithHourString. Please use this format: YYYY-MM-DD-HH in UTC (e.g. 2025-12-31-13 for hour 13:00-13:59 UTC on Dec 31, 2025)", 400, callContext) { val formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd-HH") val localDateTime = java.time.LocalDateTime.parse(dateWithHourString, formatter) java.util.Date.from(localDateTime.atZone(java.time.ZoneOffset.UTC).toInstant()) diff --git a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala index 72d24219f..198b5bc31 100644 --- a/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala +++ b/obp-api/src/main/scala/code/ratelimiting/MappedRateLimiting.scala @@ -264,15 +264,16 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait with Logger private def getActiveCallLimitsByConsumerIdAtDateCached(consumerId: String, dateWithHour: String): List[RateLimiting] = { // Cache key uses standardized prefix: rl_active_{consumerId}_{dateWithHour} // Create Date objects for start and end of the hour from the date_with_hour string + // IMPORTANT: Hour format is in UTC for consistency across all servers val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH") val localDateTime = LocalDateTime.parse(dateWithHour, formatter) - // Start of hour: 00 mins, 00 seconds + // Start of hour: 00 mins, 00 seconds (UTC) val startOfHour = localDateTime.withMinute(0).withSecond(0) val startInstant = startOfHour.atZone(java.time.ZoneOffset.UTC).toInstant() val startDate = Date.from(startInstant) - // End of hour: 59 mins, 59 seconds + // End of hour: 59 mins, 59 seconds (UTC) val endOfHour = localDateTime.withMinute(59).withSecond(59) val endInstant = endOfHour.atZone(java.time.ZoneOffset.UTC).toInstant() val endDate = Date.from(endInstant) @@ -292,10 +293,11 @@ object MappedRateLimitingProvider extends RateLimitingProviderTrait with Logger } } - def getActiveCallLimitsByConsumerIdAtDate(consumerId: String, date: Date): Future[List[RateLimiting]] = Future { + def getActiveCallLimitsByConsumerIdAtDate(consumerId: String, dateUtc: Date): Future[List[RateLimiting]] = Future { // Convert the provided date parameter (not current time!) to hour format + // Date is timezone-agnostic (millis since epoch), we interpret it as UTC def dateWithHour: String = { - val instant = date.toInstant() + val instant = dateUtc.toInstant() val localDateTime = LocalDateTime.ofInstant(instant, java.time.ZoneOffset.UTC) val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH") localDateTime.format(formatter) diff --git a/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala b/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala index f27b106ea..01a7250b1 100644 --- a/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala +++ b/obp-api/src/main/scala/code/ratelimiting/RateLimiting.scala @@ -56,7 +56,7 @@ trait RateLimitingProviderTrait { perMonth: Option[String]): Future[Box[RateLimiting]] def deleteByRateLimitingId(rateLimitingId: String): Future[Box[Boolean]] def getByRateLimitingId(rateLimitingId: String): Future[Box[RateLimiting]] - def getActiveCallLimitsByConsumerIdAtDate(consumerId: String, date: Date): Future[List[RateLimiting]] + def getActiveCallLimitsByConsumerIdAtDate(consumerId: String, dateUtc: Date): Future[List[RateLimiting]] } trait RateLimitingTrait { diff --git a/obp-api/src/test/scala/code/api/v6_0_0/RateLimitsTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/RateLimitsTest.scala index 683e2e3ae..c6c9754cc 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/RateLimitsTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/RateLimitsTest.scala @@ -26,7 +26,7 @@ TESOBE (http://www.tesobe.com/) package code.api.v6_0_0 import code.api.util.APIUtil.OAuth._ -import code.api.util.ApiRole.{CanDeleteRateLimits, CanGetRateLimits, CanCreateRateLimits} +import code.api.util.ApiRole.{CanCreateRateLimits, CanDeleteRateLimits, CanGetRateLimits} import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0 import code.consumer.Consumers diff --git a/test-results/warning_analysis.tmp b/test-results/warning_analysis.tmp deleted file mode 100644 index e69de29bb..000000000