This is an automated email from the ASF dual-hosted git repository.

edimitrova pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git

commit 9c6b382058578ac75b88055a13aa83944901fb88
Author: Ekaterina Dimitrova <ekaterina.dimitr...@datastax.com>
AuthorDate: Tue Dec 14 23:04:43 2021 -0500

    Backward compatibility framework for configuration parameters
    patch by Ekaterina Dimitrova; reviewed by Caleb Rackliffe, David Capwell, 
Michael Semb Wever and Benjamin Lerer for CASSANDRA-15234
---
 .../org/apache/cassandra/config/Converters.java    | 125 +++++++++++++++++++
 src/java/org/apache/cassandra/config/Replaces.java |   9 +-
 .../org/apache/cassandra/config/ReplacesList.java  |   2 +-
 .../cassandra/config/YamlConfigurationLoader.java  | 134 ++++++++++++++++++---
 .../apache/cassandra/db/virtual/SettingsTable.java |  16 +++
 5 files changed, 263 insertions(+), 23 deletions(-)

diff --git a/src/java/org/apache/cassandra/config/Converters.java 
b/src/java/org/apache/cassandra/config/Converters.java
new file mode 100644
index 0000000..9a832cc
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/Converters.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.config;
+
+import java.util.function.Function;
+
+/**
+ * Converters for backward compatibility with the old cassandra.yaml where 
duration, data rate and
+ * data storage configuration parameters were provided only by value and the 
expected unit was part of the configuration
+ * parameter name(suffix). (CASSANDRA-15234)
+ * It is important to be noted that this converter is not intended to be used 
when we don't change name of a configuration
+ * parameter but we want to add unit. This would always default to the old 
value provided without a unit at the moment.
+ * In case this functionality is needed at some point, please, raise a Jira 
ticket.
+ */
+public enum Converters
+{
+    /**
+     * This converter is used when we change the name of a cassandra.yaml 
configuration parameter but we want to be
+     * able to still use the old name too. No units involved.
+     */
+    IDENTITY(null, o -> o, o-> o),
+    MILLIS_DURATION(Long.class,
+                    o -> DurationSpec.inMilliseconds((Long) o),
+                    o -> ((DurationSpec)o).toMilliseconds()),
+    MILLIS_DOUBLE_DURATION(Double.class,
+                           o ->  DurationSpec.inDoubleMilliseconds((Double) o),
+                           o -> ((DurationSpec)o).toMilliseconds()),
+    /**
+     * This converter is used to support backward compatibility for parameters 
where in the past -1 was used as a value
+     * Example: credentials_update_interval_in_ms = -1 and 
credentials_update_interval = null (quantity of 0ms) are equal.
+     */
+    MILLIS_CUSTOM_DURATION(Long.class,
+                           o -> (Long)o == -1 ? new DurationSpec("0ms") : 
DurationSpec.inMilliseconds((Long) o),
+                           o -> ((DurationSpec)o).toMilliseconds() == 0 ? -1 : 
((DurationSpec)o).toMilliseconds()),
+    SECONDS_DURATION(Long.class,
+                     o -> DurationSpec.inSeconds((Long) o),
+                     o -> ((DurationSpec)o).toSeconds()),
+    MINUTES_DURATION(Long.class,
+                     o -> DurationSpec.inMinutes((Long) o),
+                     o -> ((DurationSpec)o).toMinutes()),
+    MEBIBYTES_DATA_STORAGE(Long.class,
+                          o -> DataStorageSpec.inMebibytes((Long) o),
+                          o -> ((DataStorageSpec)o).toMebibytes()),
+    KIBIBYTES_DATASTORAGE(Long.class,
+                          o -> DataStorageSpec.inKibibytes((Long) o),
+                          o -> ((DataStorageSpec)o).toKibibytes()),
+    BYTES_DATASTORAGE(Long.class,
+                      o -> DataStorageSpec.inBytes((Long) o),
+                      o -> ((DataStorageSpec)o).toBytes()),
+    MEBIBYTES_PER_SECOND_DATA_RATE(Long.class,
+                                   o -> 
DataRateSpec.inMebibytesPerSecond((Long) o),
+                                   o -> 
((DataRateSpec)o).toMebibytesPerSecond()),
+    /**
+     * This converter is a custom one to support backward compatibility for 
stream_throughput_outbound and
+     * inter_dc_stream_throughput_outbound which were provided in megatibs per 
second prior CASSANDRA-15234.
+     */
+    MEGABITS_TO_MEBIBYTES_PER_SECOND_DATA_RATE(Long.class,
+                                               o -> 
DataRateSpec.megabitsPerSecondInMebibytesPerSecond((Long)o),
+                                               o -> 
((DataRateSpec)o).toMegabitsPerSecond());
+
+    private final Class<?> inputType;
+    private final Function<Object, Object> convert;
+    private final Function<Object, Object> reverseConvert;
+
+    Converters(Class<?> inputType, Function<Object, Object> convert, 
Function<Object, Object> reverseConvert)
+    {
+        this.inputType = inputType;
+        this.convert = convert;
+        this.reverseConvert = reverseConvert;
+    }
+
+    /**
+     * A method to identify what type is needed to be returned by the 
converter used for a configuration parameter
+     * in {@link Replaces} annotation in {@link Config}
+     *
+     * @return class type
+     */
+    public Class<?> getInputType()
+    {
+        return inputType;
+    }
+
+    /**
+     * Apply the converter specified as part of the {@link Replaces} 
annotation in {@link Config}
+     *
+     * @param value we will use from cassandra.yaml to create a new {@link 
Config} parameter of type {@link DurationSpec},
+     * {@link DataRateSpec} or {@link DataStorageSpec}
+     * @return new object of type {@link DurationSpec}, {@link DataRateSpec} 
or {@link DataStorageSpec}
+     */
+    public Object convert(Object value)
+    {
+        if (value == null) return null;
+        return convert.apply(value);
+    }
+
+    /**
+     * Apply the converter specified as part of the {@link Replaces} 
annotation in {@link Config} to get config parameters'
+     * values in the old format pre-CASSANDRA-15234 and in the right units, 
used in the Virtual Tables to ensure backward
+     * compatibility
+     *
+     * @param value we will use to calculate the output value
+     * @return the numeric value
+     */
+    public Object deconvert(Object value)
+    {
+        if (value == null) return 0;
+        return reverseConvert.apply(value);
+    }
+}
diff --git a/src/java/org/apache/cassandra/config/Replaces.java 
b/src/java/org/apache/cassandra/config/Replaces.java
index 93bdcb5..e1354b9 100644
--- a/src/java/org/apache/cassandra/config/Replaces.java
+++ b/src/java/org/apache/cassandra/config/Replaces.java
@@ -25,8 +25,8 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * Repeatable annotation for providing old name and whether the
- * config parameters we annotate are deprecated and we need to warn the users. 
(CASSANDRA-17141)
+ * Repeatable annotation for providing old name, converter from old to new 
type and whether the
+ * config parameters we annotate are deprecated and we need to warn the users. 
(CASSANDRA-17141, CASSANDRA-15234)
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ ElementType.FIELD})
@@ -39,6 +39,11 @@ public @interface Replaces
     String oldName();
 
     /**
+     * @return which converter we need depending on the old default unit that 
was used
+     */
+    Converters converter() default Converters.IDENTITY;
+
+    /**
      * @return whether the parameter should be marked as deprecated or not and 
warning sent to the user
      */
     boolean deprecated() default false;
