Merge branch 'cassandra-4.1' into trunk
diff --git a/.build/build-rat.xml b/.build/build-rat.xml
index 5632664..6a3d72e 100644
--- a/.build/build-rat.xml
+++ b/.build/build-rat.xml
@@ -58,6 +58,8 @@
                  <exclude NAME="**/doc/antora.yml"/>
                  <exclude name="**/test/conf/cassandra.yaml"/>
                  <exclude name="**/test/conf/cassandra-old.yaml"/>
+                 <exclude name="**/test/conf/cassandra-converters-special-cases-old-names.yaml"/>
+                 <exclude name="**/test/conf/cassandra-converters-special-cases.yaml"/>
                  <exclude name="**/test/conf/cassandra_encryption.yaml"/>
                  <exclude name="**/test/conf/cdc.yaml"/>
                  <exclude name="**/test/conf/commitlog_compression_LZ4.yaml"/>
diff --git a/CHANGES.txt b/CHANGES.txt
index 47d08b3..7c9137f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -20,6 +20,8 @@
  * Add guardrail for ALTER TABLE ADD / DROP / REMOVE column operations (CASSANDRA-17495)
  * Rename DisableFlag class to EnableFlag on guardrails (CASSANDRA-17544)
 Merged from 4.1:
+ * Fix sstable_preemptive_open_interval disabled value. sstable_preemptive_open_interval = null backward compatible with
+   sstable_preemptive_open_interval_in_mb = -1 (CASSANDRA-17737)
  * Remove usages of Path#toFile() in the snapshot apparatus (CASSANDRA-17769)
  * Fix Settings Virtual Table to update paxos_variant after startup and rename enable_uuid_sstable_identifiers to
    uuid_sstable_identifiers_enabled as per our config naming conventions (CASSANDRA-17738)
diff --git a/NEWS.txt b/NEWS.txt
index 1589219..fa12563 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -198,9 +198,13 @@
 
 Upgrading
 ---------
+    - `sstable_preemptive_open_interval_in_mb` being negative for disabled is equivalent to `sstable_preemptive_open_interval`
+      being null again. In the JMX MBean `org.apache.cassandra.db:type=StorageService`, the setter method
+      `setSSTablePreemptiveOpenIntervalInMB`still takes `intervalInMB` negative numbers for disabled.
     - `enable_uuid_sstable_identifiers` parameter from 4.1 alpha1 was renamed to `uuid_sstable_identifiers_enabled`.
     - `index_summary_resize_interval_in_minutes = -1` is equivalent to index_summary_resize_interval being set to `null` or
-      disabled. JMX MBean `IndexSummaryManager`, `setResizeIntervalInMinutes` method still takes `resizeIntervalInMinutes = -1` for disabled.
+      disabled. In the JMX MBean `org.apache.cassandra.db:type=IndexSummaryManager`, the setter method `setResizeIntervalInMinutes` still takes
+      `resizeIntervalInMinutes = -1` for disabled.
     - min_tracked_partition_size_bytes parameter from 4.1 alpha1 was renamed to min_tracked_partition_size.
     - Parameters of type data storage, duration and data rate cannot be set to Long.MAX_VALUE (former parameters of long type)
       and Integer.MAX_VALUE (former parameters of int type). Those numbers are used during conversion between units to prevent
diff --git a/build.xml b/build.xml
index 7595c1b..ca346c9 100644
--- a/build.xml
+++ b/build.xml
@@ -627,7 +627,7 @@
             <exclusion groupId="net.java.dev.jna" artifactId="jna" />
             <exclusion groupId="net.java.dev.jna" artifactId="jna-platform" />
           </dependency>
-          <dependency groupId="com.google.code.findbugs" artifactId="jsr305" version="2.0.2" scope="provided"/>
+          <dependency groupId="com.google.code.findbugs" artifactId="jsr305" version="2.0.2"/>
           <dependency groupId="com.clearspring.analytics" artifactId="stream" version="2.5.2">
             <exclusion groupId="it.unimi.dsi" artifactId="fastutil" />
           </dependency>
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index 9d0ce11..dd388f2 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -1004,6 +1004,8 @@
 # are completely written, and used in place of the prior sstables for
 # any range that has been written. This helps to smoothly transfer reads 
 # between the sstables, reducing page cache churn and keeping hot rows hot
+# Set sstable_preemptive_open_interval to null for disabled which is equivalent to
+# sstable_preemptive_open_interval_in_mb being negative
 # Min unit: MiB
 sstable_preemptive_open_interval: 50MiB
 
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index 0dc9cbc..834a3f6 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -28,6 +28,8 @@
 import java.util.TreeMap;
 import java.util.function.Supplier;
 
+import javax.annotation.Nullable;
+
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -426,7 +428,8 @@
     @Replaces(oldName = "trickle_fsync_interval_in_kb", converter = Converters.KIBIBYTES_DATASTORAGE, deprecated = true)
     public DataStorageSpec.IntKibibytesBound trickle_fsync_interval = new DataStorageSpec.IntKibibytesBound("10240KiB");
 
-    @Replaces(oldName = "sstable_preemptive_open_interval_in_mb", converter = Converters.MEBIBYTES_DATA_STORAGE_INT, deprecated = true)
+    @Nullable
+    @Replaces(oldName = "sstable_preemptive_open_interval_in_mb", converter = Converters.NEGATIVE_MEBIBYTES_DATA_STORAGE_INT, deprecated = true)
     public volatile DataStorageSpec.IntMebibytesBound sstable_preemptive_open_interval = new DataStorageSpec.IntMebibytesBound("50MiB");
 
     public volatile boolean key_cache_migrate_during_compaction = true;
@@ -504,6 +507,7 @@
 
     @Replaces(oldName = "index_summary_capacity_in_mb", converter = Converters.MEBIBYTES_DATA_STORAGE_LONG, deprecated = true)
     public volatile DataStorageSpec.LongMebibytesBound index_summary_capacity;
+    @Nullable
     @Replaces(oldName = "index_summary_resize_interval_in_minutes", converter = Converters.MINUTES_CUSTOM_DURATION, deprecated = true)
     public volatile DurationSpec.IntMinutesBound index_summary_resize_interval = new DurationSpec.IntMinutesBound("60m");
 
