scalaz / ioeffect

An effect monad for Scalaz 7.2

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`IO`s created with `par` fail with `ClassCastException` if performed within loop

ktonga opened this issue · comments

So it seems something is going on wrong with par implementation, when performed in isolation it seems to work but it starts having unpredictable behavior when performed multiple consecutive times, I realized about this since I have some property-based test with default sampling of 100 performing some IO using par combinator.

You can find the test i'm talking about here: https://github.com/ktonga/scala-poc/blob/topic/parallel-io/src/test/scala/com/brickx/autoinvest/ProgramSpec.scala#L26

This tests used to run ok when IOs were combined using apply2 from default Applicative instance but started failing after I introduced the use of par in this WIP PR: ktonga/scala-poc#6

I'll try to add a minimal example that reproduces the bug to this project for easy debugging.

Cheers.

@ktonga Thanks, I will investigate and fix (assuming it's in 8 which has some bug fixes relative to this version).

Thanks John! I haven't tested it on 8 since i found it in an app where I'm only using the port.

@jdegoes I added to scalaz8's RTSSpec and it failed:

[error]     ! multiple par of two values
[error]  java.lang.ClassCastException: scala.Tuple2 cannot be cast to java.lang.Integer (file:1)

Awesome, thanks—fix coming shortly.

commented

Apparently not? Latest fixes were backported (I think)

Also, it is exceptionally annoying that specs2 swallows these things.

java.lang.ClassCastException: scala.Tuple2 cannot be cast to java.lang.Integer
	at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:101)
	at scalaz.ioeffect.RTSSpec.$anonfun$testPar$2(RTSTest.scala:376)
	at scala.runtime.java8.JFunction0$mcI$sp.apply(JFunction0$mcI$sp.java:12)
	at org.specs2.matcher.Expectable.value$lzycompute(Expectable.scala:21)
	at org.specs2.matcher.Expectable.value(Expectable.scala:21)
	at org.specs2.matcher.EqualityMatcher.apply(EqualityMatcher.scala:27)
	at org.specs2.matcher.Expectable.applyMatcher(Expectable.scala:51)
	at org.specs2.matcher.MustExpectable.must_$eq$eq$eq(MustExpectable.scala:20)
	at scalaz.ioeffect.RTSSpec.$anonfun$testPar$1(RTSTest.scala:377)
	at scalaz.ioeffect.RTSSpec.$anonfun$testPar$1$adapted(RTSTest.scala:372)
	at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:234)
	at scala.collection.immutable.Range.foreach(Range.scala:156)
	at scala.collection.TraversableLike.map(TraversableLike.scala:234)
	at scala.collection.TraversableLike.map$(TraversableLike.scala:227)
	at scalaz.ioeffect.RTSSpec.testPar(RTSTest.scala:372)
	at scalaz.ioeffect.RTSSpec.$anonfun$is$46(RTSTest.scala:84)
	at org.specs2.specification.AroundTimeout$$anon$1.result$lzycompute$1(ExamplesTimeout.scala:37)
	at org.specs2.specification.AroundTimeout$$anon$1.result$1(ExamplesTimeout.scala:37)
	at org.specs2.specification.AroundTimeout$$anon$1.$anonfun$around$3(ExamplesTimeout.scala:40)
	at org.specs2.matcher.MatchResult$$anon$13.asResult(MatchResult.scala:349)
	at org.specs2.execute.AsResult$.apply(AsResult.scala:32)
	at org.specs2.specification.AroundTimeout$$anon$1.around(ExamplesTimeout.scala:40)
	at org.specs2.specification.Around.apply(Context.scala:137)
	at org.specs2.specification.Around.apply$(Context.scala:137)
	at org.specs2.specification.AroundTimeout$$anon$1.apply(ExamplesTimeout.scala:35)
	at org.specs2.specification.AroundTimeout.upTo(ExamplesTimeout.scala:32)
	at org.specs2.specification.AroundTimeout.upTo$(ExamplesTimeout.scala:31)
	at scalaz.ioeffect.RTSSpec.upTo(RTSTest.scala:15)
	at scalaz.ioeffect.RTSSpec.$anonfun$is$45(RTSTest.scala:84)
	at org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
	at org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:21)
	at org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:119)
	at org.specs2.execute.Result$$anon$11.asResult(Result.scala:246)
	at org.specs2.execute.AsResult$.apply(AsResult.scala:32)
	at org.specs2.specification.core.AsExecution$$anon$1.$anonfun$execute$1(AsExecution.scala:15)
	at org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
	at org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:21)
	at org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:119)
	at org.specs2.execute.Result$$anon$11.asResult(Result.scala:246)
	at org.specs2.execute.AsResult$.apply(AsResult.scala:32)
	at org.specs2.execute.AsResult$.$anonfun$safely$1(AsResult.scala:40)
	at org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
	at org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:21)
	at org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:119)
	at org.specs2.execute.AsResult$.safely(AsResult.scala:40)
	at org.specs2.specification.core.Execution$.$anonfun$result$1(Execution.scala:305)
	at org.specs2.specification.core.Execution$.$anonfun$withEnvSync$3(Execution.scala:323)
	at org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
	at org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:21)
	at org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:119)
	at org.specs2.execute.Result$$anon$11.asResult(Result.scala:246)
	at org.specs2.execute.AsResult$.apply(AsResult.scala:32)
	at org.specs2.execute.AsResult$.$anonfun$safely$1(AsResult.scala:40)
	at org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
	at org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:21)
	at org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:119)
	at org.specs2.execute.AsResult$.safely(AsResult.scala:40)
	at org.specs2.specification.core.Execution$.$anonfun$withEnvSync$2(Execution.scala:323)
	at org.specs2.specification.core.Execution.$anonfun$startExecution$3(Execution.scala:135)
	at scala.util.Success.$anonfun$map$1(Try.scala:251)
	at scala.util.Success.map(Try.scala:209)
	at scala.concurrent.Future.$anonfun$map$1(Future.scala:289)
	at scala.concurrent.impl.Promise.liftedTree1$1(Promise.scala:29)
	at scala.concurrent.impl.Promise.$anonfun$transform$1(Promise.scala:29)
	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

