diff --git a/changelog/README.md b/changelog/README.md index fb943a3e6d5..e48125960f8 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -7,6 +7,7 @@ - [new feature] JAVA-1362: Send query options flags as [int] for Protocol V5+. - [improvement] JAVA-1367: Make protocol negotiation more resilient. - [bug] JAVA-1397: Handle duration as native datatype in protocol v5+. +- [improvement] JAVA-1308: CodecRegistry performance improvements. Merged from 3.1.x branch: diff --git a/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java b/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java index 65928cf56ce..e62957f8f71 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java +++ b/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java @@ -18,7 +18,6 @@ import com.datastax.driver.core.exceptions.CodecNotFoundException; import com.google.common.base.Objects; import com.google.common.cache.*; -import com.google.common.collect.ImmutableSet; import com.google.common.reflect.TypeToken; import com.google.common.util.concurrent.UncheckedExecutionException; import org.slf4j.Logger; @@ -27,6 +26,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.nio.ByteBuffer; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -49,8 +49,7 @@ * The only reason to create your own instances is if you have multiple {@code Cluster} objects that use different * sets of codecs. In that case, use {@link com.datastax.driver.core.Cluster.Builder#withCodecRegistry(CodecRegistry)} * to associate the registry with the cluster: - *
- * {@code
+ * 
{@code
  * CodecRegistry myCodecRegistry = new CodecRegistry();
  * myCodecRegistry.register(myCodec1, myCodec2, myCodec3);
  * Cluster cluster = Cluster.builder().withCodecRegistry(myCodecRegistry).build();
@@ -68,8 +67,7 @@
  * To create a custom codec, write a class that extends {@link TypeCodec}, create an instance, and pass it to one of
  * the {@link #register(TypeCodec) register} methods; for example, one could create a codec that maps CQL
  * timestamps to JDK8's {@code java.time.LocalDate}:
- * 
- * {@code
+ * 
{@code
  * class LocalDateCodec extends TypeCodec {
  *    ...
  * }
@@ -85,8 +83,7 @@
  * 
  * 

* Example: - *

- * {@code
+ * 
{@code
  * Row row = session.executeQuery("select date from some_table where pk = 1").one();
  * java.time.LocalDate date = row.get(0, java.time.LocalDate.class); // uses LocalDateCodec registered above}
  * 
@@ -104,7 +101,7 @@ * as well as all {@code Map} types whose keys and/or values are {@code java.time.LocalDate}. * This works recursively for nested collections; *
  • {@link UserType user types}, mapped to {@link UDTValue} objects. Custom codecs are available recursively - * to the UDT's fields, so if one of your fields is a {@code timestamp} you can use your {@code LocaDateCodec} to retrieve + * to the UDT's fields, so if one of your fields is a {@code timestamp} you can use your {@code LocalDateCodec} to retrieve * it as a {@code java.time.LocalDate};
  • *
  • {@link TupleType tuple types}, mapped to {@link TupleValue} (with the same rules for nested fields);
  • *
  • {@link com.datastax.driver.core.DataType.CustomType custom types}, mapped to {@code ByteBuffer}.
  • @@ -129,7 +126,7 @@ * When the registry looks up a codec, the rules of precedence are: *
      *
    • if a result was previously cached for that mapping, it is returned;
    • - *
    • otherwise, the registry checks the list of "basic" codecs: the default ones, and the ones that were explicitly + *
    • otherwise, the registry checks the list of built-in codecs – the default ones – and the ones that were explicitly * registered (in the order that they were registered). It calls each codec's {@code accepts} methods to determine if * it can handle the mapping, and if so returns it;
    • *
    • otherwise, the registry tries to generate a codec, according to the rules outlined above.
    • @@ -141,29 +138,55 @@ public final class CodecRegistry { private static final Logger logger = LoggerFactory.getLogger(CodecRegistry.class); - @SuppressWarnings("unchecked") - private static final ImmutableSet> PRIMITIVE_CODECS = ImmutableSet.of( - TypeCodec.blob(), - TypeCodec.cboolean(), - TypeCodec.smallInt(), - TypeCodec.tinyInt(), + private static final Map> BUILT_IN_CODECS_MAP = new EnumMap>(DataType.Name.class); + + static { + BUILT_IN_CODECS_MAP.put(DataType.Name.ASCII, TypeCodec.ascii()); + BUILT_IN_CODECS_MAP.put(DataType.Name.BIGINT, TypeCodec.bigint()); + BUILT_IN_CODECS_MAP.put(DataType.Name.BLOB, TypeCodec.blob()); + BUILT_IN_CODECS_MAP.put(DataType.Name.BOOLEAN, TypeCodec.cboolean()); + BUILT_IN_CODECS_MAP.put(DataType.Name.COUNTER, TypeCodec.counter()); + BUILT_IN_CODECS_MAP.put(DataType.Name.DECIMAL, TypeCodec.decimal()); + BUILT_IN_CODECS_MAP.put(DataType.Name.DOUBLE, TypeCodec.cdouble()); + BUILT_IN_CODECS_MAP.put(DataType.Name.FLOAT, TypeCodec.cfloat()); + BUILT_IN_CODECS_MAP.put(DataType.Name.INET, TypeCodec.inet()); + BUILT_IN_CODECS_MAP.put(DataType.Name.INT, TypeCodec.cint()); + BUILT_IN_CODECS_MAP.put(DataType.Name.TEXT, TypeCodec.varchar()); + BUILT_IN_CODECS_MAP.put(DataType.Name.TIMESTAMP, TypeCodec.timestamp()); + BUILT_IN_CODECS_MAP.put(DataType.Name.UUID, TypeCodec.uuid()); + BUILT_IN_CODECS_MAP.put(DataType.Name.VARCHAR, TypeCodec.varchar()); + BUILT_IN_CODECS_MAP.put(DataType.Name.VARINT, TypeCodec.varint()); + BUILT_IN_CODECS_MAP.put(DataType.Name.TIMEUUID, TypeCodec.timeUUID()); + BUILT_IN_CODECS_MAP.put(DataType.Name.SMALLINT, TypeCodec.smallInt()); + BUILT_IN_CODECS_MAP.put(DataType.Name.TINYINT, TypeCodec.tinyInt()); + BUILT_IN_CODECS_MAP.put(DataType.Name.DATE, TypeCodec.date()); + BUILT_IN_CODECS_MAP.put(DataType.Name.TIME, TypeCodec.time()); + BUILT_IN_CODECS_MAP.put(DataType.Name.DURATION, TypeCodec.duration()); + } + + // roughly sorted by popularity + private static final TypeCodec[] BUILT_IN_CODECS = new TypeCodec[]{ + TypeCodec.varchar(), // must be declared before AsciiCodec so it gets chosen when CQL type not available + TypeCodec.uuid(), // must be declared before TimeUUIDCodec so it gets chosen when CQL type not available + TypeCodec.timeUUID(), + TypeCodec.timestamp(), TypeCodec.cint(), TypeCodec.bigint(), - TypeCodec.counter(), + TypeCodec.blob(), TypeCodec.cdouble(), TypeCodec.cfloat(), - TypeCodec.varint(), TypeCodec.decimal(), - TypeCodec.varchar(), // must be declared before AsciiCodec so it gets chosen when CQL type not available - TypeCodec.ascii(), - TypeCodec.timestamp(), + TypeCodec.varint(), + TypeCodec.inet(), + TypeCodec.cboolean(), + TypeCodec.smallInt(), + TypeCodec.tinyInt(), TypeCodec.date(), TypeCodec.time(), - TypeCodec.uuid(), // must be declared before TimeUUIDCodec so it gets chosen when CQL type not available - TypeCodec.timeUUID(), - TypeCodec.inet(), - TypeCodec.duration() - ); + TypeCodec.duration(), + TypeCodec.counter(), + TypeCodec.ascii() + }; /** * The default {@code CodecRegistry} instance. @@ -181,7 +204,7 @@ private static final class CacheKey { private final TypeToken javaType; - public CacheKey(DataType cqlType, TypeToken javaType) { + CacheKey(DataType cqlType, TypeToken javaType) { this.javaType = javaType; this.cqlType = cqlType; } @@ -209,7 +232,18 @@ public int hashCode() { private class TypeCodecCacheLoader extends CacheLoader> { @Override public TypeCodec load(CacheKey cacheKey) { - return findCodec(cacheKey.cqlType, cacheKey.javaType); + checkNotNull(cacheKey.cqlType, "Parameter cqlType cannot be null"); + if (logger.isTraceEnabled()) + logger.trace("Loading codec into cache: [{} <-> {}]", + CodecRegistry.toString(cacheKey.cqlType), + CodecRegistry.toString(cacheKey.javaType)); + for (TypeCodec codec : codecs) { + if (codec.accepts(cacheKey.cqlType) && (cacheKey.javaType == null || codec.accepts(cacheKey.javaType))) { + logger.trace("Already existing codec found: {}", codec); + return codec; + } + } + return createCodec(cacheKey.cqlType, cacheKey.javaType); } } @@ -218,7 +252,7 @@ public TypeCodec load(CacheKey cacheKey) { * Weights are computed mainly according to the CQL type: *
        *
      1. Manually-registered codecs always weigh 0; - *
      2. Codecs for primtive types weigh 0; + *
      3. Codecs for primitive types weigh 0; *
      4. Codecs for collections weigh the total weight of their inner types + the weight of their level of deepness; *
      5. Codecs for UDTs and tuples weigh the total weight of their inner types + the weight of their level of deepness, but cannot weigh less than 1; *
      6. Codecs for custom (non-CQL) types weigh 1. @@ -278,9 +312,7 @@ public void onRemoval(RemovalNotification> notification) } /** - * The list of registered codecs. - * This list is initialized with the built-in codecs; - * User-defined codecs are appended to the list. + * The list of user-registered codecs. */ private final CopyOnWriteArrayList> codecs; @@ -294,13 +326,13 @@ public void onRemoval(RemovalNotification> notification) * Creates a new instance initialized with built-in codecs for all the base CQL types. */ public CodecRegistry() { - this.codecs = new CopyOnWriteArrayList>(PRIMITIVE_CODECS); + this.codecs = new CopyOnWriteArrayList>(); this.cache = defaultCacheBuilder().build(new TypeCodecCacheLoader()); } private CacheBuilder> defaultCacheBuilder() { CacheBuilder> builder = CacheBuilder.newBuilder() - // 19 primitive codecs + collections thereof = 19*3 + 19*19 = 418 codecs, + // lists, sets and maps of 20 primitive types = 20 + 20 + 20*20 = 440 codecs, // so let's start with roughly 1/4 of that .initialCapacity(100) .maximumWeight(1000) @@ -323,20 +355,24 @@ private CacheBuilder> defaultCacheBuilder() { * @return this CodecRegistry (for method chaining). */ public CodecRegistry register(TypeCodec newCodec) { + for (TypeCodec oldCodec : BUILT_IN_CODECS) { + if (oldCodec.accepts(newCodec.getCqlType()) && oldCodec.accepts(newCodec.getJavaType())) { + logger.warn("Ignoring codec {} because it collides with previously registered codec {}", newCodec, oldCodec); + return this; + } + } for (TypeCodec oldCodec : codecs) { if (oldCodec.accepts(newCodec.getCqlType()) && oldCodec.accepts(newCodec.getJavaType())) { logger.warn("Ignoring codec {} because it collides with previously registered codec {}", newCodec, oldCodec); return this; } } - CacheKey key = new CacheKey(newCodec.getCqlType(), newCodec.getJavaType()); TypeCodec existing = cache.getIfPresent(key); if (existing != null) { logger.warn("Ignoring codec {} because it collides with previously generated codec {}", newCodec, existing); return this; } - this.codecs.add(newCodec); return this; } @@ -474,13 +510,16 @@ public TypeCodec codecFor(DataType cqlType, T value) { @SuppressWarnings("unchecked") private TypeCodec lookupCodec(DataType cqlType, TypeToken javaType) { checkNotNull(cqlType, "Parameter cqlType cannot be null"); + TypeCodec codec = BUILT_IN_CODECS_MAP.get(cqlType.getName()); + if (codec != null && (javaType == null || codec.accepts(javaType))) { + logger.trace("Returning built-in codec {}", codec); + return (TypeCodec) codec; + } if (logger.isTraceEnabled()) logger.trace("Querying cache for codec [{} <-> {}]", toString(cqlType), toString(javaType)); - CacheKey cacheKey = new CacheKey(cqlType, javaType); try { - TypeCodec codec = cache.get(cacheKey); - logger.trace("Returning cached codec {}", codec); - return (TypeCodec) codec; + CacheKey cacheKey = new CacheKey(cqlType, javaType); + codec = cache.get(cacheKey); } catch (UncheckedExecutionException e) { if (e.getCause() instanceof CodecNotFoundException) { throw (CodecNotFoundException) e.getCause(); @@ -491,6 +530,8 @@ private TypeCodec lookupCodec(DataType cqlType, TypeToken javaType) { } catch (ExecutionException e) { throw new CodecNotFoundException(e.getCause(), cqlType, javaType); } + logger.trace("Returning cached codec {}", codec); + return (TypeCodec) codec; } @SuppressWarnings("unchecked") @@ -498,9 +539,19 @@ private TypeCodec findCodec(DataType cqlType, TypeToken javaType) { checkNotNull(cqlType, "Parameter cqlType cannot be null"); if (logger.isTraceEnabled()) logger.trace("Looking for codec [{} <-> {}]", toString(cqlType), toString(javaType)); + + // Look at the built-in codecs first + for (TypeCodec codec : BUILT_IN_CODECS) { + if (codec.accepts(cqlType) && (javaType == null || codec.accepts(javaType))) { + logger.trace("Built-in codec found: {}", codec); + return (TypeCodec) codec; + } + } + + // Look at the user-registered codecs next for (TypeCodec codec : codecs) { if (codec.accepts(cqlType) && (javaType == null || codec.accepts(javaType))) { - logger.trace("Codec found: {}", codec); + logger.trace("Already registered codec found: {}", codec); return (TypeCodec) codec; } } @@ -512,9 +563,19 @@ private TypeCodec findCodec(DataType cqlType, T value) { checkNotNull(value, "Parameter value cannot be null"); if (logger.isTraceEnabled()) logger.trace("Looking for codec [{} <-> {}]", toString(cqlType), value.getClass()); + + // Look at the built-in codecs first + for (TypeCodec codec : BUILT_IN_CODECS) { + if ((cqlType == null || codec.accepts(cqlType)) && codec.accepts(value)) { + logger.trace("Built-in codec found: {}", codec); + return (TypeCodec) codec; + } + } + + // Look at the user-registered codecs next for (TypeCodec codec : codecs) { if ((cqlType == null || codec.accepts(cqlType)) && codec.accepts(value)) { - logger.trace("Codec found: {}", codec); + logger.trace("Already registered codec found: {}", codec); return (TypeCodec) codec; } } @@ -598,7 +659,7 @@ private TypeCodec maybeCreateCodec(DataType cqlType, TypeToken javaTyp return null; } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private TypeCodec maybeCreateCodec(DataType cqlType, T value) { checkNotNull(value); diff --git a/driver-core/src/main/java/com/datastax/driver/core/TypeCodec.java b/driver-core/src/main/java/com/datastax/driver/core/TypeCodec.java index 01a8f82f1fb..4af6073848b 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/TypeCodec.java +++ b/driver-core/src/main/java/com/datastax/driver/core/TypeCodec.java @@ -579,7 +579,7 @@ public DataType getCqlType() { * the given {@code javaType}, and {@code false} otherwise. * @throws NullPointerException if {@code javaType} is {@code null}. */ - public boolean accepts(TypeToken javaType) { + public boolean accepts(TypeToken javaType) { checkNotNull(javaType, "Parameter javaType cannot be null"); return this.javaType.equals(javaType.wrap()); } diff --git a/driver-core/src/test/java/com/datastax/driver/core/CodecRegistryTest.java b/driver-core/src/test/java/com/datastax/driver/core/CodecRegistryTest.java index 08a7ed388c9..3cc6b63a854 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/CodecRegistryTest.java +++ b/driver-core/src/test/java/com/datastax/driver/core/CodecRegistryTest.java @@ -62,7 +62,8 @@ public static Object[][] cql() { {DataType.time(), TypeCodec.time()}, {DataType.uuid(), TypeCodec.uuid()}, {DataType.timeuuid(), TypeCodec.timeUUID()}, - {DataType.inet(), TypeCodec.inet()} + {DataType.inet(), TypeCodec.inet()}, + {DataType.duration(), TypeCodec.duration()} }; } @@ -87,7 +88,8 @@ public static Object[][] cqlAndJava() { {DataType.time(), Long.class, TypeCodec.time()}, {DataType.uuid(), UUID.class, TypeCodec.uuid()}, {DataType.timeuuid(), UUID.class, TypeCodec.timeUUID()}, - {DataType.inet(), InetAddress.class, TypeCodec.inet()} + {DataType.inet(), InetAddress.class, TypeCodec.inet()}, + {DataType.duration(), Duration.class, TypeCodec.duration()} }; } @@ -108,7 +110,8 @@ public static Object[][] value() { {new Date(42), TypeCodec.timestamp()}, {LocalDate.fromDaysSinceEpoch(42), TypeCodec.date()}, {UUID.randomUUID(), TypeCodec.uuid()}, - {mock(InetAddress.class), TypeCodec.inet()} + {mock(InetAddress.class), TypeCodec.inet()}, + {Duration.from("1mo2d3h"), TypeCodec.duration()} }; } @@ -133,7 +136,8 @@ public static Object[][] cqlAndValue() { {DataType.time(), 42L, TypeCodec.time()}, {DataType.uuid(), UUID.randomUUID(), TypeCodec.uuid()}, {DataType.timeuuid(), UUID.randomUUID(), TypeCodec.timeUUID()}, - {DataType.inet(), mock(InetAddress.class), TypeCodec.inet()} + {DataType.inet(), mock(InetAddress.class), TypeCodec.inet()}, + {DataType.duration(), Duration.from("1mo2d3h"), TypeCodec.duration()} }; } @@ -195,7 +199,7 @@ public void should_find_codec_by_cql_type_and_value(DataType cqlType, Object val public void should_find_newly_registered_codec_by_cql_type() { // given CodecRegistry registry = new CodecRegistry(); - TypeCodec expected = mockCodec(list(text()), listOf(String.class)); + TypeCodec expected = mockCodec(list(text()), listOf(String.class)); registry.register(expected); // when TypeCodec actual = registry.codecFor(list(text())); @@ -209,7 +213,7 @@ public void should_find_newly_registered_codec_by_cql_type() { public void should_find_default_codec_if_cql_type_already_registered() { // given CodecRegistry registry = new CodecRegistry(); - TypeCodec newCodec = mockCodec(text(), of(StringBuilder.class)); + TypeCodec newCodec = mockCodec(text(), of(StringBuilder.class)); registry.register(newCodec); // when TypeCodec actual = registry.codecFor(text()); @@ -226,7 +230,7 @@ public void should_find_default_codec_if_cql_type_already_registered() { public void should_find_newly_registered_codec_by_cql_type_and_java_type() { // given CodecRegistry registry = new CodecRegistry(); - TypeCodec expected = mockCodec(list(text()), listOf(String.class)); + TypeCodec expected = mockCodec(list(text()), listOf(String.class)); registry.register(expected); // when TypeCodec actual = registry.codecFor(list(text()), listOf(String.class)); @@ -443,7 +447,7 @@ public void should_ignore_codec_colliding_with_already_registered_codec() { CodecRegistry registry = new CodecRegistry(); - TypeCodec newCodec = mockCodec(cint(), of(Integer.class)); + TypeCodec newCodec = mockCodec(cint(), of(Integer.class)); registry.register(newCodec); @@ -464,7 +468,7 @@ public void should_ignore_codec_colliding_with_already_generated_codec() { // Force generation of a list token from the default token registry.codecFor(list(cint()), listOf(Integer.class)); - TypeCodec newCodec = mockCodec(list(cint()), listOf(Integer.class)); + TypeCodec newCodec = mockCodec(list(cint()), listOf(Integer.class)); registry.register(newCodec); @@ -490,8 +494,9 @@ private void stopCapturingLogs(MemoryAppender logs) { registryLogger.removeAppender(logs); } - private TypeCodec mockCodec(DataType cqlType, TypeToken javaType) { - TypeCodec newCodec = mock(TypeCodec.class); + private TypeCodec mockCodec(DataType cqlType, TypeToken javaType) { + @SuppressWarnings("unchecked") + TypeCodec newCodec = mock(TypeCodec.class); when(newCodec.getCqlType()).thenReturn(cqlType); when(newCodec.getJavaType()).thenReturn(javaType); when(newCodec.accepts(cqlType)).thenReturn(true);