diff --git a/src/java/org/apache/cassandra/config/ReplacesList.java 
b/src/java/org/apache/cassandra/config/ReplacesList.java
index a2b0c96..eec4e4c 100644
--- a/src/java/org/apache/cassandra/config/ReplacesList.java
+++ b/src/java/org/apache/cassandra/config/ReplacesList.java
@@ -24,7 +24,7 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * Contatining annotation type for the repeatable annotation {@link Replaces}
+ * Concatenating annotation type for the repeatable annotation {@link Replaces}
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ ElementType.FIELD})
diff --git a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java 
b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
index dc9b7fb..4fc82b4 100644
--- a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
+++ b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
@@ -25,12 +25,16 @@ import java.lang.reflect.Field;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
@@ -38,9 +42,10 @@ import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
 
-import org.apache.cassandra.io.util.File;
 import org.apache.commons.lang3.SystemUtils;
 
+import org.apache.cassandra.io.util.File;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -65,6 +70,7 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
     /**
      * Inspect the classpath to find storage configuration file
      */
+    @VisibleForTesting
     private static URL getStorageConfigURL() throws ConfigurationException
     {
         String configUrl = System.getProperty("cassandra.config");
@@ -106,6 +112,8 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
     {
         if (storageConfigURL == null)
             storageConfigURL = getStorageConfigURL();
+
+        validateConfigFile();
         return loadConfig(storageConfigURL);
     }
 
@@ -125,7 +133,6 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
                 throw new AssertionError(e);
             }
 
