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

Add timeout capability to Future implementation of FetchMonadError #127

Merged
merged 14 commits into from
Aug 15, 2017

Conversation

justinhj
Copy link
Contributor

The attached PR adds timeout capability to FetchMonadError[Future]

I also modified Query.Sync to return a completed future which I think is the intent rather than run it in a Future

All tests pass but I didn't yet create tests for the timeout, I can do so if the PR looks like something you might want

Only thing I'm not sure about is whether the use of synchronize is needed. I'm not sure but I think Promise may be safe to use from multiple threads because it uses an AtomicReference but didn't find any confirmation of that.

@justinhj
Copy link
Contributor Author

Removed synchronization on Promise and simplified the PR

@peterneyens peterneyens self-requested a review July 18, 2017 16:31
@justinhj
Copy link
Contributor Author

Have added tests for the two cases of a future failing because it timed out and succeeding because it finished before the timeout occurred.

override def name = "ArticleFuture"
override def fetchOne(id: ArticleId): Query[Option[Article]] =
Query.async((ok, fail) => {
Thread.sleep(delay.toMillis)
Copy link
Contributor

Choose a reason for hiding this comment

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

Thread.sleep doesn't work on ScalaJS, which is the reason that Travis is complaining.

case Async(ac, timeout) => {

case Sync(e) =>
Future.successful({e.value})
Copy link
Contributor

Choose a reason for hiding this comment

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

Evaluating e could potentially throw an exception and those aren't caught by Future.successful.


implicit val dsWillTimeout = ConfigurableTimeoutDatasource(250 milliseconds, 750 milliseconds)

def article(id: Int): Fetch[Article] = Fetch(ArticleId(id))
Copy link
Contributor

Choose a reason for hiding this comment

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

If you added an implicit DataSource[ArticleId, Article] you wouldn't need to implement this method in all the tests.

def article(id: Int)(implicit DS: DataSource[ArticeId, Article]): Fetch[Article] =
  Fetch(ArticleId(id))  // or Fetch(ArticleId(id))(DS)

import scala.concurrent.duration._
import org.scalatest._
import cats.data.NonEmptyList
import scala.language.postfixOps
Copy link
Contributor

Choose a reason for hiding this comment

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

You shouldn't need this as we have "-language:postfixOps" as a scalacOption.

val expTime = System.nanoTime + ms*1000000
while(System.nanoTime < expTime) {}
println("sleep finished")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure if we can actually test the time out in ScalaJS.

I think that if you block like this, you block everything in JS, so the TimerTask cannot finish the promise with a TimeoutExecption (which is happening in the failing test on Travis).

We could revert back to using Thread.sleep and add this as a JVM only test, by moving it to jvm/src/test/scala, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I think that makes sense. I've been trying to think of a way to isolate the timeout behaviour in a way that can be demonstrated to work with scala js but haven't come up with a way yet.

@justinhj
Copy link
Contributor Author

justinhj commented Aug 3, 2017

I've updated the PR so that the tests that require non-blocking sleep are now in the JVM module. Seems travis build is failing but this is due to a timeout building the docs

timer.schedule(timerTask, timeout.toMillis)

// Execute the user's action
ec.execute(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This conversion (and the one below) to Runnable only works in 2.12 I think which added support for Java 8 SAMs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops my bad. IntelliJ suggested it and it seemed like a good idea. I'll change it back

@justinhj
Copy link
Contributor Author

justinhj commented Aug 4, 2017

I think everything looks good now, just getting a timeout with Travis building the docs

@peterneyens
Copy link
Contributor

There definitely is something strange going on when combining Timer and tut.

Commenting out implicits.timer (and as a consequence timer.schedule(...)) makes tut run fine.

A minimal tut file which makes tut hang (notice that we aren't even using the time out here, since we are using Query.sync):

// ```tut
import scala.concurrent.{Await, Future}
import scala.concurrent.duration.Duration
import scala.concurrent.ExecutionContext.Implicits.global

import fetch.{FetchMonadError, Query}
import fetch.implicits._

val query: Query[Option[Int]] = Query.sync { Some(1) }

val future: Future[Option[Int]] = FetchMonadError[Future].runQuery(query)
Await.result(future, Duration.Inf)
// ```

If I change the Sync from Future(...) to the line below, tut runs fine for the minimal file, not for the complete docs.md file though ...

case Sync(e) =>
  cats.ApplicativeError[Future, Throwable].catchNonFatalEval(e)

It seems like I need to do some more investigating to see what is going on ...

@justinhj
Copy link
Contributor Author

justinhj commented Aug 4, 2017

Ah timer is supposed to run on a daemon thread and if it doesn't then it can stop a program from terminating. I'll check the code and see if I'm doing that correctly

@codecov-io
Copy link

codecov-io commented Aug 5, 2017

Codecov Report

Merging #127 into master will increase coverage by 0.43%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #127      +/-   ##
==========================================
+ Coverage      78%   78.43%   +0.43%     
==========================================
  Files          14       14              
  Lines         300      306       +6     
  Branches        2        2              
==========================================
+ Hits          234      240       +6     
  Misses         66       66
Impacted Files Coverage Δ
shared/src/main/scala/implicits.scala 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ae93d41...af1f8d9. Read the comment docs.

@justinhj
Copy link
Contributor Author

justinhj commented Aug 5, 2017

I've updated so Timer is on a daemon thread and also created lazily so it is not initialized by programs that don't use it

implicit ec: ExecutionContext
): FetchMonadError[Future] =
// Shared Timer object to schedule timeouts
lazy val timer: Timer = new Timer(true)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make this private[fetch] ?


case class ArticleId(id: Int)
case class Article(id: Int, content: String) {
def author: Int = id + 1
Copy link
Contributor

Choose a reason for hiding this comment

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

I seems this author method isn't used, so we can drop it, right?

Remove unused author ID from test
Rename test variable to be more accurate in the output
case _ =>

// Execute the user's action
ec.execute(new Runnable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpicking, we can create this Runnable once before the timeout match { ... }.
Even more nitpicking, we can lose the curly braces around ac(p.trySuccess, p.tryFailure).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, maybe there's a better naming for the runnable than 'runnable', suggestions welcome

implicit ec: ExecutionContext
): FetchMonadError[Future] =
// Shared Timer object to schedule timeouts
private[fetch] lazy val timer: Timer = new Timer(true)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably name the daimon thread as well, something like "fetch-future-timeout-daemon" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah good idea, will help avoid confusion when people look at running threads and don't know what it's for

Copy link
Contributor

@peterneyens peterneyens left a comment

Choose a reason for hiding this comment

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

Thanks @justinhj!

@peterneyens peterneyens merged commit b1207c1 into xebia-functional:master Aug 15, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants