Skip to content

Commit f64cc03

Browse files
authored
Merge pull request #103 from guardian/support-logging-on-changed-values
Support logging on updated values
2 parents edc6e3d + ccd633e commit f64cc03

File tree

4 files changed

+125
-3
lines changed

4 files changed

+125
-3
lines changed

core/src/main/scala/com/gu/etagcaching/Loading.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.gu.etagcaching
22

3+
import com.gu.etagcaching.Loading.{OnUpdate, Update}
34
import com.gu.etagcaching.fetching.{ETaggedData, Fetching, MissingOrETagged}
45

56
import scala.concurrent.{ExecutionContext, Future}
@@ -25,6 +26,11 @@ trait Loading[K, V] {
2526
* any new data, and can just reuse our old data, saving us CPU time and network bandwidth.
2627
*/
2728
def fetchThenParseIfNecessary(k: K, oldV: ETaggedData[V])(implicit ec: ExecutionContext): Future[MissingOrETagged[V]]
29+
30+
/**
31+
* Add a handler for doing side-effectful logging of updates.
32+
*/
33+
def onUpdate(handler: Update[K,V] => Unit): Loading[K, V] = OnUpdate(this)(handler)
2834
}
2935

3036
object Loading {
@@ -38,5 +44,32 @@ object Loading {
3844
case Some(freshResponse) => freshResponse.map(parse)
3945
}
4046
}
47+
48+
/**
49+
* Represents an update event for a given key.
50+
*/
51+
case class Update[K, V](key: K, oldV: Option[V], newV: Option[V])
52+
53+
/**
54+
* Wrapper round an underlying instance of Loading which adds handler for doing side-effectful logging of updates.
55+
*/
56+
case class OnUpdate[K, V](underlying: Loading[K, V])(handler: Update[K,V] => Unit)
57+
extends Loading[K, V] {
58+
59+
override def fetchAndParse(key: K)(implicit ec: ExecutionContext): Future[MissingOrETagged[V]] =
60+
handle(key, None, underlying.fetchAndParse(key))
61+
62+
override def fetchThenParseIfNecessary(key: K, oldV: ETaggedData[V])(implicit ec: ExecutionContext): Future[MissingOrETagged[V]] =
63+
handle(key, oldV.toOption, underlying.fetchThenParseIfNecessary(key, oldV))
64+
65+
private def handle(key: K, oldV: Option[V], fut: Future[MissingOrETagged[V]])(implicit ec: ExecutionContext): Future[MissingOrETagged[V]] = {
66+
for {
67+
wrappedNewV <- fut
68+
} {
69+
handler(Update(key, oldV, wrappedNewV.toOption))
70+
}
71+
fut
72+
}
73+
}
4174
}
4275

core/src/test/scala/com/gu/etagcaching/FreshnessPolicyTest.scala

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@ package com.gu.etagcaching
22

33
import com.github.blemale.scaffeine.{AsyncLoadingCache, Scaffeine}
44
import com.gu.etagcaching.FreshnessPolicy.{AlwaysWaitForRefreshedValue, TolerateOldValueWhileRefreshing}
5+
import com.gu.etagcaching.fetching.{ETaggedData, Fetching, MissingOrETagged}
6+
import org.scalatest.OptionValues
57
import org.scalatest.concurrent.ScalaFutures
68
import org.scalatest.concurrent.TimeLimits.failAfter
79
import org.scalatest.flatspec.AnyFlatSpec
810
import org.scalatest.matchers.should.Matchers
911
import org.scalatest.time.SpanSugar._
1012

11-
import scala.concurrent.ExecutionContext.Implicits.global
12-
import scala.concurrent.Future
13+
import java.util.concurrent.atomic.AtomicInteger
14+
import scala.concurrent.{ExecutionContext, Future}
1315

