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

Support asynchronous data sources #54

Merged
40 commits merged into from
Jun 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
02cde66
Depend on monix-eval
May 27, 2016
8b43e76
Update DataSource to be implemented in terms of Task
May 27, 2016
69081f8
Update docs
May 29, 2016
e837ad8
Uniquify implicit name
May 29, 2016
2dfa3b4
Add tut compilation to CI
May 29, 2016
6a14cc4
Minor changes in docs
May 29, 2016
c22d13c
Update README
May 29, 2016
e2b1647
:fire:
May 29, 2016
5e48ae0
Only use fetchMany when there are batches
May 29, 2016
985f0c3
Update README
May 29, 2016
adbf420
Fix link
May 29, 2016
e8d5e87
Support asynchronous data sources
May 31, 2016
7f9f86a
wip update docs
May 31, 2016
02cfb03
Extract Monix integration into a subproject
May 31, 2016
620c1e8
Unsafe FetchMonadError instance for Eval
May 31, 2016
b775ea9
Stabilize Query type and support automatic batching
Jun 1, 2016
c7f0c3a
Update docs
Jun 1, 2016
1a4d6ff
Recover FetchMonadError[Eval]#runQuery implementation
Jun 1, 2016
6bde038
FetchMonadError[Id]
Jun 4, 2016
f5b83ab
Improve FetchMonadError[Future]
Jun 4, 2016
a58c251
Update docs to run fetches to Id
Jun 4, 2016
3aef47b
Update README
Jun 4, 2016
be095c0
Silence irrelevant output
Jun 4, 2016
0c8ca32
Document Query constructors
Jun 4, 2016
ba68bce
Document Monix Task as the target monad
Jun 4, 2016
1b3d118
Improve FetchMonadError[Task] for Monix
Jun 5, 2016
d2f99f2
Document how to bring your own concurrency monad
Jun 5, 2016
0d92468
Fix dependency specs
Jun 6, 2016
158a12a
Typo
Jun 6, 2016
2234f68
Concurrency monads
Jun 6, 2016
a34ae16
:fire:
Jun 6, 2016
ad842d6
:fire: are
Jun 6, 2016
d22eefe
Update README
Jun 6, 2016
8088021
Minor changes to README
Jun 7, 2016
b229d8f
Add monix tests to CI
Jun 7, 2016
8525f67
Unbreak Travis config
Jun 7, 2016
20172b0
Improvements to README
Jun 7, 2016
00e0a7d
Minor changes to DataSourceCache trait
Jun 10, 2016
daa9d59
Simplify Query constructors
Jun 10, 2016
d80da1f
Report coverage after running JVM tests
Jun 10, 2016
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
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ scala:
jdk:
- oraclejdk8
script:
- sbt coverage 'fetchJVM/test'
- sbt coverage 'fetchJVM/test' 'fetchJVM/coverageReport'
- sbt 'monixJVM/test' 'monixJS/test'
- sbt 'fetchJS/test'
- sbt 'docs/tut'
- sbt 'readme/tut'
after_success:
- sbt 'fetchJVM/coverageReport'
- bash <(curl -s https://codecov.io/bash) -t 47609994-e0cd-4f3b-a28d-eb558142c3bb
101 changes: 53 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ A library for Simple & Efficient data access in Scala and Scala.js

Add the following dependency to your project's build file.

For Scala 2.11.x:

```scala
"com.fortysevendeg" %% "fetch" %% "0.2.0"
"com.fortysevendeg" %% "fetch" % "0.2.0"

Choose a reason for hiding this comment

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

I'm guessing that these versions will change once a new build is made from this PR.

Copy link
Author

Choose a reason for hiding this comment

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

Yep, these changes will be released in a 0.3.0 version

```

Or, if using Scala.js:
Or, if using Scala.js (0.6.x):

```scala
"com.fortysevendeg" %%% "fetch" %% "0.2.0"
"com.fortysevendeg" %%% "fetch" % "0.2.0"
```

Fetch is available for the following Scala and Scala.js versions:

- Scala 2.11.x
- Scala.js 0.6.x


## Remote data

Expand All @@ -34,42 +34,41 @@ has a latency cost, such as databases or web services.

## Define your data sources

For telling `Fetch` how to get the data you want, you must implement the `DataSource` typeclass. Data sources have a `fetch` method that
defines how to fetch such a piece of data.
To tell `Fetch` how to get the data you want, you must implement the `DataSource` typeclass. Data sources have `fetchOne` and `fetchMany` methods that define how to fetch such a piece of data.

Data Sources take two type parameters:

Choose a reason for hiding this comment

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

I would add something into the Identity section here like "to fetch the data, this is usually some for of identifier".

Copy link
Author

Choose a reason for hiding this comment

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

Good suggestion, i'll make sure i go into more detail about what I mean by Identity.

<ol>
<li><code>Identity</code> is a type that has enough information to fetch the data</li>
<li><code>Result</code> is the type of data we want to fetch</li>
<li><code>Identity</code> is a type that has enough information to fetch the data. For a users data source, this would be a user's unique ID.</li>
<li><code>Result</code> is the type of data we want to fetch. For a users data source, this would the `User` type.</li>
</ol>

```scala
import cats.data.NonEmptyList

trait DataSource[Identity, Result]{
def fetchOne(id: Identity): Eval[Option[Result]]
def fetchMany(ids: NonEmptyList[Identity]): Eval[Map[Identity, Result]]
def fetchOne(id: Identity): Query[Option[Result]]
def fetchMany(ids: NonEmptyList[Identity]): Query[Map[Identity, Result]]
}
```

We'll implement a dummy data source that can convert integers to strings. For convenience, we define a `fetchString` function that lifts identities (`Int` in our dummy data source) to a `Fetch`.

Choose a reason for hiding this comment

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

Is it worth showing a non-convenient version of this? i.e. how would it look without the fetchString function?

Copy link
Author

Choose a reason for hiding this comment

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

is just a matter of using Fetch#apply with an identity as shown in the fetchString implementation. i want to encourage people to write a little DSL (getUser, getWhatever) for their fetches since it also helps with inference.


```scala
import cats.Eval
import cats.data.NonEmptyList
import cats.std.list._

import fetch._

implicit object ToStringSource extends DataSource[Int, String]{
override def fetchOne(id: Int): Eval[Option[String]] = {
Eval.later({
println(s"ToStringSource $id")
override def fetchOne(id: Int): Query[Option[String]] = {
Query.sync({
println(s"[${Thread.currentThread.getId}] One ToString $id")
Option(id.toString)
})
}
override def fetchMany(ids: NonEmptyList[Int]): Eval[Map[Int, String]] = {
Eval.later({
println(s"ToStringSource $ids")
override def fetchMany(ids: NonEmptyList[Int]): Query[Map[Int, String]] = {
Query.sync({
println(s"[${Thread.currentThread.getId}] Many ToString $ids")
ids.unwrap.map(i => (i, i.toString)).toMap
})
}
Expand All @@ -83,21 +82,26 @@ def fetchString(n: Int): Fetch[String] = Fetch(n) // or, more explicitly: Fetch(
Now that we can convert `Int` values to `Fetch[String]`, let's try creating a fetch.

```scala
import fetch.implicits._
import fetch.syntax._

val fetchOne: Fetch[String] = fetchString(1)
```

Now that we have created a fetch, we can run it to a target monad. Note that the target monad (`Eval` in our example) needs to implement `MonadError[M, Throwable]`, we provide an instance for `Eval` in `fetch.implicits._`, that's why we imported it.
We'll run our fetches to the ambien `Id` monad in our examples. Note that in real-life scenarios you'll want to run a fetch to a concurrency monad such as `Future` or `Task`, synchronous execution of a fetch is only supported in Scala and not Scala.js and is meant for experimentation purposes.

```scala
val result: String = fetchOne.runA[Eval].value
// ToStringSource 1
// result: String = 1
import cats.Id
import fetch.unsafe.implicits._
import fetch.syntax._
```

As you can see in the previous example, the `ToStringSource` is queried once to get the value of 1.
Let's run it and wait for the fetch to complete:

```scala
fetchOne.runA[Id]
// [169] One ToString 1
// res3: cats.Id[String] = 1
```

## Batching

Expand All @@ -112,27 +116,29 @@ val fetchThree: Fetch[(String, String, String)] = (fetchString(1) |@| fetchStrin
When executing the above fetch, note how the three identities get batched and the data source is only queried once.

Choose a reason for hiding this comment

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

Is it fair to say that we could also use fetchMany here to achieve the same result? If so, that'd be worth showing, perhaps before the tuples version.

Copy link
Author

Choose a reason for hiding this comment

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

Since identities are batched fetchMany is used to get them. The point is that you don't have to manually call fetchMany, i do it for you when there is many things of the same type to fetch.


```scala
val result: (String, String, String) = fetchThree.runA[Eval].value
// ToStringSource OneAnd(1,List(2, 3))
// result: (String, String, String) = (1,2,3)
fetchThree.runA[Id]
// [169] Many ToString OneAnd(1,List(2, 3))
// res5: cats.Id[(String, String, String)] = (1,2,3)
```

## Concurrency
## Parallelism

If we combine two independent fetches from different data sources, the fetches will be run concurrently. First, let's add a data source that fetches a string's size.
If we combine two independent fetches from different data sources, the fetches can be run in parallel. First, let's add a data source that fetches a string's size.

This time, instead of creating the results with `Query#sync` we are going to do it with `Query#async` for emulating an asynchronous data source.

```scala
implicit object LengthSource extends DataSource[String, Int]{
override def fetchOne(id: String): Eval[Option[Int]] = {
Eval.later({
println(s"LengthSource $id")
Option(id.size)
override def fetchOne(id: String): Query[Option[Int]] = {
Query.async((ok, fail) => {
println(s"[${Thread.currentThread.getId}] One Length $id")
ok(Option(id.size))
})
}
override def fetchMany(ids: NonEmptyList[String]): Eval[Map[String, Int]] = {
Eval.later({
println(s"LengthSource $ids")
ids.unwrap.map(i => (i, i.size)).toMap
override def fetchMany(ids: NonEmptyList[String]): Query[Map[String, Int]] = {
Query.async((ok, fail) => {
println(s"[${Thread.currentThread.getId}] Many Length $ids")
ok(ids.unwrap.map(i => (i, i.size)).toMap)
})
}
}
Expand All @@ -146,13 +152,13 @@ And now we can easily receive data from the two sources in a single fetch.
val fetchMulti: Fetch[(String, Int)] = (fetchString(1) |@| fetchLength("one")).tupled
```

Note how the two independent data fetches are run concurrently, minimizing the latency cost of querying the two data sources. If our target monad was a concurrency monad like `Future`, they'd run in parallel, each in its own logical thread.
Note how the two independent data fetches run in parallel, minimizing the latency cost of querying the two data sources.

```scala
val result: (String, Int) = fetchMulti.runA[Eval].value
// ToStringSource OneAnd(1,List())
// LengthSource OneAnd(one,List())
// result: (String, Int) = (1,3)
fetchMulti.runA[Id]
// [169] One ToString 1
// [170] One Length one
// res7: cats.Id[(String, Int)] = (1,3)
```

## Caching

Choose a reason for hiding this comment

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

I think it's worth calling out in the opening paragraphs about identity, that you are using the identity and the thing that is fetched for the given the identity interchangeably throughout the examples.

Copy link
Author

Choose a reason for hiding this comment

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

good catch, i'll reword that since it'll probably cause confusion.

Expand All @@ -169,8 +175,7 @@ val fetchTwice: Fetch[(String, String)] = for {
While running it, notice that the data source is only queried once. The next time the identity is requested it's served from the cache.

```scala
val result: (String, String) = fetchTwice.runA[Eval].value
// ToStringSource 1
// result: (String, String) = (1,1)
fetchTwice.runA[Id]
// [169] One ToString 1
// res8: cats.Id[(String, String)] = (1,1)
```

19 changes: 18 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ lazy val docs = (project in file("docs"))
.settings(
moduleName := "fetch-docs"
)
.dependsOn(fetchJVM)
.dependsOn(fetchJVM, fetchMonixJVM)
.enablePlugins(JekyllPlugin)
.settings(docsSettings: _*)
.settings(noPublishSettings)
Expand Down Expand Up @@ -127,3 +127,20 @@ lazy val readme = (project in file("tut"))
.settings(readmeSettings: _*)
.settings(noPublishSettings)

lazy val monixSettings = (
libraryDependencies ++= Seq(
"io.monix" %%% "monix-eval" % "2.0-RC5"
)
)

lazy val monix = crossProject.in(file("monix"))
.dependsOn(fetch)
.settings(moduleName := "fetch-monix")
.settings(allSettings:_*)
.jsSettings(fetchJSSettings:_*)
.settings(monixSettings: _*)
.enablePlugins(AutomateHeaderPlugin)


lazy val fetchMonixJVM = monix.jvm
lazy val fetchMonixJS = monix.js
Loading