Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memo docs from community with love. #152

Merged
merged 5 commits into from
Feb 13, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions docs/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Memo

## Dependency
It is available in general package `"ru.tinkoff" %% "tofu" % Versions.tofu` and as standalone dependency `"ru.tinkoff" %% "tofu-memo" % Versions.tofu`

## Functionality
* A cache of a single value on access.
* A cache of a values mapping on access.
* TTL. Expired values are discarded on access.
* Forced invalidation based on time.

There are no
* Infinite caching (workaround is ttl `FiniteDuration(Long.MaxValue, TimeUnit.NANOSECONDS)` = 292 days).
* Background renewal of cached values.
* Forced invalidation of single key (mapping cache).

## Examples
### Single value cache
```scala
import tofu.memo._
import cats.effect.{Clock, Sync}
import cats.syntax.applicative._
import cats.syntax.flatMap._
import cats.syntax.functor._
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global
import scala.concurrent.duration.FiniteDuration
import java.util.concurrent.TimeUnit

def effect[F[_] : Sync]: F[Int] = Sync[F].delay(println("called")).as(335)

def f[F[_] : Sync : Clock] =
for {
kasha <- Cached[F](effect)(FiniteDuration(10L, TimeUnit.MINUTES), CacheControl.empty.pure[F]).ref
v1 <- kasha
v2 <- kasha
v3 <- kasha
} yield List(v1,v2,v3).sum

f[Task].runSyncUnsafe()

/*
called
res0: Int = 1005
*/
```
### Value mapping cache
```scala
import tofu.memo._
import cats.effect.{Clock, Sync}
import cats.syntax.applicative._
import cats.syntax.flatMap._
import cats.syntax.functor._
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global
import scala.concurrent.duration.FiniteDuration
import java.util.concurrent.TimeUnit

def effect[F[_] : Sync]: Int => F[String] =
x =>
Sync[F].delay(println("called")).as(s"Number $x")

def f[F[_] : Sync : Clock] =
for {
kasha <- CachedFunc[F](effect)(FiniteDuration(10L, TimeUnit.MINUTES), CacheControl.empty.pure[F]).refVals.ref
v1 <- kasha(335)
_ <- Sync[F].delay(println(v1))
v2 <- kasha(335)
_ <- Sync[F].delay(println(v2))
v3 <- kasha(336)
_ <- Sync[F].delay(println(v3))
} yield ()

f[Task].runSyncUnsafe()

/*
called
Number 335
Number 335
called
Number 336
*/
```

### TTL
```scala
import tofu.memo._
import cats.effect.{Clock, Sync, Timer}
import cats.syntax.applicative._
import cats.syntax.flatMap._
import cats.syntax.functor._
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global
import scala.concurrent.duration.FiniteDuration
import java.util.concurrent.TimeUnit

def longEffect[F[_] : Sync : Timer]: Int => F[String] =
x =>
Timer[F].sleep(FiniteDuration(1L, TimeUnit.SECONDS)) >>
Sync[F].delay(println("called")).as(s"Number $x")

def f[F[_] : Sync : Clock : Timer](ttl : FiniteDuration) =
for {
kasha <- CachedFunc[F](longEffect)(ttl, CacheControl.empty.pure[F]).refVals.ref
v1 <- kasha(335)
_ <- Sync[F].delay(println(v1))
_ <- Timer[F].sleep(ttl - FiniteDuration(2L, TimeUnit.SECONDS))
v2 <- kasha(335)
_ <- Sync[F].delay(println(v2))
_ <- Timer[F].sleep(FiniteDuration(3L, TimeUnit.SECONDS))
v3 <- kasha(335)
_ <- Sync[F].delay(println(v3))
} yield ()

f[Task](FiniteDuration(10L, TimeUnit.SECONDS)).runSyncUnsafe()

/*
called
Number 335
Number 335
called
Number 335
*/
```
There is a pitfall with TTL. Cached value keeps time of access to cache and not time when the effect completes. Just keep it in mind. Modification of the last example
```scala
def longEffect[F[_] : Sync : Timer]: Int => F[String] =
x =>
Timer[F].sleep(FiniteDuration(3L, TimeUnit.SECONDS)) >>
Sync[F].delay(println("called")).as(s"Number $x")
```
returns

```
called
Number 335
called
Number 335
Number 335
```
### Forced invalidation
To invalidate cache update CacheControl and set it to current time:
```scala
import tofu.memo._
import cats.effect.{Clock, Sync, Timer}
import cats.effect.concurrent.Ref
import cats.syntax.applicative._
import cats.syntax.flatMap._
import cats.syntax.functor._
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global
import scala.concurrent.duration.FiniteDuration
import java.util.concurrent.TimeUnit

def effect[F[_] : Sync : Timer]: Int => F[String] =
x =>
Sync[F].delay(println("called")).as(s"Number $x")

def f[F[_] : Sync : Clock : Timer](ttl : FiniteDuration) =
for {
ref <- Ref.of[F, CacheControl](CacheControl.empty)
kasha <- CachedFunc[F](effect)(ttl, ref.get).refVals.ref
v1 <- kasha(335)
_ <- Sync[F].delay(println(v1))
_ <- Timer[F].sleep(FiniteDuration(2L, TimeUnit.SECONDS))
v2 <- kasha(335)
_ <- Sync[F].delay(println(v2))
now <- Clock[F].realTime(TimeUnit.MILLISECONDS)
_ <- ref.update(_.copy(InvalidationTS(now)))
_ <- Timer[F].sleep(FiniteDuration(3L, TimeUnit.SECONDS))
v3 <- kasha(335)
_ <- Sync[F].delay(println(v3))
} yield ()

f[Task](FiniteDuration(1000L, TimeUnit.SECONDS)).runSyncUnsafe()

/*
called
Number 335
Number 335
called
Number 335
*/
```
How it works. Cached value remembers time of read access during update (T_update). On any read access tofu selects a time to compare with T_update (update value when `T_selected > T_update`). Selection is based on a formula `max(T_now - TTL, T_CacheControl)`. If you set `T_CacheControl = T_now` then always T_CacheControl is more than T_update. After update of a value, T_update equals T_CacheControl.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to emphasize the name of the block to make it more distinguishable visually from other text, something like header would do.
Also, not sure about tofu selects part. Library itself doesn't select anything, it's an implementation that makes any work.

Copy link
Contributor Author

@optician optician Feb 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This text is a part of the sample. Why it have be separated?

Library itself doesn't select anything

It's about selection of a computational branch.

7 changes: 7 additions & 0 deletions website/sidebars.json
Original file line number Diff line number Diff line change
@@ -20,6 +20,13 @@
"env",
"hascontext"
]
},
{
"type": "subcategory",
"label": "Utilities",
"ids": [
"memo"
]
}
]
}