Skip to content

Commit 0ecffbb

Browse files
authored
fix: Reset counter store on startup when env var exists (#700)
* fix: Reset counter store on startup when env var exists * chore: PR suggestions
1 parent 1dae14b commit 0ecffbb

4 files changed

Lines changed: 176 additions & 1 deletion

File tree

src/bootstrap/config_processor.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,10 @@ where
751751
app_state.network_repository.drop_all_entries().await?;
752752
app_state.plugin_repository.drop_all_entries().await?;
753753
app_state.api_key_repository.drop_all_entries().await?;
754+
app_state
755+
.transaction_counter_store
756+
.drop_all_entries()
757+
.await?;
754758
}
755759

756760
info!("Processing config file");
@@ -1796,6 +1800,48 @@ mod tests {
17961800
Ok(())
17971801
}
17981802

1803+
#[tokio::test]
1804+
async fn test_reset_storage_on_start_clears_transaction_counter() -> Result<()> {
1805+
let config = create_minimal_test_config();
1806+
let server_config = Arc::new(create_test_server_config_with_settings(
1807+
RepositoryStorageType::InMemory,
1808+
true,
1809+
));
1810+
1811+
let app_state = ThinData(create_test_app_state());
1812+
1813+
// Seed transaction counter with a value that simulates an inflated nonce
1814+
app_state
1815+
.transaction_counter_store
1816+
.set("test-relayer-1", "0xABC", 999)
1817+
.await
1818+
.unwrap();
1819+
assert_eq!(
1820+
app_state
1821+
.transaction_counter_store
1822+
.get("test-relayer-1", "0xABC")
1823+
.await
1824+
.unwrap(),
1825+
Some(999)
1826+
);
1827+
1828+
// Process config with reset_storage_on_start = true
1829+
process_config_file(config, server_config, &app_state).await?;
1830+
1831+
// Transaction counter should have been cleared
1832+
assert_eq!(
1833+
app_state
1834+
.transaction_counter_store
1835+
.get("test-relayer-1", "0xABC")
1836+
.await
1837+
.unwrap(),
1838+
None,
1839+
"Transaction counter should be cleared when RESET_STORAGE_ON_START is true"
1840+
);
1841+
1842+
Ok(())
1843+
}
1844+
17991845
#[tokio::test]
18001846
async fn test_should_process_config_file_redis_storage_empty_repositories() -> Result<()> {
18011847
let config = create_minimal_test_config();

src/repositories/transaction_counter/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ pub trait TransactionCounterTrait {
5959

6060
async fn set(&self, relayer_id: &str, address: &str, value: u64)
6161
-> Result<(), RepositoryError>;
62+
63+
/// Remove all stored counter entries from the underlying backend.
64+
/// Intended for startup reset flows when `RESET_STORAGE_ON_START` is enabled.
65+
async fn drop_all_entries(&self) -> Result<(), RepositoryError>;
6266
}
6367

6468
/// Enum wrapper for different transaction counter repository implementations
@@ -137,6 +141,15 @@ impl TransactionCounterTrait for TransactionCounterRepositoryStorage {
137141
}
138142
}
139143
}
144+
145+
async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
146+
match self {
147+
TransactionCounterRepositoryStorage::InMemory(counter) => {
148+
counter.drop_all_entries().await
149+
}
150+
TransactionCounterRepositoryStorage::Redis(counter) => counter.drop_all_entries().await,
151+
}
152+
}
140153
}
141154

142155
#[cfg(test)]
@@ -175,4 +188,17 @@ mod tests {
175188
let new_value = repo.decrement("test_relayer", "0x1234").await.unwrap();
176189
assert_eq!(new_value, 100);
177190
}
191+
192+
#[tokio::test]
193+
async fn test_enum_wrapper_drop_all_entries() {
194+
let repo = TransactionCounterRepositoryStorage::new_in_memory();
195+
196+
repo.set("relayer_1", "0x1234", 100).await.unwrap();
197+
repo.set("relayer_2", "0x5678", 200).await.unwrap();
198+
199+
repo.drop_all_entries().await.unwrap();
200+
201+
assert_eq!(repo.get("relayer_1", "0x1234").await.unwrap(), None);
202+
assert_eq!(repo.get("relayer_2", "0x5678").await.unwrap(), None);
203+
}
178204
}

src/repositories/transaction_counter/transaction_counter_in_memory.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ impl TransactionCounterTrait for InMemoryTransactionCounter {
6666
.insert((relayer_id.to_string(), address.to_string()), value);
6767
Ok(())
6868
}
69+
70+
async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
71+
self.store.clear();
72+
Ok(())
73+
}
6974
}
7075

7176
#[cfg(test)]
@@ -104,6 +109,23 @@ mod tests {
104109
assert_eq!(store.get(relayer_id, address).await.unwrap(), Some(100));
105110
}
106111

