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

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


The following commit(s) were added to refs/heads/trunk by this push:
     new c68dd59d4a Implementation of CEP-49: Hardware-accelerated compression
c68dd59d4a is described below

commit c68dd59d4aab78598d97aab0be08b723ffd53bb3
Author: Kokoori, Shylaja <[email protected]>
AuthorDate: Thu Dec 4 09:41:09 2025 -0800

    Implementation of CEP-49: Hardware-accelerated compression
    
    The implementation of this CEP adds the ability to plug-in custom 
compressors providers
    which might delegate the de/compression to a specialized hardware. An 
operator does not
    need to change any schema definition, the mere implementation of a 
compressor provider
    put on a class path and its related configuration in cassandra.yaml will 
transparently
    start to use appropriate compressor, e.g. backed by a specialized hardware.
    
    patch by Shylaja Kokoori; reviewed by Joey Lynch, Stefan Miklosovic for 
CASSANDRA-20975
    
    Co-authored-by: Stefan Miklosovic <[email protected]>
---
 CHANGES.txt                                        |   1 +
 NEWS.txt                                           |   4 +
 conf/cassandra.yaml                                |  18 +
 conf/cassandra_latest.yaml                         |  18 +
 .../pages/managing/operating/compression.adoc      |  47 ++
 src/java/org/apache/cassandra/config/Config.java   |   1 +
 .../cassandra/config/DatabaseDescriptor.java       |  16 +
 .../io/compress/AbstractCompressionProvider.java   |  95 ++++
 .../cassandra/io/compress/CompressionMetadata.java |   2 +-
 .../cassandra/io/compress/CompressorRegistry.java  | 305 ++++++++++++
 .../io/compress/DefaultCompressionProvider.java    | 106 ++++
 .../apache/cassandra/io/compress/ICompressor.java  |  15 +
 .../apache/cassandra/schema/CompressionParams.java |  53 +-
 .../apache/cassandra/service/StartupChecks.java    | 158 ++++++
 .../org/apache/cassandra/utils/FBUtilities.java    |  21 +
 .../distributed/test/CompressorProviderTest.java   | 552 +++++++++++++++++++++
 .../config/DatabaseDescriptorRefTest.java          |   4 +
 .../io/compress/CompressorRegistryTest.java        | 311 ++++++++++++
 .../cassandra/schema/CompressionParamsTest.java    |  73 +++
 .../cassandra/service/StartupChecksTest.java       |  90 +++-
 .../cassandra/utils/CompressionProviderHelper.java | 226 +++++++++
 21 files changed, 2057 insertions(+), 59 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 87ee90e3a7..73e8de6c7e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 7.0
+ * Implementation of CEP-49: Hardware-accelerated compression (CASSANDRA-20975)
  * Avoid using ObjectUtils.getFirstNonNull in Schema (CASSANDRA-21394)
  * Allow nodetool garbagecollect to take a user defined list of SSTables 
(CASSANDRA-16767)
  * Add a guardrail for misprepared statements (CASSANDRA-21139)
diff --git a/NEWS.txt b/NEWS.txt
index d9207d89a6..028266355f 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -80,6 +80,10 @@ using the provided 'sstableupgrade' tool.
 New features
 ------------
 
+    - CEP-49 - it is possible to implement custom compression providers, e.g.
+      integrating with a specialized hardware which makes de/compression 
faster.
+      See CASSANDRA-20975 for more information.
+
 Upgrading
 ---------
 
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index 422ad0a482..0420e1c9ec 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -2999,3 +2999,21 @@ compression_dictionary_cache_size: 10
 # Expired dictionaries will be removed from memory but can be reloaded if 
needed.
 # Min unit: s
 compression_dictionary_cache_expire: 24h
+
+# Configuring pluggable compressor providers.
+# Keys of compressor_providers map to implementations of ICompressor. FQCN are 
allowed to be keys as well.
+# Cassandra will attempt to load the provider with the specified class_name 
for each compressor key.
+# You can only override built-in compressors.
+# Parameters:
+#   - fail_on_missing_provider: If "false", falls back to 
DefaultCompressionProvider
+#     when the plugin fails to load. If "true", throws an exception on failure.
+#     The default value is "false".
+# If this section is commented out, Cassandra uses DefaultCompressionProvider
+# with built-in compressor implementation.
+#compressor_providers:
+#  LZ4Compressor:
+#    class_name: org.apache.cassandra.io.compress.DefaultCompressionProvider
+#    parameters:
+#      fail_on_missing_provider: "false"
+#  org.apache.cassandra.io.compress.ZstdCompressor:
+#    class_name: org.apache.cassandra.io.compress.DefaultCompressionProvider
diff --git a/conf/cassandra_latest.yaml b/conf/cassandra_latest.yaml
index ee68c70082..de6d872718 100644
--- a/conf/cassandra_latest.yaml
+++ b/conf/cassandra_latest.yaml
@@ -2742,3 +2742,21 @@ compression_dictionary_cache_size: 10
 # Expired dictionaries will be removed from memory but can be reloaded if 
needed.
 # Min unit: s
 compression_dictionary_cache_expire: 24h
+
+# Configuring pluggable compressor providers.
+# Keys of compressor_providers map to implementations of ICompressor. FQCN are 
allowed to be keys as well.
+# Cassandra will attempt to load the provider with the specified class_name 
for each compressor key.
+# You can only override built-in compressors.
+# Parameters:
+#   - fail_on_missing_provider: If "false", falls back to 
DefaultCompressionProvider
+#     when the plugin fails to load. If "true", throws an exception on failure.
+#     The default value is "false".
+# If this section is commented out, Cassandra uses DefaultCompressionProvider
+# with built-in compressor implementation.
+#compressor_providers:
+#  LZ4Compressor:
+#    class_name: org.apache.cassandra.io.compress.DefaultCompressionProvider
+#    parameters:
+#      fail_on_missing_provider: "false"
+#  org.apache.cassandra.io.compress.ZstdCompressor:
+#    class_name: org.apache.cassandra.io.compress.DefaultCompressionProvider
diff --git a/doc/modules/cassandra/pages/managing/operating/compression.adoc 
b/doc/modules/cassandra/pages/managing/operating/compression.adoc
index 134c256934..04ab4d13eb 100644
--- a/doc/modules/cassandra/pages/managing/operating/compression.adoc
+++ b/doc/modules/cassandra/pages/managing/operating/compression.adoc
@@ -416,6 +416,53 @@ Importing a dictionary to a table from a file should 
happen only against one nod
 dictionary will be eventually stored in 
`system_distributed.compression_dictionaries` table and reused
 cluster-wide. When imports happen from multiple nodes, the highest-version 
dictionary will be used.
 
+== Plugin compression providers
+
+Pluggable compression providers allow replacing the built-in compressor 
implementation
+with a custom one — for example, to delegate compression to a 
hardware-accelerated library.
+
+Providers are configured in `cassandra.yaml` under the `compressor_providers` 
map.
+Each key identifies a built-in compressor by its simple class name (e.g. 
`LZ4Compressor`) its FQCN, or
+its registered abbreviation (e.g. `lz4`). Only built-in compressors can be 
overridden; new compressor types cannot be introduced this way.
+
+Example:
+[source,yaml]
+----
+compressor_providers:
+  LZ4Compressor:
+    class_name: com.example.MyLZ4Provider
+    parameters:
+      fail_on_missing_provider: "true"
+  org.apache.cassandra.io.compress.ZstdCompressor:
+    class_name: com.example.MyZstdProvider
+----
+
+Each provider entry requires:
+
+* `class_name`: Fully qualified class name of an `AbstractCompressionProvider` 
subclass.
+* `parameters` (optional): A string map passed to the provider's `init()` 
method.
+  The reserved key `fail_on_missing_provider` (default `"false"`) controls 
fallback behaviour:
+     - If `"false"`: when initialization, health checks, or compressor 
creation fails,
+       falls back to `DefaultCompressionProvider`
+     - If `"true"`: throws an exception instead of falling back
+
+If `compressor_providers` is omitted entirely, `DefaultCompressionProvider` is 
used
+for all compressor types, which is the same behaviour as previous Cassandra 
releases.
+
+==== Implementing a custom provider
+
+A custom provider must extend `AbstractCompressionProvider`, implement the 
following methods and add the jar
+to the classpath, before node startup:
+
+* `boolean isHealthy()` — returns `true` if the provider is ready.
+* `ICompressor createCompressor(Class<?> compressorClass, Map<String, String> 
options)` — factory
+  that returns a compressor instance configured with the given options.
+
+The `ICompressor` returned by `createCompressor()` must override 
`serializedAs()` to return
+the exact built-in compressor class it is substituting (e.g. 
`LZ4Compressor.class`). This
+ensures the SSTable metadata records the canonical name, which is required for
+compatibility across nodes and restarts.
+
 == Advanced Use
 
 Advanced users can provide their own compression class by implementing
diff --git a/src/java/org/apache/cassandra/config/Config.java 
b/src/java/org/apache/cassandra/config/Config.java
index 7cac3e8ed7..d6f68cbe1f 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -89,6 +89,7 @@ public class Config
     public ParameterizedClass crypto_provider;
     public ParameterizedClass network_authorizer;
     public ParameterizedClass cidr_authorizer;
+    public Map<String, ParameterizedClass> compressor_providers = new 
HashMap<>();
 
     @Replaces(oldName = "permissions_validity_in_ms", converter = 
Converters.MILLIS_DURATION_INT, deprecated = true)
     public volatile DurationSpec.IntMillisecondsBound permissions_validity = 
new DurationSpec.IntMillisecondsBound("2s");
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java 
b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index e03f13723f..a1ef3fb789 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -97,6 +97,7 @@ import 
org.apache.cassandra.gms.IEndpointStateChangeSubscriber;
 import org.apache.cassandra.gms.IFailureDetector;
 import org.apache.cassandra.gms.VersionedValue;
 import org.apache.cassandra.io.FSWriteError;
+import org.apache.cassandra.io.compress.CompressorRegistry;
 import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.big.BigFormat;
 import org.apache.cassandra.io.util.DiskOptimizationStrategy;
@@ -375,6 +376,8 @@ public class DatabaseDescriptor
 
         applySSTableFormats();
 
+        applyCompressorProviders();
+
         applySimpleConfig();
 
         applyPartitioner();
@@ -439,6 +442,7 @@ public class DatabaseDescriptor
         applyCompatibilityMode();
         diskOptimizationStrategy = new SpinningDiskOptimizationStrategy();
         applySSTableFormats();
+        applyCompressorProviders();
     }
 
     private static void assertNotDaemonInitialized()
@@ -552,6 +556,8 @@ public class DatabaseDescriptor
 
         applySSTableFormats();
 
+        applyCompressorProviders();
+
         applyCryptoProvider();
 
         applySimpleConfig();
@@ -581,6 +587,11 @@ public class DatabaseDescriptor
         applyStartupChecks();
     }
 
+    public static void applyCompressorProviders()
+    {
+        
CompressorRegistry.instance.registerProviders(conf.compressor_providers);
+    }
+
     private static void applySimpleConfig()
     {
         //Doing this first before all other things in case other pieces of 
config want to construct
@@ -2031,6 +2042,11 @@ public class DatabaseDescriptor
         DatabaseDescriptor.cryptoProvider = cryptoProvider;
     }
 
+    public static Map<String, ParameterizedClass> getCompressionProviders()
+    {
+        return conf.compressor_providers;
+    }
+
     /**
      * Returns the authenticator configured for this node.
      */