diff --git a/src/java/org/apache/cassandra/config/Converters.java b/src/java/org/apache/cassandra/config/Converters.java
index d825cb9..d622397 100644
--- a/src/java/org/apache/cassandra/config/Converters.java
+++ b/src/java/org/apache/cassandra/config/Converters.java
@@ -40,10 +40,10 @@
     IDENTITY(null, null, o -> o, o -> o),
     MILLIS_DURATION_LONG(Long.class, DurationSpec.LongMillisecondsBound.class,
                          DurationSpec.LongMillisecondsBound::new,
-                         o -> o.toMilliseconds()),
+                         o -> o == null ? null : o.toMilliseconds()),
     MILLIS_DURATION_INT(Integer.class, DurationSpec.IntMillisecondsBound.class,
                         DurationSpec.IntMillisecondsBound::new,
-                        DurationSpec.IntMillisecondsBound::toMilliseconds),
+                        o -> o == null ? null : o.toMilliseconds()),
     MILLIS_DURATION_DOUBLE(Double.class, DurationSpec.IntMillisecondsBound.class,
                            o -> Double.isNaN(o) ? new DurationSpec.IntMillisecondsBound(0) :
                                 new DurationSpec.IntMillisecondsBound(o, TimeUnit.MILLISECONDS),
@@ -57,10 +57,10 @@
                            o -> o == null ? -1 : o.toMilliseconds()),
     SECONDS_DURATION(Integer.class, DurationSpec.IntSecondsBound.class,
                      DurationSpec.IntSecondsBound::new,
-                     DurationSpec.IntSecondsBound::toSeconds),
+                     o -> o == null ? null : o.toSeconds()),
     NEGATIVE_SECONDS_DURATION(Integer.class, DurationSpec.IntSecondsBound.class,
                               o -> o < 0 ? new DurationSpec.IntSecondsBound(0) : new DurationSpec.IntSecondsBound(o),
-                              DurationSpec.IntSecondsBound::toSeconds),
+                              o -> o == null ? null : o.toSeconds()),
     /**
      * This converter is used to support backward compatibility for Duration parameters where we added the opportunity
      * for the users to add a unit in the parameters' values but we didn't change the names. (key_cache_save_period,
@@ -69,26 +69,29 @@
      */
     SECONDS_CUSTOM_DURATION(String.class, DurationSpec.IntSecondsBound.class,
                             DurationSpec.IntSecondsBound::inSecondsString,
-                            o -> Long.toString(o.toSeconds())),
+                            o -> o == null ? null : Long.toString(o.toSeconds())),
     /**
      * This converter is used to support backward compatibility for parameters where in the past -1 was used as a value
      * Example:  index_summary_resize_interval_in_minutes = -1 and  index_summary_resize_interval = null are equal.
      */
     MINUTES_CUSTOM_DURATION(Integer.class, DurationSpec.IntMinutesBound.class,
-                     o -> o == -1 ? null : new DurationSpec.IntMinutesBound(o),
-                     o -> o == null ? -1 : o.toMinutes()),
+                            o -> o == -1 ? null : new DurationSpec.IntMinutesBound(o),
+                            o -> o == null ? -1 : o.toMinutes()),
     MEBIBYTES_DATA_STORAGE_LONG(Long.class, DataStorageSpec.LongMebibytesBound.class,
                                 DataStorageSpec.LongMebibytesBound::new,
-                                DataStorageSpec.LongMebibytesBound::toMebibytes),
+                                o -> o == null ? null : o.toMebibytes()),
     MEBIBYTES_DATA_STORAGE_INT(Integer.class, DataStorageSpec.IntMebibytesBound.class,
                                DataStorageSpec.IntMebibytesBound::new,
-                               DataStorageSpec.IntMebibytesBound::toMebibytes),
+                               o -> o == null ? null : o.toMebibytes()),
+    NEGATIVE_MEBIBYTES_DATA_STORAGE_INT(Integer.class, DataStorageSpec.IntMebibytesBound.class,
+                                        o -> o < 0 ? null : new DataStorageSpec.IntMebibytesBound(o),
+                                        o -> o == null ? -1 : o.toMebibytes()),
     KIBIBYTES_DATASTORAGE(Integer.class, DataStorageSpec.IntKibibytesBound.class,
                           DataStorageSpec.IntKibibytesBound::new,
-                          DataStorageSpec.IntKibibytesBound::toKibibytes),
+                          o -> o == null ? null : o.toKibibytes()),
     BYTES_DATASTORAGE(Integer.class, DataStorageSpec.IntBytesBound.class,
                       DataStorageSpec.IntBytesBound::new,
-                      DataStorageSpec.IntBytesBound::toBytes),
+                      o -> o == null ? null : o.toBytes()),
     /**
      * This converter is used to support backward compatibility for parameters where in the past negative number was used as a value
      * Example: native_transport_max_concurrent_requests_in_bytes_per_ip = -1 and native_transport_max_request_data_in_flight_per_ip = null
@@ -96,17 +99,17 @@
      */
     BYTES_CUSTOM_DATASTORAGE(Long.class, DataStorageSpec.LongBytesBound.class,
                              o -> o == -1 ? null : new DataStorageSpec.LongBytesBound(o),
-                             DataStorageSpec.LongBytesBound::toBytes),
+                             o -> o == null ? null : o.toBytes()),
     MEBIBYTES_PER_SECOND_DATA_RATE(Integer.class, DataRateSpec.IntMebibytesPerSecondBound.class,
                                    DataRateSpec.IntMebibytesPerSecondBound::new,
-                                   DataRateSpec.IntMebibytesPerSecondBound::toMebibytesPerSecondAsInt),
+                                   o -> o == null ? null : o.toMebibytesPerSecondAsInt()),
     /**
      * 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(Integer.class, DataRateSpec.IntMebibytesPerSecondBound.class,
                                                i -> DataRateSpec.IntMebibytesPerSecondBound.megabitsPerSecondInMebibytesPerSecond(i),
-                                               DataRateSpec.IntMebibytesPerSecondBound::toMegabitsPerSecondAsInt);
+                                               o -> o == null ? null : o.toMegabitsPerSecondAsInt());
     private final Class<?> oldType;
     private final Class<?> newType;
     private final Function<Object, Object> convert;
@@ -164,7 +167,6 @@
      */
     public Object unconvert(Object value)
     {
-        if (value == null) return null;
         return reverseConvert.apply(value);
     }
 }
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 809eb8f..e025ecc 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -3081,14 +3081,21 @@
         conf.key_cache_migrate_during_compaction = migrateCacheEntry;
     }
 
+    /** This method can return negative number for disabled */
     public static int getSSTablePreemptiveOpenIntervalInMiB()
     {
+        if (conf.sstable_preemptive_open_interval == null)
+            return -1;
         return conf.sstable_preemptive_open_interval.toMebibytes();
     }
 
