feat(core): add redis lock during insertion of event in event table during initial attempt of outgoing webhook delivery (#7579)

Co-authored-by: Debarati Ghatak <debarati.ghatak@Debarati-Ghatak-FW569NC29L.local>
This commit is contained in:
Debarati Ghatak
2025-04-28 18:43:32 +05:30
committed by GitHub
parent d2ff66bb7e
commit eed84fe63b
11 changed files with 177 additions and 9 deletions

View File

@ -2,6 +2,8 @@ use std::marker::PhantomData;
use common_utils::{errors::CustomResult, ext_traits::ValueExt};
use error_stack::ResultExt;
use redis_interface as redis;
use router_env::tracing;
use crate::{
core::{
@ -9,6 +11,8 @@ use crate::{
payments::helpers,
},
db::{get_and_deserialize_key, StorageInterface},
errors::RouterResult,
routes::app::SessionStateInfo,
services::logger,
types::{self, api, domain, PaymentAddress},
SessionState,
@ -154,3 +158,106 @@ pub(crate) fn get_idempotent_event_id(
pub(crate) fn generate_event_id() -> String {
common_utils::generate_time_ordered_id("evt")
}
const WEBHOOK_LOCK_PREFIX: &str = "WEBHOOK_LOCK";
pub(super) async fn perform_redis_lock<A>(
state: &A,
unique_locking_key: &str,
merchant_id: common_utils::id_type::MerchantId,
) -> RouterResult<Option<String>>
where
A: SessionStateInfo,
{
let lock_value: String = uuid::Uuid::new_v4().to_string();
let redis_conn = state
.store()
.get_redis_conn()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error connecting to redis")?;
let redis_locking_key = format!(
"{}_{}_{}",
WEBHOOK_LOCK_PREFIX,
merchant_id.get_string_repr(),
unique_locking_key
);
let redis_lock_expiry_seconds = state.conf().webhooks.redis_lock_expiry_seconds;
let redis_lock_result = redis_conn
.set_key_if_not_exists_with_expiry(
&redis_locking_key.as_str().into(),
lock_value.clone(),
Some(i64::from(redis_lock_expiry_seconds)),
)
.await;
match redis_lock_result {
Ok(redis::SetnxReply::KeySet) => {
logger::info!("Lock acquired for for {redis_locking_key}");
Ok(Some(lock_value))
}
Ok(redis::SetnxReply::KeyNotSet) => {
logger::info!("Lock already held for {redis_locking_key}");
Ok(None)
}
Err(err) => Err(err
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error acquiring redis lock")),
}
}
pub(super) async fn free_redis_lock<A>(
state: &A,
unique_locking_key: &str,
merchant_id: common_utils::id_type::MerchantId,
lock_value: Option<String>,
) -> RouterResult<()>
where
A: SessionStateInfo,
{
let redis_conn = state
.store()
.get_redis_conn()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error connecting to redis")?;
let redis_locking_key = format!(
"{}_{}_{}",
WEBHOOK_LOCK_PREFIX,
merchant_id.get_string_repr(),
unique_locking_key
);
match redis_conn
.get_key::<Option<String>>(&redis_locking_key.as_str().into())
.await
{
Ok(val) => {
if val == lock_value {
match redis_conn
.delete_key(&redis_locking_key.as_str().into())
.await
{
Ok(redis::types::DelReply::KeyDeleted) => {
logger::info!("Lock freed {redis_locking_key}");
tracing::Span::current().record("redis_lock_released", redis_locking_key);
Ok(())
}
Ok(redis::types::DelReply::KeyNotDeleted) => Err(
errors::ApiErrorResponse::InternalServerError,
)
.attach_printable("Status release lock called but key is not found in redis"),
Err(error) => Err(error)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error while deleting redis key"),
}
} else {
Err(errors::ApiErrorResponse::InternalServerError)
.attach_printable("The redis value which acquired the lock is not equal to the redis value requesting for releasing the lock")
}
}
Err(error) => Err(error)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error while deleting redis key"),
}
}