diff --git 
a/src/java/org/apache/cassandra/io/compress/AbstractCompressionProvider.java 
b/src/java/org/apache/cassandra/io/compress/AbstractCompressionProvider.java
new file mode 100644
index 0000000000..2194b018ca
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/compress/AbstractCompressionProvider.java
@@ -0,0 +1,95 @@
+/*
+ * 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.io.compress;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Abstract base class for compression providers.
+ * Provides common functionality for loading and managing compressors
+ * with configurable fallback behavior for plugin compression libraries.
+ */
+public abstract class AbstractCompressionProvider
+{
+    /**
+     * Configuration key for enabling fallback to the default provider.
+     */
+    public static final String FAIL_ON_MISSING_PROVIDER = 
"fail_on_missing_provider";
+
+    private Map<String, String> parameters = Collections.emptyMap();
+
+    /**
+     * Checks if this compression provider is in a healthy state and ready to 
use.
+     *
+     * <p>This method should perform any necessary health checks, such as 
verifying
+     * that required native libraries are loaded, able to do 
compress/decompress successfully, etc.</p>
+     *
+     * @return true if the provider is healthy and ready to use, false 
otherwise
+     * @throws Exception if an error occurs during the health check
+     */
+    public abstract boolean isHealthy() throws Exception;
+
+    /**
+     * Creates a new compressor instance with the given compression parameters.
+     *
+     * <p>This method is called when a new compressor is needed. The 
implementation
+     * should create and configure a compressor based on the provided 
options.</p>
+     *
+     * @param options Configuration options for the compressor. May be null or 
empty.
+     * @return A new ICompressor instance
+     * @throws IllegalStateException if the compressor cannot be created
+     */
+    public abstract ICompressor createCompressor(Class<?> compressorClass, 
Map<String, String> options) throws IllegalStateException;
+
+    /**
+     * Initialises this provider with the {@code parameters} block from its
+     * {@code compressor_providers} entry in cassandra.yaml. Called by {@link 
CompressorRegistry}
+     * exactly once, after no-arg construction and before {@link 
#isHealthy()}. May be empty but is
+     * never null.
+     * <p>
+     * The map still contains the registry-reserved key {@link 
#FAIL_ON_MISSING_PROVIDER} when set:
+     * the registry leaves it in place so that the default {@link 
#isFailOnMissingProvider()} (which
+     * reads it back out of the stored parameters) reflects the configured 
value at compressor-creation
+     * time. Subclasses should ignore that key when interpreting their own 
options.
+     * <p>
+     * The default implementation stores the map so subclasses can retrieve it 
via
+     * {@link #getParameters()}. Subclasses that override this method must call
+     * {@code super.init(parameters)} if they want {@code getParameters()} to 
work.
+     */
+    public void init(Map<String, String> parameters)
+    {
+        if (parameters != null)
+            this.parameters = parameters;
+    }
+
+    /**
+     * Returns the {@code parameters} this provider was initialised with, or 
an empty map if
+     * {@link #init} has not been called.
+     */
+    public Map<String, String> getParameters()
+    {
+        return parameters;
+    }
+
+    public boolean isFailOnMissingProvider()
+    {
+        return 
Boolean.parseBoolean(getParameters().getOrDefault(FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString()));
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java 
b/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java
index 2370d25ef2..ab2ae87024 100644
--- a/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java
+++ b/src/java/org/apache/cassandra/io/compress/CompressionMetadata.java
@@ -497,7 +497,7 @@ public class CompressionMetadata extends 
WrappedSharedCloseable
         {
             try
             {
-                
out.writeUTF(parameters.getSstableCompressor().getClass().getSimpleName());
+                
out.writeUTF(parameters.getSstableCompressor().serializedAs().getSimpleName());
                 out.writeInt(parameters.getOtherOptions().size());
                 for (Map.Entry<String, String> entry : 
parameters.getOtherOptions().entrySet())
                 {
diff --git a/src/java/org/apache/cassandra/io/compress/CompressorRegistry.java 
b/src/java/org/apache/cassandra/io/compress/CompressorRegistry.java
new file mode 100644
index 0000000000..5267d691d2
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/compress/CompressorRegistry.java
@@ -0,0 +1,305 @@
+/*
+ * 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.io.compress;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.utils.FBUtilities;
+
+import static 
org.apache.cassandra.io.compress.AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER;
+
+/**
+ * CompressorRegistry manages the registration and retrieval of compression 
providers.
+ * Compression providers supply compressor implementations used for 
compressing data.
+ */
+public final class CompressorRegistry
+{
+    private static final Logger logger = 
LoggerFactory.getLogger(CompressorRegistry.class);
+
+    @VisibleForTesting
+    public static final AbstractCompressionProvider 
DEFAULT_COMPRESSION_PROVIDER = new DefaultCompressionProvider();
+
+    /**
+     * Map of compressor class names to their registered providers. Keyed by 
FQN (not {@code Class<?>})
+     * so that registration of built-in compressors does not force their 
classloading — that would
+     * trigger native library loads (snappy, zstd) at tool/client init time. 
See CASSANDRA-20975.
+     */
+    private static final Map<String, AbstractCompressionProvider> 
compressionProviders = new HashMap<>();
+
+    /**
+     * Singleton instance of the registry.
+     */
+    public static final CompressorRegistry instance = new CompressorRegistry();
+
+    private CompressorRegistry()
+    {
+    }
+
+    @VisibleForTesting
+    public void reset()
+    {
+        compressionProviders.clear();
+    }
+
+    /**
+     * Enum representing in-built supported compressor types and their 
abbreviations.
+     * </p>
+     * The compressor class is stored as a FQN string rather than a {@code 
Class<?>} literal so that
+     * iterating the enum (e.g. during {@link #registerProviders}) does not 
force classloading of
+     * every built-in compressor — important because {@link SnappyCompressor} 
and the {@code Zstd*}
+     * compressors trigger native-library loads in their static initializers, 
which we want to defer
+     * until the compressor is actually used.
+     */
+    public enum CompressorType
+    {
+        DEFLATE("org.apache.cassandra.io.compress.DeflateCompressor", 
"deflate"),
+        LZ4("org.apache.cassandra.io.compress.LZ4Compressor", "lz4"),
+        NOOP("org.apache.cassandra.io.compress.NoopCompressor", "noop"),
+        SNAPPY("org.apache.cassandra.io.compress.SnappyCompressor", "snappy"),
+        ZSTD("org.apache.cassandra.io.compress.ZstdCompressor", "zstd"),
+        
ZSTD_DICTIONARY("org.apache.cassandra.io.compress.ZstdDictionaryCompressor", 
"zstd_dictionary");
+
+        public final String compressorClassName;
+        public final String simpleName;
+        public final String abbreviation;
+
+        CompressorType(String compressorClassName, String abbreviation)
+        {
+            this.compressorClassName = compressorClassName;
+            this.simpleName = 
compressorClassName.substring(compressorClassName.lastIndexOf('.') + 1);
+            this.abbreviation = abbreviation;
+        }
+
+        /**
+         * Resolves the compressor {@link Class} on demand. First call forces 
classloading of the
+         * underlying compressor implementation (and any side effects of its 
static initializer).
+         */
+        public Class<?> compressorClass()
+        {
+            return FBUtilities.classForName(compressorClassName, "compressor");
+        }
+    }
+
+    /**
+     * Returns the compression provider for the given compressor class. If the 
class is not one of
+     * the built-in {@link CompressorType} entries (e.g. a 3rd-party / 
user-supplied compressor not
+     * configured in {@code compressor_providers}), returns {@link 
#DEFAULT_COMPRESSION_PROVIDER},
+     * which reflectively invokes the class's static {@code create(Map)} 
factory.
+     *
+     * @param compressorClass class of the compressor
+     * @return the compression provider instance, never null
+     */
+    public AbstractCompressionProvider getProvider(Class<?> compressorClass)
+    {
+        return compressionProviders.getOrDefault(compressorClass.getName(), 
DEFAULT_COMPRESSION_PROVIDER);
+    }
+
+    /**
+     * Returns a filtered view of the registered custom compression providers, 
with key as compressor class.
+     * <p>
+     * Note that this method triggers classloading for each compressor entry 
that passes the filter,
+     * as the internal FQN string keys are resolved to {@code Class<?>} via 
{@link FBUtilities#classForName}.
+     * Call this method only after the server has fully initialised (e.g. from 
startup checks),
+     * not at tool or client init time.
+
+     * @return map of compressor {@link Class} to their registered providers 
for entries matching
+     * the filter; never {@code null}
+     */
+    public Map<Class<?>, AbstractCompressionProvider> getCustomProviders()
+    {
+        return compressionProviders.entrySet()
+                                   .stream()
+                                   .filter(e -> e.getValue() != 
CompressorRegistry.DEFAULT_COMPRESSION_PROVIDER)
+                                   .collect(Collectors.toMap(e -> 
FBUtilities.classForName(e.getKey(), "compressor"),
+                                                             
Map.Entry::getValue));
+    }
+
+    /**
+     * Creates a compressor. Firstly, we get a provider for this compressor 
class, or we use default compressor provider
+     * if there is not such a mapping. Then we try to use that provider to 
instantiate a compressor with given parameters.
+     * When the construction of a compressor fails, if a provider is 
configured is not configured to fallback,
+     * an exception is thrown, otherwise a compressor by a default compressor 
provider is created.
+     *
+     * @param compressorClass    compressor class to create a compressor of
+     * @param compressionOptions compressor options
+     * @return an instance of a given compressor class
+     */
+    public ICompressor getCompressor(Class<?> compressorClass, Map<String, 
String> compressionOptions)
+    {
+        AbstractCompressionProvider provider = getProvider(compressorClass);
+        ICompressor compressor;
+        try
+        {
+            compressor = provider.createCompressor(compressorClass, 
compressionOptions);
+        }
+        catch (Throwable t)
+        {
+            if (provider == DEFAULT_COMPRESSION_PROVIDER)
+                throw t;
+
+            logger.warn("Failed to create compressor {}. Will attempt fallback 
to default if enabled. Message: {}",
+                        compressorClass.getName(),
+                        t.getMessage());
+
+            if (provider.isFailOnMissingProvider())
+                throw t;
+
+            compressor = 
DEFAULT_COMPRESSION_PROVIDER.createCompressor(compressorClass, 
compressionOptions);
+        }
+
+        // Validate the masquerade target for both the provider and the 
fallback path: whatever is
+        // returned must serialize as exactly the compressor class that was 
requested, otherwise the
+        // schema / on-disk format would record a name that cannot be resolved 
on peers and restarts.
+        Class<? extends ICompressor> serializedAs = compressor.serializedAs();
+        if (serializedAs == null || serializedAs.getSimpleName().isEmpty())
+            throw new 
ConfigurationException(String.format("ICompressor.serializedAs() of a 
compressor created by provider %s returned %s. " +
+                                                           "It must return a 
non-anonymous built-in compressor class in package " +
+                                                           
"org.apache.cassandra.io.compress.",
+                                                           provider.getClass(),
+                                                           serializedAs));
+        if (serializedAs != compressorClass)
+            throw new ConfigurationException(String.format("The result of 
ICompressor.serializedAs(), %s, of a compressor object created " +
+                                                           "by a compressor 
provider %s does not match the compressor class to get a compressor for. " +
+                                                           "You need to 
override serializedAs() method of your custom compressor and return " +
+                                                           "base compressor 
class it is the substitute for.",
+                                                           serializedAs,
+                                                           
provider.getClass()));
+        return compressor;
+    }
+
+    /**
+     * Populates the registry with compression providers specified in the 
configuration.
+     * Should be called once during initialization to ensure providers are 
registered and available
+     * for use. If a provider fails to initialize and fallback is enabled, the 
default provider is used.
+     * <p>
+     * Each invocation constructs fresh provider instances and re-runs {@link 
AbstractCompressionProvider#init}
+     * without tearing down any previously registered instance, so a custom 
provider that acquires
+     * resources (native memory, threads, handles) in {@code init()} must 
tolerate being discarded if
+     * this is called more than once (as tests do via {@link #reset()}).
+     *
+     * @param providerOptions map of compressor names to their configuration
+     * @throws ConfigurationException if a provider fails to initialize and 
fallback is not enabled
+     */
+    public void registerProviders(Map<String, ParameterizedClass> 
providerOptions)
+    {
+        if (providerOptions == null)
+            return;
+
+        Set<String> validKeys = new HashSet<>();
+        for (CompressorType type : CompressorType.values())
+        {
+            validKeys.add(type.compressorClassName);
+            validKeys.add(type.simpleName);
+            validKeys.add(type.abbreviation);
+        }
+
+        for (String key : providerOptions.keySet())
+        {
+            if (!validKeys.contains(key))
+                throw new ConfigurationException("Unknown compressor key '" + 
key + "' in compressor_providers. " +
+                                                 "Expected a built-in 
compressor's fully qualified class name, simple class name, " +
+                                                 "or abbreviation: " + 
validKeys);
+        }
+
+        for (CompressorType type : CompressorType.values())
+        {
+            ParameterizedClass providerConfig = 
findProviderConfig(providerOptions, type);
+            if (providerConfig == null)
+            {
+                compressionProviders.put(type.compressorClassName, 
DEFAULT_COMPRESSION_PROVIDER);
+            }
+            else
+            {
+                AbstractCompressionProvider provider = 
resolveProvider(providerConfig);
+                compressionProviders.put(type.compressorClassName, provider);
+                logger.info("Adding '{}' provider for '{}'", 
provider.getClass().getName(), type.compressorClassName);
+            }
+        }
+    }
+
+    private ParameterizedClass findProviderConfig(Map<String, 
ParameterizedClass> providerOptions, CompressorType type)
+    {
+        ParameterizedClass parameterizedClass = 
providerOptions.get(type.compressorClassName);
+
+        if (parameterizedClass == null)
+            parameterizedClass = providerOptions.get(type.simpleName);
+
+        if (parameterizedClass == null)
+            parameterizedClass = providerOptions.get(type.abbreviation);
+
+        return parameterizedClass;
+    }
+
+    /**
+     * Returns a compression provider with configuration specified in the 
config file.
+     * If the provider fails to initialize or is not healthy, will attempt to 
fall back to the default provider
+     * if enabled in the configuration.
+     *
+     * @param providerConfig the configuration for the provider
+     * @return the compression provider instance
+     * @throws ConfigurationException if both the specified and fallback 
providers fail to initialize
+     */
+    AbstractCompressionProvider resolveProvider(ParameterizedClass 
providerConfig)
+    {
+        if (providerConfig.class_name == null)
+            throw new ConfigurationException("compressor_providers entry is 
missing required 'class_name': " + providerConfig);
+
+        Map<String, String> p = providerConfig.parameters == null ? 
Collections.emptyMap() : providerConfig.parameters;
+
+        try
+        {
+            AbstractCompressionProvider compressionProvider = 
FBUtilities.newCompressionProvider(providerConfig.class_name);
+            compressionProvider.init(new HashMap<>(p));
+
+            if (compressionProvider.isHealthy())
+            {
+                return compressionProvider;
+            }
+            else
+            {
+                logger.warn("Compression provider {} is not healthy, 
attempting fallback.", providerConfig.class_name);
+            }
+        }
+        catch (Throwable e)
+        {
+            logger.warn("Failed to initialize specified compression provider 
{}. Will attempt fallback to default if enabled.",
+                        providerConfig.class_name,
+                        e);
+        }
+
+        boolean failOnMissingProvider = 
Boolean.parseBoolean(p.getOrDefault(FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString()));
+
+        if (!failOnMissingProvider)
+            return DEFAULT_COMPRESSION_PROVIDER;
+
+        throw new ConfigurationException("Failed to initialize compression 
provider " + providerConfig);
+    }
+}
diff --git 
a/src/java/org/apache/cassandra/io/compress/DefaultCompressionProvider.java 
b/src/java/org/apache/cassandra/io/compress/DefaultCompressionProvider.java
new file mode 100644
index 0000000000..8ac09d4dee
--- /dev/null
+++ b/src/java/org/apache/cassandra/io/compress/DefaultCompressionProvider.java
@@ -0,0 +1,106 @@
+/*
+ * 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.io.compress;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+
+import static java.lang.String.format;
+
+/**
+ * Default implementation of {@link AbstractCompressionProvider} 
+ * 
+ * This provider acts as default implementation which creates compressor for 
built-in algorithms (LZ4, Snappy, Deflate, etc.)
+ * It relies on Cassandra's built-in compression implementations and does not 
require any external dependencies.
+ * It is used when:
+ *   No custom compression provider is configured
+ *   A custom compression provider fails and fallback is enabled
+ */
+public class DefaultCompressionProvider extends AbstractCompressionProvider
+{
+    /**
+     * Checks if this compression provider is healthy and ready to use.
+     * The default provider is always considered healthy as it has no external
+     * dependencies and relies on Cassandra's built-in compression mechanisms.
+     *
+     * @return Always returns {@code true}
+     */
+    @Override
+    public boolean isHealthy()
+    {
+        return true;
+    }
+
+    /**
+     * Creates a new compressor instance with the given compression parameters.
+     *
+     * @param compressionOptions Configuration options for the compressor
+     * @return A new ICompressor instance
+     * @throws ConfigurationException if the compressor class does not have a 
valid create method,
+     * if there are unknown compression options, or if the compressor creation 
fails
+     */
+    @Override
+    public ICompressor createCompressor(Class<?> compressorClass, Map<String, 
String> compressionOptions) throws IllegalStateException
+    {
+        try
+        {
+            Method method = compressorClass.getMethod("create", Map.class);
+            ICompressor compressor = (ICompressor)method.invoke(null, 
compressionOptions);
+            // Check for unknown options
+            for (String provided : compressionOptions.keySet())
+                if (!compressor.supportedOptions().contains(provided))
+                    throw new ConfigurationException("Unknown compression 
options " + provided);
+            return compressor;
+        }
+        catch (NoSuchMethodException e)
+        {
+            throw new ConfigurationException("create method not found", e);
+        }
+        catch (SecurityException e)
+        {
+            throw new ConfigurationException("Access forbidden", e);
+        }
+        catch (IllegalAccessException e)
+        {
+            throw new ConfigurationException("Cannot access method create in " 
+ compressorClass.getName(), e);
+        }
+        catch (InvocationTargetException e)
+        {
+            if (e.getTargetException() instanceof ConfigurationException)
+                throw (ConfigurationException) e.getTargetException();
+
+            Throwable cause = e.getCause() == null
+                            ? e
+                            : e.getCause();
+
+            throw new ConfigurationException(format("%s.create() threw an 
error: %s %s",
+                                                    
compressorClass.getSimpleName(),
+                                                    cause.getClass().getName(),
+                                                    cause.getMessage()),
+                                             e);
+        }
+        catch (ExceptionInInitializerError e)
+        {
+            throw new ConfigurationException("Cannot initialize class " + 
compressorClass.getName());
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/io/compress/ICompressor.java 
b/src/java/org/apache/cassandra/io/compress/ICompressor.java
index 950ae03e3d..88b1a06eb0 100644
--- a/src/java/org/apache/cassandra/io/compress/ICompressor.java
+++ b/src/java/org/apache/cassandra/io/compress/ICompressor.java
@@ -88,4 +88,19 @@ public interface ICompressor
     {
         return ImmutableSet.copyOf(EnumSet.allOf(Uses.class));
     }
+
+    /**
+     * Compressor class recorded in the table schema and SSTable compression 
metadata for
+     * compressors built by this instance. Plugin wrappers should override 
this to return the
+     * built-in they substitute for, so schema and on-disk format stay 
portable to peers and
+     * restarts without the plugin.
+     * <p>
+     * Contract: the returned class must (a) live in {@code 
org.apache.cassandra.io.compress}.
+     * Serializers write only its simple name; deserialization prepends that 
package.
+     * (b) have a non-empty simple name (no anonymous/synthetic classes).
+     */
+    default Class<? extends ICompressor> serializedAs()
+    {
+        return getClass();
+    }
 }
diff --git a/src/java/org/apache/cassandra/schema/CompressionParams.java 
b/src/java/org/apache/cassandra/schema/CompressionParams.java
index 0992f118ff..905e6aa039 100644
--- a/src/java/org/apache/cassandra/schema/CompressionParams.java
+++ b/src/java/org/apache/cassandra/schema/CompressionParams.java
@@ -18,8 +18,6 @@
 package org.apache.cassandra.schema;
 
 import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -36,6 +34,7 @@ import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.db.compression.CompressionDictionary;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.compress.CompressorRegistry;
 import org.apache.cassandra.io.compress.DeflateCompressor;
 import org.apache.cassandra.io.compress.ICompressor;
 import org.apache.cassandra.io.compress.IDictionaryCompressor;
@@ -63,6 +62,7 @@ public final class CompressionParams
     public static final String ENABLED = "enabled";
     public static final String MIN_COMPRESS_RATIO = "min_compress_ratio";
 
+    private static final CompressorRegistry registry = 
CompressorRegistry.instance;
     public static final CompressionParams DEFAULT = 
!CassandraRelevantProperties.DETERMINISM_SSTABLE_COMPRESSION_DEFAULT.getBoolean()
                                                     ? noCompression()
                                                     : new 
CompressionParams(LZ4Compressor.create(Collections.emptyMap()),
@@ -321,47 +321,7 @@ public final class CompressionParams
             return null;
         }
 
-        try
-        {
-            Method method = compressorClass.getMethod("create", Map.class);
-            ICompressor compressor = (ICompressor)method.invoke(null, 
compressionOptions);
-            // Check for unknown options
-            for (String provided : compressionOptions.keySet())
-                if (!compressor.supportedOptions().contains(provided))
-                    throw new ConfigurationException("Unknown compression 
options " + provided);
-            return compressor;
-        }
-        catch (NoSuchMethodException e)
-        {
-            throw new ConfigurationException("create method not found", e);
-        }
-        catch (SecurityException e)
-        {
-            throw new ConfigurationException("Access forbiden", e);
-        }
-        catch (IllegalAccessException e)
-        {
-            throw new ConfigurationException("Cannot access method create in " 
+ compressorClass.getName(), e);
-        }
-        catch (InvocationTargetException e)
-        {
-            if (e.getTargetException() instanceof ConfigurationException)
-                throw (ConfigurationException) e.getTargetException();
-
-            Throwable cause = e.getCause() == null
-                            ? e
-                            : e.getCause();
-
-            throw new ConfigurationException(format("%s.create() threw an 
error: %s %s",
-                                                    
compressorClass.getSimpleName(),
-                                                    cause.getClass().getName(),
-                                                    cause.getMessage()),
-                                             e);
-        }
-        catch (ExceptionInInitializerError e)
-        {
-            throw new ConfigurationException("Cannot initialize class " + 
compressorClass.getName());
-        }
+        return registry.getCompressor(compressorClass, compressionOptions);
     }
 
     public static ICompressor createCompressor(ParameterizedClass compression) 
throws ConfigurationException
@@ -509,7 +469,8 @@ public final class CompressionParams
             return Collections.singletonMap(ENABLED, "false");
 
         Map<String, String> options = new HashMap<>(otherOptions);
-        options.put(CLASS, sstableCompressor.getClass().getName());
+        // Use the one saved in the registry, we don't want to save the name 
of the service provider compressor here!
+        options.put(CLASS, sstableCompressor.serializedAs().getName());
         options.put(CHUNK_LENGTH_IN_KB, chunkLengthInKB());
         if (minCompressRatio != DEFAULT_MIN_COMPRESS_RATIO)
             options.put(MIN_COMPRESS_RATIO, String.valueOf(minCompressRatio));
@@ -555,7 +516,7 @@ public final class CompressionParams
         public void serialize(CompressionParams parameters, DataOutputPlus 
out, int version) throws IOException
         {
             assert version >= MessagingService.VERSION_40;
-            
out.writeUTF(parameters.sstableCompressor.getClass().getSimpleName());
+            
out.writeUTF(parameters.sstableCompressor.serializedAs().getSimpleName());
             out.writeInt(parameters.otherOptions.size());
             for (Map.Entry<String, String> entry : 
parameters.otherOptions.entrySet())
             {
@@ -596,7 +557,7 @@ public final class CompressionParams
         public long serializedSize(CompressionParams parameters, int version)
         {
             assert version >= MessagingService.VERSION_40;
-            long size = 
TypeSizes.sizeof(parameters.sstableCompressor.getClass().getSimpleName());
+            long size = 
TypeSizes.sizeof(parameters.sstableCompressor.serializedAs().getSimpleName());
             size += TypeSizes.sizeof(parameters.otherOptions.size());
             for (Map.Entry<String, String> entry : 
parameters.otherOptions.entrySet())
             {
diff --git a/src/java/org/apache/cassandra/service/StartupChecks.java 
b/src/java/org/apache/cassandra/service/StartupChecks.java
index 86ab41688e..97a18da636 100644
--- a/src/java/org/apache/cassandra/service/StartupChecks.java
+++ b/src/java/org/apache/cassandra/service/StartupChecks.java
@@ -21,6 +21,7 @@ import java.io.BufferedReader;
 import java.io.IOException;
 import java.lang.management.ManagementFactory;
 import java.lang.management.RuntimeMXBean;
+import java.nio.ByteBuffer;
 import java.nio.file.FileStore;
 import java.nio.file.FileVisitResult;
 import java.nio.file.FileVisitor;
@@ -30,12 +31,14 @@ import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Random;
 import java.util.ServiceConfigurationError;
 import java.util.ServiceLoader;
 import java.util.Set;
@@ -67,6 +70,9 @@ import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.db.SystemKeyspace;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.StartupException;
+import org.apache.cassandra.io.compress.AbstractCompressionProvider;
+import org.apache.cassandra.io.compress.CompressorRegistry;
+import org.apache.cassandra.io.compress.ICompressor;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.UUIDBasedSSTableId;
 import org.apache.cassandra.io.util.File;
@@ -131,6 +137,7 @@ public class StartupChecks
                                                                       
checkSystemKeyspaceState,
                                                                       
checkLegacyAuthTables,
                                                                       
checkKernelParamsForAsyncProfiler,
+                                                                      
checkCustomCompressionProviders,
                                                                       new 
DataResurrectionCheck());
 
     public List<StartupCheck> getChecks()
@@ -346,6 +353,157 @@ public class StartupChecks
         }
     };
 
+    public static final StartupCheck checkCustomCompressionProviders = new 
StartupCheck()
+    {
+        @Override
+        public String name()
+        {
+            return "custom_compression_providers";
+        }
+
+        @Override
+        public void execute(StartupChecksConfiguration configuration) throws 
StartupException
+        {
+            if (configuration.isDisabled(name()))
+                return;
+
+            // Resolving the custom providers forces classloading (and 
native-lib init) of each
+            // configured compressor; a missing class or failed native init is 
a configuration error.
+            Map<Class<?>, AbstractCompressionProvider> providers = 
getCustomProviders();
+
+            if (providers.isEmpty())
+                return;
+
+            long seed = (new Random()).nextLong();
+            Random random = new Random(seed);
+            byte[] payload = smokeTestPayload(random);
+
+            logger.info("Running compression smoke test for {} custom 
provider(s) with seed {}. " +
+                        "To reproduce a failure, regenerate the 4 KiB payload 
with new java.util.Random({}).",
+                        providers.size(), seed, seed);
+
+            List<String> failedProviders = new ArrayList<>();
+            for (Map.Entry<Class<?>, AbstractCompressionProvider> entry : 
providers.entrySet())
+            {
+                Class<?> compressorClass = entry.getKey();
+                AbstractCompressionProvider provider = entry.getValue();
+
+                ICompressor custom;
+
+                try
+                {
+                    custom = provider.createCompressor(compressorClass, 
Collections.emptyMap());
+                }
+                catch (Throwable t)
+                {
+                    throw new 
StartupException(StartupException.ERR_WRONG_CONFIG,
+                                               String.format("Unable to 
instantiate a compressor for class %s " +
+                                                             "for the purposes 
of a startup check from provider %s.",
+                                                             
compressorClass.getName(),
+                                                             
provider.getClass().getName()));
+                }
+
+                if (custom.serializedAs() != compressorClass)
+                {
+                    throw new 
StartupException(StartupException.ERR_WRONG_CONFIG,
+                                               String.format("Provider %s 
returned a compressor whose serializedAs() is %s, " +
+                                                             "but it must be 
%s (the built-in it substitutes for).",
+                                                             
provider.getClass().getName(),
+                                                             
custom.serializedAs(),
+                                                             
compressorClass.getName()));
+                }
+
+                ICompressor builtin = 
CompressorRegistry.DEFAULT_COMPRESSION_PROVIDER.createCompressor(compressorClass,
 Collections.emptyMap());
+
+                try
+                {
+                    // Round trip both ways so the custom and built-in 
compressors are proven to share
+                    // an on-disk-compatible format (peers/restarts without 
the plugin read the data).
+                    assertCompatibleRoundTrip(custom, builtin, payload);
+                    assertCompatibleRoundTrip(builtin, custom, payload);
+
+                    logger.info("Compression smoke test passed for custom 
provider {} ({}).",
+                                provider.getClass().getName(), 
compressorClass.getSimpleName());
+                }
+                catch (Throwable t)
+                {
+                    logger.error("Compression smoke test failed for custom 
provider {} ({}); reproduce with seed {}.",
+                                 provider.getClass().getName(), 
compressorClass.getSimpleName(), seed, t);
+                    failedProviders.add(provider.getClass().getName() + " -> " 
+ compressorClass.getSimpleName());
+                }
+            }
+
+            if (!failedProviders.isEmpty())
+            {
+                throw new 
StartupException(StartupException.ERR_WRONG_MACHINE_STATE,
+                                           String.format("The following custom 
compression providers failed smoke test: %s. " +
+                                                         "Providers substitute 
for a built-in compressor, so non byte-compatible output " +
+                                                         "is silent data 
corruption when read without the provider. Reproduce with the seed %s.",
+                                                         Joiner.on(", 
").join(failedProviders),
+                                                         seed));
+            }
+        }
+
+        private Map<Class<?>, AbstractCompressionProvider> 
getCustomProviders() throws StartupException
+        {
+            Map<Class<?>, AbstractCompressionProvider> providers;
+            try
+            {
+                providers = CompressorRegistry.instance.getCustomProviders();
+            }
+            catch (Throwable t)
+            {
+                throw new StartupException(StartupException.ERR_WRONG_CONFIG,
+                                           "Failed to load configured custom 
compression providers; " +
+                                           "check compressor_providers in 
cassandra.yaml", t);
+            }
+            return providers;
+        }
+        // Compresses payload with `compressor`, decompresses with 
`decompressor`, and verifies the
+        // result matches - proving the two share an on-disk-compatible 
format. Throws on mismatch.
+        private void assertCompatibleRoundTrip(ICompressor compressor, 
ICompressor decompressor, byte[] payload) throws IOException
+        {
+            ByteBuffer input = 
compressor.preferredBufferType().allocate(payload.length);
+            int compressedLength = 
compressor.initialCompressedBufferLength(payload.length);
+            ByteBuffer compressed = 
compressor.preferredBufferType().allocate(compressedLength);
+            input.put(payload);
+            input.flip();
+            compressor.compress(input, compressed);
+            compressed.flip();
+
+            // Within a compress/uncompress call the in/out buffers must share 
a type, but the compressor
+            // and decompressor may prefer different ones; only re-stage the 
compressed bytes in that case.
+            if (compressor.preferredBufferType() != 
decompressor.preferredBufferType())
+            {
+                ByteBuffer staged = 
decompressor.preferredBufferType().allocate(compressed.remaining());
+                staged.put(compressed);
+                staged.flip();
+                compressed = staged;
+            }
+
+            ByteBuffer output = 
decompressor.preferredBufferType().allocate(payload.length);
+            decompressor.uncompress(compressed, output);
+            output.flip();
+
+            if (!output.equals(ByteBuffer.wrap(payload)))
+            {
+                throw new IOException(String.format("Round-trip mismatch: 
compressed with %s, decompressed with %s",
+                                                    
compressor.getClass().getName(), decompressor.getClass().getName()));
+            }
+        }
+        private byte[] smokeTestPayload(Random random)
+        {
+            // 4 KiB payload: the first half zeros (highly compressible, 
exercises the real compression
+            // path), the second half high-entropy bytes (incompressible, 
exercises the compressor's
+            // worst-case output sizing via initialCompressedBufferLength).
+            byte[] testPayload = new byte[4 * 1024];
+            byte[] randomHalf = new byte[testPayload.length / 2];
+            random.nextBytes(randomHalf);
+            System.arraycopy(randomHalf, 0, testPayload, testPayload.length / 
2, randomHalf.length);
+            return testPayload;
+        }
+    };
+
     public static final StartupCheck checkValidLaunchDate = new StartupCheck()
     {
         @Override
diff --git a/src/java/org/apache/cassandra/utils/FBUtilities.java 
b/src/java/org/apache/cassandra/utils/FBUtilities.java
index 924dcd6118..a0ec48390c 100644
--- a/src/java/org/apache/cassandra/utils/FBUtilities.java
+++ b/src/java/org/apache/cassandra/utils/FBUtilities.java
@@ -86,6 +86,7 @@ import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.compress.AbstractCompressionProvider;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.format.StatsComponent;
 import org.apache.cassandra.io.sstable.metadata.MetadataType;
@@ -733,6 +734,26 @@ public class FBUtilities
         }
     }
 
+    public static AbstractCompressionProvider newCompressionProvider(String 
className) throws ConfigurationException
+    {
+        try
+        {
+            if (!className.contains("."))
+                className = "org.apache.cassandra.io.compress." + className;
+
+            Class<?> compressionProviderClass = 
FBUtilities.classForName(className, "compression service provider");
+            return (AbstractCompressionProvider) 
compressionProviderClass.getConstructor().newInstance();
+        }
+        catch (ConfigurationException e)
+        {
+            throw e;
+        }
+        catch (Exception e)
+        {
+            throw new ConfigurationException(String.format("Unable to create 
an instance of the compression service provider for %s", className), e);
+        }
+    }
+
     /**
      * @return The Class for the given name.
      * @param classname Fully qualified classname.
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/CompressorProviderTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/CompressorProviderTest.java
new file mode 100644
index 0000000000..cb755a834c
--- /dev/null
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/CompressorProviderTest.java
@@ -0,0 +1,552 @@
+/*
+ * 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.distributed.test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import 
org.apache.cassandra.distributed.api.IIsolatedExecutor.SerializableCallable;
+import org.apache.cassandra.io.compress.AbstractCompressionProvider;
+import org.apache.cassandra.io.compress.CompressionMetadata;
+import org.apache.cassandra.io.compress.CompressorRegistry;
+import org.apache.cassandra.io.compress.DefaultCompressionProvider;
+import org.apache.cassandra.io.compress.ICompressor;
+import org.apache.cassandra.io.compress.LZ4Compressor;
+import org.apache.cassandra.io.compress.SnappyCompressor;
+import org.apache.cassandra.io.sstable.format.CompressionInfoComponent;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Distributed tests for pluggable compression providers configured via
+ * the {@code compressor_providers} option in cassandra.yaml.
+ * <p>
+ * Covers (CASSANDRA-20975):
+ * - default providers are registered when no plugin is configured;
+ * - a custom healthy provider is wired into the registry and used by table 
compression;
+ * - an unhealthy provider falls back to the default when fallback is enabled;
+ * - an unhealthy provider fails node startup when fallback is disabled.
+ */
+public class CompressorProviderTest extends TestBaseImpl
+{
+    @Test
+    public void testDefaultProvidersRegisteredWhenNoConfig() throws Throwable
+    {
+        try (Cluster cluster = init(Cluster.build(1).start()))
+        {
+            cluster.get(1).runOnInstance(() -> {
+                for (CompressorRegistry.CompressorType type : 
CompressorRegistry.CompressorType.values())
+                {
+                    AbstractCompressionProvider provider = 
CompressorRegistry.instance.getProvider(type.compressorClass());
+                    assertNotNull("Provider missing for " + type, provider);
+                    assertSame("Built-in compressor " + type + " must use the 
default provider when none is configured",
+                               DefaultCompressionProvider.class, 
provider.getClass());
+                }
+            });
+        }
+    }
+
+    @Test
+    public void testCustomProviderRegisteredAndUsedForTableCompression() 
throws Throwable
+    {
+        Map<String, Object> snappyProviderOptions = new HashMap<>();
+        snappyProviderOptions.put("class_name", 
HealthyTestProvider.class.getName());
+        snappyProviderOptions.put("parameters", 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString()));
+
+        Map<String, Object> providers = new HashMap<>();
+        providers.put(SnappyCompressor.class.getSimpleName(), 
snappyProviderOptions);
+
+        try (Cluster cluster = init(Cluster.build(1)
+                                           .withConfig(config -> 
config.set("compressor_providers", providers))
+                                           .start()))
+        {
+            // The registry must hold our custom provider for SnappyCompressor 
and the default
+            // for every other built-in type.
+            cluster.get(1).runOnInstance(() -> {
+                AbstractCompressionProvider snappy = 
CompressorRegistry.instance.getProvider(SnappyCompressor.class);
+                assertNotNull(snappy);
+                assertEquals(HealthyTestProvider.class.getName(), 
snappy.getClass().getName());
+
+                for (CompressorRegistry.CompressorType type : 
CompressorRegistry.CompressorType.values())
+                {
+                    if (type.compressorClass() == SnappyCompressor.class)
+                        continue;
+                    AbstractCompressionProvider provider = 
CompressorRegistry.instance.getProvider(type.compressorClass());
+                    assertSame(DefaultCompressionProvider.class, 
provider.getClass());
+                }
+            });
+
+            // Reset the counter so the assertion below only sees calls 
produced by this test's DDL/writes.
+            cluster.get(1).runOnInstance(() -> 
HealthyTestProvider.CREATE_CALLS.set(0));
+
+            // End-to-end: create a table that uses Snappy compression. The 
compressor must be sourced
+            // through HealthyTestProvider; writes and reads must succeed 
because the provider
+            // delegates compressor creation to the default provider.
+            cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".plugin_snappy 
(k int PRIMARY KEY, v text) " +
+                                 "WITH compression = {'class': 
'SnappyCompressor', 'chunk_length_in_kb': '4'}");
+
+            for (int i = 0; i < 100; i++)
+                cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + 
".plugin_snappy (k, v) VALUES (?, ?)",
+                                               ConsistencyLevel.ONE, i, 
"value-" + i);
+
+            cluster.get(1).flush(KEYSPACE);
+
+            Object[][] rows = cluster.coordinator(1).execute("SELECT count(*) 
FROM " + KEYSPACE + ".plugin_snappy",
+                                                             
ConsistencyLevel.ONE);
+            assertEquals(100L, rows[0][0]);
+
+            int creates = 
cluster.get(1).callOnInstance((SerializableCallable<Integer>) () -> 
HealthyTestProvider.CREATE_CALLS.get());
+            assertTrue("HealthyTestProvider.createCompressor should have been 
invoked for table compression, was " + creates,
+                       creates > 0);
+
+            int createsSnappy = 
cluster.get(1).callOnInstance((SerializableCallable<Integer>) () -> 
HealthyTestProvider.CREATE_SNAPPY_CALLS.get());
+            assertTrue("HealthyTestProvider.createCompressor should have been 
invoked for table compression, was " + createsSnappy,
+                       createsSnappy > 0);
+
+            // The schema must record the *concrete* compressor class, not the 
provider — this is what
+            // peers and future restarts use to rebuild the compression chain.
+            Object[][] schemaRows = cluster.coordinator(1).execute(
+            "SELECT compression FROM system_schema.tables WHERE keyspace_name 
= ? AND table_name = ?",
+            ConsistencyLevel.ONE, KEYSPACE, "plugin_snappy");
+            @SuppressWarnings("unchecked")
+            Map<String, String> compressionOptions = (Map<String, String>) 
schemaRows[0][0];
+            assertEquals("system_schema.tables.compression must record the 
concrete compressor class, not the provider",
+                         SnappyCompressor.class.getName(), 
compressionOptions.get("class"));
+
+            // Reconstruct CompressionMetadata from the flushed SSTable on 
disk and check the
+            // rebuilt compressor's serializedAs() — this is the class name 
that was actually
+            // written into *-CompressionInfo.db, and the one a peer without 
the plugin would
+            // read. getClass() on the rebuilt instance would just yield 
MySnappyCompressor again
+            // because the plugin replays its substitution during 
CompressionMetadata.open.
+            String onDiskSerializedAs = 
cluster.get(1).callOnInstance((SerializableCallable<String>) () -> {
+                ColumnFamilyStore cfs = 
Keyspace.open(KEYSPACE).getColumnFamilyStore("plugin_snappy");
+                SSTableReader sstable = 
cfs.getLiveSSTables().iterator().next();
+                try (CompressionMetadata metadata = 
CompressionInfoComponent.loadIfExists(sstable.descriptor, null))
+                {
+                    Assert.assertNotNull(metadata);
+                    return 
metadata.parameters.getSstableCompressor().serializedAs().getName();
+                }
+            });
+            assertEquals("Rebuilt CompressionMetadata.serializedAs() must 
yield the underlying compressor - " +
+                         "what the plugin advertises for portability - not the 
wrapper",
+                         SnappyCompressor.class.getName(), onDiskSerializedAs);
+        }
+    }
+
+    @Test
+    public void testCompressWithCustomProviderAndDecompressWithDefault() 
throws Throwable
+    {
+        //This test will simulate a scenario where data is compressed with a 
custom provider and then read on a node which does not have the provider 
configured,
+        // In the initial phase the test does write/read checks like 
testCustomProviderRegisteredAndUsedForTableCompression then it brings down the 
cluster,
+        // restarts with DefaultCompressionProvider for SnappyCompressor, and 
checks that the data can still be read.
+        Map<String, Object> snappyProviderOptions = new HashMap<>();
+        snappyProviderOptions.put("class_name", 
HealthyTestProvider.class.getName());
+        snappyProviderOptions.put("parameters", 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString()));
+
+        Map<String, Object> providers = new HashMap<>();
+        providers.put(SnappyCompressor.class.getSimpleName(), 
snappyProviderOptions);
+
+        try (Cluster cluster = init(Cluster.build(1)
+                                           .withConfig(config -> 
config.set("compressor_providers", providers))
+                                           .start()))
+        {
+            // Mae sure registry holds the custom provider for SnappyCompressor
+            cluster.get(1).runOnInstance(() -> {
+                AbstractCompressionProvider snappy = 
CompressorRegistry.instance.getProvider(SnappyCompressor.class);
+                assertNotNull(snappy);
+                assertEquals(HealthyTestProvider.class.getName(), 
snappy.getClass().getName());
+            });
+
+            // Reset the counter so the assertion below only sees calls 
produced by this test's DDL/writes.
+            cluster.get(1).runOnInstance(() -> 
HealthyTestProvider.CREATE_CALLS.set(0));
+
+            // End-to-end: create a table that uses Snappy compression
+            cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".plugin_snappy 
(k int PRIMARY KEY, v text) " +
+                                 "WITH compression = {'class': 
'SnappyCompressor', 'chunk_length_in_kb': '4'}");
+
+            for (int i = 0; i < 100; i++)
+                cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + 
".plugin_snappy (k, v) VALUES (?, ?)",
+                                               ConsistencyLevel.ONE, i, 
"value-" + i);
+
+            cluster.get(1).flush(KEYSPACE);
+
+            Object[][] rows = cluster.coordinator(1).execute("SELECT count(*) 
FROM " + KEYSPACE + ".plugin_snappy",
+                                                             
ConsistencyLevel.ONE);
+            assertEquals(100L, rows[0][0]);
+
+            int creates = 
cluster.get(1).callOnInstance((SerializableCallable<Integer>) () -> 
HealthyTestProvider.CREATE_CALLS.get());
+            assertTrue("HealthyTestProvider.createCompressor should have been 
invoked for table compression, was " + creates,
+                       creates > 0);
+
+            int createsSnappy = 
cluster.get(1).callOnInstance((SerializableCallable<Integer>) () -> 
HealthyTestProvider.CREATE_SNAPPY_CALLS.get());
+            assertTrue("HealthyTestProvider.createCompressor should have been 
invoked for table compression, was " + createsSnappy,
+                       createsSnappy > 0);
+
+            // Ensure schema records the *concrete* compressor class, not the 
provider
+            Object[][] schemaRows = cluster.coordinator(1).execute(
+            "SELECT compression FROM system_schema.tables WHERE keyspace_name 
= ? AND table_name = ?",
+            ConsistencyLevel.ONE, KEYSPACE, "plugin_snappy");
+            @SuppressWarnings("unchecked")
+            Map<String, String> compressionOptions = (Map<String, String>) 
schemaRows[0][0];
+            assertEquals("system_schema.tables.compression must record the 
concrete compressor class, not the provider",
+                         SnappyCompressor.class.getName(), 
compressionOptions.get("class"));
+
+            // Reconstruct CompressionMetadata from the flushed SSTable on 
disk and check the
+            // rebuilt compressor's serializedAs()
+            String onDiskSerializedAs = 
cluster.get(1).callOnInstance((SerializableCallable<String>) () -> {
+                ColumnFamilyStore cfs = 
Keyspace.open(KEYSPACE).getColumnFamilyStore("plugin_snappy");
+                SSTableReader sstable = 
cfs.getLiveSSTables().iterator().next();
+                try (CompressionMetadata metadata = 
CompressionInfoComponent.loadIfExists(sstable.descriptor, null))
+                {
+                    Assert.assertNotNull(metadata);
+                    return 
metadata.parameters.getSstableCompressor().serializedAs().getName();
+                }
+            });
+            assertEquals("Rebuilt CompressionMetadata.serializedAs() must 
yield the underlying compressor - " +
+                         "what the plugin advertises for portability - not the 
wrapper",
+                         SnappyCompressor.class.getName(), onDiskSerializedAs);
+
+            // Simulate a node which does not have plugin configured
+            // Shut down the cluster, change provider class name to default, 
and restart. It should be able to use the Default provider for SnappyCompressor,
+            // and be able to read the existing data.
+            cluster.get(1).shutdown().get();
+            snappyProviderOptions = new HashMap<>();
+            snappyProviderOptions.put("class_name", 
DefaultCompressionProvider.class.getName());
+            snappyProviderOptions.put("parameters", 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString()));
+            providers.clear();
+            providers.put(SnappyCompressor.class.getSimpleName(), 
snappyProviderOptions);
+            cluster.get(1).config().set("compressor_providers", providers);
+            cluster.get(1).startup();
+            cluster.get(1).runOnInstance(() -> {
+                AbstractCompressionProvider snappy = 
CompressorRegistry.instance.getProvider(SnappyCompressor.class);
+                assertNotNull(snappy);
+                assertEquals(DefaultCompressionProvider.class.getName(), 
snappy.getClass().getName());
+            });
+
+            //The node should be able to decompress the SSTables
+            // that were written by the plugin node, because serializedAs() 
ensured the
+            // on-disk CompressionInfo.db recorded SnappyCompressor (not 
MySnappyCompressor).
+            Object[][] rowsOnNewNode = cluster.coordinator(1).execute(
+            "SELECT count(*) FROM " + KEYSPACE + ".plugin_snappy", 
ConsistencyLevel.ONE);
+            assertEquals("The new node which does not have the plugin must 
read all rows written by the plugin node",
+                         100L, rowsOnNewNode[0][0]);
+
+        }
+    }
+
+    @Test
+    public void testCompressOn2NodesWithAndWithoutCustomProvider() throws 
Throwable
+    {
+        // This test will simulate a scenario with a cluster containing 2 
nodes, one with custom provider configured and another with default provider.
+        // A table is created with Snappy compression on the first node with a 
custom provider, writes data and flushes to disk. Data is then read from both 
nodes
+        // to verify that both can read the data regardless of the provider 
they have configured.
+        Map<String, Object> snappyProviderOptionsCustom = new HashMap<>();
+        snappyProviderOptionsCustom.put("class_name", 
HealthyTestProvider.class.getName());
+        snappyProviderOptionsCustom.put("parameters", 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString()));
+
+        Map<String, Object> snappyProviderOptionsDefault = new HashMap<>();
+        snappyProviderOptionsDefault.put("class_name", 
DefaultCompressionProvider.class.getName());
+        snappyProviderOptionsDefault.put("parameters", 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString()));
+
+        Map<String, Object> firstNodeProvider = new HashMap<>();
+        firstNodeProvider.put(SnappyCompressor.class.getSimpleName(), 
snappyProviderOptionsCustom);
+        Map<String, Object> secondNodeProvider = new HashMap<>();
+        secondNodeProvider.put(SnappyCompressor.class.getSimpleName(), 
snappyProviderOptionsDefault);
+
+        try (Cluster cluster = init(Cluster.build(2)
+                                           .withConfig(config -> {
+                                               if (config.num() == 1)
+                                                   
config.set("compressor_providers", firstNodeProvider);   // custom 
HealthyTestProvider
+                                               else
+                                                   
config.set("compressor_providers", secondNodeProvider);  // 
DefaultCompressionProvider
+                                               config.set("accord.enabled", 
false); // disable accord to avoid schema disagreement during startup due to 
different compressor providers
+                                           })
+                                           .start()))
+        {
+            // First node must use HealthyTestProvider for SnappyCompressor,
+            // Second node must use DefaultCompressionProvider for 
SnappyCompressor
+            cluster.get(1).runOnInstance(() -> {
+                AbstractCompressionProvider snappy = 
CompressorRegistry.instance.getProvider(SnappyCompressor.class);
+                assertNotNull(snappy);
+                assertEquals(HealthyTestProvider.class.getName(), 
snappy.getClass().getName());
+            });
+            cluster.get(2).runOnInstance(() -> {
+                AbstractCompressionProvider snappy = 
CompressorRegistry.instance.getProvider(SnappyCompressor.class);
+                assertNotNull(snappy);
+                assertEquals(DefaultCompressionProvider.class.getName(), 
snappy.getClass().getName());
+            });
+
+
+            // Reset the counter so the assertion below only sees calls 
produced by this test's DDL/writes.
+            cluster.get(1).runOnInstance(() -> 
HealthyTestProvider.CREATE_CALLS.set(0));
+
+            // End-to-end: create a table that uses Snappy compression.
+            cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".plugin_snappy 
(k int PRIMARY KEY, v text) " +
+                                 "WITH compression = {'class': 
'SnappyCompressor', 'chunk_length_in_kb': '4'}");
+
+            for (int i = 0; i < 100; i++)
+                cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + 
".plugin_snappy (k, v) VALUES (?, ?)",
+                                               ConsistencyLevel.ALL, i, 
"value-" + i);
+
+            cluster.get(1).flush(KEYSPACE);
+            cluster.get(2).flush(KEYSPACE);
+
+            // Read from node 1
+            Object[][] rowsFromNode1 = cluster.coordinator(1).execute("SELECT 
count(*) FROM " + KEYSPACE + ".plugin_snappy",
+                                                                      
ConsistencyLevel.ONE);
+            assertEquals(100L, rowsFromNode1[0][0]);
+
+            // Read from node 2
+            Object[][] rowsFromNode2 = cluster.coordinator(2).execute("SELECT 
count(*) FROM " + KEYSPACE + ".plugin_snappy",
+                                                                      
ConsistencyLevel.ONE);
+            assertEquals(100L, rowsFromNode2[0][0]);
+
+            int creates = 
cluster.get(1).callOnInstance((SerializableCallable<Integer>) () -> 
HealthyTestProvider.CREATE_CALLS.get());
+            assertTrue("HealthyTestProvider.createCompressor should have been 
invoked for table compression, was " + creates,
+                       creates > 0);
+
+            int createsSnappy = 
cluster.get(1).callOnInstance((SerializableCallable<Integer>) () -> 
HealthyTestProvider.CREATE_SNAPPY_CALLS.get());
+            assertTrue("HealthyTestProvider.createCompressor should have been 
invoked for table compression, was " + createsSnappy,
+                       createsSnappy > 0);
+
+            // The schema must record the *concrete* compressor class, not the 
provider — this is what
+            // peers and future restarts use to rebuild the compression chain.
+            Object[][] schemaRows = cluster.coordinator(1).execute(
+            "SELECT compression FROM system_schema.tables WHERE keyspace_name 
= ? AND table_name = ?",
+            ConsistencyLevel.ALL, KEYSPACE, "plugin_snappy");
+            @SuppressWarnings("unchecked")
+            Map<String, String> compressionOptions = (Map<String, String>) 
schemaRows[0][0];
+            assertEquals("system_schema.tables.compression must record the 
concrete compressor class, not the provider",
+                         SnappyCompressor.class.getName(), 
compressionOptions.get("class"));
+
+            // Reconstruct CompressionMetadata from the flushed SSTable on 
disk and check the
+            // rebuilt compressor's serializedAs()
+            String onDiskSerializedAsNode1 = 
cluster.get(1).callOnInstance((SerializableCallable<String>) () -> {
+                ColumnFamilyStore cfs = Keyspace.open(KEYSPACE)
+                                                
.getColumnFamilyStore("plugin_snappy");
+                SSTableReader sstable = 
cfs.getLiveSSTables().iterator().next();
+                try (CompressionMetadata metadata = 
CompressionInfoComponent.loadIfExists(sstable.descriptor, null))
+                {
+                    Assert.assertNotNull(metadata);
+                    return 
metadata.parameters.getSstableCompressor().serializedAs().getName();
+                }
+            });
+            assertEquals("Rebuilt CompressionMetadata.serializedAs() must 
yield the underlying compressor - " +
+                         "what the plugin advertises for portability - not the 
wrapper",
+                         SnappyCompressor.class.getName(), 
onDiskSerializedAsNode1);
+
+            // Node 2 (DefaultCompressionProvider): also verify the on-disk 
SSTable is readable and
+            // records SnappyCompressor — confirming it can decompress without 
the plugin.
+            String onDiskSerializedAsNode2 = 
cluster.get(2).callOnInstance((SerializableCallable<String>) () -> {
+                ColumnFamilyStore cfs = Keyspace.open(KEYSPACE)
+                                                
.getColumnFamilyStore("plugin_snappy");
+                SSTableReader sstable = 
cfs.getLiveSSTables().iterator().next();
+                try (CompressionMetadata metadata = 
CompressionInfoComponent.loadIfExists(sstable.descriptor, null))
+                {
+                    Assert.assertNotNull(metadata);
+                    return 
metadata.parameters.getSstableCompressor().serializedAs().getName();
+                }
+            });
+            assertEquals("Node 2 (DefaultCompressionProvider): on-disk 
serializedAs() must also yield the concrete compressor class",
+                         SnappyCompressor.class.getName(), 
onDiskSerializedAsNode2);
+        }
+
+    }
+
+    @Test
+    public void testUnhealthyProviderWithFallbackBootsWithDefault() throws 
Throwable
+    {
+        Map<String, Object> lz4ProviderOptions = new HashMap<>();
+        lz4ProviderOptions.put("class_name", 
UnhealthyTestProvider.class.getName());
+        lz4ProviderOptions.put("parameters", 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString()));
+
+        Map<String, Object> providers = new HashMap<>();
+        providers.put(LZ4Compressor.class.getSimpleName(), lz4ProviderOptions);
+
+        try (Cluster cluster = init(Cluster.build(1)
+                                           .withConfig(config -> 
config.set("compressor_providers", providers))
+                                           .start()))
+        {
+            cluster.get(1).runOnInstance(() -> {
+                AbstractCompressionProvider lz4Provider = 
CompressorRegistry.instance.getProvider(LZ4Compressor.class);
+                assertNotNull(lz4Provider);
+                assertSame("Unhealthy provider with fallback=true must resolve 
to the default",
+                           DefaultCompressionProvider.class,
+                           lz4Provider.getClass());
+            });
+
+            // Sanity check: table-level LZ4 compression still works because 
the fallback default is registered.
+            cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".fallback_lz4 
(k int PRIMARY KEY, v text) " +
+                                 "WITH compression = {'class': 
'LZ4Compressor'}");
+
+            cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + 
".fallback_lz4 (k, v) VALUES (1, 'ok')",
+                                           ConsistencyLevel.ONE);
+            cluster.get(1).flush(KEYSPACE);
+
+            Object[][] rows = cluster.coordinator(1).execute("SELECT v FROM " 
+ KEYSPACE + ".fallback_lz4 WHERE k = 1",
+                                                             
ConsistencyLevel.ONE);
+            assertEquals("ok", rows[0][0]);
+        }
+    }
+
+    @Test
+    public void testUnhealthyProviderWithoutFallbackFailsStartup() throws 
Throwable
+    {
+        Map<String, Object> lz4ProviderOptions = new HashMap<>();
+        lz4ProviderOptions.put("class_name", 
UnhealthyTestProvider.class.getName());
+        lz4ProviderOptions.put("parameters", 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString()));
+
+        Map<String, Object> providers = new HashMap<>();
+        providers.put(LZ4Compressor.class.getSimpleName(), lz4ProviderOptions);
+
+        try (Cluster cluster = Cluster.build(1)
+                                      .withConfig(config -> 
config.set("compressor_providers", providers))
+                                      .createWithoutStarting())
+        {
+            try
+            {
+                cluster.get(1).startup();
+                fail("Startup should have failed because the configured 
compressor provider is unhealthy and fallback is disabled");
+            }
+            catch (Throwable t)
+            {
+                Throwable cause = t;
+                String expected = "Failed to initialize compression provider";
+                while (cause != null && (cause.getMessage() == null || 
!cause.getMessage().contains(expected)))
+                    cause = cause.getCause();
+                assertNotNull("Expected ConfigurationException containing '" + 
expected + "' in the cause chain, got: " + t, cause);
+            }
+        }
+    }
+
+    @Test
+    public void testUnknownCompressorKeyFailsStartup() throws Throwable
+    {
+        // 'no_such_compressor' matches no built-in compressor (neither FQCN 
nor simple name nor
+        // abbreviation), so registerProviders must reject it at apply-time 
and the node must
+        // refuse to start with a ConfigurationException.
+        Map<String, Object> bogusProviderOptions = new HashMap<>();
+        bogusProviderOptions.put("class_name", 
HealthyTestProvider.class.getName());
+        bogusProviderOptions.put("parameters", 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString()));
+
+        Map<String, Object> providers = new HashMap<>();
+        providers.put("no_such_compressor", bogusProviderOptions);
+
+        try (Cluster cluster = Cluster.build(1)
+                                      .withConfig(config -> 
config.set("compressor_providers", providers))
+                                      .createWithoutStarting())
+        {
+            try
+            {
+                cluster.get(1).startup();
+                fail("Startup should have failed because compressor_providers 
contains a key that maps to no known compressor");
+            }
+            catch (Throwable t)
+            {
+                Throwable configException = t;
+                String expectedClass = 
"org.apache.cassandra.exceptions.ConfigurationException";
+                String expectedMessage = "Unknown compressor key 
'no_such_compressor'";
+                while (configException != null
+                       && 
(!expectedClass.equals(configException.getClass().getName())
+                           || configException.getMessage() == null
+                           || 
!configException.getMessage().contains(expectedMessage)))
+                    configException = configException.getCause();
+                assertNotNull("Expected ConfigurationException with message 
containing '" + expectedMessage
+                              + "' in the cause chain, got: " + t, 
configException);
+            }
+        }
+    }
+
+    /**
+     * A custom provider that reports healthy and delegates compressor 
creation to the
+     * built-in default provider. Used to prove the registry actually invokes 
the plugin
+     * during table compression rather than silently using the default.
+     * <p>
+     * Must be public with a no-arg constructor so {@code 
FBUtilities.newCompressionProvider}
+     * can instantiate it via reflection.
+     */
+    public static class HealthyTestProvider extends AbstractCompressionProvider
+    {
+        // Statics live in the InstanceClassLoader where the provider is 
loaded. Always read and
+        // mutate these via runOnInstance / callOnInstance — touching them 
directly from the test
+        // thread reads a separate copy in the test classloader and silently 
returns zero.
+        public static final AtomicInteger CREATE_CALLS = new AtomicInteger();
+        public static final AtomicInteger CREATE_SNAPPY_CALLS = new 
AtomicInteger();
+
+        @Override
+        public boolean isHealthy()
+        {
+            return true;
+        }
+
+        @Override
+        public ICompressor createCompressor(Class<?> compressorClass, 
Map<String, String> options)
+        {
+            CREATE_CALLS.incrementAndGet();
+
+            // inject here our custom compressor, mimmicking pluggable 
providers
+            if (compressorClass == SnappyCompressor.class)
+            {
+                CREATE_SNAPPY_CALLS.incrementAndGet();
+                return new MySnappyCompressor();
+            }
+
+            return 
CompressorRegistry.DEFAULT_COMPRESSION_PROVIDER.createCompressor(compressorClass,
 options);
+        }
+    }
+
+    public static class MySnappyCompressor extends SnappyCompressor
+    {
+        @Override
+        public Class<? extends ICompressor> serializedAs()
+        {
+            // Tell Cassandra to record SnappyCompressor in schema/SSTable 
metadata so that the
+            // schema is portable to peers and restarts without this plugin.
+            return SnappyCompressor.class;
+        }
+    }
+
+    public static class UnhealthyTestProvider extends 
AbstractCompressionProvider
+    {
+        @Override
+        public boolean isHealthy()
+        {
+            return false;
+        }
+
+        @Override
+        public ICompressor createCompressor(Class<?> compressorClass, 
Map<String, String> options)
+        {
+            throw new IllegalStateException("unhealthy provider must not be 
asked to create compressors");
+        }
+    }
+}
diff --git 
a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java 
b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
index 33b5c6f513..1704936544 100644
--- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
+++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
@@ -244,6 +244,10 @@ public class DatabaseDescriptorRefTest
     "org.apache.cassandra.io.FSError",
     "org.apache.cassandra.io.FSWriteError",
     "org.apache.cassandra.io.MessageVersionProvider",