Hmm, this is really weird since it passes locally and in Travis CI (in fact I have two tests now that test essentially the same thing). I'll work harder to reproduce this locally.

commented

TIL testOnly -- fullstacktrace is how to surface stack traces in specs2 .

commented

To reproduce for me

ioeffect/testOnly *RTS* -- fullStackTrace
  @inline
  final def nextInstr[E](value: Any, stack: Stack): IO[E, Any] =
    if (!stack.isEmpty()) stack.pop()(value).asInstanceOf[IO[E, Any]] else null

is where it's happening. I'm guessing a tuple is added to the stack incorrectly.

commented

Some printlns to help...

  @inline
  final def nextInstr[E](value: Any, stack: Stack): IO[E, Any] = try {
    if (!stack.isEmpty()) stack.pop()(value).asInstanceOf[IO[E, Any]] else null
  } catch {
    case t: ClassCastException =>
      println(s"GOT $value in stack $stack")

      throw t
  }
GOT 1 in stack scalaz.ioeffect.RTS$Stack@5ad19e47
GOT (1,\/-(2)) in stack scalaz.ioeffect.RTS$Stack@5e0b8c09
GOT FlatMap(FlatMap(AsyncEffect(scalaz.ioeffect.RTS$FiberContext$$Lambda$863/110484312@431ccd3f),scalaz.ioeffect.IO$$$Lambda$656/285346270@479d6400),scalaz.ioeffect.IO$$Lambda$926/1075799910@120b03a7) in stack scalaz.ioeffect.RTS$Stack@5e0b8c09
commented

btw, should those vars not be marked @volatile?

It was really easy for me to reproduce it, i just ran that single test a few times in a row in both repos and the test failed every single time with class cast exception. Do we have real parallelism in CI? some times with the vm has a tiny cpu the thread pool has only a couple threads. This seems to happen only when a lot of stuff is going on at the same time.

Thanks, this is helpful. I'll make sure I actually fix it this time. 😄

Oh my, this was one of the most fun bugs I've fixed in a long time. PR coming soon. 😄

@ktonga Can you verify?

@jdegoes will do! Thanks.

commented

Raise a PR with the backport and I'll release tomorrow as 2.2.0

Can confirm, bug seems to be fixed in z8. Will work on the backport. Thanks John!

Fixed by #34