Skip to content

Commit

Permalink
Fix #20856: Serialize Waiting and Evaluating as if null.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sjrd committed Jul 22, 2024
1 parent 4c9cf0a commit e4a6b62
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 2 deletions.
20 changes: 18 additions & 2 deletions library/src/scala/runtime/LazyVals.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/run/i20856.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
succeeded: BOMB: test
65 changes: 65 additions & 0 deletions tests/run/i20856.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// scalajs: --skip

import java.io.*

class Message(content: String) extends Serializable:
//@transient
lazy val bomb: String =
Thread.sleep(200)
"BOMB: " + content

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()

def deserialize(bytes: Array[Byte]): Message =
val byteStream = ByteArrayInputStream(bytes)
val objectStream = ObjectInputStream(byteStream)
try
objectStream.readObject().asInstanceOf[Message]
finally
objectStream.close()
byteStream.close()

def main(args: Array[String]): Unit =
val bytes = locally:
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

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("wtf")

if !msg.isEmpty() then
println(s"succeeded: $msg")
else
read.interrupt()
throw new AssertionError("failed to read bomb in 1s!")
end main
end Test

0 comments on commit e4a6b62

Please sign in to comment.