Skip to content

Commit 1a007bd

Browse files
committed
Merge branch 'release/0.3.4'
2 parents 0e685f9 + a706745 commit 1a007bd

File tree

6 files changed

+212
-31
lines changed

6 files changed

+212
-31
lines changed

CHANGES.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# chill #
22

3+
### 0.3.4
4+
* Bugfixes for Externalizer with looped object graphs https://github.com/twitter/chill/pull/143
5+
36
### 0.3.3
47
* Serialize synthetic fields by default: https://github.com/twitter/chill/pull/135
58
* Prefer Java to Kryo, but check both in Externalizer: https://github.com/twitter/chill/pull/138

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ Discussion occurs primarily on the [Chill mailing list](https://groups.google.co
147147

148148
## Maven
149149

150-
Chill modules are available on Maven Central. The current groupid and version for all modules is, respectively, `"com.twitter"` and `0.3.2`.
150+
Chill modules are available on Maven Central. The current groupid and version for all modules is, respectively, `"com.twitter"` and `0.3.3`.
151151

152152
Current published artifacts are
153153

chill-scala/src/main/scala/com/twitter/chill/Externalizer.scala

+101-27
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@ import _root_.java.io.{
2525
ObjectOutputStream
2626
}
2727

28+
import com.esotericsoftware.kryo.serializers.JavaSerializer
29+
import com.esotericsoftware.kryo.DefaultSerializer
30+
import _root_.java.util.concurrent.atomic.{AtomicBoolean, AtomicReference}
31+
import com.esotericsoftware.kryo.KryoSerializable
32+
2833
object Externalizer {
34+
/* Tokens used to distinguish if we used Kryo or Java */
35+
private val KRYO = 0
36+
private val JAVA = 1
37+
2938
def apply[T](t: T): Externalizer[T] = {
3039
val x = new Externalizer[T]
3140
x.set(t)
@@ -39,24 +48,38 @@ object Externalizer {
3948
* work. Of course, Java serialization may fail if the contained
4049
* item is not Java serializable
4150
*/
42-
class Externalizer[T] extends Externalizable {
43-
private var item: Option[T] = None
51+
class Externalizer[T] extends Externalizable with KryoSerializable {
52+
// Either points to a result or a delegate Externalizer to fufil that result.
53+
private var item: Either[Externalizer[T], Option[T]] = Right(None)
54+
import Externalizer._
55+
56+
@transient private val doesJavaWork = new AtomicReference[Option[Boolean]](None)
57+
@transient private val testing = new AtomicBoolean(false)
58+
// For backwards compatibility
59+
private def KRYO = Externalizer.KRYO
4460

45-
def getOption: Option[T] = item
46-
def get: T = item.get // This should never be None when get is called
61+
// No vals or var's below this line!
62+
63+
def getOption: Option[T] = item match {
64+
case Left(e) => e.getOption
65+
case Right(i) => i
66+
}
67+
68+
def get: T = getOption.get // This should never be None when get is called
4769

4870
/** Unfortunately, Java serialization requires mutable objects if
4971
* you are going to control how the serialization is done.
5072
* Use the companion object to creat new instances of this
5173
*/
5274
def set(it: T): Unit = {
53-
assert(item.isEmpty, "Tried to call .set on an already constructed Externalizer")
54-
item = Some(it)
75+
item match {
76+
case Left(e) => e.set(it)
77+
case Right(x) =>
78+
assert(x.isEmpty, "Tried to call .set on an already constructed Externalizer")
79+
item = Right(Some(it))
80+
}
5581
}
5682

57-
/* Tokens used to distinguish if we used Kryo or Java */
58-
private val KRYO = 0
59-
private val JAVA = 1
6083

6184
/** Override this to configure Kryo creation with a named subclass,
6285
* e.g.
@@ -70,35 +93,47 @@ class Externalizer[T] extends Externalizable {
7093
(new ScalaKryoInstantiator).setReferences(true)
7194

7295
// 1 here is 1 thread, since we will likely only serialize once
73-
private val kpool = KryoPool.withByteArrayOutputStream(1, kryo)
96+
// this should not be a val because we don't want to capture a reference
97+
98+
99+
def javaWorks: Boolean =
100+
doesJavaWork.get match {
101+
case Some(v) => v
102+
case None => probeJavaWorks
103+
}
74104

75105
/** Try to round-trip and see if it works without error
76106
*/
77-
lazy val javaWorks: Boolean = {
107+
private def probeJavaWorks: Boolean = {
108+
if(!testing.compareAndSet(false, true)) return true
78109
try {
79110
val baos = new ByteArrayOutputStream()
80111
val oos = new ObjectOutputStream(baos)
81-
oos.writeObject(item)
112+
oos.writeObject(getOption)
82113
val bytes = baos.toByteArray
83114
val testInput = new ByteArrayInputStream(bytes)
84115
val ois = new ObjectInputStream(testInput)
85116
ois.readObject // this may throw
117+
doesJavaWork.set(Some(true))
86118
true
87119
}
88120
catch {
89121
case t: Throwable =>
90122
Option(System.getenv.get("CHILL_EXTERNALIZER_DEBUG"))
91123
.filter(_.toBoolean)
92124
.foreach { _ => t.printStackTrace }
125+
doesJavaWork.set(Some(false))
93126
false
94127
}
128+
finally {
129+
testing.set(false)
130+
}
95131
}
96132

97-
private def safeToBytes: Option[Array[Byte]] = {
133+
private def safeToBytes(kryo: KryoInstantiator): Option[Array[Byte]] = {
98134
try {
99-
val bytes = kpool.toBytesWithClass(item)
100-
// Make sure we can read without throwing
101-
fromBytes(bytes)
135+
val kpool = KryoPool.withByteArrayOutputStream(1, kryo)
136+
val bytes = kpool.toBytesWithClass(getOption)
102137
Some(bytes)
103138
}
104139
catch {
@@ -109,41 +144,80 @@ class Externalizer[T] extends Externalizable {
109144
None
110145
}
111146
}
112-
private def fromBytes(b: Array[Byte]): Option[T] =
113-
kpool.fromBytes(b).asInstanceOf[Option[T]]
147+
private def fromBytes(b: Array[Byte], kryo: KryoInstantiator): Option[T] =
148+
KryoPool.withByteArrayOutputStream(1, kryo)
149+
.fromBytes(b)
150+
.asInstanceOf[Option[T]]
114151

115-
def readExternal(in: ObjectInput) {
152+
override def readExternal(in: ObjectInput) = maybeReadJavaKryo(in, kryo)
153+
154+
private def maybeReadJavaKryo(in: ObjectInput, kryo: KryoInstantiator) {
116155
in.read match {
117156
case JAVA =>
118-
item = in.readObject.asInstanceOf[Option[T]]
157+
item = Right(in.readObject.asInstanceOf[Option[T]])
119158
case KRYO =>
120159
val sz = in.readInt
121160
val buf = new Array[Byte](sz)
122161
in.readFully(buf)
123-
item = fromBytes(buf)
162+
item = Right(fromBytes(buf, kryo))
124163
}
125164
}
126165

127166
protected def writeJava(out: ObjectOutput): Boolean =
128167
javaWorks && {
129168
out.write(JAVA)
130-
out.writeObject(item)
169+
out.writeObject(getOption)
131170
true
132171
}
133172

134-
protected def writeKryo(out: ObjectOutput): Boolean =
135-
safeToBytes.map { bytes =>
173+
protected def writeKryo(out: ObjectOutput): Boolean = writeKryo(out, kryo)
174+
175+
protected def writeKryo(out: ObjectOutput, kryo: KryoInstantiator): Boolean =
176+
safeToBytes(kryo).map { bytes =>
136177
out.write(KRYO)
137178
out.writeInt(bytes.size)
138179
out.write(bytes)
139180
true
140181
}.getOrElse(false)
141182

142-
def writeExternal(out: ObjectOutput) {
143-
writeJava(out) || writeKryo(out) || {
144-
val inner = item.get
183+
private def maybeWriteJavaKryo(out: ObjectOutput, kryo: KryoInstantiator) {
184+
writeJava(out) || writeKryo(out, kryo) || {
185+
val inner = get
145186
sys.error("Neither Java nor Kyro works for class: %s instance: %s\nexport CHILL_EXTERNALIZER_DEBUG=true to see both stack traces"
146187
.format(inner.getClass, inner))
147188
}
148189
}
190+
191+
override def writeExternal(out: ObjectOutput) = maybeWriteJavaKryo(out, kryo)
192+
193+
def write (kryo: Kryo, output: Output): Unit = {
194+
val resolver = kryo.getReferenceResolver
195+
resolver.getWrittenId(item) match {
196+
case -1 =>
197+
output.writeInt(-1)
198+
resolver.addWrittenObject(item)
199+
val oStream = new ObjectOutputStream(output)
200+
maybeWriteJavaKryo(oStream, () => kryo)
201+
oStream.flush
202+
case n =>
203+
output.writeInt(n)
204+
}
205+
}
206+
207+
def read (kryo: Kryo, input: Input): Unit = {
208+
doesJavaWork.set(None)
209+
testing.set(false)
210+
val state = input.readInt()
211+
val resolver = kryo.getReferenceResolver
212+
state match {
213+
case -1 =>
214+
val objId = resolver.nextReadId(this.getClass)
215+
resolver.setReadObject(objId, this)
216+
maybeReadJavaKryo(new ObjectInputStream(input), () => kryo)
217+
case n =>
218+
val z = resolver.getReadObject(this.getClass, n).asInstanceOf[Externalizer[T]]
219+
if(!(z eq this)) item = Left(z)
220+
}
221+
}
222+
149223
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
Copyright 2012 Twitter, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package com.twitter.chill
18+
19+
import org.specs._
20+
21+
import scala.collection.immutable.BitSet
22+
import scala.collection.immutable.ListMap
23+
import scala.collection.immutable.HashMap
24+
25+
import scala.collection.mutable.{ArrayBuffer => MArrayBuffer, HashMap => MHashMap}
26+
import _root_.java.util.PriorityQueue
27+
import _root_.java.util.Locale
28+
import scala.collection.mutable
29+
30+
class ExtSomeRandom(val x: Int)
31+
32+
class ExternalizerSpec extends Specification with BaseProperties {
33+
34+
noDetailedDiffs() //Fixes issue for scala 2.9
35+
36+
def getKryo = KryoSerializer.registered.newKryo
37+
38+
"KryoSerializers and KryoDeserializers" should {
39+
"Externalizer handle circular references with Java" in {
40+
val l = Array[AnyRef]("asdf", "defs")
41+
val ext = Externalizer(l)
42+
l(1) = ext
43+
44+
ext.javaWorks must be_==(true)
45+
}
46+
"Externalizer handle circular references with Java2" in {
47+
val l = Array[AnyRef](null)
48+
val ext = Externalizer(l)
49+
l.update(0, ext) // make a loop
50+
(l(0) eq ext) must beTrue
51+
ext.javaWorks must be_==(true)
52+
//jrt(ext).get.toList must_==(l.toList)
53+
// Try Kryo also
54+
//rt(ext).get.toList must_==(l.toList)
55+
56+
val nonJavaSer = Array(new ExtSomeRandom(2))
57+
jrt(nonJavaSer) must throwA[Exception]
58+
val ext2 = Externalizer(nonJavaSer)
59+
jrt(ext2).get(0).x must be_==(2)
60+
ext2.javaWorks must beFalse
61+
62+
63+
}
64+
65+
"Externalizer handle circular references with kryo only serialzable objects" in {
66+
// Add on non-java serialziable and a loop
67+
val l3 = Array[AnyRef](null, null)
68+
val ext3 = Externalizer(l3)
69+
l3.update(0, ext3) // make a loop
70+
l3.update(1, new ExtSomeRandom(3)) // make a loop
71+
(l3(0) eq ext3) must beTrue
72+
ext3.javaWorks must be_==(false)
73+
(jrt(ext3).get)(1).asInstanceOf[ExtSomeRandom].x must_==(l3(1).asInstanceOf[ExtSomeRandom].x)
74+
}
75+
76+
"Externalizer circular reference with scala tuples(java and kryo Serializable" in {
77+
val l4 = Array[AnyRef](null, null)
78+
val ext4 = Externalizer(l4)
79+
l4.update(0, ext4) // make a loop
80+
l4.update(1, (3, 7)) // make a loop
81+
(l4(0) eq ext4) must beTrue
82+
ext4.javaWorks must be_==(true)
83+
(rt(ext4).get)(1) must_==(l4(1))
84+
(jrt(ext4).get)(1) must_==(l4(1))
85+
}
86+
87+
"Externalizer handle circular references with non Kryo Serializable members" in {
88+
val l4 = Array[AnyRef](null, null)
89+
val ext4 = Externalizer(l4)
90+
l4.update(0, ext4) // make a loop
91+
l4.update(1, new Locale("en")) // make a loop
92+
(l4(0) eq ext4) must beTrue
93+
ext4.javaWorks must be_==(true)
94+
(rt(new EmptyScalaKryoInstantiator(), ext4).get)(1)
95+
(jrt(ext4).get)(1) must_==(l4(1))
96+
}
97+
}
98+
}

chill-scala/src/test/scala/com/twitter/chill/KryoSpec.scala

+6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import scala.collection.immutable.HashMap
2424

2525
import scala.collection.mutable.{ArrayBuffer => MArrayBuffer, HashMap => MHashMap}
2626
import _root_.java.util.PriorityQueue
27+
import _root_.java.util.Locale
2728
import scala.collection.mutable
2829
/*
2930
* This is just a test case for Kryo to deal with. It should
@@ -163,6 +164,11 @@ class KryoSpec extends Specification with BaseProperties {
163164
ext.javaWorks must be_==(false)
164165
jrt(ext).get.x must_==(l.x)
165166
}
167+
"Externalizer can RT with Kryo" in {
168+
val l = new SomeRandom(10)
169+
val ext = Externalizer(l)
170+
rt(ext).get.x must_==(l.x)
171+
}
166172
"handle Regex" in {
167173
val test = """\bhilarious""".r
168174
val roundtripped = rt(test)

project/Build.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ object ChillBuild extends Build {
1212

1313
val sharedSettings = Project.defaultSettings ++ mimaDefaultSettings ++ Seq(
1414

15-
version := "0.3.3",
15+
version := "0.3.4",
1616
organization := "com.twitter",
1717
scalaVersion := "2.9.3",
1818
crossScalaVersions := Seq("2.9.3", "2.10.0"),
@@ -122,7 +122,7 @@ object ChillBuild extends Build {
122122
settings = sharedSettings
123123
).settings(
124124
name := "chill",
125-
previousArtifact := Some("com.twitter" % "chill_2.9.3" % "0.3.0"),
125+
previousArtifact := Some("com.twitter" % "chill_2.9.3" % "0.3.3"),
126126
libraryDependencies ++= Seq(
127127
"org.ow2.asm" % "asm-commons" % "4.0"
128128
)
@@ -156,7 +156,7 @@ object ChillBuild extends Build {
156156
"Clojars Repository" at "http://clojars.org/repo",
157157
"Conjars Repository" at "http://conjars.org/repo"
158158
),
159-
libraryDependencies += "storm" % "storm" % "0.9.0-wip9"
159+
libraryDependencies += "storm" % "storm" % "0.9.0-wip9" % "provided"
160160
).dependsOn(chillJava)
161161

162162
// This can only have java deps!

0 commit comments

Comments
 (0)