+    /** Negative number for disabled */
     public static void setSSTablePreemptiveOpenIntervalInMiB(int mib)
     {
-        conf.sstable_preemptive_open_interval = new DataStorageSpec.IntMebibytesBound(mib);
+        if (mib < 0)
+            conf.sstable_preemptive_open_interval = null;
+        else
+            conf.sstable_preemptive_open_interval = new DataStorageSpec.IntMebibytesBound(mib);
     }
 
     public static boolean getTrickleFsync()
diff --git a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
index 09f3f1e..1dcd595 100644
--- a/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
+++ b/src/java/org/apache/cassandra/config/YamlConfigurationLoader.java
@@ -30,6 +30,8 @@
 import java.util.Objects;
 import java.util.Set;
 
+import javax.annotation.Nullable;
+
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -363,10 +365,13 @@
 
             return new ForwardingProperty(result.getName(), result)
             {
+                boolean allowsNull = result.getAnnotation(Nullable.class) != null;
+
                 @Override
                 public void set(Object object, Object value) throws Exception
                 {
-                    if (value == null && get(object) != null)
+                    // TODO: CASSANDRA-17785, add @Nullable to all nullable Config properties and remove value == null
+                    if (value == null && get(object) != null && !allowsNull)
                         nullProperties.add(getName());
 
                     result.set(object, value);
diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java
index 102f5ec..6bbc516 100644
--- a/src/java/org/apache/cassandra/service/StorageService.java
+++ b/src/java/org/apache/cassandra/service/StorageService.java
@@ -6020,11 +6020,13 @@
         return DatabaseDescriptor.getPartitionerName();
     }
 
+    /** Negative number for disabled */
     public void setSSTablePreemptiveOpenIntervalInMB(int intervalInMB)
     {
         DatabaseDescriptor.setSSTablePreemptiveOpenIntervalInMiB(intervalInMB);
     }
 
+    /** This method can return negative number for disabled */
     public int getSSTablePreemptiveOpenIntervalInMB()
     {
         return DatabaseDescriptor.getSSTablePreemptiveOpenIntervalInMiB();
diff --git a/test/conf/cassandra-converters-special-cases-old-names.yaml b/test/conf/cassandra-converters-special-cases-old-names.yaml
new file mode 100644
index 0000000..144844b
--- /dev/null
+++ b/test/conf/cassandra-converters-special-cases-old-names.yaml
@@ -0,0 +1,7 @@
+#
+# This test config is used to test the Converters added for configuration backward compatibility in 4.1 post
+# CASSANDRA-15234. It tests setting old Config names (CASSANDRA-17737)
+#
+sstable_preemptive_open_interval_in_mb: -1
+index_summary_resize_interval_in_minutes: -1
+
diff --git a/test/conf/cassandra-converters-special-cases.yaml b/test/conf/cassandra-converters-special-cases.yaml
new file mode 100644
index 0000000..227c962
--- /dev/null
+++ b/test/conf/cassandra-converters-special-cases.yaml
@@ -0,0 +1,7 @@
+#
+# This test config is used to test the Converters added for configuration backward compatibility in 4.1 post
+# CASSANDRA-15234. It tests setting new Config names (CASSANDRA-17737)
+#
+sstable_preemptive_open_interval:
+index_summary_resize_interval:
+
diff --git a/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java b/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
index f7bfd53..1e1dd96 100644
--- a/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
+++ b/test/unit/org/apache/cassandra/config/YamlConfigurationLoaderTest.java
@@ -24,6 +24,7 @@
 import java.net.URL;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Predicate;
 
@@ -111,6 +112,39 @@
     }
 
     @Test
+    public void readConvertersSpecialCasesFromConfig()
+    {
+        Config c = load("test/conf/cassandra-converters-special-cases.yaml");
+        assertThat(c.sstable_preemptive_open_interval).isNull();
+        assertThat(c.index_summary_resize_interval).isNull();
+
+        c = load("test/conf/cassandra-converters-special-cases-old-names.yaml");
+        assertThat(c.sstable_preemptive_open_interval).isNull();
+        assertThat(c.index_summary_resize_interval).isNull();
+    }
+
+    @Test
+    public void readConvertersSpecialCasesFromMap()
+    {
+        Map<String, Object> map = new HashMap<>();
+        map.put("sstable_preemptive_open_interval", null);
+        map.put("index_summary_resize_interval", null);
+
+        Config c = YamlConfigurationLoader.fromMap(map, true, Config.class);
+        assert c.sstable_preemptive_open_interval == null;
+        assert c.index_summary_resize_interval == null;
+
+        map = ImmutableMap.of(
+        "sstable_preemptive_open_interval_in_mb", "-1",
+        "index_summary_resize_interval_in_minutes", "-1"
+        );
+        c = YamlConfigurationLoader.fromMap(map, Config.class);
+
+        assertThat(c.sstable_preemptive_open_interval).isNull();
+        assertThat(c.index_summary_resize_interval).isNull();
+    }
+
+    @Test
     public void readThresholdsFromConfig()
     {
         Config c = load("test/conf/cassandra.yaml");
@@ -311,6 +345,10 @@
         assertThatThrownBy(() -> from("stream_throughput_outbound_megabits_per_sec", -2).stream_throughput_outbound.toMegabitsPerSecondAsInt())
         .hasRootCauseInstanceOf(IllegalArgumentException.class)
         .hasRootCauseMessage("Invalid data rate: value must be non-negative");
+
+        // NEGATIVE_MEBIBYTES_DATA_STORAGE_INT
+        assertThat(from("sstable_preemptive_open_interval_in_mb", "1").sstable_preemptive_open_interval.toMebibytes()).isEqualTo(1);
+        assertThat(from("sstable_preemptive_open_interval_in_mb", -2).sstable_preemptive_open_interval).isNull();
     }
 
     private static Config from(Object... values)
diff --git a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java
index 98b29b5..26a9e88 100644
--- a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java
+++ b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java
@@ -56,6 +56,8 @@
         config = new Config();
         config.client_encryption_options.applyConfig();
         config.server_encryption_options.applyConfig();
+        config.sstable_preemptive_open_interval = null;
+        config.index_summary_resize_interval = null;
         table = new SettingsTable(KS_NAME, config);
         VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(table)));
         disablePreparedReuseForTest();