+    "org.apache.cassandra.io.compress.AbstractCompressionProvider",
+    "org.apache.cassandra.io.compress.CompressorRegistry",
+    "org.apache.cassandra.io.compress.CompressorRegistry$CompressorType",
+    "org.apache.cassandra.io.compress.DefaultCompressionProvider",
     "org.apache.cassandra.io.compress.ICompressor",
     "org.apache.cassandra.io.compress.ICompressor$Uses",
     "org.apache.cassandra.io.compress.LZ4Compressor",
diff --git 
a/test/unit/org/apache/cassandra/io/compress/CompressorRegistryTest.java 
b/test/unit/org/apache/cassandra/io/compress/CompressorRegistryTest.java
new file mode 100644
index 0000000000..c043a0a3b6
--- /dev/null
+++ b/test/unit/org/apache/cassandra/io/compress/CompressorRegistryTest.java
@@ -0,0 +1,311 @@
+/*
+ * 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.io.compress;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import 
org.apache.cassandra.utils.CompressionProviderHelper.SerializedTestCompressor;
+import 
org.apache.cassandra.utils.CompressionProviderHelper.TestCompressionProvider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static 
org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
+
+public class CompressorRegistryTest
+{
+    private Config config;
+
+    @Before
+    public void setUp()
+    {
+        config = DatabaseDescriptor.loadConfig();
+    }
+
+    @After
+    public void tearDown()
+    {
+        CompressorRegistry.instance.reset();
+        config.compressor_providers.clear();
+        DatabaseDescriptor.setConfig(config);
+    }
+
+    @Test
+    public void testRegisterAndGetInbuiltCompressor()
+    {
+        DatabaseDescriptor.setConfig(config);
+        DatabaseDescriptor.applyCompressorProviders();
+
+        for (CompressorRegistry.CompressorType type : 
CompressorRegistry.CompressorType.values())
+        {
+            AbstractCompressionProvider provider = 
CompressorRegistry.instance.getProvider(type.compressorClass());
+            assertThat(provider).isNotNull();
+            
assertThat(provider).isEqualTo(CompressorRegistry.DEFAULT_COMPRESSION_PROVIDER);
+        }
+    }
+
+    @Test
+    public void testRegisterServiceProvider() throws Exception
+    {
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString());
+        config.compressor_providers.put(NoopCompressor.class.getSimpleName(), 
new ParameterizedClass(TestCompressionProvider.class.getName(), params));
+
+        DatabaseDescriptor.setConfig(config);
+        DatabaseDescriptor.applyCompressorProviders();
+
+        AbstractCompressionProvider provider = 
CompressorRegistry.instance.getProvider(NoopCompressor.class);
+        assertThat(provider).isNotNull();
+        assertThat(provider.isHealthy()).isTrue();
+        
assertThat(provider.getClass()).isEqualTo(TestCompressionProvider.class);
+    }
+
+    @Test
+    public void testProviderConfigKeyVariants()
+    {
+        // The registry resolves a compressor type to its plugin via three 
accepted key forms in
+        // compressor_providers: fully qualified class name, simple class 
name, and the
+        // CompressorType abbreviation. Verify each form independently routes 
to the plugin.
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString());
+        String[] keys = {
+        NoopCompressor.class.getName(),                                  // 
FQCN
+        NoopCompressor.class.getSimpleName(),                            // 
simple name
+        CompressorRegistry.CompressorType.NOOP.abbreviation              // 
abbreviation
+        };
+
+        for (String key : keys)
+        {
+            CompressorRegistry.instance.reset();
+            config.compressor_providers.clear();
+            config.compressor_providers.put(key, new 
ParameterizedClass(TestCompressionProvider.class.getName(), params));
+
+            DatabaseDescriptor.setConfig(config);
+            DatabaseDescriptor.applyCompressorProviders();
+
+            AbstractCompressionProvider provider = 
CompressorRegistry.instance.getProvider(NoopCompressor.class);
+            assertThat(provider).as("provider resolved via key '%s'", 
key).isNotNull();
+            assertThat(provider.getClass()).as("provider class resolved via 
key '%s'", key)
+                                           
.isEqualTo(TestCompressionProvider.class);
+        }
+    }
+
+    @Test
+    public void testUnhealthyProviderWithFallback()
+    {
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString(),
+                                            
TestCompressionProvider.FAIL_HEALTH, "true");
+        ParameterizedClass parameterizedClass = new 
ParameterizedClass(TestCompressionProvider.class.getName(), params);
+
+        AbstractCompressionProvider provider = 
CompressorRegistry.instance.resolveProvider(parameterizedClass);
+        assertThat(provider).isNotNull();
+        
assertThat(provider.getClass()).isEqualTo(DefaultCompressionProvider.class);
+    }
+
+    @Test
+    public void testUnhealthyProviderWithoutFallback()
+    {
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString(),
+                                            
TestCompressionProvider.FAIL_HEALTH, "true");
+        ParameterizedClass parameterizedClass = new 
ParameterizedClass(TestCompressionProvider.class.getName(), params);
+
+        assertThatExceptionOfType(ConfigurationException.class)
+        .isThrownBy(() -> 
CompressorRegistry.instance.resolveProvider(parameterizedClass))
+        .withMessageContaining("Failed to initialize compression provider " + 
parameterizedClass);
+    }
+
+    @Test
+    public void testProviderHealthCheckExceptionWithFallback()
+    {
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString(),
+                                            
TestCompressionProvider.FAIL_HEALTH_EXCEPTION, "true");
+        ParameterizedClass parameterizedClass = new 
ParameterizedClass(TestCompressionProvider.class.getName(), params);
+
+        AbstractCompressionProvider provider = 
CompressorRegistry.instance.resolveProvider(parameterizedClass);
+        assertThat(provider).isNotNull();
+        
assertThat(provider.getClass()).isEqualTo(DefaultCompressionProvider.class);
+    }
+
+    @Test
+    public void testProviderHealthCheckExceptionWithoutFallbackThrows()
+    {
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString(),
+                                            
TestCompressionProvider.FAIL_HEALTH_EXCEPTION, "true");
+        ParameterizedClass parameterizedClass = new 
ParameterizedClass(TestCompressionProvider.class.getName(), params);
+
+        assertThatExceptionOfType(ConfigurationException.class)
+        .isThrownBy(() -> 
CompressorRegistry.instance.resolveProvider(parameterizedClass))
+        .withMessageContaining("Failed to initialize compression provider " + 
parameterizedClass);
+    }
+
+    @Test
+    public void testProviderInstantiationFailureFallsBackToDefault()
+    {
+        // FBUtilities.newCompressionProvider throws ConfigurationException 
for an unknown class —
+        // this exercises the catch block in resolveProvider, distinct from 
the isHealthy()==false
+        // path. With fallback enabled, the default provider must be returned 
and the original
+        // exception's cause chain should be available in the warn log (not 
just its message).
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString());
+        ParameterizedClass parameterizedClass = new 
ParameterizedClass("org.apache.cassandra.does.not.Exist", params);
+
+        AbstractCompressionProvider provider = 
CompressorRegistry.instance.resolveProvider(parameterizedClass);
+        
assertThat(provider).isEqualTo(CompressorRegistry.DEFAULT_COMPRESSION_PROVIDER);
+    }
+
+    @Test
+    public void testNullParametersDoesNotNPE()
+    {
+        // ParameterizedClass.parameters is nullable: SnakeYAML uses the 
no-arg constructor and
+        // leaves the field null when the yaml entry has only 'class_name:' 
(no 'parameters:'
+        // block). resolveProvider must tolerate this rather than NPE on 
getOrDefault.
+        ParameterizedClass pc = new ParameterizedClass();
+        pc.class_name = "org.apache.cassandra.does.not.Exist";
+        pc.parameters = new HashMap<>();
+        
pc.parameters.put(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString());
+
+        AbstractCompressionProvider provider = 
CompressorRegistry.instance.resolveProvider(pc);
+        
assertThat(provider).isEqualTo(CompressorRegistry.DEFAULT_COMPRESSION_PROVIDER);
+    }
+
+    @Test
+    public void testMissingClassNameThrows()
+    {
+        // A compressor_providers entry with no 'class_name' is a config 
mistake and must fail
+        // loudly — silently substituting the default would mask typos like 
'clas_name:'.
+        ParameterizedClass parameterizedClass = new ParameterizedClass(null,
+                                                                       
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString()));
+
+        assertThatExceptionOfType(ConfigurationException.class)
+        .isThrownBy(() -> 
CompressorRegistry.instance.resolveProvider(parameterizedClass))
+        .withMessageContaining("missing required 'class_name'");
+    }
+
+    @Test
+    public void testProviderInstantiationFailureWithoutFallbackThrows()
+    {
+        // Same catch block as above, but with fallback disabled — the 
resolver must surface a
+        // ConfigurationException rather than silently use the default.
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString());
+        ParameterizedClass parameterizedClass = new 
ParameterizedClass("org.apache.cassandra.does.not.Exist", params);
+
+        assertThatExceptionOfType(ConfigurationException.class)
+        .isThrownBy(() -> 
CompressorRegistry.instance.resolveProvider(parameterizedClass))
+        .withMessageContaining("Failed to initialize compression provider " + 
parameterizedClass);
+    }
+
+    @Test
+    public void testInitFailureWithFallback()
+    {
+        // An exception during provider.init() will be treated the same as an 
unhealthy provider:
+        // if fallback is enabled, the default provider will be used
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString(),
+                                            TestCompressionProvider.FAIL_INIT, 
"true");
+        ParameterizedClass parameterizedClass = new 
ParameterizedClass(TestCompressionProvider.class.getName(), params);
+
+        AbstractCompressionProvider provider = 
CompressorRegistry.instance.resolveProvider(parameterizedClass);
+        assertThat(provider).isNotNull();
+        
assertThat(provider.getClass()).isEqualTo(DefaultCompressionProvider.class);
+    }
+
+    @Test
+    public void testInitFailureWithoutFallbackThrows()
+    {
+        // If provider.init() fails and if fallback is disabled, an exception 
will be thrown
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString(),
+                                            TestCompressionProvider.FAIL_INIT, 
"true");
+        ParameterizedClass parameterizedClass = new 
ParameterizedClass(TestCompressionProvider.class.getName(), params);
+
+        assertThatExceptionOfType(ConfigurationException.class)
+        .isThrownBy(() -> 
CompressorRegistry.instance.resolveProvider(parameterizedClass))
+        .withMessageContaining("Failed to initialize compression provider " + 
parameterizedClass);
+    }
+
+    @Test
+    public void testCreateCompressorFailureWithFallback()
+    {
+        // This tests a scenario when provider fails to get a compressor,
+        // if fallback is enabled, the registry should still return a 
compressor from the default provider instead of failing the request.
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString(),
+                                            
TestCompressionProvider.FAIL_CREATE, "true");
+        
config.compressor_providers.put(SnappyCompressor.class.getSimpleName(), new 
ParameterizedClass(TestCompressionProvider.class.getName(), params));
+
+        DatabaseDescriptor.setConfig(config);
+        DatabaseDescriptor.applyCompressorProviders();
+        ICompressor compressor = 
CompressorRegistry.instance.getCompressor(SnappyCompressor.class, Map.of());
+        assertThat(compressor).isInstanceOf(SnappyCompressor.class);
+    }
+
+    @Test
+    public void testCreateCompressorFailureWithoutFallbackThrows()
+    {
+        // if fallback is disabled and provider fails to get a compressor, the 
registry should throw an exception
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString(),
+                                            
TestCompressionProvider.FAIL_CREATE, "true");
+        ParameterizedClass parameterizedClass = new 
ParameterizedClass(TestCompressionProvider.class.getName(), params);
+        
config.compressor_providers.put(SnappyCompressor.class.getSimpleName(), 
parameterizedClass);
+
+        DatabaseDescriptor.setConfig(config);
+        DatabaseDescriptor.applyCompressorProviders();
+
+        assertThatExceptionOfType(IllegalStateException.class)
+        .isThrownBy(() -> 
CompressorRegistry.instance.getCompressor(SnappyCompressor.class, Map.of()))
+        .withMessageContaining("compressor instantiation failed");
+    }
+
+    @Test
+    public void testCustomProviderAndCompressorWithSerializedAs() throws 
Exception
+    {
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.FALSE.toString());
+        
config.compressor_providers.put(SnappyCompressor.class.getSimpleName(), new 
ParameterizedClass(TestCompressionProvider.class.getName(), params));
+
+        DatabaseDescriptor.setConfig(config);
+        DatabaseDescriptor.applyCompressorProviders();
+
+        AbstractCompressionProvider provider = 
CompressorRegistry.instance.getProvider(SnappyCompressor.class);
+        assertThat(provider).isNotNull();
+        assertThat(provider.isHealthy()).isTrue();
+        ICompressor compressor = 
CompressorRegistry.instance.getCompressor(SnappyCompressor.class, Map.of());
+        
assertThat(compressor.getClass()).isEqualTo(SerializedTestCompressor.class);
+    }
+
+    @Test
+    public void testCustomProviderAndCompressorWithoutSerializedAs() throws 
Exception
+    {
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString(),
+                                            
TestCompressionProvider.FAIL_SERIALIZED_AS, "true");
+        
config.compressor_providers.put(SnappyCompressor.class.getSimpleName(), new 
ParameterizedClass(TestCompressionProvider.class.getName(), params));
+
+        DatabaseDescriptor.setConfig(config);
+        DatabaseDescriptor.applyCompressorProviders();
+
+        AbstractCompressionProvider provider = 
CompressorRegistry.instance.getProvider(SnappyCompressor.class);
+        assertThat(provider).isNotNull();
+        assertThat(provider.isHealthy()).isTrue();
+        assertThatExceptionOfType(RuntimeException.class)
+        .isThrownBy(() -> 
CompressorRegistry.instance.getCompressor(SnappyCompressor.class, Map.of()))
+        .withMessageContaining("You need to override serializedAs() method of 
your custom compressor and return " +
+                               "base compressor class it is the substitute 
for.");
+    }
+
+}
diff --git a/test/unit/org/apache/cassandra/schema/CompressionParamsTest.java 
b/test/unit/org/apache/cassandra/schema/CompressionParamsTest.java
index 70f1fbbc19..05b98e86aa 100644
--- a/test/unit/org/apache/cassandra/schema/CompressionParamsTest.java
+++ b/test/unit/org/apache/cassandra/schema/CompressionParamsTest.java
@@ -18,10 +18,20 @@
 
 package org.apache.cassandra.schema;
 
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableMap;
+
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.compress.ICompressor;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -71,4 +81,67 @@ public class CompressionParamsTest
         .as("Noop compression should not enable dictionary compression")
         .isFalse();
     }
+
+    @Test
+    public void testNonBuiltInCompressorClassResolvesViaDefaultProvider()
+    {
+        // A 3rd-party / user-supplied compressor class is not in 
CompressorType, so the registry
+        // has no entry for it. CompressionParams.createCompressor must fall 
back to the default
+        // provider, which reflectively invokes the class's static create(Map) 
factory - otherwise
+        // every existing pluggable compressor outside 
org.apache.cassandra.io.compress NPEs.
+        Map<String, String> opts = ImmutableMap.of("class", 
CustomTestCompressor.class.getName(),
+                                                   "chunk_length_in_kb", "8");
+        CompressionParams params = CompressionParams.fromMap(opts);
+
+        
assertThat(params.getSstableCompressor()).isInstanceOf(CustomTestCompressor.class);
+        assertThat(params.klass()).isEqualTo(CustomTestCompressor.class);
+    }
+
+    public static class CustomTestCompressor implements ICompressor
+    {
+        public static CustomTestCompressor create(Map<String, String> options)
+        {
+            return new CustomTestCompressor();
+        }
+
+        @Override
+        public int initialCompressedBufferLength(int chunkLength)
+        {
+            return chunkLength;
+        }
+
+        @Override
+        public int uncompress(byte[] in, int io, int il, byte[] out, int oo) 
throws IOException
+        {
+            return 0;
+        }
+
+        @Override
+        public void compress(ByteBuffer in, ByteBuffer out) throws IOException
+        {
+        }
+
+        @Override
+        public void uncompress(ByteBuffer in, ByteBuffer out) throws 
IOException
+        {
+        }
+
+        @Override
+        public BufferType preferredBufferType()
+        {
+            return BufferType.OFF_HEAP;
+        }
+
+        @Override
+        public boolean supports(BufferType bufferType)
+        {
+            return true;
+        }
+
+        @Override
+        public Set<String> supportedOptions()
+        {
+            return Collections.emptySet();
+        }
+    }
 }
diff --git a/test/unit/org/apache/cassandra/service/StartupChecksTest.java 
b/test/unit/org/apache/cassandra/service/StartupChecksTest.java
index 34fc287643..8ce618b094 100644
--- a/test/unit/org/apache/cassandra/service/StartupChecksTest.java
+++ b/test/unit/org/apache/cassandra/service/StartupChecksTest.java
@@ -53,12 +53,16 @@ import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.config.CassandraRelevantProperties;
 import org.apache.cassandra.config.Config.DiskAccessMode;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
 import org.apache.cassandra.config.StartupChecksConfiguration;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.StartupException;
+import org.apache.cassandra.io.compress.AbstractCompressionProvider;
+import org.apache.cassandra.io.compress.CompressorRegistry;
+import org.apache.cassandra.io.compress.SnappyCompressor;
 import org.apache.cassandra.io.filesystem.ForwardingFileSystem;
 import org.apache.cassandra.io.filesystem.ForwardingFileSystemProvider;
 import org.apache.cassandra.io.filesystem.ForwardingPath;
@@ -66,6 +70,8 @@ import org.apache.cassandra.io.util.File;
 import org.apache.cassandra.schema.SchemaConstants;
 import org.apache.cassandra.service.DataResurrectionCheck.Heartbeat;
 import org.apache.cassandra.utils.Clock;
+import 
org.apache.cassandra.utils.CompressionProviderHelper.CompatibleSnappyProvider;
+import 
org.apache.cassandra.utils.CompressionProviderHelper.IncompatibleSnappyProvider;
 import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.SystemInfo;
 
@@ -75,6 +81,7 @@ import static 
org.apache.cassandra.io.util.FileUtils.createTempFile;
 import static 
org.apache.cassandra.service.DataResurrectionCheck.HEARTBEAT_FILE_CONFIG_PROPERTY;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
@@ -151,7 +158,8 @@ public class StartupChecksTest
         new File(sstableDir).deleteRecursive();
         Path snapshotDir = sstableDir.resolve("snapshots");
         Files.createDirectories(snapshotDir);
-        copyInvalidLegacySSTables(snapshotDir); startupChecks.verify(options);
+        copyInvalidLegacySSTables(snapshotDir);
+        startupChecks.verify(options);
 
         // and in a backups directory
         new File(sstableDir).deleteRecursive();
@@ -218,16 +226,17 @@ public class StartupChecksTest
         Path legacySSTableRoot = 
Paths.get(TEST_INVALID_LEGACY_SSTABLE_ROOT.getString(),
                                            "Keyspace1",
                                            "Standard1");
-        for (String filename : new String[]{"Keyspace1-Standard1-ic-0-TOC.txt",
-                                            
"Keyspace1-Standard1-ic-0-Digest.sha1",
-                                            "legacyleveled.json"})
+        for (String filename : new String[]{ 
"Keyspace1-Standard1-ic-0-TOC.txt",
+                                             
"Keyspace1-Standard1-ic-0-Digest.sha1",
+                                             "legacyleveled.json" })
             Files.copy(Paths.get(legacySSTableRoot.toString(), filename), 
targetDir.resolve(filename));
     }
 
     @Test
     public void testDataResurrectionCheck() throws Exception
     {
-        DataResurrectionCheck check = new DataResurrectionCheck() {
+        DataResurrectionCheck check = new DataResurrectionCheck()
+        {
             @Override
             List<String> getKeyspaces()
             {
@@ -254,7 +263,8 @@ public class StartupChecksTest
     @Test
     public void testDataResurrectionCheckLastModifiedFallback() throws 
Exception
     {
-        DataResurrectionCheck check = new DataResurrectionCheck() {
+        DataResurrectionCheck check = new DataResurrectionCheck()
+        {
             @Override
             List<String> getKeyspaces()
             {
@@ -287,7 +297,8 @@ public class StartupChecksTest
         }
     }
 
-    private void verifySuccess(StartupChecks tests) {
+    private void verifySuccess(StartupChecks tests)
+    {
         try
         {
             tests.verify(options);
@@ -376,7 +387,8 @@ public class StartupChecksTest
 
         ServiceLoader<StartupCheck> loader = mock(ServiceLoader.class);
         doReturn(List.of(externalCheck).iterator()).when(loader).iterator();
-        try (MockedStatic<ServiceLoader> serviceLoader = 
Mockito.mockStatic(ServiceLoader.class)) {
+        try (MockedStatic<ServiceLoader> serviceLoader = 
Mockito.mockStatic(ServiceLoader.class))
+        {
             serviceLoader.when(() -> 
ServiceLoader.load(StartupCheck.class)).thenReturn(loader);
 
             StartupChecks checks = new 
StartupChecks().withDefaultTests().withServiceLoaderTests();
@@ -426,7 +438,8 @@ public class StartupChecksTest
         // two times! We model loading of two checks with same name
         doReturn(List.of(externalCheck, 
externalCheck).iterator()).when(loader).iterator();
 
-        try (MockedStatic<ServiceLoader> serviceLoader = 
Mockito.mockStatic(ServiceLoader.class)) {
+        try (MockedStatic<ServiceLoader> serviceLoader = 
Mockito.mockStatic(ServiceLoader.class))
+        {
             serviceLoader.when(() -> 
ServiceLoader.load(StartupCheck.class)).thenReturn(loader);
 
             try
@@ -478,7 +491,8 @@ public class StartupChecksTest
         // two times! We model loading of two checks with same name
         doReturn(List.of(externalCheck, 
externalCheck).iterator()).when(loader).iterator();
 
-        try (MockedStatic<ServiceLoader> serviceLoader = 
Mockito.mockStatic(ServiceLoader.class)) {
+        try (MockedStatic<ServiceLoader> serviceLoader = 
Mockito.mockStatic(ServiceLoader.class))
+        {
             serviceLoader.when(() -> 
ServiceLoader.load(StartupCheck.class)).thenReturn(loader);
 
             try
@@ -594,7 +608,6 @@ public class StartupChecksTest
                                                     "Standard1"));
         for (File f : legacySSTableRoot.tryList())
             Files.copy(f.toPath(), targetDir.resolve(f.name()));
-
     }
 
     private void verifyFailure(StartupChecks tests, String message)
@@ -615,7 +628,60 @@ public class StartupChecksTest
     {
         // Non-existent directories should be skipped, not added to 
unsupported list
         List<String> unsupported = 
StartupChecks.findDirectIOUnsupportedLocations(
-            new String[] { "/this/path/does/not/exist/for/testing" });
+        new String[]{ "/this/path/does/not/exist/for/testing" });
         assertThat(unsupported).isEmpty();
     }
+
+    @Test
+    public void testCompatibleCompressionProvider()
+    {
+        // This test will go through the smoke test verifing with a valid 
custom provider,
+        // providing a compressor compatible with Snappy
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString());
+        Map<String, ParameterizedClass> providerOptions = Map.of(
+        SnappyCompressor.class.getSimpleName(),
+        new ParameterizedClass(CompatibleSnappyProvider.class.getName(), 
params));
+
+        CompressorRegistry.instance.reset();
+        CompressorRegistry.instance.registerProviders(providerOptions);
+        try
+        {
+            StartupChecks.checkCustomCompressionProviders.execute(options);
+        }
+        catch (Throwable t)
+        {
+            fail("This exception should not be thrown since the provider is 
compatible " +
+                 "with the compressor it is supposed to create: " + 
t.getMessage());
+        }
+        finally
+        {
+            CompressorRegistry.instance.reset(); // only registry state 
touched — safe to reset
+        }
+    }
+
+    @Test
+    public void testIncompatibleCompressionProvider()
+    {
+        // This test is trying to simulate a failure scenario by providing a 
custom provider which is not compatible
+        // with the compressor it is supposed to create. In this case, we are 
providing a compressor which is compatible
+        // with Snappy, but we are registering it as provider for 
NoopCompressor.
+        // The smoke test should fail and throw an exception alerting that 
using this provider may lead to data corruption.
+        Map<String, String> params = 
Map.of(AbstractCompressionProvider.FAIL_ON_MISSING_PROVIDER, 
Boolean.TRUE.toString());
+        Map<String, ParameterizedClass> providerOptions = Map.of(
+        SnappyCompressor.class.getSimpleName(),
+        new ParameterizedClass(IncompatibleSnappyProvider.class.getName(), 
params));
+
+        CompressorRegistry.instance.reset();
+        CompressorRegistry.instance.registerProviders(providerOptions);
+        try
+        {
+            assertThatThrownBy(() -> 
StartupChecks.checkCustomCompressionProviders.execute(options))
+            .isInstanceOf(StartupException.class)
+            .hasMessageContaining("The following custom compression providers 
failed smoke test");
+        }
+        finally
+        {
+            CompressorRegistry.instance.reset();
+        }
+    }
 }
diff --git 
a/test/unit/org/apache/cassandra/utils/CompressionProviderHelper.java 
b/test/unit/org/apache/cassandra/utils/CompressionProviderHelper.java
new file mode 100644
index 0000000000..5a4b99afd0
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/CompressionProviderHelper.java
@@ -0,0 +1,226 @@
+/*
+ * 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.utils;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.cassandra.io.compress.AbstractCompressionProvider;
+import org.apache.cassandra.io.compress.BufferType;
+import org.apache.cassandra.io.compress.ICompressor;
+import org.apache.cassandra.io.compress.SnappyCompressor;
+
+/**
+ * Shared test compression provider and compressor tests.
+ * Provides reusable providers and compressors used across
+ * {@code CompressorRegistryTest}, {@code StartupChecksTest}, and any future 
tests
+ * that exercise the compression provider interface.
+ */
+public final class CompressionProviderHelper
+{
+    private CompressionProviderHelper() {}
+
+    // 
-------------------------------------------------------------------------
+    // Providers
+    // 
-------------------------------------------------------------------------
+
+    // Test compression provider for testing various edge cases around 
initialization, health checks, and
+    // compressor creation.
+    public static class TestCompressionProvider extends 
AbstractCompressionProvider
+    {
+        // ParameterizedClass.parameters are used to pass different test 
scenarios .
+        // resolveProvider() only strips FAIL_ON_MISSING_PROVIDER, so get to 
init().
+        public static final String FAIL_INIT             = "fail_init";
+        public static final String FAIL_HEALTH           = "fail_health";
+        public static final String FAIL_HEALTH_EXCEPTION = 
"fail_health_exception";
+        public static final String FAIL_CREATE           = "fail_create";
+        public static final String FAIL_SERIALIZED_AS    = 
"fail_serialized_as";
+
+        private boolean flag(String key)
+        {
+            return Boolean.parseBoolean(getParameters().getOrDefault(key, 
"false"));
+        }
+
+        @Override
+        public void init(Map<String, String> parameters)
+        {
+            super.init(parameters);          //use this to store and provide 
test variants
+            if (flag(FAIL_INIT))
+                throw new RuntimeException("init failed: something went wrong 
with startup");
+        }
+
+        @Override
+        public ICompressor createCompressor(Class<?> compressorClass, 
Map<String, String> options) throws IllegalStateException
+        {
+            if (flag(FAIL_CREATE))
+            {
+                throw new IllegalStateException("compressor instantiation 
failed");
+            }
+            return flag(FAIL_SERIALIZED_AS)
+                ? PlainTestCompressor.create(options)
+                : SerializedTestCompressor.create(options);
+        }
+
+        @Override
+        public boolean isHealthy()
+        {
+            if (flag(FAIL_HEALTH_EXCEPTION))
+                throw new RuntimeException("health check failed");
+            if (flag(FAIL_HEALTH)) return false;
+            return true;
+        }
+    }
+
+    public static class CompatibleSnappyProvider extends 
AbstractCompressionProvider
+    {
+        @Override
+        public boolean isHealthy()
+        {
+            return true;
+        }
+
+        @Override
+        public ICompressor createCompressor(Class<?> compressorClass, 
Map<String, String> options)
+        {
+            return new MySnappyCompressor();
+        }
+    }
+
+    public static class IncompatibleSnappyProvider extends 
AbstractCompressionProvider
+    {
+        @Override
+        public boolean isHealthy()
+        {
+            return true;
+        }
+
+        @Override
+        public ICompressor createCompressor(Class<?> compressorClass, 
Map<String, String> options)
+        {
+            return new IncompatibleSnappyCompressor();
+        }
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Compressors
+    // 
-------------------------------------------------------------------------
+
+    abstract static class CustomTestCompressor implements ICompressor
+    {
+        @Override
+        public int initialCompressedBufferLength(int chunkLength)
+        {
+            return chunkLength;
+        }
+
+        @Override
+        public int uncompress(byte[] in, int io, int il, byte[] out, int oo) 
throws IOException
+        {
+            return 0;
+        }
+
+        @Override
+        public void compress(ByteBuffer in, ByteBuffer out) throws IOException
+        {
+        }
+
+        @Override
+        public void uncompress(ByteBuffer in, ByteBuffer out) throws 
IOException
+        {
+        }
+
+        @Override
+        public BufferType preferredBufferType()
+        {
+            return BufferType.OFF_HEAP;
+        }
+
+        @Override
+        public boolean supports(BufferType bufferType)
+        {
+            return true;
+        }
+
+        @Override
+        public Set<String> supportedOptions()
+        {
+            return Collections.emptySet();
+        }
+    }
+
+    // This test compressor does not override serializedAs(). serialized call 
should return PlainTestCompressor.class
+    public static class PlainTestCompressor extends CustomTestCompressor
+    {
+        public static PlainTestCompressor create(Map<String, String> options)
+        {
+            return new PlainTestCompressor();
+        }
+    }
+
+    // This test compressor overrides serializedAs(), it is compatible with 
SnappyCompressor
+    public static class SerializedTestCompressor extends CustomTestCompressor
+    {
+        public static SerializedTestCompressor create(Map<String, String> 
options)
+        {
+            return new SerializedTestCompressor();
+        }
+
+        @Override
+        public Class<? extends ICompressor> serializedAs()
+        {
+            return SnappyCompressor.class;
+        }
+    }
+
+   // This test compressor overrides serializedAs(), this will create a 
compressor compatible with SnappyCompressor
+    public static class MySnappyCompressor extends SnappyCompressor
+    {
+        public static MySnappyCompressor create(Map<String, String> options)
+        {
+            return new MySnappyCompressor();
+        }
+
+        @Override
+        public Class<? extends ICompressor> serializedAs()
+        {
+            return SnappyCompressor.class;
+        }
+    }
+
+    // This compressor also is compatible with Snappy but will produce data 
which is not compatible
+    public static class IncompatibleSnappyCompressor extends SnappyCompressor
+    {
+        @Override
+        public Class<? extends ICompressor> serializedAs()
+        {
+            return SnappyCompressor.class;
+        }
+        @Override
+        public void compress(ByteBuffer src, ByteBuffer dest)
+        {
+            // Write raw uncompressed bytes instead of a valid Snappy stream.
+            // Snappy will not be able to decompress
+            // This is what the smoke test's step 1 cross-check is designed to 
catch.
+            dest.put(src);
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to