Project simulating an async payment engine with as little locking as possible.
Jump to Requirements and assumptions or see how to run.
We create a Ledger type that can be safely accessed from multiple threads:
pub struct Ledger {
clients: RwLock<HashMap<ClientId, Mutex<Wallet>>>,
}
pub struct Wallet {
available: Decimal,
held: Decimal,
locked: bool,
deposit_log: HashMap<TransactionId, DepositLog>,
}Ledgeris esentially a map fromClientIdto a correspondingWallet.- When wrapped in an
Arc, we can use theLedgerfrom multple threads. - This structure allows us to process transactions in parallel as long as they reference different clients.
Walletintegrity is assured by theMutexwrapping it, so it can't be read or mutated by 2 threads at the same time. - Client/wallet creation (
Ledgerblocking) happens just when a clientDepositsand doesn't already exist in theLedger. - Parallel processing of the transactions is achieved with the help of two private methods:
get_existing_client()which returns the protected wallet in anOption(None if it doesn't exist). This method only uses a read-lock.get_existing_or_create_client()which returns the protected wallet. It first tries to get the wallet through a read-lock, but if the client doesn't exist it creates it with a write-lock. The same write-lock gets downgraded to a read-lock and returned.- We used this mechanism to be sure that the
Ledgeris blocked for as short of a period as possible. - The only worring case here would be if we get a Deposit for a client that doesn't exist, followed very fast by another transaction on the same client. Between the dropping of the read-lock and aquiring of the write-lock those transactions could fail. This wouldn't really be an issue in a real world system because we would also have a
create_clientAPI that would be called before any transaction. - there are some other explanations in the code.
- We used this mechanism to be sure that the
- We used the
parking_lotcrate for better synchronization primitives and helpful types likeMappedRwLockReadGuard.
- Used tests, especially for the critical bits.
rust_decimalcrate provides a fixed-precisionDecimaltype suitable for financial calculations.tracingcrate provides structured logging, helpful especially in an async context. All logs/warning go tostderrsostdoutwill write just the results.thiserrorcrate helps defining Error types with less boilerplate. Errors types are created to provide good insight for tracing.- Used
csv-asyncandserdecrates to insure input is corectly parsed (parser configured to trim whitespaces and omit trailing commas). - Used the type system to ensure correctness. For example: a
ResolveorChargebackcan only apply toDiputedtransactions and only aDepositcan beDisputed. This also provides good maintability and ability to add features. - Use of common Traits like
Default,TryInto. Right nowLedger::new()maps toLedger::default(). If we need to add new fields to theLedgerstruct to acomplish other requirements we can change this without changing the API. - Used
rustfmtandclippy.
- The project is structured in a library and also an executable. This makes the code reusable. This could be further improved by separating to creates and defining interfaces through
Traits. - There are some comments for tricky parts. This could be further improved through documentation and doc-tests.
- The system should be async and multi-threadding capable.
- A client has a Wallet that keeps track of
availableandheldamounts andlockedstatus . Thetotalamounts can be computed by addingavailableandheld. - A client's Wallet gets created on the first Deposit. Other transaction types referencing an inexistent client are ignored.
- Only
Depositscan beDisputed - Input is in CSV format. Commas can be missing and whitespaces should be ignored.
| type | client | tx | amount(optional) |
|---|---|---|---|
| deposit | 1 | 1 | 10.1234 |
| withdrawal | 1 | 2 | 8 |
| dispute | 1 | 1 | |
| resolve | 1 | 1 | |
| chargeback | 1 | 1 |
- Deposit
- increases the
availableamount - fails if a deposit with the same ID has been made to that client's account.
- increases the
- Withdrawal
- decreases the
availableamount - fails if available amount is less than the withdrawal amount.
- decreases the
- Dispute
- only Deposits can be disputed
- move disputed funds from
availabletoheld.
- Resolve
- only disputed deposits can be resolved
- move disputed funds from
heldtoavailable.
- Chargeback
- only disputed deposits can be charged back
heldfunds decrease by the disputed amount- account wallet gets locked.
cargo run -- transactions.csv > accounts.csv
# or with warnings to `stderr`:
RUST_LOG=warn cargo run -- transactions.csv > accounts.csv
Input and output example:
transactions.csv
type, client, tx, amount
deposit, 1, 1, 5
dispute, 1, 1
chargeback, 1, 1
withdraw, 1, 2, 10
accounts.csv
client, available, held, total, locked
1, 0, 0, 0, true