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

abhishekrb19 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 2a0edc5a8de refactor: Make QueryBlocklistRule an interface for 
extensibility (#19457)
2a0edc5a8de is described below

commit 2a0edc5a8de887155ceff000f58c1fbd2c6ed724
Author: Abhishek Radhakrishnan <[email protected]>
AuthorDate: Mon May 18 10:23:02 2026 -0700

    refactor: Make QueryBlocklistRule an interface for extensibility (#19457)
    
    - Convert QueryBlocklistRule from a concrete class to an interface with two 
methods: getRuleName() and matches(Query<?>).
    - Moves the existing implementation to DefaultQueryBlocklistRule with a 
default impl on the interface @JsonTypeInfo(defaultImpl = 
DefaultQueryBlocklistRule.class) so existing JSON configs without a "type" 
field continue to deserialize correctly.
    - Extensions can register new rule types as Jackson subtypes via 
SimpleModule.registerSubtypes(...) and we can even introduce new union rule 
types with different logic w/o polluting existing default implementation.
---
 docs/api-reference/dynamic-configuration-api.md    |  35 ++++-
 .../server/EmbeddedBrokerDynamicConfigTest.java    |   3 +-
 ...istRule.java => DefaultQueryBlocklistRule.java} |  26 ++--
 .../apache/druid/server/QueryBlocklistRule.java    | 171 +++------------------
 .../coordinator/CoordinatorClientImplTest.java     |   4 +-
 .../druid/server/QueryBlocklistRuleTest.java       |  52 +++++--
 .../apache/druid/server/QueryLifecycleTest.java    |   4 +-
 .../server/broker/BrokerDynamicConfigTest.java     |  30 +++-
 8 files changed, 142 insertions(+), 183 deletions(-)

diff --git a/docs/api-reference/dynamic-configuration-api.md 
b/docs/api-reference/dynamic-configuration-api.md
index 259e50c8800..7805d7287c4 100644
--- a/docs/api-reference/dynamic-configuration-api.md
+++ b/docs/api-reference/dynamic-configuration-api.md
@@ -371,6 +371,7 @@ Host: http://ROUTER_IP:ROUTER_PORT
 {
   "queryBlocklist": [
     {
+      "type": "default",
       "ruleName": "block-expensive-scans",
       "dataSources": ["large_table"],
       "queryTypes": ["scan"]
@@ -440,11 +441,13 @@ curl -X POST 
"http://ROUTER_IP:ROUTER_PORT/druid/coordinator/v1/broker/config"; \
 -d '{
   "queryBlocklist": [
     {
+      "type": "default",
       "ruleName": "block-expensive-scans",
       "dataSources": ["large_table", "huge_dataset"],
       "queryTypes": ["scan"]
     },
     {
+      "type": "default",
       "ruleName": "block-debug-queries",
       "contextMatches": {
         "debug": "true"
@@ -481,11 +484,13 @@ X-Druid-Comment: Add query blocklist rules and set 
default context
 {
   "queryBlocklist": [
     {
+      "type": "default",
       "ruleName": "block-expensive-scans",
       "dataSources": ["large_table", "huge_dataset"],
       "queryTypes": ["scan"]
     },
     {
+      "type": "default",
       "ruleName": "block-debug-queries",
       "contextMatches": {
         "debug": "true"
@@ -529,22 +534,42 @@ The following table shows the dynamic configuration 
properties for the Broker.
 
 Query blocklist rules allow you to block specific queries based on datasource, 
query type, and/or query context parameters. This feature is useful for 
preventing expensive or problematic queries from impacting cluster performance.
 
-Each rule in the `queryBlocklist` array is a JSON object with the following 
properties:
+Each rule in the `queryBlocklist` array is a JSON object. The `type` field 
selects the rule implementation. If omitted, it defaults to `"default"`. A 
query is blocked if it matches ANY rule in the blocklist (OR logic between 
rules).
+
+> **Note:** The `"type"` field is not required for `"default"` rules (existing 
configs without it continue to work), but including it explicitly is 
recommended for clarity.
+
+##### `default` type
+
+The built-in rule type. Blocks queries by matching on datasource, query type, 
and/or query context parameters.
 
 |Property|Description|Required|Default|
 |--------|-----------|--------|-------|
+|`type`|Rule type identifier. Not required — rules without a `"type"` field 
are treated as `"default"` for backwards compatibility with existing 
configurations.|No|`"default"`|
 |`ruleName`|Unique name identifying this blocklist rule. Used in error 
messages when queries are blocked.|Yes|N/A|
 |`dataSources`|List of datasource names to match. A query matches if it 
references any datasource in this list.|No|Matches all datasources|
 |`queryTypes`|List of query types to match (e.g., `scan`, `timeseries`, 
`groupBy`, `topN`). A query matches if its type is in this list.|No|Matches all 
query types|
 |`contextMatches`|Map of query context parameter key-value pairs to match. A 
query matches if all specified context parameters match the provided values 
(case-sensitive string comparison).|No|Matches all contexts|
 
-**Rule matching behavior:**
+**Matching behavior:**
 
 - A query must match ALL specified criteria within a rule (AND logic) to be 
blocked by that rule
-- If any criterion is omitted, empty or null, it matches everything (e.g., 
omitting `queryTypes` or setting it to null matches all query types)
+- If any criterion is omitted, empty, or null, it matches everything (e.g., 
omitting `queryTypes` or setting it to null matches all query types)
 - For context matching: if a rule specifies context parameters, queries with 
missing or null values for those keys will not match
 - At least one criterion must be specified per rule to prevent accidentally 
blocking all queries
-- A query is blocked if it matches ANY rule in the blocklist (OR logic between 
rules)
+
+##### Custom types (extensions)
+
+Extensions can register additional rule types by adding Jackson subtypes to 
the `QueryBlocklistRule` interface. A custom rule is selected by setting 
`"type"` to its registered name:
+
+```json
+{
+  "type": "myCustomRule",
+  "ruleName": "rate-limit-heavy-users",
+  ...
+}
+```
+
+Custom types define their own matching semantics — they may use different 
criteria and logic than the `default` type described above. If a rule 
references a type whose extension is not loaded, deserialization fails with an 
error rather than silently falling back to the default type.
 
 **Error response:**
 
@@ -676,7 +701,7 @@ Host: http://ROUTER_IP:ROUTER_PORT
       "comment": "Add query blocklist rules",
       "ip": "127.0.0.1"
     },
-    "payload": 
"{\"queryBlocklist\":[{\"ruleName\":\"block-expensive-scans\",\"dataSources\":[\"large_table\"],\"queryTypes\":[\"scan\"]}],\"queryContext\":{\"priority\":0,\"timeout\":300000},\"perSegmentTimeoutConfig\":{\"large_table\":{\"perSegmentTimeoutMs\":10000,\"monitorOnly\":false}}}",
+    "payload": 
"{\"queryBlocklist\":[{\"type\":\"default\",\"ruleName\":\"block-expensive-scans\",\"dataSources\":[\"large_table\"],\"queryTypes\":[\"scan\"]}],\"queryContext\":{\"priority\":0,\"timeout\":300000},\"perSegmentTimeoutConfig\":{\"large_table\":{\"perSegmentTimeoutMs\":10000,\"monitorOnly\":false}}}",
     "auditTime": "2024-03-06T12:00:00.000Z"
   }
 ]
diff --git 
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
 
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
index 34020229434..ac48e2c5346 100644
--- 
a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
+++ 
b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/server/EmbeddedBrokerDynamicConfigTest.java
@@ -24,6 +24,7 @@ import org.apache.druid.common.config.JacksonConfigManager;
 import org.apache.druid.common.utils.IdUtils;
 import org.apache.druid.indexing.common.task.TaskBuilder;
 import org.apache.druid.query.QueryContext;
+import org.apache.druid.server.DefaultQueryBlocklistRule;
 import org.apache.druid.server.QueryBlocklistRule;
 import org.apache.druid.server.broker.BrokerDynamicConfig;
 import org.apache.druid.server.http.BrokerDynamicConfigSyncer;
@@ -102,7 +103,7 @@ public class EmbeddedBrokerDynamicConfigTest extends 
EmbeddedClusterTestBase
     Assertions.assertFalse(initialResult.isBlank());
 
     // Apply blocklist rule that matches all queries on this datasource
-    QueryBlocklistRule blockRule = new QueryBlocklistRule(
+    QueryBlocklistRule blockRule = new DefaultQueryBlocklistRule(
         "block-test-datasource",
         Set.of(dataSource),
         null,
diff --git 
a/server/src/main/java/org/apache/druid/server/QueryBlocklistRule.java 
b/server/src/main/java/org/apache/druid/server/DefaultQueryBlocklistRule.java
similarity index 85%
copy from server/src/main/java/org/apache/druid/server/QueryBlocklistRule.java
copy to 
server/src/main/java/org/apache/druid/server/DefaultQueryBlocklistRule.java
index 1242a11bff3..ae1d8662d28 100644
--- a/server/src/main/java/org/apache/druid/server/QueryBlocklistRule.java
+++ 
b/server/src/main/java/org/apache/druid/server/DefaultQueryBlocklistRule.java
@@ -32,10 +32,13 @@ import java.util.Objects;
 import java.util.Set;
 
 /**
- * A rule for matching queries against blocklist criteria. A query matches 
this rule if ALL
- * specified criteria match (AND logic). Null or empty criteria match 
everything.
+ * Default {@link QueryBlocklistRule} implementation. A query matches if ALL 
specified criteria
+ * match (AND logic). Null or empty criteria are wildcards (match everything).
+ *
+ * <p>At least one criterion must be non-empty to prevent accidentally 
blocking all queries.
+ * Deserializes from JSON with no {@code "type"} field for backwards 
compatibility.
  */
-public class QueryBlocklistRule
+public class DefaultQueryBlocklistRule implements QueryBlocklistRule
 {
   private final String ruleName;
   @Nullable
@@ -50,7 +53,7 @@ public class QueryBlocklistRule
   private final boolean hasContextCriteria;
 
   @JsonCreator
-  public QueryBlocklistRule(
+  public DefaultQueryBlocklistRule(
       @JsonProperty("ruleName") String ruleName,
       @JsonProperty("dataSources") @Nullable Set<String> dataSources,
       @JsonProperty("queryTypes") @Nullable Set<String> queryTypes,
@@ -62,7 +65,6 @@ public class QueryBlocklistRule
         "ruleName must not be null or empty"
     );
 
-    // At least one criterion must be specified to prevent accidentally 
blocking all queries
     this.hasDataSourceCriteria = dataSources != null && !dataSources.isEmpty();
     this.hasQueryTypeCriteria = queryTypes != null && !queryTypes.isEmpty();
     this.hasContextCriteria = contextMatches != null && 
!contextMatches.isEmpty();
@@ -79,6 +81,7 @@ public class QueryBlocklistRule
     this.contextMatches = contextMatches;
   }
 
+  @Override
   @JsonProperty
   public String getRuleName()
   {
@@ -106,13 +109,7 @@ public class QueryBlocklistRule
     return contextMatches;
   }
 
-  /**
-   * Returns true if the query matches ALL specified criteria (AND logic).
-   * Null or empty criteria match everything.
-   *
-   * @param query the query to check
-   * @return true if the query matches this rule, false otherwise
-   */
+  @Override
   public boolean matches(Query<?> query)
   {
     if (hasDataSourceCriteria) {
@@ -131,7 +128,6 @@ public class QueryBlocklistRule
     if (hasContextCriteria) {
       for (Map.Entry<String, String> entry : contextMatches.entrySet()) {
         Object contextValue = query.getContext().get(entry.getKey());
-        // If the query context doesn't have this key or has a null value, it 
doesn't match
         if (contextValue == null || 
!entry.getValue().equals(String.valueOf(contextValue))) {
           return false;
         }
@@ -150,7 +146,7 @@ public class QueryBlocklistRule
     if (o == null || getClass() != o.getClass()) {
       return false;
     }
-    QueryBlocklistRule that = (QueryBlocklistRule) o;
+    DefaultQueryBlocklistRule that = (DefaultQueryBlocklistRule) o;
     return Objects.equals(ruleName, that.ruleName)
            && Objects.equals(dataSources, that.dataSources)
            && Objects.equals(queryTypes, that.queryTypes)
@@ -166,7 +162,7 @@ public class QueryBlocklistRule
   @Override
   public String toString()
   {
-    return "QueryBlocklistRule{" +
+    return "DefaultQueryBlocklistRule{" +
            "ruleName='" + ruleName + '\'' +
            ", dataSources=" + dataSources +
            ", queryTypes=" + queryTypes +
diff --git 
a/server/src/main/java/org/apache/druid/server/QueryBlocklistRule.java 
b/server/src/main/java/org/apache/druid/server/QueryBlocklistRule.java
index 1242a11bff3..55b20c262a3 100644
--- a/server/src/main/java/org/apache/druid/server/QueryBlocklistRule.java
+++ b/server/src/main/java/org/apache/druid/server/QueryBlocklistRule.java
@@ -19,158 +19,37 @@
 
 package org.apache.druid.server;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
-import com.google.common.collect.Sets;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonTypeResolver;
+import org.apache.druid.jackson.StrictTypeIdResolver;
 import org.apache.druid.query.Query;
 
-import javax.annotation.Nullable;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
 /**
- * A rule for matching queries against blocklist criteria. A query matches 
this rule if ALL
- * specified criteria match (AND logic). Null or empty criteria match 
everything.
+ * A rule that determines whether a query should be blocked. Implementations 
define their own
+ * matching logic and JSON fields. Use {@link DefaultQueryBlocklistRule} for 
the standard criteria
+ * (datasources, query types, context).
+ *
+ * <p>Rules with no {@code "type"} field in JSON deserialize as {@link 
DefaultQueryBlocklistRule}
+ * for backwards compatibility. An explicit but unrecognized {@code "type"} 
value will fail
+ * deserialization rather than silently falling back to the default — this 
prevents extension
+ * rules from being misinterpreted when the extension is not loaded. 
Extensions can register
+ * additional implementations as Jackson subtypes via {@code 
SimpleModule.registerSubtypes(...)}.
+ *
+ * <p>Implementations must define {@code equals} and {@code hashCode} so that
+ * {@link org.apache.druid.server.broker.BrokerDynamicConfig} can detect 
changes correctly.
  */
-public class QueryBlocklistRule
+@JsonTypeResolver(StrictTypeIdResolver.Builder.class)
+@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "type", defaultImpl = 
DefaultQueryBlocklistRule.class)
+@JsonSubTypes({
+    @JsonSubTypes.Type(value = DefaultQueryBlocklistRule.class, name = 
"default")
+})
+public interface QueryBlocklistRule
 {
-  private final String ruleName;
-  @Nullable
-  private final Set<String> dataSources;
-  @Nullable
-  private final Set<String> queryTypes;
-  @Nullable
-  private final Map<String, String> contextMatches;
-
-  private final boolean hasDataSourceCriteria;
-  private final boolean hasQueryTypeCriteria;
-  private final boolean hasContextCriteria;
-
-  @JsonCreator
-  public QueryBlocklistRule(
-      @JsonProperty("ruleName") String ruleName,
-      @JsonProperty("dataSources") @Nullable Set<String> dataSources,
-      @JsonProperty("queryTypes") @Nullable Set<String> queryTypes,
-      @JsonProperty("contextMatches") @Nullable Map<String, String> 
contextMatches
-  )
-  {
-    Preconditions.checkArgument(
-        !Strings.isNullOrEmpty(ruleName),
-        "ruleName must not be null or empty"
-    );
-
-    // At least one criterion must be specified to prevent accidentally 
blocking all queries
-    this.hasDataSourceCriteria = dataSources != null && !dataSources.isEmpty();
-    this.hasQueryTypeCriteria = queryTypes != null && !queryTypes.isEmpty();
-    this.hasContextCriteria = contextMatches != null && 
!contextMatches.isEmpty();
-
-    Preconditions.checkArgument(
-        hasDataSourceCriteria || hasQueryTypeCriteria || hasContextCriteria,
-        "At least one criterion (dataSources, queryTypes, or contextMatches) 
must be specified. "
-        + "A rule with all null/empty criteria would block ALL queries."
-    );
-
-    this.ruleName = ruleName;
-    this.dataSources = dataSources;
-    this.queryTypes = queryTypes;
-    this.contextMatches = contextMatches;
-  }
-
-  @JsonProperty
-  public String getRuleName()
-  {
-    return ruleName;
-  }
-
-  @JsonProperty
-  @Nullable
-  public Set<String> getDataSources()
-  {
-    return dataSources;
-  }
-
-  @JsonProperty
-  @Nullable
-  public Set<String> getQueryTypes()
-  {
-    return queryTypes;
-  }
-
-  @JsonProperty
-  @Nullable
-  public Map<String, String> getContextMatches()
-  {
-    return contextMatches;
-  }
+  String getRuleName();
 
   /**
-   * Returns true if the query matches ALL specified criteria (AND logic).
-   * Null or empty criteria match everything.
-   *
-   * @param query the query to check
-   * @return true if the query matches this rule, false otherwise
+   * Returns true if the query matches this rule and should be blocked.
    */
-  public boolean matches(Query<?> query)
-  {
-    if (hasDataSourceCriteria) {
-      Set<String> queryDatasources = query.getDataSource().getTableNames();
-      if (Sets.intersection(dataSources, queryDatasources).isEmpty()) {
-        return false;
-      }
-    }
-
-    if (hasQueryTypeCriteria) {
-      if (!queryTypes.contains(query.getType())) {
-        return false;
-      }
-    }
-
-    if (hasContextCriteria) {
-      for (Map.Entry<String, String> entry : contextMatches.entrySet()) {
-        Object contextValue = query.getContext().get(entry.getKey());
-        // If the query context doesn't have this key or has a null value, it 
doesn't match
-        if (contextValue == null || 
!entry.getValue().equals(String.valueOf(contextValue))) {
-          return false;
-        }
-      }
-    }
-
-    return true;
-  }
-
-  @Override
-  public boolean equals(Object o)
-  {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-    QueryBlocklistRule that = (QueryBlocklistRule) o;
-    return Objects.equals(ruleName, that.ruleName)
-           && Objects.equals(dataSources, that.dataSources)
-           && Objects.equals(queryTypes, that.queryTypes)
-           && Objects.equals(contextMatches, that.contextMatches);
-  }
-
-  @Override
-  public int hashCode()
-  {
-    return Objects.hash(ruleName, dataSources, queryTypes, contextMatches);
-  }
-
-  @Override
-  public String toString()
-  {
-    return "QueryBlocklistRule{" +
-           "ruleName='" + ruleName + '\'' +
-           ", dataSources=" + dataSources +
-           ", queryTypes=" + queryTypes +
-           ", contextMatches=" + contextMatches +
-           '}';
-  }
+  boolean matches(Query<?> query);
 }
diff --git 
a/server/src/test/java/org/apache/druid/client/coordinator/CoordinatorClientImplTest.java
 
b/server/src/test/java/org/apache/druid/client/coordinator/CoordinatorClientImplTest.java
index d915ece0c2e..0b39eb15016 100644
--- 
a/server/src/test/java/org/apache/druid/client/coordinator/CoordinatorClientImplTest.java
+++ 
b/server/src/test/java/org/apache/druid/client/coordinator/CoordinatorClientImplTest.java
@@ -49,7 +49,7 @@ import org.apache.druid.rpc.RequestBuilder;
 import org.apache.druid.segment.column.ColumnType;
 import org.apache.druid.segment.column.RowSignature;
 import org.apache.druid.segment.metadata.DataSourceInformation;
-import org.apache.druid.server.QueryBlocklistRule;
+import org.apache.druid.server.DefaultQueryBlocklistRule;
 import org.apache.druid.server.broker.BrokerDynamicConfig;
 import org.apache.druid.server.compaction.CompactionStatusResponse;
 import org.apache.druid.server.coordination.DruidServerMetadata;
@@ -840,7 +840,7 @@ public class CoordinatorClientImplTest
   {
     final BrokerDynamicConfig brokerDynamicConfig = 
BrokerDynamicConfig.builder().withQueryBlocklist(
         List.of(
-            new QueryBlocklistRule("test", Set.of("dataSource"), null, null)
+            new DefaultQueryBlocklistRule("test", Set.of("dataSource"), null, 
null)
         )
     ).build();
 
diff --git 
a/server/src/test/java/org/apache/druid/server/QueryBlocklistRuleTest.java 
b/server/src/test/java/org/apache/druid/server/QueryBlocklistRuleTest.java
index 1594143b938..21c2b3852a4 100644
--- a/server/src/test/java/org/apache/druid/server/QueryBlocklistRuleTest.java
+++ b/server/src/test/java/org/apache/druid/server/QueryBlocklistRuleTest.java
@@ -19,10 +19,12 @@
 
 package org.apache.druid.server;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import org.apache.druid.query.Druids;
 import org.apache.druid.query.timeseries.TimeseriesQuery;
+import org.apache.druid.segment.TestHelper;
 import org.junit.Assert;
 import org.junit.Test;
 import java.util.Map;
@@ -36,7 +38,7 @@ public class QueryBlocklistRuleTest
     // Rule with all null criteria would block ALL queries - this should be 
rejected
     Assert.assertThrows(
         IllegalArgumentException.class,
-        () -> new QueryBlocklistRule("match-all", null, null, null)
+        () -> new DefaultQueryBlocklistRule("match-all", null, null, null)
     );
   }
 
@@ -46,7 +48,7 @@ public class QueryBlocklistRuleTest
     // Rule with all empty collections should also be rejected (same as null)
     Assert.assertThrows(
         IllegalArgumentException.class,
-        () -> new QueryBlocklistRule("match-all", ImmutableSet.of(), 
ImmutableSet.of(), ImmutableMap.of())
+        () -> new DefaultQueryBlocklistRule("match-all", ImmutableSet.of(), 
ImmutableSet.of(), ImmutableMap.of())
     );
   }
 
@@ -54,7 +56,7 @@ public class QueryBlocklistRuleTest
   public void testMatchByDataSource()
   {
     Set<String> dataSources = ImmutableSet.of("sensitive_data", "pii_table");
-    QueryBlocklistRule rule = new QueryBlocklistRule("block-sensitive", 
dataSources, null, null);
+    QueryBlocklistRule rule = new DefaultQueryBlocklistRule("block-sensitive", 
dataSources, null, null);
 
     // Should match when datasource is in the list
     TimeseriesQuery matchingQuery = Druids.newTimeseriesQueryBuilder()
@@ -75,7 +77,7 @@ public class QueryBlocklistRuleTest
   public void testMatchByContext()
   {
     Map<String, String> contextMatches = ImmutableMap.of("priority", "0", 
"application", "rogue-app");
-    QueryBlocklistRule rule = new QueryBlocklistRule("block-rogue-app", null, 
null, contextMatches);
+    QueryBlocklistRule rule = new DefaultQueryBlocklistRule("block-rogue-app", 
null, null, contextMatches);
 
     // Should match when all context values match
     TimeseriesQuery matchingQuery = Druids.newTimeseriesQueryBuilder()
@@ -105,7 +107,7 @@ public class QueryBlocklistRuleTest
   public void testMatchByQueryType()
   {
     Set<String> queryTypes = ImmutableSet.of("timeseries", "groupBy");
-    QueryBlocklistRule rule = new 
QueryBlocklistRule("block-timeseries-groupby", null, queryTypes, null);
+    QueryBlocklistRule rule = new 
DefaultQueryBlocklistRule("block-timeseries-groupby", null, queryTypes, null);
 
     // Should match when query type is in the list (timeseries)
     TimeseriesQuery matchingQuery = Druids.newTimeseriesQueryBuilder()
@@ -121,7 +123,7 @@ public class QueryBlocklistRuleTest
     // Rule with multiple criteria - all must match (AND logic)
     Set<String> dataSources = ImmutableSet.of("large_table");
     Map<String, String> contextMatches = ImmutableMap.of("priority", "0");
-    QueryBlocklistRule rule = new QueryBlocklistRule(
+    QueryBlocklistRule rule = new DefaultQueryBlocklistRule(
         "block-low-priority-large-table",
         dataSources,
         null,
@@ -156,7 +158,7 @@ public class QueryBlocklistRuleTest
   @Test
   public void testWildcardBehavior_nullQueryTypes()
   {
-    QueryBlocklistRule rule = new QueryBlocklistRule(
+    QueryBlocklistRule rule = new DefaultQueryBlocklistRule(
         "block-datasource-all-types",
         ImmutableSet.of("blocked_ds"),
         null,  // null means match all query types
@@ -177,7 +179,7 @@ public class QueryBlocklistRuleTest
     // Rule name cannot be null
     Assert.assertThrows(
         IllegalArgumentException.class,
-        () -> new QueryBlocklistRule(null, ImmutableSet.of("ds"), null, null)
+        () -> new DefaultQueryBlocklistRule(null, ImmutableSet.of("ds"), null, 
null)
     );
   }
 
@@ -187,7 +189,39 @@ public class QueryBlocklistRuleTest
     // Rule name cannot be empty
     Assert.assertThrows(
         IllegalArgumentException.class,
-        () -> new QueryBlocklistRule("", ImmutableSet.of("ds"), null, null)
+        () -> new DefaultQueryBlocklistRule("", ImmutableSet.of("ds"), null, 
null)
     );
   }
+
+  @Test
+  public void testDeserialize_missingType_usesDefault() throws Exception
+  {
+    ObjectMapper mapper = TestHelper.makeJsonMapper();
+    String json = "{\"ruleName\":\"block-ds\",\"dataSources\":[\"foo\"]}";
+    QueryBlocklistRule rule = mapper.readValue(json, QueryBlocklistRule.class);
+    Assert.assertTrue(rule instanceof DefaultQueryBlocklistRule);
+    Assert.assertEquals("block-ds", rule.getRuleName());
+  }
+
+  @Test
+  public void testDeserialize_explicitDefaultType() throws Exception
+  {
+    ObjectMapper mapper = TestHelper.makeJsonMapper();
+    String json = 
"{\"type\":\"default\",\"ruleName\":\"block-ds\",\"dataSources\":[\"foo\"]}";
+    QueryBlocklistRule rule = mapper.readValue(json, QueryBlocklistRule.class);
+    Assert.assertTrue(rule instanceof DefaultQueryBlocklistRule);
+    Assert.assertEquals("block-ds", rule.getRuleName());
+  }
+
+  @Test
+  public void testDeserialize_unrecognizedType_fails()
+  {
+    ObjectMapper mapper = TestHelper.makeJsonMapper();
+    String json = 
"{\"type\":\"customExtension\",\"ruleName\":\"block-ds\",\"dataSources\":[\"foo\"]}";
+    Exception e = Assert.assertThrows(
+        Exception.class,
+        () -> mapper.readValue(json, QueryBlocklistRule.class)
+    );
+    Assert.assertTrue(e.getMessage().contains("Could not resolve type id 
'customExtension'"));
+  }
 }
diff --git 
a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java 
b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java
index 96880147f40..5f8da0d21a3 100644
--- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java
+++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java
@@ -830,7 +830,7 @@ public class QueryLifecycleTest
   public void testRunSimple_queryBlocklisted()
   {
     // Create a blocklist rule that matches our test query
-    QueryBlocklistRule rule = new QueryBlocklistRule(
+    QueryBlocklistRule rule = new DefaultQueryBlocklistRule(
         "test-rule",
         ImmutableSet.of(DATASOURCE),
         null,
@@ -880,7 +880,7 @@ public class QueryLifecycleTest
   public void testRunSimple_queryNotBlocklisted()
   {
     // Create a blocklist rule that does NOT match our test query
-    QueryBlocklistRule rule = new QueryBlocklistRule(
+    QueryBlocklistRule rule = new DefaultQueryBlocklistRule(
         "test-rule",
         ImmutableSet.of("other_datasource"),
         null,
diff --git 
a/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
 
b/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
index 425ccbdd14e..6a466bcd269 100644
--- 
a/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/broker/BrokerDynamicConfigTest.java
@@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableSet;
 import nl.jqno.equalsverifier.EqualsVerifier;
 import org.apache.druid.query.QueryContext;
 import org.apache.druid.segment.TestHelper;
+import org.apache.druid.server.DefaultQueryBlocklistRule;
 import org.apache.druid.server.QueryBlocklistRule;
 import org.junit.Assert;
 import org.junit.Test;
@@ -60,12 +61,35 @@ public class BrokerDynamicConfigTest
     );
 
     List<QueryBlocklistRule> expectedBlocklist = ImmutableList.of(
-        new QueryBlocklistRule("block-wikipedia", 
ImmutableSet.of("wikipedia"), null, null)
+        new DefaultQueryBlocklistRule("block-wikipedia", 
ImmutableSet.of("wikipedia"), null, null)
     );
 
     Assert.assertEquals(expectedBlocklist, actual.getQueryBlocklist());
   }
 
+  @Test
+  public void testSerdeWithExplicitDefaultType() throws Exception
+  {
+    String jsonStr = "{\n"
+                     + "  \"queryBlocklist\": [\n"
+                     + "    {\n"
+                     + "      \"type\": \"default\",\n"
+                     + "      \"ruleName\": \"block-wikipedia\",\n"
+                     + "      \"dataSources\": [\"wikipedia\"]\n"
+                     + "    }\n"
+                     + "  ]\n"
+                     + "}\n";
+
+    BrokerDynamicConfig actual = mapper.readValue(jsonStr, 
BrokerDynamicConfig.class);
+
+    Assert.assertEquals(1, actual.getQueryBlocklist().size());
+    Assert.assertTrue(actual.getQueryBlocklist().get(0) instanceof 
DefaultQueryBlocklistRule);
+    Assert.assertEquals(
+        new DefaultQueryBlocklistRule("block-wikipedia", 
ImmutableSet.of("wikipedia"), null, null),
+        actual.getQueryBlocklist().get(0)
+    );
+  }
+
   @Test
   public void testSerdeWithNullBlocklist() throws Exception
   {
@@ -108,11 +132,11 @@ public class BrokerDynamicConfigTest
     Assert.assertNotNull(actual.getQueryBlocklist());
     Assert.assertEquals(2, actual.getQueryBlocklist().size());
 
-    QueryBlocklistRule rule1 = actual.getQueryBlocklist().get(0);
+    DefaultQueryBlocklistRule rule1 = (DefaultQueryBlocklistRule) 
actual.getQueryBlocklist().get(0);
     Assert.assertEquals("block-scan-queries", rule1.getRuleName());
     Assert.assertEquals(ImmutableSet.of("scan"), rule1.getQueryTypes());
 
-    QueryBlocklistRule rule2 = actual.getQueryBlocklist().get(1);
+    DefaultQueryBlocklistRule rule2 = (DefaultQueryBlocklistRule) 
actual.getQueryBlocklist().get(1);
     Assert.assertEquals("block-context", rule2.getRuleName());
     Assert.assertEquals(ImmutableMap.of("priority", "0"), 
rule2.getContextMatches());
   }


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

Reply via email to