-
-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: extract a more general LruCache
- Loading branch information
1 parent
fb8ad68
commit 210cfce
Showing
4 changed files
with
112 additions
and
17 deletions.
There are no files selected for viewing
40 changes: 40 additions & 0 deletions
40
common/shared/src/main/scala/org/specs2/data/LruCache.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package org.specs2.data | ||
|
||
import org.specs2.fp.* | ||
import org.specs2.control.* | ||
import org.specs2.time.* | ||
|
||
/** LRU (least recently used) cache for processing items Values can be registered and the cached cleaned so that it | ||
* doesn't go above a given size. The oldest elements are removed first. | ||
*/ | ||
class LruCache[A](maxSize: Int, systemTime: SystemTime = JavaSystemTime): | ||
private var values: Map[A, Long] = Map.empty | ||
|
||
/** Checks if a value has already been processed; if not immediately adds it to the cache. If it has been processed, | ||
* refresh its timestamp. | ||
* @return | ||
* the processed status | ||
*/ | ||
def register(value: A): Operation[ProcessedStatus] = | ||
Operation.delayed { | ||
this.synchronized: | ||
val alreadyProcessed = values.contains(value) | ||
// refresh the timestamp even if the params were already registered | ||
values += value -> systemTime.nanoTime | ||
val status = if alreadyProcessed then ProcessedStatus.AlreadyProcessed else ProcessedStatus.ToProcess | ||
while values.size > maxSize do values -= values.minBy(_._2)._1 | ||
status | ||
} | ||
|
||
/** Return the number of elements in the cache */ | ||
def size: Int = | ||
values.size | ||
|
||
/** Return the timestamp for the oldest element */ | ||
def oldestTimestamp: Long = | ||
values.minBy(_._2)._2 | ||
|
||
/** This enum describes the status of an item in the LruCache */ | ||
enum ProcessedStatus: | ||
case AlreadyProcessed | ||
case ToProcess |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
common/shared/src/main/scala/org/specs2/time/SystemTime.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package org.specs2.time | ||
|
||
/** This trait provides the current time */ | ||
trait SystemTime: | ||
def nanoTime: Long | ||
|
||
object JavaSystemTime extends SystemTime: | ||
override def nanoTime: Long = | ||
System.nanoTime() |
56 changes: 56 additions & 0 deletions
56
tests/shared/src/test/scala/org/specs2/data/LruCacheSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package org.specs2 | ||
package data | ||
|
||
import org.scalacheck.* | ||
import org.scalacheck.Arbitrary.* | ||
import org.specs2.time.* | ||
import org.specs2.fp.syntax.* | ||
import ProcessedStatus.* | ||
|
||
class LruCacheSpec extends Specification with ScalaCheck: | ||
def is = s2""" | ||
|
||
A LRU cache can be used to store elements and evict them when they have been unused for a long time | ||
A status is returned to know if an element has already been seen before $e1 | ||
The cache can not contain more than a fixed number of elements $e2 | ||
The oldest elements are always evicted first $e3 | ||
|
||
""" | ||
|
||
def e1 = | ||
val cache = LruCache[Int](maxSize = 3, systemTime = MockSystemTime()) | ||
val operations = cache.register(1) >> cache.register(2) >> cache.register(1) | ||
val status = operations.unsafeRun | ||
status === AlreadyProcessed | ||
|
||
def e2 = prop { (n: SmallInt) => | ||
val cache = LruCache[Int](maxSize = 3, systemTime = MockSystemTime()) | ||
val operations = (1 to n.value).toList.traverse(i => cache.register(i)) | ||
operations.void.unsafeRun | ||
cache.size must be_<=(3) | ||
}.set(minTestsOk = 10) | ||
|
||
def e3 = prop { (n: SmallInt) => | ||
val mockSystemTime = MockSystemTime() | ||
val cache = LruCache[Int](maxSize = 3, systemTime = mockSystemTime) | ||
val operations = (1 to n.value).toList.traverse(i => cache.register(i)) | ||
operations.void.unsafeRun | ||
cache.oldestTimestamp must be_<(mockSystemTime.nanoTime) | ||
}.set(minTestsOk = 10) | ||
|
||
/** HELPERS */ | ||
class MockSystemTime() extends SystemTime: | ||
private var times: LazyList[Long] = LazyList.from(1).map(_.toLong) | ||
|
||
def nanoTime: Long = | ||
times match { | ||
case t #:: ts => times = ts; t | ||
} | ||
|
||
case class SmallInt(value: Int) | ||
|
||
object SmallInt { | ||
given Arbitrary[SmallInt] = Arbitrary { | ||
arbitrary[Int].map(n => SmallInt((n % 10).abs + 1)) | ||
} | ||
} |