Skip to content

Commit 260c2b6

Browse files
collins-wtirumerla
andauthored
chore: Improve metrics (#649)
* chore: refactor for transcations metrics * chore: stop negatives values on metrics * chore: Fix non atomic decrements with concurrency safe CAS --------- Co-authored-by: tirumerla <tirumerla@gmail.com>
1 parent e7ad9b8 commit 260c2b6

1 file changed

Lines changed: 77 additions & 23 deletions

File tree

src/repositories/transaction/transaction_redis.rs

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::repositories::{
1717
use crate::utils::RedisConnections;
1818
use async_trait::async_trait;
1919
use chrono::Utc;
20-
use redis::AsyncCommands;
20+
use redis::{AsyncCommands, Script};
2121
use std::fmt;
2222
use std::sync::Arc;
2323
use tracing::{debug, error, warn};
@@ -1297,17 +1297,16 @@ impl TransactionRepository for RedisTransactionRepository {
12971297
const MAX_RETRIES: u32 = 3;
12981298
const BACKOFF_MS: u64 = 100;
12991299

1300-
// Fetch the original transaction state ONCE before retrying.
1301-
// This is critical: if conn.set() succeeds but update_indexes() fails,
1302-
// subsequent retries must still reference the original state to remove
1303-
// stale index entries. Otherwise, get_by_id() returns the already-updated
1304-
// record and update_indexes() skips removing the old indexes.
1305-
let original_tx = self.get_by_id(tx_id.clone()).await?;
1300+
// Optimistic CAS: only apply update if the current stored value still matches the
1301+
// expected pre-update value. This avoids duplicate status metric updates on races.
1302+
let mut original_tx = self.get_by_id(tx_id.clone()).await?;
13061303
let mut updated_tx = original_tx.clone();
13071304
updated_tx.apply_partial_update(update.clone());
13081305

13091306
let key = self.tx_key(&updated_tx.relayer_id, &tx_id);
1310-
let value = self.serialize_entity(&updated_tx, |t| &t.id, "transaction")?;
1307+
let mut original_value = self.serialize_entity(&original_tx, |t| &t.id, "transaction")?;
1308+
let mut updated_value = self.serialize_entity(&updated_tx, |t| &t.id, "transaction")?;
1309+
let mut data_updated = false;
13111310

13121311
let mut last_error = None;
13131312

@@ -1327,19 +1326,68 @@ impl TransactionRepository for RedisTransactionRepository {
13271326
}
13281327
};
13291328

1330-
// Try to update transaction data
1331-
let result: Result<(), _> = conn.set(&key, &value).await;
1332-
match result {
1333-
Ok(_) => {}
1334-
Err(e) => {
1329+
if !data_updated {
1330+
let cas_script = Script::new(
1331+
r#"
1332+
local current = redis.call('GET', KEYS[1])
1333+
if not current then
1334+
return -1
1335+
end
1336+
if current == ARGV[1] then
1337+
redis.call('SET', KEYS[1], ARGV[2])
1338+
return 1
1339+
end
1340+
return 0
1341+
"#,
1342+
);
1343+
1344+
let cas_result: i32 = match cas_script
1345+
.key(&key)
1346+
.arg(&original_value)
1347+
.arg(&updated_value)
1348+
.invoke_async(&mut conn)
1349+
.await
1350+
{
1351+
Ok(result) => result,
1352+
Err(e) => {
1353+
if attempt < MAX_RETRIES - 1 {
1354+
warn!(tx_id = %tx_id, attempt = %attempt, error = %e, "failed CAS transaction update, retrying");
1355+
last_error = Some(self.map_redis_error(e, "partial_update_cas"));
1356+
tokio::time::sleep(tokio::time::Duration::from_millis(BACKOFF_MS))
1357+
.await;
1358+
continue;
1359+
}
1360+
return Err(self.map_redis_error(e, "partial_update_cas"));
1361+
}
1362+
};
1363+
1364+
if cas_result == -1 {
1365+
return Err(RepositoryError::NotFound(format!(
1366+
"Transaction with ID {} not found",
1367+
tx_id
1368+
)));
1369+
}
1370+
1371+
if cas_result == 0 {
13351372
if attempt < MAX_RETRIES - 1 {
1336-
warn!(tx_id = %tx_id, attempt = %attempt, error = %e, "failed to set transaction data, retrying");
1337-
last_error = Some(self.map_redis_error(e, "partial_update"));
1373+
warn!(tx_id = %tx_id, attempt = %attempt, "concurrent transaction update detected, rebasing retry");
1374+
original_tx = self.get_by_id(tx_id.clone()).await?;
1375+
updated_tx = original_tx.clone();
1376+
updated_tx.apply_partial_update(update.clone());
1377+
original_value =
1378+
self.serialize_entity(&original_tx, |t| &t.id, "transaction")?;
1379+
updated_value =
1380+
self.serialize_entity(&updated_tx, |t| &t.id, "transaction")?;
13381381
tokio::time::sleep(tokio::time::Duration::from_millis(BACKOFF_MS)).await;
13391382
continue;
13401383
}
1341-
return Err(self.map_redis_error(e, "partial_update"));
1384+
return Err(RepositoryError::TransactionFailure(format!(
1385+
"Concurrent update conflict for transaction {}",
1386+
tx_id
1387+
)));
13421388
}
1389+
1390+
data_updated = true;
13431391
}
13441392

13451393
// Try to update indexes with the original pre-update state
@@ -1380,12 +1428,17 @@ impl TransactionRepository for RedisTransactionRepository {
13801428

13811429
// Track status distribution (update gauge when status changes)
13821430
if original_tx.status != *new_status {
1383-
// Decrement old status
1431+
// Decrement old status and clamp to zero to avoid negative gauges.
13841432
let old_status = &original_tx.status;
13851433
let old_status_str = format!("{old_status:?}").to_lowercase();
1386-
TRANSACTIONS_BY_STATUS
1387-
.with_label_values(&[relayer_id, &network_type, &old_status_str])
1388-
.dec();
1434+
let old_status_gauge = TRANSACTIONS_BY_STATUS.with_label_values(&[
1435+
relayer_id,
1436+
&network_type,
1437+
&old_status_str,
1438+
]);
1439+
let clamped_value = (old_status_gauge.get() - 1.0).max(0.0);
1440+
old_status_gauge.set(clamped_value);
1441+
13891442
// Increment new status
13901443
let new_status_str = format!("{new_status:?}").to_lowercase();
13911444
TRANSACTIONS_BY_STATUS
@@ -2832,10 +2885,11 @@ mod tests {
28322885
let tolerance = Duration::minutes(5);
28332886

28342887
assert!(
2835-
duration_from_before >= expected_duration - tolerance &&
2836-
duration_from_before <= expected_duration + tolerance,
2888+
duration_from_before >= expected_duration - tolerance
2889+
&& duration_from_before <= expected_duration + tolerance,
28372890
"delete_at should be approximately 6 hours from now for status: {:?}. Duration: {:?}",
2838-
status, duration_from_before
2891+
status,
2892+
duration_from_before
28392893
);
28402894
}
28412895

0 commit comments

Comments
 (0)