Using UTC and per hour for Rate Limiting

This commit is contained in:
simonredfern 2025-12-30 19:17:02 +01:00
parent efc1868fd4
commit 284743da16
7 changed files with 16 additions and 12 deletions

View File

@ -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:**

View File

@ -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
|

View File

@ -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())

View File

@ -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)

View File

@ -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 {

View File

@ -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