Skip to content
This repository has been archived by the owner on Feb 20, 2019. It is now read-only.

Fix #415 - initialization of pickler registry when switching runtime strategies #416

Merged
merged 1 commit into from
May 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Dependencies._ // see project/Dependencies.scala
import scala.xml.{Node => XmlNode, NodeSeq => XmlNodeSeq, _}
import scala.xml.transform.{RewriteRule, RuleTransformer}

val buildVersion = "0.11.0-SNAPSHOT"
val buildVersion = "0.11.0-M4"

def commonSettings = Seq(
version in ThisBuild := buildVersion,
Expand Down Expand Up @@ -58,7 +58,7 @@ def noPublish = Seq(

// Use root project
lazy val root: Project = (project in file(".")).
aggregate(core, benchmark, sandbox, macroTests).
aggregate(core, benchmark, sandbox, sandboxTests, macroTests).
settings(commonSettings ++ noPublish: _*).
settings(
name := "Scala Pickling",
Expand Down Expand Up @@ -126,6 +126,18 @@ lazy val sandbox: Project = (project in file("sandbox")).
// scalacOptions ++= Seq("-Xprint:typer")
)

/* This submodule is meant to store tests that need to be executed
* independently from the main test suite placed in `core`. */
lazy val sandboxTests: Project = (project in file("sandbox-test")).
dependsOn(core).
settings(commonSettings ++ noPublish: _*).
settings(
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-compiler" % scalaVersion.value,
scalaTest
)
)

lazy val benchmark: Project = (project in file("benchmark")).
dependsOn(core).
settings(commonSettings ++ noPublish ++ benchmarkSettings: _*).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import scala.pickling.spi.{PicklerRegistry, RuntimePicklerGenerator}

/** Default pickle registry just uses TrieMaps and delgates behavior to a runtime pickler generator. */
final class DefaultPicklerRegistry(generator: RuntimePicklerGenerator) extends PicklerRegistry with RuntimePicklerRegistryHelper {
type PicklerGenerator = FastTypeTag[_] => Pickler[_]
type UnpicklerGenerator = FastTypeTag[_] => Unpickler[_]

type PicklerGen = FastTypeTag[_] => Pickler[_]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see here, I was inconsistent in my existential usage (reality, I think I was just moving code around and never cleaned it up :) ).

type UnpicklerGen = FastTypeTag[_] => Unpickler[_]

// TODO - We need to move the special encoding for runtime classes into here, rather than in magical traits.

private val picklerMap: mutable.Map[String, Pickler[_]] = new TrieMap[String, Pickler[_]]
private val picklerGenMap: mutable.Map[String, PicklerGenerator] = new TrieMap[String, PicklerGenerator]
private val picklerGenMap: mutable.Map[String, PicklerGen] = new TrieMap[String, PicklerGen]
private val unpicklerMap: mutable.Map[String, Unpickler[_]] = new TrieMap[String, Unpickler[_]]
private val unpicklerGenMap: mutable.Map[String, UnpicklerGenerator] = new TrieMap[String, UnpicklerGenerator]
private val unpicklerGenMap: mutable.Map[String, UnpicklerGen] = new TrieMap[String, UnpicklerGen]

// During constrcution, we can now register the default picklers against our cache of picklers.
autoRegisterDefaults()
Expand Down Expand Up @@ -130,6 +132,7 @@ final class DefaultPicklerRegistry(generator: RuntimePicklerGenerator) extends P
picklerGenMap.put(typeConstructorKey, generator)

/** Registers a pickler and unpickler for a type with this registry for future use.
*
* @param key The type key for the pickler. Note: In reflective scenarios this may not include type parameters.
* In those situations, the pickler should be able to handle arbitrary (existential) type parameters.
* @param p The unpickler to register.
Expand All @@ -149,4 +152,25 @@ final class DefaultPicklerRegistry(generator: RuntimePicklerGenerator) extends P
registerPicklerGenerator(typeConstructorKey, generator)
registerUnpicklerGenerator(typeConstructorKey, generator)
}

/** Transfer the "state" between different [[scala.pickling.spi.PicklingRuntime]]s.
*
* Watch out, this operation is not thread-safe.
*
* Make a new [[scala.pickling.spi.PicklingRuntime]] aware of
* the already registered [[Pickler]]s and [[Unpickler]]s present
* in the one that will be replaced.
*/
private[pickling] def dumpStateTo(r: PicklerRegistry): Unit = {

type AnyPicklerGen = FastTypeTag[_] => Pickler[Any]
type AnyUnpicklerGen = FastTypeTag[_] => Unpickler[Any]

for(p <- picklerMap) r.registerPickler(p._1, p._2.asInstanceOf[Pickler[Any]])
for(p <- picklerGenMap) r.registerPicklerGenerator(p._1, p._2.asInstanceOf[AnyPicklerGen])
for(u <- unpicklerMap) r.registerUnpickler(u._1, u._2.asInstanceOf[Unpickler[Any]])
for(u <- unpicklerGenMap) r.registerUnpicklerGenerator(u._1, u._2.asInstanceOf[AnyUnpicklerGen])

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package internal
import java.util.concurrent.locks.ReentrantLock

import scala.pickling.refs.Share
import scala.pickling.{Pickler, Unpickler, FastTypeTag}
import scala.pickling.spi.{PicklerRegistry, RefRegistry, PicklingRuntime}
import scala.reflect.runtime

Expand Down Expand Up @@ -32,6 +31,7 @@ final class NoReflectionRuntime() extends PicklingRuntime {
override def registerUnpickler[T](key: String, p: Unpickler[T]): Unit = ()
override def registerPicklerUnpickler[T](key: String, p: Pickler[T] with Unpickler[T]): Unit = ()
override private[pickling] def clearRegisteredPicklerUnpicklerFor[T: FastTypeTag]: Unit = ()
override private[pickling] def dumpStateTo(r: scala.pickling.spi.PicklerRegistry): Unit = ()
}
override val refRegistry: RefRegistry = new DefaultRefRegistry()
override val GRL: ReentrantLock = new ReentrantLock()
Expand Down
20 changes: 14 additions & 6 deletions core/src/main/scala/scala/pickling/internal/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import java.util.concurrent.atomic.AtomicReference
import scala.language.experimental.macros
import scala.language.reflectiveCalls

import java.util.IdentityHashMap

import HasCompat._

package object internal {

import scala.reflect.runtime.{universe => ru}
import spi._
import ru._
import compat._

Expand All @@ -24,10 +23,19 @@ package object internal {

}
}
private[this] var currentRuntimeVar = new AtomicReference[spi.PicklingRuntime](initDefaultRuntime)
def currentRuntime: spi.PicklingRuntime = currentRuntimeVar.get
// Here we inject a new runtime for usage.
def replaceRuntime(r: spi.PicklingRuntime): Unit = {
private[this] var currentRuntimeVar = new AtomicReference[PicklingRuntime](initDefaultRuntime)
def currentRuntime: PicklingRuntime = currentRuntimeVar.get

/** Replace the old [[PicklingRuntime]] keeping its state. This operation
* is not thread-safe and it's expected to be executed in a single thread.
*
* Note that we don't do anything with the [[RefRegistry]] because in
* future versions we are going to change how cyclic references work.
*/
def replaceRuntime(r: PicklingRuntime): Unit = {
if(r.picklers.isLookupEnabled) {
currentRuntime.picklers.dumpStateTo(r.picklers)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick note: This is not thread safe. It may be ok to assume that we'll only ever swap runtimes on one thread, but if the issue is somehow the runtime is being created on one thread and replaced on another, this won't work.

I think in the event there are multiple instantiation threads and this function hasn't completed first, we're already screwed, so it's probably ok as is, we just need to heavily document how to safely override the runtime and instantiate picklers.

E.g. the documentation for how to repalce runtime right now shows how to create a new object to contain your entire "protocol". That's part 1 to making sure this is safe. Part #2 is to make sure you pre-allocate that object in your "main" thread before you spawn sub-threads.

I had originally punted a bit on this problem, but given we're adding this functionality (which does need to be there), we should really document this problem and possible user solutions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I didn't even thought about thread safety.

As you have correctly pointed out, the real problem is that we're not pre-allocating the object PicklingProtocol before using it. I forgot to explain that, my apologies. However, we cannot rely on the user to do this, since even if the pickling protocol is imported in scope, it won't be preallocated. One has to explicitly pull something from the protocol, like implicitly[Pickler[List[Int]]]. Then, it gets initialized and the runtime changes.

Since we cannot rely on the user always doing that, I implemented this ugly solution. Yet it is the only one that will work no matter what the user decides to do. We should probably work more on the thread safety. Do you think it would be a good idea to mark this method as synchronized? I'd do it just to shape a more robust api and make explicit the fact that it's not thread-safe. Anyway, as the user will rarely use it, the synchronized overhead will be minimal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with synchronizing this method is you'd need to ALSO synchronize all other access to the pickler registry, whcih slows down general runtime. We already use thread-safe lookup (in the eventual consistency version of thread safety), so I'd hate to furtehr slow down lookup with a full up synchronize.

There are ways to force things to initialize without forcing implicitly, specifically if you use the "cake" approach for your default picklers. It's not pretty, but it is workable.

In any case, as i said, I think what you have is fine, we just need to document WHY we think it's fine and where it is not fine :). As long as it's documented, users may be surprised, but able to cope. that's better than surprised and have no idea how to resolve the issue.

The best case would be if we had a legitimate solution to the problem. Given the current architecture, I think we do not, but I'm happy to hear alternative ideas :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this is good for a short-term solution, but we should think about a new "cake" approach, because the way to customize the runtime is not sleek. Especially if this runtime strategy depends on implicits that the user may put into scope (like StaticOnly, I'll open an issue to explain what I mean by this).

But, atm, I agree that we should stick with this and greatly document it. I do not see any other alternative solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree :) Looking forward to readi gthe bug.

}
currentRuntimeVar.lazySet(r)
}

Expand Down
9 changes: 9 additions & 0 deletions core/src/main/scala/scala/pickling/spi/PicklerRegistry.scala
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,13 @@ trait PicklerRegistry {
*/
private[pickling] def clearRegisteredPicklerUnpicklerFor[T: FastTypeTag]: Unit

/** Transfer the "state" between different [[PicklingRuntime]]s.
*
* Watch out, this operation is not thread-safe.
*
* Make a new [[PicklingRuntime]] aware of the already registered [[Pickler]]s
* and [[Unpickler]]s present in the one that will be replaced.
*/
private[pickling] def dumpStateTo(r: PicklerRegistry): Unit

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package scala.pickling.registry

import org.scalatest.FunSuite

import scala.pickling._
import scala.pickling.internal.HybridRuntime
import scala.pickling.json.JsonFormats
import scala.pickling.pickler.AllPicklers

object Protocol extends {
val oldRuntime = internal.currentRuntime
val currentRuntime = new HybridRuntime
val onlyLookup = internal.replaceRuntime(currentRuntime)
} with Ops with AllPicklers with JsonFormats

class SwitchRuntimeRegistryInit extends FunSuite {

import Protocol._

test("registry should be initialized when switching runtime strategies") {

case class Foo(i: Int)
val pf = implicitly[AbstractPicklerUnpickler[Foo]]

// If the test passes, this should not initialize
// the registry again. If it fails it does.
implicitly[Pickler[List[String]]]

try {
val lookup = currentRuntime.picklers.lookupPickler(pf.tag.key)
assert(lookup !== None)

val pf2 = implicitly[AbstractPicklerUnpickler[Foo]]
val lookup2 = currentRuntime.picklers.lookupPickler(pf.tag.key)
assert(lookup === lookup2)
} finally {
internal.replaceRuntime(oldRuntime)
// Just in case it doesn't get init
implicitly[Pickler[List[String]]]
}

}

}