Scala - differents between eventually timeout and Thread.sleep()

771 Views Asked by At

I have some async (ZIO) code, which I need to test. If I create a testing part using Thread.sleep() it works fine and I always get response:

for {
 saved <- database.save(smth)
 result <- eventually {
   Thread.sleep(20000)
   database.search(...) 
 }
} yield result

But if I made same logic using timeout and interval from eventually then it never works correctly ( I got timeouts):

for {
   saved <- database.save(smth)
   result <- eventually(timeout(Span(20, Seconds)), interval(Span(20, Seconds))) {
     database.search(...) 
   }
} yield result

I do not understand why timeout and interval works different then Thread.sleep. It should be doing exactly same thing. Can someone explain it to me and tell how I should change this code to do not need to use Thread.sleep()?

2

There are 2 best solutions below

0
On BEST ANSWER

Assuming database.search(...) returns ZIO[] object.

eventually{database.search(...)} most probably succeeds immediately after the first try.

It successfully created a task to query the database. Then database is queried without any retry logic.

Regarding how to make it work:

val search: ZIO[Any, Throwable, String] = ???
val retried: ZIO[Any with Clock, Throwable, Option[String]] = search.retry(Schedule.spaced(Duration.fromMillis(1000))).timeout(Duration.fromMillis(20000))

Something like that should work. But I believe that more elegant solutions exist.

0
On

The other answer from @simpadjo addresses the "what" quite succinctly. I'll add some additional context as to why you might see this behavior.

for {
  saved <- database.save(smth)
  result <- eventually {
    Thread.sleep(20000)
    database.search(...) 
  }
} yield result

There are three different technologies being mixed here which is causing some confusion.

First is ZIO which is an asynchronous programming library that uses it's own custom runtime and execution model to perform tasks. The second is eventually which comes from ScalaTest and is useful for checking asynchronous computations by effectively polling the state of a value. And thirdly, there is Thread.sleep which is a Java api that literally suspends the current thread and prevents task progression until the timer expires.

eventually uses a simple retry mechanism that differs based on whether you are using a normal value or a Future from the scala standard library. Basically it runs the code in the block and if it throws then it sleeps the current thread and then retries it based on some interval configuration, eventually timing out. Notably in this case the behavior is entirely synchronous, meaning that as long as the value in the {} doesn't throw an exception it won't keep retrying.

Thread.sleep is a heavy weight operation and in this case it is effectively blocking the function being passed to eventually from progressing for 20 seconds. Meaning that by the time the database.search is called the operation has likely completed.

The second variant is different, it executes the code in the eventually block immediately, if it throws an exception then it will attempt it again based on the interval/timeout logic that your provide. In this scenario the save may not have completed (or propagated if it is eventually consistent). Because you are returning a ZIO which is designed not to throw, and eventually doesn't understand ZIO it will simply return the search attempt with no retry logic.

The accepted answer:

val retried: ZIO[Any with Clock, Throwable, Option[String]] = search.retry(Schedule.spaced(Duration.fromMillis(1000))).timeout(Duration.fromMillis(20000))

works because the retry and timeout are using the built-in ZIO operators which do understand how to actually retry and timeout a ZIO. Meaning that if search fails the retry will handle it until it succeeds.