diff --git a/project/Build.scala b/project/Build.scala index 2331b5de39..08b3a9c71b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -103,7 +103,7 @@ object Util extends Build { sharedSettings ++ Unidoc.settings ) aggregate( - utilCore, utilCodec, utilCollection, utilCache, utilReflect, + utilRegistry, utilCore, utilCodec, utilCollection, utilCache, utilReflect, utilLogging, utilTest, utilThrift, utilHashing, utilJvm, utilZk, utilZkCommon, utilClassPreloader, utilBenchmark, utilApp, utilEvents, utilStats, utilEval @@ -274,6 +274,16 @@ object Util extends Build { name := "util-logging" ).dependsOn(utilCore, utilApp, utilStats) + + lazy val utilRegistry = Project( + id = "util-registry", + base = file("util-registry"), + settings = Project.defaultSettings ++ + sharedSettings + ).settings( + name := "util-registry" + ) + lazy val utilStats = Project( id = "util-stats", base = file("util-stats"), diff --git a/util-registry/BUILD b/util-registry/BUILD new file mode 100644 index 0000000000..0af0813b84 --- /dev/null +++ b/util-registry/BUILD @@ -0,0 +1,13 @@ +maven_layout() + +jar_library(name='util-registry', + dependencies=[ + 'util/util-core/src/main/scala', + ] +) + +jar_library(name='tests', + dependencies=[ + 'util/util-core/src/test/scala' + ] +) diff --git a/util-registry/src/main/scala/BUILD b/util-registry/src/main/scala/BUILD new file mode 100644 index 0000000000..12ac0c2a20 --- /dev/null +++ b/util-registry/src/main/scala/BUILD @@ -0,0 +1,8 @@ +scala_library(name='scala', + provides = scala_artifact( + org = 'com.twitter', + name = 'util-registry', + repo = artifactory, + ), + sources=rglobs('*.scala') +) diff --git a/util-registry/src/main/scala/com/twitter/registry/Registry.scala b/util-registry/src/main/scala/com/twitter/registry/Registry.scala new file mode 100644 index 0000000000..e51dced939 --- /dev/null +++ b/util-registry/src/main/scala/com/twitter/registry/Registry.scala @@ -0,0 +1,61 @@ +package com.twitter.registry + +import java.util.NoSuchElementException + +private[registry] final case class Entry(key: Seq[String], value: String) + +private[registry] object Entry { + val TupledMethod: ((Seq[String], String)) => Entry = (Entry.apply _).tupled +} + +/** + * This is an expert-level API; it is not meant for end-users. + * + * The registry is a hierarchical key/value store, where all keys are sequences + * of Strings, and values are Strings. + * + * Keys and values must be non-control ascii, and must not contain the '/' + * character. If you pass in a key or value with an invalid character, it will + * silently be removed. If this makes your key clash with another key, it will + * overwrite. + */ +private[registry] trait Registry extends Iterable[Entry] { + /** + * Provides an iterator over the registry. + * + * It is the responsibility of the caller to synchronize if they would like to + * iterate in multiple threads, but the iterator is guaranteed not to change as + * it is called. + */ + def iterator(): Iterator[Entry] + + /** + * Registers a value in the registry, and returns the old value (if any). + */ + def put(key: Seq[String], value: String): Option[String] +} + +private[registry] class NaiveRegistry extends Registry { + private[this] var registry = Map.empty[Seq[String], String] + + def iterator(): Iterator[Entry] = synchronized(registry).iterator.map(Entry.TupledMethod) + + def put(key: Seq[String], value: String): Option[String] = { + val sanitizedKey = key.map(sanitize) + val sanitizedValue = sanitize(value) + synchronized { + val result = registry.get(sanitizedKey) + registry += sanitizedKey -> sanitizedValue + result + } + } + + private[this] def sanitize(key: String): String = + key.filter { char => char > 31 && char <= 127 && char != '/' } +} + +private[registry] object GlobalRegistry { + private[this] val registry = new NaiveRegistry + + def get: Registry = registry +} diff --git a/util-registry/src/test/scala/BUILD b/util-registry/src/test/scala/BUILD new file mode 100644 index 0000000000..636a119ed8 --- /dev/null +++ b/util-registry/src/test/scala/BUILD @@ -0,0 +1,8 @@ +junit_tests(name='scala', + dependencies=[ + '3rdparty/jvm/junit', + '3rdparty/jvm/org/scalatest', + 'util/util-registry/src/main/scala', + ], + sources=rglobs('*.scala') +) diff --git a/util-registry/src/test/scala/com/twitter/registry/NaiveRuntimeRegistry.scala b/util-registry/src/test/scala/com/twitter/registry/NaiveRuntimeRegistry.scala new file mode 100644 index 0000000000..25fff6412e --- /dev/null +++ b/util-registry/src/test/scala/com/twitter/registry/NaiveRuntimeRegistry.scala @@ -0,0 +1,10 @@ +package com.twitter.registry + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class NaiveRegistryTest extends RegistryTest { + def mkRegistry(): Registry = new NaiveRegistry() + def name: String = "NaiveRegistry" +} diff --git a/util-registry/src/test/scala/com/twitter/registry/RegistryTest.scala b/util-registry/src/test/scala/com/twitter/registry/RegistryTest.scala new file mode 100644 index 0000000000..d0f646db44 --- /dev/null +++ b/util-registry/src/test/scala/com/twitter/registry/RegistryTest.scala @@ -0,0 +1,76 @@ +package com.twitter.registry + +import java.lang.{Character => JCharacter} +import org.scalatest.FunSuite + +abstract class RegistryTest extends FunSuite { + def mkRegistry(): Registry + def name: String + + test(s"$name can insert a key/value pair and then read it") { + val registry = mkRegistry() + registry.put(Seq("foo"), "bar") + assert(registry.toSet == Set(Entry(Seq("foo"), "bar"))) + } + + test(s"$name's iterator is not affected by adding an element") { + val registry = mkRegistry() + registry.put(Seq("foo"), "bar") + val iter = registry.iterator() + registry.put(Seq("foo"), "baz") + assert(iter.next() == Entry(Seq("foo"), "bar")) + assert(!iter.hasNext) + } + + test(s"$name can overwrite old element") { + val registry = mkRegistry() + registry.put(Seq("foo"), "bar") + registry.put(Seq("foo"), "baz") + assert((registry.toSet) == Set(Entry(Seq("foo"), "baz"))) + } + + test(s"$name can return the old element when replacing") { + val registry = mkRegistry() + registry.put(Seq("foo"), "bar") + assert(registry.put(Seq("foo"), "baz") == Some("bar")) + assert(registry.toSet == Set(Entry(Seq("foo"), "baz"))) + } + + test(s"$name can support multiple elements") { + val registry = mkRegistry() + registry.put(Seq("foo"), "bar") + registry.put(Seq("baz"), "qux") + assert(registry.toSet == Set(Entry(Seq("foo"), "bar"), Entry(Seq("baz"), "qux"))) + } + + test(s"$name can support nontrivial keys") { + val registry = mkRegistry() + registry.put(Seq("foo", "bar", "baz"), "qux") + assert(registry.toSet == Set(Entry(Seq("foo", "bar", "baz"), "qux"))) + } + + test(s"$name can support empty keys") { + val registry = mkRegistry() + registry.put(Seq(), "qux") + assert(registry.toSet == Set(Entry(Seq(), "qux"))) + } + + test(s"$name can sanitize bad values") { + val registry = mkRegistry() + registry.put(Seq("foo"), "q/ux") + assert(registry.toSet == Set(Entry(Seq("foo"), "qux"))) + } + + test(s"$name can sanitize bad keys") { + val registry = mkRegistry() + registry.put(Seq("fo☃o", s"bar${JCharacter.toString(31)}"), "qux") + assert(registry.toSet == Set(Entry(Seq("foo", "bar"), "qux"))) + } + + test(s"$name can support keys that are subsequences of other keys") { + val registry = mkRegistry() + registry.put(Seq("foo"), "bar") + registry.put(Seq("foo", "baz"), "qux") + assert(registry.toSet == Set(Entry(Seq("foo"), "bar"), Entry(Seq("foo", "baz"), "qux"))) + } +}