Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • lara/cs206-demos
  • gcharles/cs206-demos
  • gambhir/cs206-demos
3 results
Show changes
package midterm22
import org.junit.*
import org.junit.Assert.*
class Part2Test:
val testArray2 = Array(0, 50, 7, 1, 28, 42)
val testList2 = List(0, 50, 7, 1, 28, 42)
@Test
def testQuestion4Pos() =
assert(contains(testArray2, 7))
@Test
def testQuestion4Neg() =
assert(!contains(testArray2, 8))
@Test
def testQuestion6Pos() =
assert(contains(testList2, 7))
@Test
def testQuestion6Neg() =
assert(!contains(testList2, 8))
package midterm22
import instrumentation.Monitor
import instrumentation.MockedMonitor
import org.junit.*
import org.junit.Assert.*
import instrumentation.*
import java.util.concurrent.atomic.AtomicInteger
class Part4Test:
// This test can result in a deadlock because locks can be called in any
// order. Here, Thread 1 locks Node 3 first and then Node 2, whereas Thread 2
// locks Node 2 first and then Node 3. This will lead to a deadlock.
@Test
def testQuestion9() =
TestUtils.assertDeadlock(
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
def increment(e: Int) = sum += e
(
List(
() =>
// Thread 1
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes
lockFun(nodes, increment),
),
results => (true, "")
)
)
)
// This will not lead to a deadlock because the lock acquire happens in a
// particular order. Thread 1 acquires locks in order 1->2->3->4, whereas
// Thread 2 acquires locks in order 2->3->5.
@Test
def testQuestion10() =
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
def increment(e: Int) = sum += e
(
List(
() =>
// Thread 1
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid > y.guid)
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid > y.guid)
lockFun(nodes, increment),
),
results => (true, "")
)
)
// This will not lead to a deadlock because the lock acquire happens in a
// particular order. Thread 1 acquires locks in order 4->3->2->1, whereas
// Thread 2 acquires locks in order 5->3->2.
@Test
def testQuestion11() =
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
def increment(e: Int) = sum += e
(
List(
() =>
// Thread 1
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
),
results => (true, "")
)
)
// This test can result in a deadlock because locks are not called in any
// order. Thread 1 acquire order (3->2->4->1), Thread 2 acquire order
// (2->3->5). Thread 1 locks Node3 first and then Node2, whereas Thread 2
// locks Node 2 first and then Node3. This will lead to a deadlock.
@Test
def testQuestion12() =
TestUtils.assertDeadlock(
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
def increment(e: Int) = sum += e
(
List(
() =>
// Thread 1
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.tail.appended(nodes(0))
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.tail.appended(nodes(0))
lockFun(nodes, increment),
),
results => (true, "")
)
)
)
// sum returns wrong answer because there is a data race on the sum variable.
@Test(expected = classOf[AssertionError])
def testQuestion13() =
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
def increment(e: Int) =
val previousSum = scheduler.exec { sum }("Get sum")
scheduler.exec { sum = previousSum + e }("Write sum")
(
List(
() =>
// Thread 1
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
),
results =>
if sum != 20 then
(false, f"Wrong sum: expected 20 but got $sum")
else
(true, "")
)
)
// sum value will be correct here because "sum += e" is protected by a lock.
@Test
def testQuestion14() =
TestHelper.testManySchedules(
2,
sched =>
val allNodes = (for i <- 0 to 6 yield ScheduledNode(i, sched)).toList
val monitor = new MockedMonitor: // Monitor is a type of a lock.
def scheduler = sched
// Shared by all threads
var sum: Int = 0
def increment(e: Int) =
monitor.synchronized { sum += e }
(
List(
() =>
// Thread 1
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
),
results =>
if sum != 20 then
(false, f"Wrong sum: expected 20 but got $sum")
else
(true, "")
)
)
// total will give correct output here as it is an atomic instruction.
@Test
def testQuestion15() =
TestHelper.testManySchedules(
2,
sched =>
val allNodes = (for i <- 0 to 6 yield ScheduledNode(i, sched)).toList
// Shared by all threads
var total: AtomicInteger = new AtomicInteger(0)
def increment(e: Int) =
total.addAndGet(e)
(
List(
() =>
// Thread 1
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
),
results =>
if total.get != 20 then
(false, f"Wrong total: expected 20 but got $total")
else
(true, "")
)
)
class ScheduledNode(value: Int, val scheduler: Scheduler) extends Node(value)
with MockedMonitor
package midterm22
import org.junit.*
import org.junit.Assert.*
import instrumentation.*
class Part6Test:
@Test(expected = classOf[AssertionError])
def testQuestion21() =
TestHelper.testManySchedules(
2,
scheduler =>
val ticketsManager = ScheduledTicketsManager(1, scheduler)
(
List(
() =>
// Thread 1
ticketsManager.getTicket(),
() =>
// Thread 2
ticketsManager.getTicket()
),
results =>
if ticketsManager.remainingTickets < 0 then
(false, "Sold more tickets than available!")
else (true, "")
)
)
class ScheduledTicketsManager(totalTickets: Int, val scheduler: Scheduler)
extends TicketsManager(totalTickets)
with MockedMonitor
package midterm22
import org.junit.*
import org.junit.Assert.*
import instrumentation.*
class Part7Test:
@Test
def testNicManagerSequential() =
val nicsManager = NICManager(4)
assertEquals((0, 1), nicsManager.assignNICs())
assertEquals((2, 3), nicsManager.assignNICs())
@Test
def testQuestion22() =
testNicManagerParallel(2, 3)
@Test
def testQuestion23() =
val nicsManager = NICManager(2)
// Thread 1
assertEquals((0, 1), nicsManager.assignNICs())
nicsManager.nics(0).assigned = false
nicsManager.nics(1).assigned = false
// Thread 2
assertEquals((0, 1), nicsManager.assignNICs())
nicsManager.nics(0).assigned = false
nicsManager.nics(1).assigned = false
@Test
def testQuestion24() =
testNicManagerParallel(3, 2, true)
@Test
def testQuestion24NotLimitingRecvNICs() =
TestUtils.assertMaybeDeadlock(
testNicManagerParallel(3, 2)
)
def testNicManagerParallel(
threads: Int,
nics: Int,
limitRecvNICs: Boolean = false
) =
TestHelper.testManySchedules(
threads,
scheduler =>
val nicsManager = ScheduledNicsManager(nics, scheduler)
val tasks =
for i <- 0 until threads yield () =>
// Thread i
val (recvNIC, sendNIC) = nicsManager.assignNICs(limitRecvNICs)
// Do something with NICs...
// Un-assign NICs
nicsManager.nics(recvNIC).assigned = false
nicsManager.nics(sendNIC).assigned = false
(
tasks.toList,
results =>
if nicsManager.nics.count(_.assigned) != 0 then
(false, f"All NICs should have been released.")
else (true, "")
)
)
class ScheduledNicsManager(n: Int, scheduler: Scheduler)
extends NICManager(n):
class ScheduledNIC(
_index: Int,
_assigned: Boolean,
val scheduler: Scheduler
) extends NIC(_index, _assigned)
with MockedMonitor:
override def index = scheduler.exec { super.index }(
"",
Some(res => f"read NIC.index == $res")
)
override def assigned = scheduler.exec { super.assigned }(
"",
Some(res => f"read NIC.assigned == $res")
)
override def assigned_=(v: Boolean) =
scheduler.exec { super.assigned = v }(
f"write NIC.assigned = $v"
)
override val nics =
(for i <- 0 until n yield ScheduledNIC(i, false, scheduler)).toList
package midterm22
import org.junit.*
import org.junit.Assert.*
import instrumentation.*
import annotation.nowarn
import scala.collection.concurrent.TrieMap
import scala.collection.concurrent.{TrieMap, Map}
class Part8Test:
@Test
def usage() =
val insta = Instagram()
assertEquals(1, insta.add())
assertEquals(2, insta.add())
insta.follow(1, 2)
assertEquals(insta.graph, Map(1 -> List(2), 2 -> List()))
insta.follow(2, 1)
insta.unfollow(1, 2)
assertEquals(insta.graph, Map(1 -> List(), 2 -> List(1)))
insta.follow(3, 1) // fails silently
assertEquals(insta.graph, Map(1 -> List(), 2 -> List(1)))
insta.remove(1)
assertEquals(insta.graph, Map(2 -> List()))
insta.unfollow(1, 2) // fails silently
@Test
def testParallelFollowABRemoveA() =
TestHelper.testManySchedules(
2,
scheduler =>
val insta = new Instagram:
override val graph =
ScheduledTrieMap(TrieMap[Int, List[Int]](), scheduler)
val u1 = insta.add()
val u2 = insta.add()
(
List(
() =>
// Thread 1
insta.follow(u1, u2),
() =>
// Thread 2
insta.remove(u1)
),
results =>
val size = insta.graph.size
if size != 1 then
(false, f"Wrong number of user: expected 1 but got ${size}")
else validateGraph(insta)
)
)
@Test
def testParallelFollowABRemoveB() =
TestHelper.testManySchedules(
2,
scheduler =>
val insta = new Instagram:
override val graph =
ScheduledTrieMap(TrieMap[Int, List[Int]](), scheduler)
val u1 = insta.add()
val u2 = insta.add()
(
List(
() =>
// Thread 1
insta.follow(u1, u2),
() =>
// Thread 2
insta.remove(u2)
),
results =>
val size = insta.graph.size
if size != 1 then
(false, f"Wrong number of user: expected 1 but got ${size}")
else validateGraph(insta)
)
)
@Test
def testParallelFollowACRemoveB() =
TestHelper.testManySchedules(
2,
scheduler =>
val insta = new Instagram:
override val graph =
ScheduledTrieMap(TrieMap[Int, List[Int]](), scheduler)
val u1 = insta.add()
val u2 = insta.add()
val u3 = insta.add()
insta.follow(u1, u2)
(
List(
() =>
// Thread 1
insta.follow(u1, u3),
() =>
// Thread 2
insta.remove(u2)
),
results =>
val size = insta.graph.size
if size != 2 then
(false, f"Wrong number of user: expected 2 but got ${size}")
else validateGraph(insta)
)
)
@Test
def testParallelFollow() =
TestHelper.testManySchedules(
2,
scheduler =>
val insta = new Instagram:
override val graph =
ScheduledTrieMap(TrieMap[Int, List[Int]](), scheduler)
val u1 = insta.add()
val u2 = insta.add()
val u3 = insta.add()
(
List(
() =>
// Thread 1
insta.follow(u1, u2),
() =>
// Thread 2
insta.follow(u1, u3)
),
results =>
val u1FollowingSize = insta.graph(u1).size
if u1FollowingSize != 2 then
(
false,
f"Wrong number of users followed by user 1: expected 2 but got ${u1FollowingSize}"
)
else validateGraph(insta)
)
)
@Test
def testParallelRemove() =
TestHelper.testManySchedules(
2,
scheduler =>
val insta = new Instagram:
override val graph =
ScheduledTrieMap(TrieMap[Int, List[Int]](), scheduler)
// Setup
val u1 = insta.add()
val u2 = insta.add()
val u3 = insta.add()
insta.follow(u1, u2)
insta.follow(u2, u1)
insta.follow(u2, u3)
insta.follow(u3, u1)
(
List(
() =>
// Thread 1
insta.remove(u2),
() =>
// Thread 2
insta.remove(u3)
),
results =>
val size = insta.graph.size
if size != 1 then
(false, f"Wrong number of user: expected 1 but got ${size}")
else validateGraph(insta)
)
)
// We test wrong code here, so we expect an assertion error. You can replace
// the next line by `@Test` if you want to see the error with the failing
// schedule.
@Test(expected = classOf[AssertionError])
def testParallelWrongAdd() =
TestHelper.testManySchedules(
2,
scheduler =>
val insta = new Instagram:
override val graph =
ScheduledTrieMap(TrieMap[Int, List[Int]](), scheduler)
// This implementation of `add` is wrong, because two threads might
// allocate the same id.
// Consider the following schedule:
// T1: res = 1
// T2: res = 2
// T2: graph.update(2, Nil)
// T2: 2
// T1: graph.update(2, Nil)
// T1: 2
override def add(): Int =
val res = maxId.incrementAndGet
graph.update(maxId.get, Nil)
res
(
List(
() =>
// Thread 1
insta.add(),
() =>
// Thread 2
insta.add()
),
results =>
if results(0) != results(1) then
(false, f"Allocated twice id ${results(0)}")
else validateGraph(insta)
)
)
// We test wrong code here, so we expect an assertion error. You can replace
// the next line by `@Test` if you want to see the error with the failing
// schedule.
@Test(expected = classOf[AssertionError])
def testParallelWrongRemove() =
TestHelper.testManySchedules(
2,
scheduler =>
val insta = new Instagram:
override val graph =
ScheduledTrieMap(TrieMap[Int, List[Int]](), scheduler)
// This implementation of `remove` is wrong because we don't retry to
// call `graph.replace` when it fails. Therefore, user 1 might end up
// following user 2 that has been removed, or not following user 3
// which is concurrently followed.
override def remove(idToRemove: Int): Unit =
graph.remove(idToRemove)
for (key, value) <- graph do
graph.replace(key, value, value.filter(_ != idToRemove))
// Note: writing `graph(key) = value.filter(_ != idToRemove)` would also
// be wrong because it does not check the previous value.
// Therefore, it could erase a concurrent update.
val u1 = insta.add()
val u2 = insta.add()
val u3 = insta.add()
insta.follow(u1, u2)
(
List(
() =>
// Thread 1
insta.follow(u1, u3),
() =>
// Thread 2
insta.remove(u2)
),
results =>
val size = insta.graph.size
if insta.graph(u1).size != 1 then
(
false,
f"Wrong number of users followed by 1: expected 1 but got ${insta.graph(u1)}"
)
else validateGraph(insta)
)
)
// We test wrong code here, so we expect an assertion error. You can replace
// the next line by `@Test` if you want to see the error with the failing
// schedule.
@Test(expected = classOf[AssertionError])
def testParallelWrongUnfollow() =
TestHelper.testManySchedules(
2,
scheduler =>
val insta = new Instagram:
override val graph =
ScheduledTrieMap(TrieMap[Int, List[Int]](), scheduler)
override def unfollow(a: Int, b: Int): Unit =
if !graph.contains(a) then return
val prev = graph(a) // Might throw java.util.NoSuchElementException
if !graph.replace(a, prev, prev.filter(_ != b)) then unfollow(a, b)
val u1 = insta.add()
val u2 = insta.add()
insta.follow(u1, u2)
(
List(
() =>
// Thread 1
insta.unfollow(u1, u2),
() =>
// Thread 2
insta.remove(u1)
),
results =>
val size = insta.graph.size
if size != 1 then
(false, f"Wrong number of user: expected 1 but got ${size}")
else validateGraph(insta)
)
)
@nowarn
def validateGraph(insta: Instagram): (Boolean, String) =
for (a, following) <- insta.graph; b <- following do
if !insta.graph.contains(b) then
return (false, f"User $a follows non-existing user $b")
(true, "")
final class ScheduledIterator[T](
private val myIterator: Iterator[T],
private val scheduler: Scheduler
) extends Iterator[T]:
override def hasNext =
myIterator.hasNext
override def next() =
scheduler.exec(myIterator.next)("", Some(res => f"Iterator.next == $res"))
override def knownSize: Int =
myIterator.knownSize
final class ScheduledTrieMap[K, V](
private val myMap: Map[K, V],
private val scheduler: Scheduler
) extends Map[K, V]:
override def apply(key: K): V =
scheduler.exec(myMap(key))(
"",
Some(res => f"TrieMap.apply($key) == $res")
)
override def contains(key: K): Boolean =
scheduler.exec(myMap.contains(key))(
"",
Some(res => f"TrieMap.contains($key) == $res")
)
override def get(key: K): Option[V] =
scheduler.exec(myMap.get(key))(
"",
Some(res => f"TrieMap.get($key) == $res")
)
override def addOne(kv: (K, V)) =
scheduler.exec(myMap.addOne(kv))(f"TrieMap.addOne($kv)")
this
override def subtractOne(k: K) =
scheduler.exec(myMap.subtractOne(k))(f"TrieMap.subtractOne($k)")
this
override def iterator() =
scheduler.log("TrieMap.iterator")
ScheduledIterator(myMap.iterator, scheduler)
override def replace(k: K, v: V): Option[V] =
scheduler.exec(myMap.replace(k, v))(
"",
Some(res => f"TrieMap.replace($k, $v) == $res")
)
override def replace(k: K, oldvalue: V, newvalue: V): Boolean =
scheduler.exec(myMap.replace(k, oldvalue, newvalue))(
"",
Some(res => f"TrieMap.replace($k, $oldvalue, $newvalue) == $res")
)
override def putIfAbsent(k: K, v: V): Option[V] =
scheduler.exec(myMap.putIfAbsent(k, v))(
"",
Some(res => f"TrieMap.putIfAbsent($k, $v)")
)
override def remove(k: K): Option[V] =
scheduler.exec(myMap.remove(k))(
"",
Some(res => f"TrieMap.remove($k)")
)
override def remove(k: K, v: V): Boolean =
scheduler.exec(myMap.remove(k, v))(
"",
Some(res => f"TrieMap.remove($k, $v)")
)
package midterm23
import instrumentation.*
import java.util.concurrent.atomic.AtomicInteger
class BarberShopSolutionTest extends munit.FunSuite:
val implementations =
Map[String, (Int, Scheduler) => ScheduledBarberShopSolution](
"BarberShopSolution1" -> (new BarberShopSolution1(_)
with ScheduledBarberShopSolution(_)),
"BarberShopSolution2" -> (new BarberShopSolution2(_)
with ScheduledBarberShopSolution(_)),
"BarberShopSolution3" -> (new BarberShopSolution3(_)
with ScheduledBarberShopSolution(_))
)
for (name, makeShop) <- implementations do
for nCustomers <- 1 to 3 do
test(f"$name with $nCustomers customer(s)") {
testBarberShop(nCustomers, makeShop)
}
package midterm23
import instrumentation.{MockedMonitor, Monitor, Scheduler}
import java.util.concurrent.ConcurrentLinkedQueue
import scala.jdk.CollectionConverters.IterableHasAsScala
import scala.annotation.tailrec
trait ScheduledBarberShopSolution(scheduler0: Scheduler)
extends AbstractBarberShopSolution:
enum Event:
case HairCut
case CustomerLeaves
val trace = ConcurrentLinkedQueue[Event]()
override def notifyBarber = scheduler0.exec { super.notifyBarber }(
"",
Some(res => f"read notifyBarber == $res")
)
override def notifyBarber_=(v: Boolean) =
scheduler0.exec { super.notifyBarber = v }(
f"write notifyBarber = $v"
)
override def notifyCustomer = scheduler0.exec { super.notifyCustomer }(
"",
Some(res => f"read notifyCustomer == $res")
)
override def notifyCustomer_=(v: Boolean) =
scheduler0.exec { super.notifyCustomer = v }(
f"write notifyCustomer = $v"
)
override def hairCut() =
log("hair cut")
trace.add(Event.HairCut)
def customerTrace(n: Int): Unit =
this.customer(n)
log("customer leaves")
trace.add(Event.CustomerLeaves)
private val _lockObj = new MockedMonitor:
override def scheduler: Scheduler = scheduler0
override def lockObj: Monitor = _lockObj
override def log(s: String): Unit = scheduler0.log(s)
def nHaircuts: Int =
trace.asScala.filter(_ == Event.HairCut).size
def customerLeftEarly(): Boolean =
@tailrec
def loop(it: Iterable[Event], n: Int): Boolean =
if it.isEmpty then false
else
val newN = it.head match
case Event.HairCut => n + 1
case Event.CustomerLeaves => n - 1
if newN < 0 then true
else loop(it.tail, newN)
loop(trace.asScala, 0)
package midterm23
import instrumentation.{Scheduler, TestHelper}
/** Tests a barber shop implementation.
*
* @param nCustomers
* The number of customers to simulate.
* @param makeShop
* A function that creates a barber shop given the number of customers and a
* scheduler.
* @param checkEarlyCustomer
* If true, checks that no customer left early, i.e. that there is always a
* number of terminated customer threads equal or less than to the number of
* haircuts done.
*/
def testBarberShop(
nCustomers: Int,
makeShop: (Int, Scheduler) => ScheduledBarberShopSolution,
checkEarlyCustomer: Boolean = true
) =
TestHelper.testManySchedules(
nCustomers + 1,
scheduler =>
val barberShop = makeShop(nCustomers, scheduler)
(
(() => barberShop.barber())
:: (for i <- 1 to nCustomers
yield () => barberShop.customerTrace(i)).toList,
results =>
if barberShop.nHaircuts != nCustomers then
(false, f"Unexpected number of hair cuts: ${barberShop.nHaircuts}")
else if checkEarlyCustomer && barberShop.customerLeftEarly() then
(false, f"A customer left early")
else
(true, "")
)
)