1use std::path::Path;
6
7use iota_sdk_types::{Address, ObjectId, Transaction};
8use serde::{Deserialize, Serialize};
9use tracing::info;
10use typed_store::{DBMapUtils, Map, TypedStoreError, rocks::DBMap};
11use uuid::Uuid;
12
13#[derive(DBMapUtils, Clone)]
21pub struct WriteAheadLog {
22 pub log: DBMap<ObjectId, Entry>,
23}
24
25#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
26pub struct Entry {
27 pub uuid: uuid::Bytes,
28 pub recipient: Address,
30 pub tx: Transaction,
31 pub retry_count: u64,
32 pub in_flight: bool,
33}
34
35impl WriteAheadLog {
36 pub(crate) fn open(path: &Path) -> Self {
37 Self::open_tables_read_write(
38 path.to_path_buf(),
39 typed_store::rocks::MetricConf::new("faucet_write_ahead_log"),
40 None,
41 None,
42 )
43 }
44
45 pub(crate) fn reserve(
49 &mut self,
50 uuid: Uuid,
51 coin: ObjectId,
52 recipient: Address,
53 tx: Transaction,
54 ) -> Result<(), TypedStoreError> {
55 if self.log.contains_key(&coin)? {
56 return Err(TypedStoreError::Serialization(format!(
59 "Duplicate WAL entry for coin {coin:?}",
60 )));
61 }
62
63 let uuid = *uuid.as_bytes();
64 self.log.insert(
65 &coin,
66 &Entry {
67 uuid,
68 recipient,
69 tx,
70 retry_count: 0,
71 in_flight: true,
72 },
73 )
74 }
75
76 pub(crate) fn reclaim(&self, coin: ObjectId) -> Result<Option<Entry>, TypedStoreError> {
80 match self.log.get(&coin) {
81 Ok(entry) => Ok(entry),
82 Err(TypedStoreError::Serialization(_)) => {
83 self.log
87 .remove(&coin)
88 .unwrap_or_else(|_| panic!("coin: {coin:?} unable to be removed from log."));
89 Ok(None)
90 }
91 Err(err) => Err(err),
92 }
93 }
94
95 pub(crate) fn commit(&mut self, coin: ObjectId) -> Result<(), TypedStoreError> {
98 self.log.remove(&coin)
99 }
100
101 pub(crate) fn increment_retry_count(&mut self, coin: ObjectId) -> Result<(), TypedStoreError> {
102 if let Some(mut entry) = self.log.get(&coin)? {
103 entry.retry_count += 1;
104 self.log.insert(&coin, &entry)?;
105 }
106 Ok(())
107 }
108
109 pub(crate) fn set_in_flight(
110 &mut self,
111 coin: ObjectId,
112 bool: bool,
113 ) -> Result<(), TypedStoreError> {
114 if let Some(mut entry) = self.log.get(&coin)? {
115 entry.in_flight = bool;
116 self.log.insert(&coin, &entry)?;
117 } else {
118 info!(
119 ?coin,
120 "Attempted to set inflight a coin that was not in the WAL."
121 );
122
123 return Err(TypedStoreError::RocksDB(format!(
124 "Coin object {coin:?} not found in WAL."
125 )));
126 }
127 Ok(())
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use iota_sdk_types::ObjectReference;
134 use iota_types::{
135 base_types::random_object_ref,
136 transaction::{TEST_ONLY_GAS_UNIT_FOR_TRANSFER, TransactionAPI},
137 };
138
139 use super::*;
140
141 #[tokio::test]
142 async fn reserve_reclaim_reclaim() {
143 let tmp_dir = iota_common::tempdir();
144 let mut wal = WriteAheadLog::open(&tmp_dir.path().join("wal"));
145
146 let uuid = Uuid::new_v4();
147 let coin = random_object_ref();
148 let (recv, tx) = random_request(coin);
149
150 assert!(wal.reserve(uuid, coin.object_id, recv, tx.clone()).is_ok());
151
152 let Some(entry) = wal.reclaim(coin.object_id).unwrap() else {
154 panic!("entry not found for {}", coin.object_id);
155 };
156
157 assert_eq!(uuid, Uuid::from_bytes(entry.uuid));
158 assert_eq!(recv, entry.recipient);
159 assert_eq!(tx, entry.tx);
160
161 let Some(entry) = wal.reclaim(coin.object_id).unwrap() else {
163 panic!("entry not found for {}", coin.object_id);
164 };
165
166 assert_eq!(uuid, Uuid::from_bytes(entry.uuid));
167 assert_eq!(recv, entry.recipient);
168 assert_eq!(tx, entry.tx);
169 }
170
171 #[tokio::test]
172 async fn test_increment_wal() {
173 let tmp_dir = iota_common::tempdir();
174 let mut wal = WriteAheadLog::open(&tmp_dir.path().join("wal"));
175 let uuid = Uuid::new_v4();
176 let coin = random_object_ref();
177 let (recv0, tx0) = random_request(coin);
178
179 wal.reserve(uuid, coin.object_id, recv0, tx0).unwrap();
181 wal.increment_retry_count(coin.object_id).unwrap();
182
183 let entry = wal.reclaim(coin.object_id).unwrap().unwrap();
184 assert_eq!(entry.retry_count, 1);
185 }
186
187 #[tokio::test]
188 async fn reserve_reserve() {
189 let tmp_dir = iota_common::tempdir();
190 let mut wal = WriteAheadLog::open(&tmp_dir.path().join("wal"));
191
192 let uuid = Uuid::new_v4();
193 let coin = random_object_ref();
194 let (recv0, tx0) = random_request(coin);
195 let (recv1, tx1) = random_request(coin);
196
197 wal.reserve(uuid, coin.object_id, recv0, tx0).unwrap();
199
200 assert!(matches!(
202 wal.reserve(uuid, coin.object_id, recv1, tx1),
203 Err(TypedStoreError::Serialization(_)),
204 ));
205 }
206
207 #[tokio::test]
208 async fn reserve_reclaim_commit_reclaim() {
209 let tmp_dir = iota_common::tempdir();
210 let mut wal = WriteAheadLog::open(&tmp_dir.path().join("wal"));
211
212 let uuid = Uuid::new_v4();
213 let coin = random_object_ref();
214 let (recv, tx) = random_request(coin);
215
216 wal.reserve(uuid, coin.object_id, recv, tx.clone()).unwrap();
217
218 let Some(entry) = wal.reclaim(coin.object_id).unwrap() else {
220 panic!("entry not found for {}", coin.object_id);
221 };
222
223 assert_eq!(uuid, Uuid::from_bytes(entry.uuid));
224 assert_eq!(recv, entry.recipient);
225 assert_eq!(tx, entry.tx);
226
227 wal.commit(coin.object_id).unwrap();
229
230 assert_eq!(Ok(None), wal.reclaim(coin.object_id));
232 }
233
234 #[tokio::test]
235 async fn reserve_commit_reserve() {
236 let tmp_dir = iota_common::tempdir();
237 let mut wal = WriteAheadLog::open(&tmp_dir.path().join("wal"));
238
239 let uuid = Uuid::new_v4();
240 let coin = random_object_ref();
241 let (recv0, tx0) = random_request(coin);
242 let (recv1, tx1) = random_request(coin);
243
244 wal.reserve(uuid, coin.object_id, recv0, tx0).unwrap();
246
247 wal.commit(coin.object_id).unwrap();
249
250 wal.reserve(uuid, coin.object_id, recv1, tx1).unwrap();
252 }
253
254 fn random_request(coin: ObjectReference) -> (Address, Transaction) {
255 let gas_price = 1;
256 let send = Address::random();
257 let recv = Address::random();
258 (
259 recv,
260 Transaction::new_pay_iota(
261 send,
262 vec![coin],
263 vec![recv],
264 vec![1000],
265 coin,
266 gas_price * TEST_ONLY_GAS_UNIT_FOR_TRANSFER,
267 gas_price,
268 )
269 .unwrap(),
270 )
271 }
272}