Skip to content

Commit e242753

Browse files
committed
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.
1 parent 4c9cf0a commit e242753

File tree

3 files changed

+89
-2
lines changed

3 files changed

+89
-2
lines changed

Diff for: library/src/scala/runtime/LazyVals.scala

+18-2
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,29 @@ object LazyVals {
5252
* Used to indicate the state of a lazy val that is being
5353
* evaluated and of which other threads await the result.
5454
*/
55-
final class Waiting extends CountDownLatch(1) with LazyValControlState
55+
final class Waiting extends CountDownLatch(1) with LazyValControlState {
56+
/* #20856 If not fully evaluated yet, serialize as if not-evaluat*ing* yet.
57+
* This strategy ensures the "serializability" condition of parallel
58+
* programs--not to be confused with the data being `java.io.Serializable`.
59+
* Indeed, if thread A is evaluating the lazy val while thread B attempts
60+
* to serialize its owner object, there is also an alternative schedule
61+
* where thread B serializes the owner object *before* A starts evaluating
62+
* the lazy val. Therefore, forcing B to see the non-evaluating state is
63+
* correct.
64+
*/
65+
private def writeReplace(): Any = null
66+
}
5667

5768
/**
5869
* Used to indicate the state of a lazy val that is currently being
5970
* evaluated with no other thread awaiting its result.
6071
*/
61-
object Evaluating extends LazyValControlState
72+
object Evaluating extends LazyValControlState {
73+
/* #20856 If not fully evaluated yet, serialize as if not-evaluat*ing* yet.
74+
* See longer comment in `Waiting.writeReplace()`.
75+
*/
76+
private def writeReplace(): Any = null
77+
}
6278

6379
/**
6480
* Used to indicate the state of a lazy val that has been evaluated to

Diff for: tests/run/i20856.check

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
succeeded: BOMB: test

Diff for: tests/run/i20856.scala

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// scalajs: --skip
2+
3+
import java.io.*
4+
5+
class Message(content: String) extends Serializable:
6+
//@transient
7+
lazy val bomb: String =
8+
Thread.sleep(200)
9+
"BOMB: " + content
10+
end Message
11+
12+
object Test:
13+
def serialize(obj: Message): Array[Byte] =
14+
val byteStream = ByteArrayOutputStream()
15+
val objectStream = ObjectOutputStream(byteStream)
16+
try
17+
objectStream.writeObject(obj)
18+
byteStream.toByteArray
19+
finally
20+
objectStream.close()
21+
byteStream.close()
22+
end serialize
23+
24+
def deserialize(bytes: Array[Byte]): Message =
25+
val byteStream = ByteArrayInputStream(bytes)
26+
val objectStream = ObjectInputStream(byteStream)
27+
try
28+
objectStream.readObject().asInstanceOf[Message]
29+
finally
30+
objectStream.close()
31+
byteStream.close()
32+
end deserialize
33+
34+
def main(args: Array[String]): Unit =
35+
val bytes =
36+
val msg = Message("test")
37+
38+
val touch = Thread(() => {
39+
msg.bomb // start evaluation before serialization
40+
()
41+
})
42+
touch.start()
43+
44+
Thread.sleep(50) // give some time for the fork to start lazy val rhs eval
45+
46+
serialize(msg) // serialize in the meantime so that we capture Waiting state
47+
end bytes
48+
49+
val deserializedMsg = deserialize(bytes)
50+
51+
@volatile var msg = ""
52+
@volatile var started = false
53+
val read = Thread(() => {
54+
started = true
55+
msg = deserializedMsg.bomb
56+
()
57+
})
58+
read.start()
59+
60+
Thread.sleep(1000)
61+
if !started then
62+
throw Exception("ouch, the thread has not started yet after 1s")
63+
64+
if !msg.isEmpty() then
65+
println(s"succeeded: $msg")
66+
else
67+
read.interrupt()
68+
throw new AssertionError("failed to read bomb in 1s!")
69+
end main
70+
end Test

0 commit comments

Comments
 (0)