112+
#[tokio::test]
113+
async fn test_drop_all_entries() {
114+
let store = InMemoryTransactionCounter::new();
115+
116+
store.set("relayer_1", "0x1234", 100).await.unwrap();
117+
store.set("relayer_1", "0x5678", 200).await.unwrap();
118+
store.set("relayer_2", "0x1234", 300).await.unwrap();
119+
120+
assert_eq!(store.get("relayer_1", "0x1234").await.unwrap(), Some(100));
121+
122+
store.drop_all_entries().await.unwrap();
123+
124+
assert_eq!(store.get("relayer_1", "0x1234").await.unwrap(), None);
125+
assert_eq!(store.get("relayer_1", "0x5678").await.unwrap(), None);
126+
assert_eq!(store.get("relayer_2", "0x1234").await.unwrap(), None);
127+
}
128+
107129
#[tokio::test]
108130
async fn test_multiple_relayers() {
109131
let store = InMemoryTransactionCounter::new();

src/repositories/transaction_counter/transaction_counter_redis.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,54 @@ impl TransactionCounterTrait for RedisTransactionCounter {
212212
debug!(value = %value, "counter set");
213213
Ok(())
214214
}
215+
216+
async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
217+
let mut conn = self
218+
.get_connection(self.connections.primary(), "drop_all_entries")
219+
.await?;
220+
221+
let pattern = format!("{}:{}:*", self.key_prefix, COUNTER_PREFIX);
222+
debug!(pattern = %pattern, "dropping all transaction counter entries");
223+
224+
// Phase 1: Collect all matching keys without mutating the keyspace.
225+
// Deleting during SCAN can cause hash table rehashing, which may skip keys.
226+
let mut cursor: u64 = 0;
227+
let mut all_keys: Vec<String> = Vec::new();
228+
229+
loop {
230+
let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
231+
.cursor_arg(cursor)
232+
.arg("MATCH")
233+
.arg(&pattern)
234+
.arg("COUNT")
235+
.arg(100)
236+
.query_async(&mut conn)
237+
.await
238+
.map_err(|e| self.map_redis_error(e, "drop_all_entries_scan"))?;
239+
240+
all_keys.extend(keys);
241+
242+
cursor = next_cursor;
243+
if cursor == 0 {
244+
break;
245+
}
246+
}
247+
248+
// Phase 2: Batch delete all collected keys.
249+
if !all_keys.is_empty() {
250+
let mut pipe = redis::pipe();
251+
pipe.atomic();
252+
for key in &all_keys {
253+
pipe.del(key);
254+
}
255+
pipe.exec_async(&mut conn)
256+
.await
257+
.map_err(|e| self.map_redis_error(e, "drop_all_entries_delete"))?;
258+
}
259+
260+
debug!(total_deleted = %all_keys.len(), "dropped all transaction counter entries");
261+
Ok(())
262+
}
215263
}
216264

217265
#[cfg(test)]
@@ -222,6 +270,10 @@ mod tests {
222270
use uuid::Uuid;
223271

224272
async fn setup_test_repo() -> RedisTransactionCounter {
273+
setup_test_repo_with_prefix("test_counter").await
274+
}
275+
276+
async fn setup_test_repo_with_prefix(prefix: &str) -> RedisTransactionCounter {
225277
let redis_url =
226278
std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
227279
let cfg = deadpool_redis::Config::from_url(&redis_url);
@@ -235,7 +287,7 @@ mod tests {
235287
);
236288
let connections = Arc::new(RedisConnections::new_single_pool(pool));
237289

238-
RedisTransactionCounter::new(connections, "test_counter".to_string())
290+
RedisTransactionCounter::new(connections, prefix.to_string())
239291
.expect("Failed to create Redis transaction counter")
240292
}
241293

@@ -369,6 +421,35 @@ mod tests {
369421
assert_eq!(repo.get(&relayer_2, &address_1).await.unwrap(), Some(300));
370422
}
371423

424+
#[tokio::test]
425+
#[ignore = "Requires active Redis instance"]
426+
async fn test_drop_all_entries() {
427+
let prefix = format!("test_drop_{}", uuid::Uuid::new_v4());
428+
let repo = setup_test_repo_with_prefix(&prefix).await;
429+
let relayer_1 = uuid::Uuid::new_v4().to_string();
430+
let relayer_2 = uuid::Uuid::new_v4().to_string();
431+
let address_1 = uuid::Uuid::new_v4().to_string();
432+
let address_2 = uuid::Uuid::new_v4().to_string();
433+
434+
// Set up multiple counters
435+
repo.set(&relayer_1, &address_1, 100).await.unwrap();
436+
repo.set(&relayer_1, &address_2, 200).await.unwrap();
437+
repo.set(&relayer_2, &address_1, 300).await.unwrap();
438+
439+
// Verify they exist
440+
assert_eq!(repo.get(&relayer_1, &address_1).await.unwrap(), Some(100));
441+
assert_eq!(repo.get(&relayer_1, &address_2).await.unwrap(), Some(200));
442+
assert_eq!(repo.get(&relayer_2, &address_1).await.unwrap(), Some(300));
443+
444+
// Drop all
445+
repo.drop_all_entries().await.unwrap();
446+
447+
// Verify all are gone
448+
assert_eq!(repo.get(&relayer_1, &address_1).await.unwrap(), None);
449+
assert_eq!(repo.get(&relayer_1, &address_2).await.unwrap(), None);
450+
assert_eq!(repo.get(&relayer_2, &address_1).await.unwrap(), None);
451+
}
452+
372453
#[tokio::test]
373454
#[ignore = "Requires active Redis instance"]
374455
async fn test_concurrent_get_and_increment() {

0 commit comments

Comments
 (0)