From cd52665f3596feb53eeccd1c7dc4158c1fe68300 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sat, 27 Dec 2025 07:12:29 +0100 Subject: [PATCH] RateLimitingUtil adding status to interpret redis key result --- .../scala/code/api/util/RateLimitingUtil.scala | 13 +++++++++++-- .../scala/code/api/v3_1_0/JSONFactory3.1.0.scala | 5 +++-- .../scala/code/api/v6_0_0/APIMethods600.scala | 5 ++++- .../scala/code/api/v6_0_0/JSONFactory6.0.0.scala | 15 +++++++-------- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala index 3cb012351..37a167258 100644 --- a/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala +++ b/obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala @@ -228,7 +228,7 @@ object RateLimitingUtil extends MdcLoggable { def consumerRateLimitState(consumerKey: String): immutable.Seq[((Option[Long], Option[Long], String), LimitCallPeriod)] = { - def getCallCounterForPeriod(consumerKey: String, period: LimitCallPeriod): ((Option[Long], Option[Long]), LimitCallPeriod) = { + def getCallCounterForPeriod(consumerKey: String, period: LimitCallPeriod): ((Option[Long], Option[Long], String), LimitCallPeriod) = { val key = createUniqueKey(consumerKey, period) // get TTL @@ -256,7 +256,16 @@ object RateLimitingUtil extends MdcLoggable { case None => Some(0L) // Redis unavailable -> 0 TTL } - ((calls, normalizedTtl), period) + + // Calculate status based on Redis TTL response + val status = ttlOpt match { + case Some(ttl) if ttl > 0 => "ACTIVE" // Counter running with time remaining + case Some(-2) => "NO_COUNTER" // Key does not exist, never been set + case Some(ttl) if ttl <= 0 => "EXPIRED" // Key expired (TTL=0) or no expiry (TTL=-1) + case None => "REDIS_UNAVAILABLE" // Redis connection failed + } + + ((calls, normalizedTtl, status), period) } getCallCounterForPeriod(consumerKey, RateLimitingPeriod.PER_SECOND) :: diff --git a/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala b/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala index 1d8897a1e..a640f7efa 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala @@ -809,7 +809,7 @@ object JSONFactory310{ def createBadLoginStatusJson(badLoginStatus: BadLoginAttempt) : BadLoginStatusJson = { BadLoginStatusJson(badLoginStatus.username,badLoginStatus.badAttemptsSinceLastSuccessOrReset, badLoginStatus.lastFailureDate) } - def createCallLimitJson(consumer: Consumer, rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]) : CallLimitJson = { + def createCallLimitJson(consumer: Consumer, rateLimits: List[((Option[Long], Option[Long], String), LimitCallPeriod)]) : CallLimitJson = { val redisRateLimit = rateLimits match { case Nil => None case _ => @@ -817,7 +817,8 @@ object JSONFactory310{ rateLimits.filter(_._2 == period) match { case x :: Nil => x._1 match { - case (Some(x), Some(y)) => Some(RateLimit(Some(x), Some(y))) + case (Some(x), Some(y), _) => Some(RateLimit(Some(x), Some(y))) + // Ignore status field for v3.1.0 API (backward compatibility) case _ => None } 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 694cd0af2..b9c10d22e 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 @@ -236,7 +236,10 @@ trait APIMethods600 { | |**Status Values:** |- `ACTIVE`: Rate limit counter is active and tracking calls. Both `calls_made` and `reset_in_seconds` will have numeric values. - |- `UNKNOWN`: Data is not available. This could mean the rate limit period has expired, no rate limit is configured, or the data cannot be retrieved. Both `calls_made` and `reset_in_seconds` will be null. + |- `NO_COUNTER`: Key does not exist - the consumer has not made any API calls in this time period yet. + |- `EXPIRED`: The rate limit counter has expired (TTL reached 0). The counter will be recreated on the next API call. + |- `REDIS_UNAVAILABLE`: Cannot retrieve data from Redis. This indicates a system connectivity issue. + |- `DATA_MISSING`: Unexpected error - period data is missing from the response. This should not occur under normal circumstances. | |${userAuthenticationMessage(true)} | diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index 55eee8026..2d55641c1 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -402,19 +402,18 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { def createRedisCallCountersJson( // Convert list to map for easy lookup by period - rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)] + rateLimits: List[((Option[Long], Option[Long], String), LimitCallPeriod)] ): RedisCallCountersJsonV600 = { - val grouped: Map[LimitCallPeriod, (Option[Long], Option[Long])] = + val grouped: Map[LimitCallPeriod, (Option[Long], Option[Long], String)] = rateLimits.map { case (limits, period) => period -> limits }.toMap def getCallCounterForPeriod(period: RateLimitingPeriod.Value): RateLimitV600 = grouped.get(period) match { - // ACTIVE: Both calls and TTL exist, and TTL > 0 (key has time remaining) - // UNKNOWN: Missing data, TTL <= 0 (expired), or Redis unavailable - case Some((Some(calls), Some(ttl))) if ttl > 0 => - RateLimitV600(Some(calls), Some(ttl), "ACTIVE") + // Use status calculated by RateLimitingUtil (ACTIVE, NO_COUNTER, EXPIRED, REDIS_UNAVAILABLE) + case Some((calls, ttl, status)) => + RateLimitV600(calls, ttl, status) case _ => - RateLimitV600(None, None, "UNKNOWN") + RateLimitV600(None, None, "DATA_MISSING") } RedisCallCountersJsonV600( @@ -591,7 +590,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { } def createActiveCallLimitsJsonV600FromCallLimit( - + rateLimit: code.api.util.RateLimitingJson.CallLimit, rateLimitIds: List[String], activeDate: java.util.Date