scp2: report

This commit is contained in:
2025-11-07 12:49:30 +01:00
parent 1f88d14f9d
commit 1057dd009e
2 changed files with 2893 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
---
title: "scala project part 2"
date: 2025-11-07
author: fredrik robertsen
margin:
x: 3.5cm
y: 4cm
fontsize: 14pt
mainfont: libertinus serif
lineheight: 1.5
pagesize: a4
---
# task 3
## how does it all work?
we essentially implement a queue that we can put our transactions in. these
transactions are then processed in turn by the bank using threads. to ensure
thread safety we are writing the core functionality purely, i.e. returning
new instances of accounts rather than modifying them, which could incur race
conditions. immutable state makes things easy to work with.
all we need are some accounts with a balance and a unique id, some registry to
track these accounts and some checks to make sure that a transaction is
processed correctly, i.e. that there are enough available funds.
then we can model the transaction as a finite state automaton using a state enum,
making it succeed if enough money is available, otherwise fail.
## what was easy
implementing the `Account`s, `TransactionPool`s, `Transaction`s and the simpler
parts of `Bank`, such as `createAccount` and `getAccount` were all easy tasks,
only requiring some lookups for what is available in scala/java.
these didn't require a lot of work or many lines of code per implementation,
since most of it is just setters and getters.
## what was hard
transaction processing in `Bank`.
proper case handling to ensure correct state often becomes unwieldy, and this
was no exception. it was a case of running the tests, seeing what failed, then
reasoning about why it failed, then attempting to patch it by inserting some
if/else statements that hopefully fail certain transactions that should have
been failed.
even then, it wasn't very hard, as this is only a toy program with very few
moving parts. we are doing ourselves a great service by using immtutable state,
making it much easier to figure out what part of the code must be wrong when
tests fail.
## implementation
i will be going three the three main pain-points that had the most impact on
making the tests pass.
the first is enqueuing a `Transaction`:
```scala
def transfer(from: String, to: String, amount: Double): Unit = {
if (amount <= 0) {
val t = new Transaction(from, to, amount)
t.fail()
completedTransactions.add(t)
return
}
val f = getAccount(from)
val t = getAccount(to)
if (f.isEmpty || t.isEmpty) {
val t = new Transaction(from, to, amount)
t.fail()
completedTransactions.add(t)
return
}
if (f.get.balance < amount) {
val t = new Transaction(from, to, amount)
t.fail()
completedTransactions.add(t)
return
}
transactionsPool.add(new Transaction(from, to, amount))
}
```
the initial TODO-comment only mentioned to ad the transaction to the pool. but
upon running the tests it quickly became apparent that we need some case
handling for when transactions are illegal. though i'm sure i could've put this
in `Transaction` and that would've probably been better, i just did it here. in
short: i'm ensuring that amount is positive, accounts exist in our registry and
there is enough money in the account who is transfered from.
then processesing the transactions wasn't so bad using the comments that were
already in place. we also get to use some neat scala syntax, like
`filter(_.isPending)`:
```scala
def processTransactions: Unit = {
val workers: List[Thread] = transactionsPool.iterator.toList
.filter(_.isPending)
.map(processSingleTransaction)
workers.map(element => element.start())
workers.map(element => element.join())
val succeded: List[Transaction] =
transactionsPool.iterator.toList.filter(_.succeeded)
val failed: List[Transaction] =
transactionsPool.iterator.toList.filter(_.failed)
succeded.map(transactionsPool.remove(_))
succeded.map(completedTransactions.add(_))
failed.map(t => {
t.setPending()
t.incrementAttempt()
if (t.exceeded) {
transactionsPool.remove(t)
completedTransactions.add(t)
}
})
if (!transactionsPool.isEmpty) {
processTransactions
}
}
```
then lastly we have to actually process the transactions, one at a time. i first
did this:
```scala
private def processSingleTransaction(t: Transaction): Thread =
new Thread(() => {
accountsRegistry.synchronized {
val fromOpt = getAccount(t.from)
val toOpt = getAccount(t.to)
(fromOpt, toOpt) match {
case (Some(from), Some(to)) =>
from.withdraw(t.amount) match {
case Right(updatedFrom) =>
to.deposit(t.amount) match {
case Right(updatedTo) =>
accountsRegistry(t.from) = updatedFrom
accountsRegistry(t.to) = updatedTo
t.succeed()
case Left(_) => t.fail()
}
case Left(_) => t.fail()
}
case _ => t.fail()
}
}
})
```
here we are getting the accounts as Options, then pattern matching to obtain the
accounts, otherwise failing the transaction. then performing a withdrawal of the
amount, followed by a deposit, then updating the registry with new accounts with
updated balances. only then will the transaction succeed.
but my time with haskell has taught me that this exact pattern is a monadic
pattern. in haskell, we could use `do`-notation to remove all this fluff where
we are failing, and instead focus on the parts that don't lead to a failure.
i already know `Option` and `Either` are monads, so this pattern is fair to
expect. so i looked at how to emulate this syntax in scala, and lo and behold;
we have `do`-notation in scala in the form of `for`/`yield`:
```scala
private def processSingleTransaction(t: Transaction): Thread =
new Thread(() => {
accountsRegistry.synchronized {
for {
from <- getAccount(t.from)
to <- getAccount(t.to)
updatedFrom <- from.withdraw(t.amount).toOption
updatedTo <- to.deposit(t.amount).toOption
} yield {
accountsRegistry(t.from) = updatedFrom
accountsRegistry(t.to) = updatedTo
t.succeed()
}
}
})
```
with this refactor, we still pass the tests and obtain much more readable code.
it's easy to read that we are getting the accounts, getting new accounts from
some updates (wrapped as options, failing if the we obtain a `Left`), then we do
some work with these values if we succeed with all steps, i.e. updating the
registry. very beautiful!

File diff suppressed because it is too large Load Diff