14-
class FreshnessPolicyTest extends AnyFlatSpec with Matchers with ScalaFutures {
16+
class FreshnessPolicyTest extends AnyFlatSpec with Matchers with ScalaFutures with OptionValues {
1517

1618
case class DemoCache(policy: FreshnessPolicy) {
19+
import scala.concurrent.ExecutionContext.Implicits.global
20+
1721
lazy val exampleCache: AsyncLoadingCache[String, Int] = {
1822
def simulateWork(task: String, key: String, millis: Long): Unit = {
1923
println(s"$task begin: $key")
@@ -54,4 +58,24 @@ class FreshnessPolicyTest extends AnyFlatSpec with Matchers with ScalaFutures {
5458
demo.read() shouldBe 0
5559
}
5660
}
61+
62+
it should "mean that ETagCache won't make additional requests if a value is cached locally" in {
63+
val fetching: Fetching[String, Int] = TestFetching.withIncrementingValues
64+
65+
val eTagCache = new ETagCache[String, Int](
66+
fetching.thenParsing(identity),
67+
TolerateOldValueWhileRefreshing,
68+
_.maximumSize(1).expireAfterWrite(100.millis)
69+
)(ExecutionContext.Implicits.global)
70+
71+
eTagCache.get("KEY").futureValue.value shouldBe 0
72+
eTagCache.get("KEY").futureValue.value shouldBe 0
73+
eTagCache.get("KEY").futureValue.value shouldBe 0
74+
Thread.sleep(200)
75+
eTagCache.get("KEY").futureValue.value shouldBe 1
76+
Thread.sleep(30)
77+
eTagCache.get("KEY").futureValue.value shouldBe 1
78+
Thread.sleep(30)
79+
eTagCache.get("KEY").futureValue.value shouldBe 1
80+
}
5781
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.gu.etagcaching
2+
3+
import com.gu.etagcaching.FreshnessPolicy.TolerateOldValueWhileRefreshing
4+
import com.gu.etagcaching.Loading.Update
5+
import com.gu.etagcaching.fetching.Fetching
6+
import org.scalatest.OptionValues
7+
import org.scalatest.concurrent.{Eventually, ScalaFutures}
8+
import org.scalatest.flatspec.AnyFlatSpec
9+
import org.scalatest.matchers.should.Matchers
10+
11+
import scala.collection.mutable
12+
import scala.concurrent.ExecutionContext.Implicits.global
13+
import scala.concurrent.duration._
14+
15+
class LoadingTest extends AnyFlatSpec with Matchers with ScalaFutures with OptionValues with Eventually {
16+
"onUpdate" should "give callbacks that allow logging updates" in {
17+
val updates: mutable.Buffer[Update[String, Int]] = mutable.Buffer.empty
18+
19+
val fetching: Fetching[String, Int] = TestFetching.withIncrementingValues
20+
21+
val cache = new ETagCache(
22+
fetching.thenParsing(identity).onUpdate { update =>
23+
updates.append(update)
24+
},
25+
TolerateOldValueWhileRefreshing,
26+
_.maximumSize(1).refreshAfterWrite(100.millis)
27+
)
28+
29+
val expectedUpdates = Seq(
30+
Update("key", None, Some(0)),
31+
Update("key", Some(0), Some(1))
32+
)
33+
34+
cache.get("key").futureValue shouldBe Some(0)
35+
updates shouldBe expectedUpdates.take(1)
36+
37+
Thread.sleep(105)
38+
39+
eventually { cache.get("key").futureValue shouldBe Some(1) }
40+
updates.toSeq shouldBe expectedUpdates
41+
42+
Thread.sleep(105)
43+
updates.toSeq shouldBe expectedUpdates // No updates if we're not requesting the key from the cache
44+
}
45+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.gu.etagcaching
2+
3+
import com.gu.etagcaching.fetching.{ETaggedData, Fetching, MissingOrETagged}
4+
5+
import java.util.concurrent.atomic.AtomicInteger
6+
import scala.concurrent.{ExecutionContext, Future}
7+
8+
object TestFetching {
9+
10+
def withIncrementingValues: Fetching[String, Int] = new Fetching[String, Int] {
11+
val counter = new AtomicInteger()
12+
13+
override def fetch(key: String)(implicit ec: ExecutionContext): Future[MissingOrETagged[Int]] = {
14+
val count = counter.getAndIncrement()
15+
Future.successful(ETaggedData(count.toString, count))
16+
}
17+
override def fetchOnlyIfETagChanged(key: String, eTag: String)(implicit ec: ExecutionContext): Future[Option[MissingOrETagged[Int]]] =
18+
fetch(key)(ec).map(Some(_))
19+
}
20+
}

0 commit comments

Comments
 (0)