-
             Constructor constructor = new CustomConstructor(Config.class, 
Yaml.class.getClassLoader());
             Map<Class<?>, Map<String, Replacement>> replacements = 
getNameReplacements(Config.class);
             PropertiesChecker propertiesChecker = new 
PropertiesChecker(replacements);
@@ -138,8 +145,48 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
         catch (YAMLException e)
         {
             throw new ConfigurationException("Invalid yaml: " + url + 
SystemUtils.LINE_SEPARATOR
-                                             +  " Error: " + e.getMessage(), 
false);
+                                             + " Error: " + e.getMessage(), 
false);
+        }
+    }
+
+    private static String readStorageConfig(URL url)
+    {
+        String content;
+
+        try
+        {
+            content = new String 
(Files.readAllBytes(Paths.get(String.valueOf(url).substring(5))));
         }
+        catch (IOException e)
+        {
+            throw new ConfigurationException("Invalid yaml: " + url, e);
+        }
+
+        return content;
+    }
+
+    private static void validateConfigFile()
+    {
+        String content = 
YamlConfigurationLoader.readStorageConfig(storageConfigURL);
+
+        if (isBlank("commitlog_sync_period", content))
+            throw new IllegalArgumentException("You should provide a value for 
commitlog_sync_period or comment it in " +
+                                               "order to get a default one");
+
+        if (isBlank("commitlog_sync_group_window", content))
+            throw new IllegalArgumentException("You should provide a value for 
commitlog_sync_group_window or comment it in " +
+                                               "order to get a default one");
+    }
+
+    /**
+     * This method helps to preserve the behavior of parameters which were 
originally of primitive type and
+     * without default value in Config.java (CASSANDRA-15234)
+     */
+    private static boolean isBlank(String property, String content)
+    {
+        Pattern p = Pattern.compile(String.format("%s%s *: *$", '^', 
property), Pattern.MULTILINE);
+        Matcher m = p.matcher(content);
+        return m.find();
     }
 
     @VisibleForTesting
@@ -171,6 +218,7 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
         return value;
     }
 
