How does `failAfter` work

I asked this question on discord and got no response. I’ll reäsk it here as perhaps the answer is not so trivial.

Does anyone know something about org.scalatest.concurrent.TimeLimits.failAfter ? I have the following test that I waiting about 45 minutes to an hour for it to complete. I finally killed it.

test("test_extract_rte") {
    // test should fail if cannot run within 60 seconds
    // perhaps need to adjust number of seconds
    failAfter(Span(60000, Millis)) {
      SAtomic.withClosedWorldView {
        for {depth <- 1 to 3
             rep <- 0 to 25
             rt = Rte.randomRte(depth)
             } check_extraction_cycle(rt, depth, rep)
      }
    }
}

Does failAfter somehow work differently than I thought it would?

Here is the doc in case it helps.

Yes, I looked at the docs, but it isn’t clear to me how my conception is wrong.

Screenshot 2024-03-21 at 12.05.18
Looks like the default implementation is don’t-time-out. that’s a surprise.

What does override mean here? Shouldn’t this be implicit, rather than override ?
Screenshot 2024-03-21 at 12.12.07

It’s “always fail on excessive duration, but don’t fail fast once duration becomes excessive”. Which may not be immediately intuitive, but it makes sense - as the different implementations suggest, there is no single “right” way of halting a computation, and interrupting a thread potentially has harmful side effects.

Yes. Perhaps a previous version of TimeLimits came with a #signaler member…? :person_shrugging:

Like @sangamon said, there is no one right way to “cancel” a random piece of code. It may even not be cancelable at all, especially if it’s only doing heavy computations on the CPU or infinitely looping without checking for thread interrupts.

2 Likes

I really don’t understand how it’s supposed to work. I’m trying to implement a timeout of 1 minute. I’ve added the line as follows, left for lunch at 12h15, came back at 13h30, and it’s still running.

  test("test_extract_rte") {
    // test should fail if cannot run within 60 seconds
    // perhaps need to adjust number of seconds
    implicit val signaler: Signaler = ThreadSignaler
    failAfter(Span(60000, Millis)) {
      SAtomic.withClosedWorldView {
        for {depth <- 1 to 3
             rep <- 0 to 25
             rt = Rte.randomRte(depth)
             } check_extraction_cycle(rt, depth, rep)
      }
    }
  }

Probably exactly what @Jasper-M describes: Too busy to be interrupted. :slightly_smiling_face:

Using ScalaTest 3.2.15, this test fails after 1 second…

import org.scalatest.concurrent.*
import org.scalatest.funsuite.*
import org.scalatest.time.SpanSugar.*

class FailAfterTest extends AnyFunSuite with TimeLimits:

  implicit val signaler: Signaler = ThreadSignaler

  private def forever(): Unit =
    while(true) {
      if(Thread.currentThread().isInterrupted) return ()
    }

  test("fail after") {
    failAfter(1.second) {
      val start = System.currentTimeMillis()
      try {
        forever()
      }
      finally {
        println(s"${System.currentTimeMillis() - start} ms")
      }
    }
  }

…but if the interrupted check is commented out, it will run indefinitely.

so I need to insert a check for interrupted in my CPU intensive loop somewhere? That’s doable.

Off-topic comment, reasons why I love cats-effect: timeout :stuck_out_tongue:

Until you have an uncancelable IO :stuck_out_tongue: But indeed having a sane way to compose a program with built-in and well-defined cancelation semantics is a big improvement.

1 Like

That’s life on the JVM, yeah.

1 Like

I’m just glad you got a bite to eat.

1 Like

I use ScalaTest to grade student code. Timeouts are a big issue, as their code typically doesn’t react to interrupts. I use various tricks, but there’s no perfect solution. I do use this little construct everywhere I can (i.e., when loops are in my tests, not their code):

@throws[InterruptedException]
inline def interruptibly[A](inline code: A): A =
   if Thread.interrupted() then throw InterruptedException() else code

so I can write:

while ... do interruptibly:
   <call student code>