-
Notifications
You must be signed in to change notification settings - Fork 535
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 serialization support #2360
Conversation
import SerializationUtil._ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is cool! But maybe we can re-use the SerializableTests
in cats-kernel-laws
? Example: https://github.com/typelevel/cats/blob/e0784fc0d95b9c4eed0bb1ddce6c69e3fd9a4a4f/tests/src/test/scala/cats/tests/FunctionSuite.scala#L46
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@armanbilge Can you please point me to the source code of SerializableTests
? I was looking for it but I couldn't find it for some reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The meat of it is in the law actually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's perfect, let's use that. Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SerializableTests doesn't require Arbitrary, I have used law directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch!
val ls = List( | ||
IO.pure(42), | ||
IO.raiseError(TestException(42)), | ||
IO.delay(42), | ||
IO.realTime, | ||
IO.monotonic, | ||
IO.executionContext, | ||
IO.pure(42).map(_ + 1), | ||
IO.pure(42).flatMap(_ => IO.pure(42)), | ||
IO.pure(42).attempt, | ||
IO.raiseError(new RuntimeException("Err")).handleErrorWith(_ => IO.pure(42)), | ||
IO.canceled, | ||
IO.pure(42).onCancel(IO.pure(42)), | ||
IO.pure(42).uncancelable, | ||
IO.pure(42).start, | ||
IO.racePair(IO.pure(42), IO.pure(42)), | ||
IO.sleep(0.second), | ||
IO.trace | ||
).map(_.as(42)).zipWithIndex |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an idea—I wonder if with some strategic overrides in AsyncGenerators
you can use it to generate IO
without evalOn
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's how this is done.
cats-effect/testkit/shared/src/main/scala/cats/effect/testkit/TestInstances.scala
Lines 68 to 74 in ca1da1f
override def recursiveGen[B: Arbitrary: Cogen](deeper: GenK[IO]) = | |
super | |
.recursiveGen[B](deeper) | |
.filterNot( | |
_._1 == "racePair" | |
) // remove the racePair generator since it reifies nondeterminism, which cannot be law-tested | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could also make TestContext
extend Serializable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes that was very nice observation, but I couldn’t see it through.
Stuck because somehow ExecutionContextImpl
instance is seeping in
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine, evalOn
was never supposed to be serializable anyway :)
kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/Generators.scala
Show resolved
Hide resolved
import SerializationUtil._ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@armanbilge Can you please point me to the source code of SerializableTests
? I was looking for it but I couldn't find it for some reason.
import java.io._ | ||
import java.util.Base64 | ||
|
||
def serialize(`object`: Serializable): Try[String] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use obj
instead of object with backticks please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deleted this file now
…rialization, switched to Serializable laws
@@ -361,8 +361,9 @@ object pure { | |||
ft.mapK(fk) | |||
} | |||
|
|||
// todo: MVar is not Serializable, release then update here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inclusion of racePair
test leads us here, we'd need to update Mvar with serialization
Thanks for the detailed review, after adding the property test for IO, I noticed a lot of trait/abstract classes missing serialization, have enhanced those in this very PR |
@@ -76,6 +77,22 @@ trait TestInstances extends ParallelFGenerators with OutcomeGenerators with Sync | |||
Arbitrary(generators.generators[A]) | |||
} | |||
|
|||
def arbitraryIOWithoutContextShift[A: Arbitrary: Cogen]: Arbitrary[IO[A]] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def arbitraryIOWithoutContextShift[A: Arbitrary: Cogen]: Arbitrary[IO[A]] = { | |
def arbitraryIOWithoutEvalOn[A: Arbitrary: Cogen]: Arbitrary[IO[A]] = { |
@gagandeepkalra This is awesome work, but would you mind running |
Hi @djspiewak, sure thing, let me get to this over the weekend. Also, what do you think about this- #2360 (comment) Should I go there and make this change, then the support would be complete, of course this PR won't have to wait. |
@gagandeepkalra I guess I'm a bit confused as to why |
I need some help, for yet I'm not able to reproduce the failure locally, would it help to rerun the case (I don't have the permission to re-run)? |
@gagandeepkalra try setting the seed like this: implicit def parameters = Parameters(seed =
Seed.fromBase64("mWUDuysFx0P-r_sHw_GGVwvm-TcqPoLNs8aOV2g8FuD=").toOption
) |
Hi @armanbilge, think I'm making a real silly error somewhere this is my testcase with your suggestion, the test would not fail wheresoever I put this seed. Please help.
|
I'm sorry! I think you ran into this bug maybe? That's going to make it pretty hard to replicate 😕 |
Here's an example of how you can make it work: checkAll(
"Kleisli[PureConc]",
GenTemporalTests[Kleisli[TimeT[PureConc[Int, *], *], MiniInt, *], Int]
.temporal[Int, Int, Int](10.millis)
// we need to bound this a little tighter because these tests take FOREVER
)(Parameters(minTestsOk = 25)) Add |
@gagandeepkalra we just updated specs2 in #2583 which should have a fix for the seed issue. You can merge the latest series/3.x and try again ... thanks! |
Actually, I probably need to release |
Oh, are we still only getting specs2 transitively in some subprojects? We should really just fix that :) |
okay so I tried skipping specs2 and working directly with Gen to produce a value then call serialize. The test passed, I'm starting to think the seed we get here is not correct. I added another case for @djspiewak's suggestion, they both generated similar looking value given the seed. code: package cats.effect
import cats.kernel.laws.SerializableLaws.serializable
import org.scalacheck.rng.Seed
import org.scalacheck.{Arbitrary, Gen, Prop}
import org.specs2.scalacheck.Parameters
import org.typelevel.discipline.specs2.mutable.Discipline
import scala.util.Try
import scala.util.control.NonFatal
class SyncIOSpec extends BaseSpec with Discipline with SyncIOPlatformSpecification {
implicit override val defaultParameters: Parameters = Parameters(
minSize = 10000,
maxSize = 10000,
seed = Seed.fromBase64("mWUDuysFx0P-r_sHw_GGVwvm-TcqPoLNs8aOV2g8FuD=").toOption
)
"sync io monad" should {
val arb: Arbitrary[SyncIO[Int]] = implicitly[Arbitrary[SyncIO[Int]]]
val p: Prop = Prop.forAll(arb.arbitrary)(serializable)(identity, implicitly, implicitly)
check(p, defaultParameters, defaultFreqMapPretty)
"test Specific Serialize" in {
val seed: Try[Seed] = Seed.fromBase64("mWUDuysFx0P-r_sHw_GGVwvm-TcqPoLNs8aOV2g8FuD=")
val value: SyncIO[Int] = arb.arbitrary.apply(Gen.Parameters.default.withInitialSeed(seed.get), seed.get).get
verifySerializableOrThrow(value)
true
}
}
def verifySerializableOrThrow[A](a: A): Unit = {
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream}
val baos = new ByteArrayOutputStream()
val oos = new ObjectOutputStream(baos)
var ois: ObjectInputStream = null // scalastyle:ignore null
try {
oos.writeObject(a)
oos.close()
val bais = new ByteArrayInputStream(baos.toByteArray)
ois = new ObjectInputStream(bais)
val a2: A = ois.readObject().asInstanceOf[A]
println("BEFORE:" + a)
println("AFTER:" + a2)
ois.close()
} catch {
case NonFatal(t) => println(t)
} finally {
oos.close()
if (ois != null) ois.close() // scalastyle:ignore null
}
}
}
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I had a go at this locally and repeatedly ran and re-ran the tests for a few hours. Never had a single failure. I think any issues here were resolved incidentally by something else in series/3.x
along the way.
Love to see accidentally unblocking a big feature. |
Hey! Fun fact: the failures are Scala 3 specific. |
Even more fun fact, it serializes correctly but doesn't deserialize. In other words, this really smells like an What we know is that hte
The only thing non-trivial in there is really |
|
Okay, couple problems here. First, accessing private[runtime] object ModuleSerializationProxy {
private val instances = new ClassValueCompat[Object] {
@nowarn("cat=deprecation") // AccessController is deprecated on JDK 17
def getModule(cls: Class[_]): Object =
java.security.AccessController.doPrivileged(
(() => cls.getField("MODULE$").get(null)): PrivilegedExceptionAction[Object])
override protected def computeValue(cls: Class[_]): Object =
try getModule(cls)
catch {
case e: PrivilegedActionException =>
rethrowRuntime(e.getCause)
}
} Speaking of which, there's a bug here! (note the above is in the Scala stdlib, shared between 2 and 3). The bug becomes more apparent when I show you private def rethrowRuntime(e: Throwable): Object = {
val cause = e.getCause
cause match {
case exception: RuntimeException => throw exception
case _ => throw new RuntimeException(cause)
}
} So… why are we calling At any rate, I'm relatively certain this is definitively problems upstream, so I think the right approach here is to make the serialization tests Scala 2 exclusive. |
Scala 3 should behave like Scala 2 for object serialization (scala/scala#7297 / scala/scala3#5775), but we emit all objects as |
Yeah that seems weird, could you open an issue at scala/scala and ping Jason who wrote that code originally? Meanwhile, I think you could redefine ModuleSerializationProxy in your own project to shadow the one coming from scala-library and add a println to figure out what the name of the deserialized class is. |
I'll see about shadowing to see if we can get the actual class. |
Fun fact: you can't really shadow it because it's not on the classpath. So you can't get it to compile. |
Not sure I get it, I think you can copy-paste ModuleSerializationProxy.scala and ClassValueCompat.scala in your project and it should compile. But it might be easier to just fire up a debugger and break on rethrowRuntime. |
@smarter Here's something cool: if I shadow everything needed to make the shadowing compile (https://gist.github.com/423c26f4ed8147d0e97b03ee3c406d03), the exception disappears and both serialization and deserialization succeed. |
Huh. Before going deeper into the rabbit hole, can you try using a more recent version of Scala 3? (latest nightly is |
... actually latest nightly is |
@smarter Same issue :-( |
I think we'll probably just disable the tests on Scala 3 for the time being. We'll make a ticket to come back and try again once scala/scala#9970 is released as part of a Scala 3 build. I just don't want to keep this PR languishing forever. I really do wish we had a more minimal reproduction though. Any thoughts on whether this would also be worth filing upstream on Dotty? |
So I tried the copy-paste ModuleSerializationProxy.scala trick myself and was able to get a better exception after removing the extra getCause:
And the reason the JVM is unhappy is that |
The trouble with using setAccessible is that it will likely stop working when attempting to use the java module system, so we might have no choice but to make every class public. |
This aligns our behavior with Scala 2 and fixes the issue encountered in typelevel/cats-effect#2360 (comment). Alternatively, we could change ModuleSerializationProxy upstream to call `setAccessible(true)` on the MODULE$ field, but this wouldn't work if the object in question is inside a Java 9+ module.
This aligns our behavior with Scala 2 and fixes the issue encountered in typelevel/cats-effect#2360 (comment). Alternatively, we could change ModuleSerializationProxy upstream to call `setAccessible(true)` on the MODULE$ field, but this wouldn't work if the object in question is inside a Java 9+ module.
Thank you for doing that! It's absolutely wild that it didn't fail for me when I did that exact series of steps.
Ahhhhhhhhh. Okay that makes sense. That definitely seems like something that will end up being a blocker for things involving serialization (coughs Spark), particularly given the fact that lambdas make it very easy to accidentally close over state which would trigger this path. |
This aligns our behavior with Scala 2 and fixes the issue encountered in typelevel/cats-effect#2360 (comment). Alternatively, we could change ModuleSerializationProxy upstream to call `setAccessible(true)` on the MODULE$ field, but this wouldn't work if the object in question is inside a Java 9+ module.
This aligns our behavior with Scala 2 and fixes the issue encountered in typelevel/cats-effect#2360 (comment). Alternatively, we could change ModuleSerializationProxy upstream to call `setAccessible(true)` on the MODULE$ field, but this wouldn't work if the object in question is inside a Java 9+ module.
This aligns our behavior with Scala 2 and fixes the issue encountered in typelevel/cats-effect#2360 (comment). Alternatively, we could change ModuleSerializationProxy upstream to call `setAccessible(true)` on the MODULE$ field, but this wouldn't work if the object in question is inside a Java 9+ module.
This aligns our behavior with Scala 2 and fixes the issue encountered in typelevel/cats-effect#2360 (comment). Alternatively, we could change ModuleSerializationProxy upstream to call `setAccessible(true)` on the MODULE$ field, but this wouldn't work if the object in question is inside a Java 9+ module.
This aligns our behavior with Scala 2 and fixes the issue encountered in typelevel/cats-effect#2360 (comment). Alternatively, we could change ModuleSerializationProxy upstream to call `setAccessible(true)` on the MODULE$ field, but this wouldn't work if the object in question is inside a Java 9+ module.
This aligns our behavior with Scala 2 and fixes the issue encountered in typelevel/cats-effect#2360 (comment). Alternatively, we could change ModuleSerializationProxy upstream to call `setAccessible(true)` on the MODULE$ field, but this wouldn't work if the object in question is inside a Java 9+ module.
resolves #587