From e2427533c563cb5ca708dbcaa51823fcc30102db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 22 Jul 2024 17:13:41 +0200 Subject: [PATCH] Fix #20856: Serialize `Waiting` and `Evaluating` as if `null`. This strategy ensures the "serializability" condition of parallel programs--not to be confused with the data being `java.io.Serializable`. Indeed, if thread A is evaluating the lazy val while thread B attempts to serialize its owner object, there is also an alternative schedule where thread B serializes the owner object *before* A starts evaluating the lazy val. Therefore, forcing B to see the non-evaluating state is correct. --- library/src/scala/runtime/LazyVals.scala | 20 ++++++- tests/run/i20856.check | 1 + tests/run/i20856.scala | 70 ++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/run/i20856.check create mode 100644 tests/run/i20856.scala diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index e38e016f5182..15220ea2410a 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -52,13 +52,29 @@ object LazyVals { * Used to indicate the state of a lazy val that is being * evaluated and of which other threads await the result. */ - final class Waiting extends CountDownLatch(1) with LazyValControlState + final class Waiting extends CountDownLatch(1) with LazyValControlState { + /* #20856 If not fully evaluated yet, serialize as if not-evaluat*ing* yet. + * This strategy ensures the "serializability" condition of parallel + * programs--not to be confused with the data being `java.io.Serializable`. + * Indeed, if thread A is evaluating the lazy val while thread B attempts + * to serialize its owner object, there is also an alternative schedule + * where thread B serializes the owner object *before* A starts evaluating + * the lazy val. Therefore, forcing B to see the non-evaluating state is + * correct. + */ + private def writeReplace(): Any = null + } /** * Used to indicate the state of a lazy val that is currently being * evaluated with no other thread awaiting its result. */ - object Evaluating extends LazyValControlState + object Evaluating extends LazyValControlState { + /* #20856 If not fully evaluated yet, serialize as if not-evaluat*ing* yet. + * See longer comment in `Waiting.writeReplace()`. + */ + private def writeReplace(): Any = null + } /** * Used to indicate the state of a lazy val that has been evaluated to diff --git a/tests/run/i20856.check b/tests/run/i20856.check new file mode 100644 index 000000000000..a677d8bd3ca6 --- /dev/null +++ b/tests/run/i20856.check @@ -0,0 +1 @@ +succeeded: BOMB: test diff --git a/tests/run/i20856.scala b/tests/run/i20856.scala new file mode 100644 index 000000000000..893ddee73adc --- /dev/null +++ b/tests/run/i20856.scala @@ -0,0 +1,70 @@ +// scalajs: --skip + +import java.io.* + +class Message(content: String) extends Serializable: + //@transient + lazy val bomb: String = + Thread.sleep(200) + "BOMB: " + content +end Message + +object Test: + def serialize(obj: Message): Array[Byte] = + val byteStream = ByteArrayOutputStream() + val objectStream = ObjectOutputStream(byteStream) + try + objectStream.writeObject(obj) + byteStream.toByteArray + finally + objectStream.close() + byteStream.close() + end serialize + + def deserialize(bytes: Array[Byte]): Message = + val byteStream = ByteArrayInputStream(bytes) + val objectStream = ObjectInputStream(byteStream) + try + objectStream.readObject().asInstanceOf[Message] + finally + objectStream.close() + byteStream.close() + end deserialize + + def main(args: Array[String]): Unit = + val bytes = + val msg = Message("test") + + val touch = Thread(() => { + msg.bomb // start evaluation before serialization + () + }) + touch.start() + + Thread.sleep(50) // give some time for the fork to start lazy val rhs eval + + serialize(msg) // serialize in the meantime so that we capture Waiting state + end bytes + + val deserializedMsg = deserialize(bytes) + + @volatile var msg = "" + @volatile var started = false + val read = Thread(() => { + started = true + msg = deserializedMsg.bomb + () + }) + read.start() + + Thread.sleep(1000) + if !started then + throw Exception("ouch, the thread has not started yet after 1s") + + if !msg.isEmpty() then + println(s"succeeded: $msg") + else + read.interrupt() + throw new AssertionError("failed to read bomb in 1s!") + end main +end Test