Skip to content

Commit b314c69

Browse files
authored
Merge pull request #1162 from matthughes/semispaceCache
Fix bug in semispace cache that allowed gen1 to contain values in evi…
2 parents 0ab5ca9 + f45e197 commit b314c69

File tree

3 files changed

+57
-10
lines changed

3 files changed

+57
-10
lines changed

modules/core/shared/src/main/scala/data/SemispaceCache.scala

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ sealed abstract case class SemispaceCache[K, V](gen0: Map[K, V], gen1: Map[K, V]
1616
assert(gen0.size <= max)
1717
assert(gen1.size <= max)
1818

19-
def insert(k: K, v: V): SemispaceCache[K, V] =
20-
if (max == 0) SemispaceCache(gen0, gen1, max, evicted + v) // immediately evict
21-
else if (gen0.size < max) SemispaceCache(gen0 + (k -> v), gen1, max, evicted - v) // room in gen0, done!
22-
else SemispaceCache(Map(k -> v), gen0, max, evicted ++ gen1.values - v)// no room in gen0, slide it down
19+
def insert(k: K, v: V): SemispaceCache[K, V] =
20+
if (max == 0) SemispaceCache(gen0, gen1, max, evicted + v) // immediately evict
21+
else if (gen0.size < max) SemispaceCache(gen0 + (k -> v), gen1 - k, max, evicted - v) // room in gen0, remove from gen1, done!
22+
else SemispaceCache(Map(k -> v), gen0, max, evicted ++ gen1.values - v) // no room in gen0, slide it down
2323

24-
def lookup(k: K): Option[(SemispaceCache[K, V], V)] =
24+
def lookup(k: K): Option[(SemispaceCache[K, V], V)] =
2525
gen0.get(k).tupleLeft(this) orElse // key is in gen0, done!
2626
gen1.get(k).map(v => (insert(k, v), v)) // key is in gen1, copy to gen0
2727

@@ -40,8 +40,14 @@ sealed abstract case class SemispaceCache[K, V](gen0: Map[K, V], gen1: Map[K, V]
4040

4141
object SemispaceCache {
4242

43-
private def apply[K, V](gen0: Map[K, V], gen1: Map[K, V], max: Int, evicted: EvictionSet[V]): SemispaceCache[K, V] =
44-
new SemispaceCache[K, V](gen0, gen1, max, evicted) {}
43+
private def apply[K, V](gen0: Map[K, V], gen1: Map[K, V], max: Int, evicted: EvictionSet[V]): SemispaceCache[K, V] = {
44+
val r = new SemispaceCache[K, V](gen0, gen1, max, evicted) {}
45+
val gen0Intersection: Set[V] = (gen0.values.toSet intersect evicted.toList.toSet)
46+
val gen1Intersection: Set[V] = (gen1.values.toSet intersect evicted.toList.toSet)
47+
assert(gen0Intersection.isEmpty, s"gen0 has overlapping values in evicted: ${gen0Intersection}")
48+
assert(gen1Intersection.isEmpty, s"gen1 has overlapping values in evicted: ${gen1Intersection}")
49+
r
50+
}
4551

4652
def empty[K, V](max: Int, trackEviction: Boolean): SemispaceCache[K, V] =
4753
SemispaceCache[K, V](Map.empty, Map.empty, max max 0, if (trackEviction) EvictionSet.empty else new EvictionSet.ZeroEvictionSet)

modules/tests/shared/src/test/scala/PrepareCacheTest.scala

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import skunk.implicits._
88
import skunk.codec.numeric.int8
99
import skunk.codec.text
1010
import skunk.codec.boolean
11+
import cats.syntax.all.*
1112

1213
class PrepareCacheTest extends SkunkTest {
1314

@@ -16,7 +17,15 @@ class PrepareCacheTest extends SkunkTest {
1617
private val pgStatementsCountByStatement = sql"select count(*) from pg_prepared_statements where statement = ${text.text}".query(int8)
1718
private val pgStatementsCount = sql"select count(*) from pg_prepared_statements".query(int8)
1819
private val pgStatements = sql"select statement from pg_prepared_statements order by prepare_time".query(text.text)
19-
20+
21+
pooledTest("concurrent prepare cache should close evicted prepared statements at end of session", max = 1, parseCacheSize = 2) { p =>
22+
List.fill(4)(
23+
p.use { s =>
24+
s.execute(pgStatementsByName)("foo").void >> s.execute(pgStatementsByStatement)("bar").void >> s.execute(pgStatementsCountByStatement)("baz").void
25+
}
26+
).sequence
27+
}
28+
2029
pooledTest("prepare cache should close evicted prepared statements at end of session", max = 1, parseCacheSize = 1) { p =>
2130
p.use { s =>
2231
s.execute(pgStatementsByName)("foo").void >>

modules/tests/shared/src/test/scala/data/SemispaceCacheTest.scala

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,36 @@ class SemispaceCacheTest extends ScalaCheckSuite {
1212

1313
val genEmpty: Gen[SemispaceCache[Int, String]] =
1414
Gen.choose(-1, 10).map(SemispaceCache.empty(_, true))
15+
16+
test("eviction should never contain values in gen0/gen1") {
17+
val cache = SemispaceCache.empty(2, true).insert("one", 1)
18+
19+
val i1 = cache.insert("one", 1)
20+
// Two doesn't exist; space in gen0, insert
21+
val i2 = i1.lookup("two").map(_._1).getOrElse(i1.insert("two", 2))
22+
assertEquals(i2.gen0, Map("one" -> 1, "two" -> 2))
23+
assertEquals(i2.gen1, Map.empty[String, Int])
24+
assertEquals(i2.evicted.toList, Nil)
25+
26+
// Three doesn't exist, hit max; slide gen0 -> gen1 and add to gen0
27+
val i3 = i2.lookup("three").map(_._1).getOrElse(i2.insert("three", 3))
28+
assertEquals(i3.gen0, Map("three" -> 3))
29+
assertEquals(i3.gen1, Map("one" -> 1, "two" -> 2))
30+
assertEquals(i3.evicted.toList, Nil)
31+
32+
// One exists in gen1; pull up to gen0 and REMOVE from gen1
33+
val i4 = i3.lookup("one").map(_._1).getOrElse(i3.insert("one", 1))
34+
assertEquals(i4.gen0, Map("one" -> 1, "three" -> 3))
35+
assertEquals(i4.gen1, Map("two" -> 2))
36+
assertEquals(i4.evicted.toList, Nil)
37+
38+
// Four doesn't exist; gen0 is full so push to gen1
39+
// insert four to gen0 and evict gen1
40+
val i5 = i4.lookup("four").map(_._1).getOrElse(i4.insert("four", 4))
41+
assertEquals(i5.gen0, Map("four" -> 4))
42+
assertEquals(i5.gen1, Map("one" -> 1, "three" -> 3))
43+
assertEquals(i5.evicted.toList, List(2))
44+
}
1545

1646
test("insert on empty cache results in eviction") {
1747
val cache = SemispaceCache.empty(0, true).insert("one", 1)
@@ -73,7 +103,7 @@ class SemispaceCacheTest extends ScalaCheckSuite {
73103
val max = c.max
74104

75105
// Load up the cache such that it overflows by 1
76-
val cʹ = (0 to max).foldLeft(c) { case (c, n) => c.insert(n, "x") }
106+
val cʹ = (0 to max).foldLeft(c) { case (c, n) => c.insert(n, n.toString) }
77107
assertEquals(cʹ.gen0.size, 1 min max)
78108
assertEquals(cʹ.gen1.size, max)
79109

@@ -82,7 +112,9 @@ class SemispaceCacheTest extends ScalaCheckSuite {
82112
case None => assertEquals(max, 0)
83113
case Some((cʹʹ, _)) =>
84114
assertEquals(cʹʹ.gen0.size, 2 min max)
85-
assertEquals(cʹʹ.gen1.size, max)
115+
// When we promote 0 to gen0, we remove it from gen1
116+
assertEquals(cʹʹ.gen1.size, max-1 max 1)
117+
assertEquals(cʹʹ.evicted.toList, Nil)
86118
}
87119

88120
}

0 commit comments

Comments
 (0)