+    @VisibleForTesting
     static class CustomConstructor extends CustomClassLoaderConstructor
     {
         CustomConstructor(Class<?> theRoot, ClassLoader classLoader)
@@ -213,6 +261,7 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
      * Utility class to check that there are no extra properties and that 
properties that are not null by default
      * are not set to null.
      */
+    @VisibleForTesting
     private static class PropertiesChecker extends PropertyUtils
     {
         private final Set<String> missingProperties = new HashSet<>();
@@ -223,22 +272,23 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
 
         private final Map<Class<?>, Map<String, Replacement>> replacements;
 
-        public PropertiesChecker(Map<Class<?>, Map<String, Replacement>> 
replacements)
+        PropertiesChecker(Map<Class<?>, Map<String, Replacement>> replacements)
         {
             this.replacements = Objects.requireNonNull(replacements, 
"Replacements should not be null");
             setSkipMissingProperties(true);
         }
 
         @Override
-        public Property getProperty(Class<? extends Object> type, String name)
+        public Property getProperty(Class<?> type, String name)
         {
             final Property result;
             Map<String, Replacement> typeReplacements = 
replacements.getOrDefault(type, Collections.emptyMap());
             if (typeReplacements.containsKey(name))
             {
                 Replacement replacement = typeReplacements.get(name);
+
                 final Property newProperty = super.getProperty(type, 
replacement.newName);
-                result = new Property(replacement.oldName, 
newProperty.getType())
+                result = new Property(replacement.oldName, replacement.oldType)
                 {
                     @Override
                     public Class<?>[] getActualTypeArguments()
@@ -249,7 +299,8 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
                     @Override
                     public void set(Object o, Object o1) throws Exception
                     {
-                        newProperty.set(o, o1);
+                        Object migratedValue = 
replacement.converter.convert(o1);
+                        newProperty.set(o, migratedValue);
                     }
 
                     @Override
@@ -270,7 +321,7 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
                         return null;
                     }
                 };
-
+                
                 if (replacement.deprecated)
                     deprecationWarnings.add(replacement.oldName);
             }
@@ -293,6 +344,7 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
                     {
                         nullProperties.add(getName());
                     }
+
                     result.set(object, value);
                 }
 
@@ -331,7 +383,7 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
                 throw new ConfigurationException("Invalid yaml. Please remove 
properties " + missingProperties + " from your cassandra.yaml", false);
 
             if (!deprecationWarnings.isEmpty())
-                logger.warn("{} parameters have been deprecated. They have new 
names; For more information, please refer to NEWS.txt", deprecationWarnings);
+                logger.warn("{} parameters have been deprecated. They have new 
names and/or value format; For more information, please refer to NEWS.txt", 
deprecationWarnings);
         }
     }
 
@@ -339,9 +391,9 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
      * @param klass to get replacements for
      * @return map of old names and replacements needed.
      */
-    private static Map<Class<?>, Map<String, Replacement>> 
getNameReplacements(Class<?> klass)
+    private static Map<Class<? extends Object>, Map<String, Replacement>> 
getNameReplacements(Class<? extends Object> klass)
     {
-        List<Replacement> replacements = getReplacements(klass);
+        List<Replacement> replacements = getReplacementsRecursive(klass);
         Map<Class<?>, Map<String, Replacement>> objectOldNames = new 
HashMap<>();
         for (Replacement r : replacements)
         {
@@ -357,24 +409,52 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
         return objectOldNames;
     }
 
+    /**
+     * @param klass to get replacements for
+     * @return map of old names and replacements needed.
+     */
+    private static List<Replacement> getReplacementsRecursive(Class<?> klass)
+    {
+        Set<Class<?>> seen = new HashSet<>(); // to make sure not to process 
the same type twice
+        List<Replacement> accum = new ArrayList<>();
+        getReplacementsRecursive(seen, accum, klass);
+        return accum.isEmpty() ? Collections.emptyList() : accum;
+    }
+
+    private static void getReplacementsRecursive(Set<Class<?>> seen,
+                                                 List<Replacement> accum,
+                                                 Class<?> klass)
+    {
+        accum.addAll(getReplacements(klass));
+        for (Field field : klass.getDeclaredFields())
+        {
+            if (seen.add(field.getType()))
+            {
+                // first time looking at this type, walk it
+                getReplacementsRecursive(seen, accum, field.getType());
+            }
+        }
+    }
+
     private static List<Replacement> getReplacements(Class<?> klass)
     {
         List<Replacement> replacements = new ArrayList<>();
         for (Field field : klass.getDeclaredFields())
         {
             String newName = field.getName();
+            Class<?> newType = field.getType();
             final ReplacesList[] byType = 
field.getAnnotationsByType(ReplacesList.class);
             if (byType == null || byType.length == 0)
             {
                 Replaces r = field.getAnnotation(Replaces.class);
                 if (r != null)
-                    addReplacement(klass, replacements, newName, r);
+                    addReplacement(klass, replacements, newName, newType, r);
             }
             else
             {
                 for (ReplacesList replacesList : byType)
                     for (Replaces r : replacesList.value())
-                        addReplacement(klass, replacements, newName, r);
+                        addReplacement(klass, replacements, newName, newType, 
r);
             }
         }
         return replacements.isEmpty() ? Collections.emptyList() : replacements;
@@ -382,24 +462,28 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
 
     private static void addReplacement(Class<?> klass,
                                        List<Replacement> replacements,
-                                       String newName,
+                                       String newName, Class<?> newType,
                                        Replaces r)
     {
         String oldName = r.oldName();
+
         boolean deprecated = r.deprecated();
 
-        replacements.add(new Replacement(klass, oldName, newName, deprecated));
+        Class<?> oldType = r.converter().getInputType();
+        if (oldType == null)
+            oldType = newType;
+
+        replacements.add(new Replacement(klass, oldName, oldType, newName, 
r.converter(), deprecated));
     }
 
     /**
-     * Holder for replacements to support backward compatibility between old 
and new names for configuration parameters
-     * backported partially from trunk(CASSANDRA-15234) to support a bug 
fix/improvement in Cassadra 4.0
-     * (CASSANDRA-17141)
+     * Holder for replacements to support backward compatibility between old 
and new names and types
+     * of configuration parameters (CASSANDRA-15234)
      */
     static final class Replacement
     {
         /**
-         * Currently we use for Config class
+         * Currently we use Config class
          */
         final Class<?> parent;
         /**
@@ -407,22 +491,32 @@ public class YamlConfigurationLoader implements 
ConfigurationLoader
          */
         final String oldName;
         /**
+         * Old type of the configuration parameter
+         */
+        final Class<?> oldType;
+        /**
          * New name used for the configuration parameter
          */
         final String newName;
         /**
-         * A flag to mark whether the old name is deprecated and fire a 
warning to the user. By default we set it to false.
+         * Converter to be used according to the old default unit which was 
provided as a suffix of the configuration
+         * parameter
          */
+        final Converters converter;
         final boolean deprecated;
 
         Replacement(Class<?> parent,
                     String oldName,
+                    Class<?> oldType,
                     String newName,
+                    Converters converter,
                     boolean deprecated)
         {
             this.parent = Objects.requireNonNull(parent);
             this.oldName = Objects.requireNonNull(oldName);
+            this.oldType = Objects.requireNonNull(oldType);
             this.newName = Objects.requireNonNull(newName);
+            this.converter = Objects.requireNonNull(converter);
             // by default deprecated is false
             this.deprecated = deprecated;
         }
diff --git a/src/java/org/apache/cassandra/db/virtual/SettingsTable.java 
b/src/java/org/apache/cassandra/db/virtual/SettingsTable.java
index b0ae018..cc91152 100644
--- a/src/java/org/apache/cassandra/db/virtual/SettingsTable.java
+++ b/src/java/org/apache/cassandra/db/virtual/SettingsTable.java
@@ -50,6 +50,13 @@ final class SettingsTable extends AbstractVirtualTable
               .collect(Collectors.toMap(Field::getName, Functions.identity()));
 
     @VisibleForTesting
+    static final Map<String, Field> ANNOTATED_FIELDS =
+        Arrays.stream(Config.class.getFields())
+              .filter(f -> !Modifier.isStatic(f.getModifiers()))
+              .filter(f -> f.isAnnotationPresent(Replaces.class))
+              .collect(Collectors.toMap(Field::getName, Functions.identity()));
+
+    @VisibleForTesting
     final Map<String, BiConsumer<SimpleDataSet, Field>> overrides =
         ImmutableMap.<String, BiConsumer<SimpleDataSet, Field>>builder()
                     .put("audit_logging_options", this::addAuditLoggingOptions)
@@ -108,6 +115,15 @@ final class SettingsTable extends AbstractVirtualTable
             if (value.getClass().isArray())
                 value = Arrays.toString((Object[]) value);
             result.row(f.getName()).column(VALUE, value.toString());
+
+            if (ANNOTATED_FIELDS.containsKey(f.getName()))
+            {
+                Replaces annotation = f.getAnnotation(Replaces.class);
+                result.row(annotation.oldName())
+                      .column(VALUE, annotation.converter()
+                                               .deconvert(value)
+                                               .toString());
+            }
         }
     }
 

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org
For additional commands, e-mail: commits-h...@cassandra.apache.org

Reply via email to