@@ -106,6 +108,22 @@
         assertRowsNet(executeNet(q));
     }
 
+    @Test
+    public void virtualTableBackwardCompatibility() throws Throwable
+    {
+        // test NEGATIVE_MEBIBYTES_DATA_STORAGE_INT converter
+        String q = "SELECT * FROM vts.settings WHERE name = 'sstable_preemptive_open_interval';";
+        assertRowsNet(executeNet(q), new Object[] {"sstable_preemptive_open_interval", null});
+        q = "SELECT * FROM vts.settings WHERE name = 'sstable_preemptive_open_interval_in_mb';";
+        assertRowsNet(executeNet(q), new Object[] {"sstable_preemptive_open_interval_in_mb", "-1"});
+
+        // test MINUTES_CUSTOM_DURATION - test to be added later whe
+        q = "SELECT * FROM vts.settings WHERE name = 'index_summary_resize_interval';";
+        assertRowsNet(executeNet(q), new Object[] {"index_summary_resize_interval", null});
+        q = "SELECT * FROM vts.settings WHERE name = 'index_summary_resize_interval_in_minutes';";
+        assertRowsNet(executeNet(q), new Object[] {"index_summary_resize_interval_in_minutes", "-1"});
+    }
+
     private String getValue(Property prop)
     {
         Object v = prop.get(config);
diff --git a/test/unit/org/apache/cassandra/service/StorageServiceTest.java b/test/unit/org/apache/cassandra/service/StorageServiceTest.java
index e42e202..694c876 100644
--- a/test/unit/org/apache/cassandra/service/StorageServiceTest.java
+++ b/test/unit/org/apache/cassandra/service/StorageServiceTest.java
@@ -21,6 +21,7 @@
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -169,6 +170,13 @@
     }
 
     @Test
+    public void testSetGetSSTablePreemptiveOpenIntervalInMB()
+    {
+        StorageService.instance.setSSTablePreemptiveOpenIntervalInMB(-1);
+        Assert.assertEquals(-1, StorageService.instance.getSSTablePreemptiveOpenIntervalInMB());
+    }
+
+    @Test
     public void testScheduledExecutorsShutdownOnDrain() throws Throwable
     {
         final AtomicInteger numberOfRuns = new AtomicInteger(0);