blemale/scaffeine

Ticker does not work with free Futures

veysiertekin opened this issue · 1 comments

Hi,

I am trying to implement a fakeTicker for testing purposes but somehow ticker works with constant Futures (like Future.successful) but does not work with ongoing Futures that use ExecutorContext.

Ticker: FakeTicker.scala

import com.github.benmanes.caffeine.cache.Ticker
import java.util.concurrent.atomic.AtomicLong
import scala.concurrent.duration.Duration

class FakeTicker extends Ticker {
  private val nanos                  = new AtomicLong()
  private val autoIncrementStepNanos = new AtomicLong()

  override def read(): Long =
    nanos.getAndAdd(autoIncrementStepNanos.get())

  def advance(duration: Duration): FakeTicker = {
    advance(duration.toNanos)
    this
  }

  def advance(nanoseconds: Long): FakeTicker = {
    nanos.addAndGet(nanoseconds)
    this
  }

  def setAutoIncrement(duration: Duration): Unit = {
    this.autoIncrementStepNanos.set(duration.toNanos)
  }
}

Working example:

import com.github.blemale.scaffeine.{ AsyncLoadingCache, Scaffeine }
import org.scalatest.freespec.AsyncFreeSpec

import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Random

class CacheSpec extends AsyncFreeSpec {
  val fakeTicker = new FakeTicker

  case class Expired(ttl: Long, data: String)

  val cache: AsyncLoadingCache[String, Expired] = Scaffeine()
    .executor(scala.concurrent.ExecutionContext.global)
    .ticker(fakeTicker)
    .refreshAfterWrite(1.hour)
    .expireAfter(
      create = (_: String, response: Expired) => response.ttl.hours,
      update = (_: String, response: Expired, _: FiniteDuration) => response.ttl.hours,
      read = (_: String, _: Expired, duration: FiniteDuration) => duration
    )
    .buildAsyncFuture[String, Expired](load(_))

  def load(s: String): Future[Expired] = {
    Future.successful(Expired(4, Random.nextString(10)))
  }

  "get" - {
    "should pass" in {
      for {
        first <- cache.get("test")
        _ = fakeTicker.advance(5.hours)
        second <- cache.get("test")
      } yield {
        assert(first != second)
        succeed
      }
    }
  }
}

Does not work on free Futures:

import com.github.blemale.scaffeine.{ AsyncLoadingCache, Scaffeine }
import org.scalatest.freespec.AsyncFreeSpec

import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Random

class CacheSpec extends AsyncFreeSpec {
  val fakeTicker = new FakeTicker

  case class Expired(ttl: Long, data: String)

  val cache: AsyncLoadingCache[String, Expired] = Scaffeine()
    .executor(scala.concurrent.ExecutionContext.global)
    .ticker(fakeTicker)
    .refreshAfterWrite(1.hour)
    .expireAfter(
      create = (_: String, response: Expired) => response.ttl.hours,
      update = (_: String, response: Expired, _: FiniteDuration) => response.ttl.hours,
      read = (_: String, _: Expired, duration: FiniteDuration) => duration
    )
    .buildAsyncFuture[String, Expired](load(_))

  def load(s: String): Future[Expired] = {
    // Changed from `Future.successful` to `Future`
    Future(Expired(4, Random.nextString(10)))
  }

  "get" - {
    "should pass" in {
      for {
        first <- cache.get("test")
        _ = fakeTicker.advance(5.hours)
        second <- cache.get("test")
      } yield {
        assert(first != second)
        succeed
      }
    }
  }
}

Sorry, I think I was wrong.

I had to set Test Suite's executor context to the same. My bad.

Working example:

import com.github.blemale.scaffeine.{ AsyncLoadingCache, Scaffeine }
import org.scalatest.freespec.AsyncFreeSpec

import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Random

class CacheSpec extends AsyncFreeSpec {
  val fakeTicker = new FakeTicker

  implicit override val executionContext: ExecutionContextExecutor = scala.concurrent.ExecutionContext.global

  case class Expired(ttl: Long, data: String)

  val cache: AsyncLoadingCache[String, Expired] = Scaffeine()
    .executor(executionContext)
    .ticker(fakeTicker)
    .refreshAfterWrite(1.hour)
    .expireAfter(
      create = (_: String, response: Expired) => response.ttl.hours,
      update = (_: String, response: Expired, _: FiniteDuration) => response.ttl.hours,
      read = (_: String, _: Expired, duration: FiniteDuration) => duration
    )
    .buildAsyncFuture[String, Expired](load(_))

  def load(s: String): Future[Expired] = {
    Future(Expired(4, Random.nextString(10)))
  }

  "get" - {
    "should pass" in {
      for {
        first <- cache.get("test")
        _ = fakeTicker.advance(5.hours)
        second <- cache.get("test")
      } yield {
        assert(first != second)
        succeed
      }
    }
  }
}