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

adelapena 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 bf1446c  Add guardrail for query page size
bf1446c is described below

commit bf1446cd85ca476ca3e6f53ed3e13e18697acfbc
Author: Kowalczyk <bkowalcz...@gmail.com>
AuthorDate: Sat Dec 11 13:17:39 2021 +0000

    Add guardrail for query page size
    
    patch by Bartlomiej; reviewed by Andrés de la Peña and Brandon Williams for 
CASSANDRA-17189
---
 CHANGES.txt                                        |   1 +
 conf/cassandra.yaml                                |   5 +
 .../apache/cassandra/config/GuardrailsOptions.java |   9 ++
 .../org/apache/cassandra/cql3/QueryProcessor.java  |   4 +-
 .../cassandra/cql3/statements/BatchStatement.java  |   4 +-
 .../cassandra/cql3/statements/DeleteStatement.java |   2 +-
 .../cql3/statements/ModificationStatement.java     |   8 +-
 .../cassandra/cql3/statements/SelectStatement.java |  16 ++-
 .../cassandra/cql3/statements/UpdateStatement.java |   2 +-
 .../apache/cassandra/db/guardrails/Guardrails.java |  30 ++++
 .../cassandra/db/guardrails/GuardrailsConfig.java  |   5 +
 .../cassandra/db/guardrails/GuardrailsMBean.java   |  18 +++
 .../db/guardrails/GuardrailPageSizeTest.java       | 154 +++++++++++++++++++++
 13 files changed, 243 insertions(+), 15 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 59a9ea4..5a99669 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.1
+ * Add guardrail for query page size (CASSANDRA-17189)
  * Allow column_index_size_in_kb to be configurable through nodetool 
(CASSANDRA-17121)
  * Emit a metric for number of local read and write calls 
  * Add non-blocking mode for CDC writes (CASSANDRA-17001)
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index 1d93632..b0c3cb2 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -1607,3 +1607,8 @@ enable_drop_compact_storage: false
 #         disallowed: []
 # Guardrail to allow/disallow user-provided timestamps. Defaults to true.
 #     user_timestamps_enabled: true
+# Guardrail to warn or abort when using a page size greater than threshold.
+# The two thresholds default to -1 to disable.
+#     page_size:
+#       warn_threshold: -1
+#       abort_threshold: -1
diff --git a/src/java/org/apache/cassandra/config/GuardrailsOptions.java 
b/src/java/org/apache/cassandra/config/GuardrailsOptions.java
index a6892d4..aa512f3 100644
--- a/src/java/org/apache/cassandra/config/GuardrailsOptions.java
+++ b/src/java/org/apache/cassandra/config/GuardrailsOptions.java
@@ -57,6 +57,8 @@ public class GuardrailsOptions implements GuardrailsConfig
     public final IntThreshold secondary_indexes_per_table = new IntThreshold();
     public final IntThreshold materialized_views_per_table = new 
IntThreshold();
     public final TableProperties table_properties = new TableProperties();
+    public final IntThreshold page_size = new IntThreshold();
+
     public volatile boolean user_timestamps_enabled = true;
 
     public void validate()
@@ -66,6 +68,7 @@ public class GuardrailsOptions implements GuardrailsConfig
         
secondary_indexes_per_table.validate("guardrails.secondary_indexes_per_table");
         
materialized_views_per_table.validate("guardrails.materialized_views_per_table");
         table_properties.validate("guardrails.table_properties");
+        page_size.validate("guardrails.page_size");
     }
 
     @Override
@@ -120,6 +123,12 @@ public class GuardrailsOptions implements GuardrailsConfig
         return user_timestamps_enabled;
     }
 
+    @Override
+    public IntThreshold getPageSize()
+    {
+        return page_size;
+    }
+
     public void setUserTimestampsEnabled(boolean enabled)
     {
         user_timestamps_enabled = enabled;
diff --git a/src/java/org/apache/cassandra/cql3/QueryProcessor.java 
b/src/java/org/apache/cassandra/cql3/QueryProcessor.java
index ed99861..80ba508 100644
--- a/src/java/org/apache/cassandra/cql3/QueryProcessor.java
+++ b/src/java/org/apache/cassandra/cql3/QueryProcessor.java
@@ -763,13 +763,13 @@ public class QueryProcessor implements QueryHandler
             {
                 ModificationStatement modificationStatement = 
((ModificationStatement) statement);
                 statementKsName = modificationStatement.keyspace();
-                statementCfName = modificationStatement.columnFamily();
+                statementCfName = modificationStatement.table();
             }
             else if (statement instanceof SelectStatement)
             {
                 SelectStatement selectStatement = ((SelectStatement) 
statement);
                 statementKsName = selectStatement.keyspace();
-                statementCfName = selectStatement.columnFamily();
+                statementCfName = selectStatement.table();
             }
             else if (statement instanceof BatchStatement)
             {
diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java 
b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java
index 054541c..24877ef 100644
--- a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java
@@ -233,10 +233,10 @@ public class BatchStatement implements CQLStatement
             String cfName = null;
             for (ModificationStatement stmt : statements)
             {
-                if (ksName != null && (!stmt.keyspace().equals(ksName) || 
!stmt.columnFamily().equals(cfName)))
+                if (ksName != null && (!stmt.keyspace().equals(ksName) || 
!stmt.table().equals(cfName)))
                     throw new InvalidRequestException("Batch with conditions 
cannot span multiple tables");
                 ksName = stmt.keyspace();
-                cfName = stmt.columnFamily();
+                cfName = stmt.table();
             }
         }
     }
diff --git a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java 
b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java
index 9ac29a0..be01481 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java
@@ -198,6 +198,6 @@ public class DeleteStatement extends ModificationStatement
     @Override
     public AuditLogContext getAuditLogContext()
     {
-        return new AuditLogContext(AuditLogEntryType.DELETE, keyspace(), 
columnFamily());
+        return new AuditLogContext(AuditLogEntryType.DELETE, keyspace(), 
table());
     }
 }
diff --git 
a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java 
b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
index b6abdca..0a35e1f 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
@@ -202,7 +202,7 @@ public abstract class ModificationStatement implements 
CQLStatement.SingleKeyspa
         return metadata.keyspace;
     }
 
-    public String columnFamily()
+    public String table()
     {
         return metadata.name;
     }
@@ -247,7 +247,7 @@ public abstract class ModificationStatement implements 
CQLStatement.SingleKeyspa
 
         // MV updates need to get the current state from the table, and might 
update the views
         // Require Permission.SELECT on the base table, and Permission.MODIFY 
on the views
-        Iterator<ViewMetadata> views = View.findAll(keyspace(), 
columnFamily()).iterator();
+        Iterator<ViewMetadata> views = View.findAll(keyspace(), 
table()).iterator();
         if (views.hasNext())
         {
             state.ensureTablePermission(metadata, Permission.SELECT);
@@ -492,7 +492,7 @@ public abstract class ModificationStatement implements 
CQLStatement.SingleKeyspa
         CQL3CasRequest request = makeCasRequest(queryState, options);
 
         try (RowIterator result = StorageProxy.cas(keyspace(),
-                                                   columnFamily(),
+                                                   table(),
                                                    request.key,
                                                    request,
                                                    
options.getSerialConsistency(),
@@ -550,7 +550,7 @@ public abstract class ModificationStatement implements 
CQLStatement.SingleKeyspa
 
     private ResultSet buildCasResultSet(RowIterator partition, QueryState 
state, QueryOptions options)
     {
-        return buildCasResultSet(keyspace(), columnFamily(), partition, 
getColumnsWithConditions(), false, state, options);
+        return buildCasResultSet(keyspace(), table(), partition, 
getColumnsWithConditions(), false, state, options);
     }
 
     static ResultSet buildCasResultSet(String ksName,
diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java 
b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
index d2643d1..7a710cc 100644
--- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
 import org.apache.cassandra.audit.AuditLogContext;
 import org.apache.cassandra.audit.AuditLogEntryType;
 import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.Schema;
 import org.apache.cassandra.schema.TableMetadata;
@@ -215,7 +216,7 @@ public class SelectStatement implements 
CQLStatement.SingleKeyspaceCqlStatement
     {
         if (table.isView())
         {
-            TableMetadataRef baseTable = View.findBaseTable(keyspace(), 
columnFamily());
+            TableMetadataRef baseTable = View.findBaseTable(keyspace(), 
table());
             if (baseTable != null)
                 state.ensureTablePermission(baseTable, Permission.SELECT);
         }
@@ -256,7 +257,8 @@ public class SelectStatement implements 
CQLStatement.SingleKeyspaceCqlStatement
 
         QueryPager pager = getPager(query, options);
 
-        return execute(Pager.forDistributedQuery(pager, cl, 
state.getClientState()),
+        return execute(state,
+                       Pager.forDistributedQuery(pager, cl, 
state.getClientState()),
                        options,
                        selectors,
                        pageSize,
@@ -379,7 +381,8 @@ public class SelectStatement implements 
CQLStatement.SingleKeyspaceCqlStatement
         }
     }
 
-    private ResultMessage.Rows execute(Pager pager,
+    private ResultMessage.Rows execute(QueryState state,
+                                       Pager pager,
                                        QueryOptions options,
                                        Selectors selectors,
                                        int pageSize,
@@ -387,6 +390,8 @@ public class SelectStatement implements 
CQLStatement.SingleKeyspaceCqlStatement
                                        int userLimit,
                                        long queryStartNanoTime) throws 
RequestValidationException, RequestExecutionException
     {
+        Guardrails.pageSize.guard(pageSize, table(), state.getClientState());
+
         if (aggregationSpec != null)
         {
             if (!restrictions.hasPartitionKeyRestrictions())
@@ -461,7 +466,8 @@ public class SelectStatement implements 
CQLStatement.SingleKeyspaceCqlStatement
 
             QueryPager pager = getPager(query, options);
 
-            return execute(Pager.forInternalQuery(pager, executionController),
+            return execute(state,
+                           Pager.forInternalQuery(pager, executionController),
                            options,
                            selectors,
                            pageSize,
@@ -494,7 +500,7 @@ public class SelectStatement implements 
CQLStatement.SingleKeyspaceCqlStatement
         return table.keyspace;
     }
 
-    public String columnFamily()
+    public String table()
     {
         return table.name;
     }
diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java 
b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java
index d8b4685..20df151 100644
--- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java
@@ -334,6 +334,6 @@ public class UpdateStatement extends ModificationStatement
     @Override
     public AuditLogContext getAuditLogContext()
     {
-        return new AuditLogContext(AuditLogEntryType.UPDATE, keyspace(), 
columnFamily());
+        return new AuditLogContext(AuditLogEntryType.UPDATE, keyspace(), 
table());
     }
 }
diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java 
b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
index 62b01ab..d299215 100644
--- a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
+++ b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
@@ -98,6 +98,18 @@ public final class Guardrails implements GuardrailsMBean
     new DisableFlag(state -> 
!CONFIG_PROVIDER.getOrCreate(state).getUserTimestampsEnabled(),
                     "User provided timestamps (USING TIMESTAMP)");
 
+    /**
+     * Guardrail on the number of elements returned within page.
+     */
+    public static final Threshold pageSize =
+    new Threshold(state -> CONFIG_PROVIDER.getOrCreate(state).getPageSize(),
+                  (isWarning, what, value, threshold) ->
+                  isWarning ? format("Query for table %s with page size %s 
exceeds warning threshold of %s.",
+                                     what, value, threshold)
+                            : format("Aborting query for table %s, page size 
%s exceeds abort threshold of %s.",
+                                     what, value, threshold));
+
+
     private Guardrails()
     {
         MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
@@ -268,6 +280,24 @@ public final class Guardrails implements GuardrailsMBean
         DEFAULT_CONFIG.setUserTimestampsEnabled(enabled);
     }
 
+    @Override
+    public int getPageSizeWarnThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getPageSize().getWarnThreshold();
+    }
+
+    @Override
+    public int getPageSizeAbortThreshold()
+    {
+        return (int) DEFAULT_CONFIG.getPageSize().getAbortThreshold();
+    }
+
+    @Override
+    public void setPageSizeThreshold(int warn, int abort)
+    {
+        DEFAULT_CONFIG.getPageSize().setThresholds(warn, abort);
+    }
+
     private static String toCSV(Set<String> values)
     {
         return values == null ? "" : String.join(",", values);
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java 
b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
index 8f64638..df00a8d 100644
--- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
@@ -79,4 +79,9 @@ public interface GuardrailsConfig
      * @return {@code true} if user-provided timestamps are allowed, {@code 
false} otherwise.
      */
     boolean getUserTimestampsEnabled();
+
+    /**
+     * @return The threshold to warn or abort when page size exceeds given 
size.
+     */
+    Threshold.Config getPageSize();
 }
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java 
b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
index a5f6c00..14ea9b0 100644
--- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
@@ -170,4 +170,22 @@ public interface GuardrailsMBean
      * @param enabled {@code true} if user-provided timestamps are allowed, 
{@code false} otherwise.
      */
     void setUserTimestampsEnabled(boolean enabled);
+
+    /**
+     * @return The threshold to warn when requested page size greater than 
threshold.
+     * -1 means disabled.
+     */
+    int getPageSizeWarnThreshold();
+
+    /**
+     * @return The threshold to prevent requesting page with more elements 
than threshold.
+     * -1 means disabled.
+     */
+    int getPageSizeAbortThreshold();
+
+    /**
+     * @param warn The threshold to warn when the requested page size is 
greater than threshold. -1 means disabled.
+     * @param abort The threshold to prevent requesting pages with more 
elements than threshold. -1 means disabled.
+     */
+    void setPageSizeThreshold(int warn, int abort);
 }
diff --git 
a/test/unit/org/apache/cassandra/db/guardrails/GuardrailPageSizeTest.java 
b/test/unit/org/apache/cassandra/db/guardrails/GuardrailPageSizeTest.java
new file mode 100644
index 0000000..4873c57
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailPageSizeTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.db.guardrails;
+
+import java.util.Collections;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.QueryState;
+import org.apache.cassandra.transport.ProtocolVersion;
+
+import static java.lang.String.format;
+
+/**
+ * Tests the guardrail for the page size, {@link Guardrails#pageSize}.
+ */
+public class GuardrailPageSizeTest extends ThresholdTester
+{
+    private static final int PAGE_SIZE_WARN_THRESHOLD = 5;
+    private static final int PAGE_SIZE_ABORT_THRESHOLD = 10;
+
+    public GuardrailPageSizeTest()
+    {
+        super(PAGE_SIZE_WARN_THRESHOLD,
+              PAGE_SIZE_ABORT_THRESHOLD,
+              DatabaseDescriptor.getGuardrailsConfig().getPageSize(),
+              Guardrails::setPageSizeThreshold,
+              Guardrails::getPageSizeWarnThreshold,
+              Guardrails::getPageSizeAbortThreshold);
+    }
+
+    @Before
+    public void setupTest()
+    {
+        createTable("CREATE TABLE IF NOT EXISTS %s (k INT, c INT, v TEXT, 
PRIMARY KEY(k, c))");
+    }
+
+    @Test
+    public void testSelectStatementAgainstPageSize() throws Throwable
+    {
+        // regular query
+        String query = "SELECT * FROM %s";
+        assertPagingValid(query, 3);
+        assertPagingValid(query, PAGE_SIZE_WARN_THRESHOLD);
+        assertPagingWarns(query, 6);
+        assertPagingWarns(query, PAGE_SIZE_ABORT_THRESHOLD);
+        assertPagingAborts(query, 11);
+
+        // aggregation query
+        query = "SELECT COUNT(*) FROM %s WHERE k=0";
+        assertPagingValid(query, 3);
+        assertPagingValid(query, PAGE_SIZE_WARN_THRESHOLD);
+        assertPagingWarns(query, 6);
+        assertPagingWarns(query, PAGE_SIZE_ABORT_THRESHOLD);
+        assertPagingAborts(query, 11);
+
+        // query with limit over thresholds
+        query = "SELECT * FROM %s LIMIT 100";
+        assertPagingValid(query, 3);
+        assertPagingValid(query, PAGE_SIZE_WARN_THRESHOLD);
+        assertPagingWarns(query, 6);
+        assertPagingWarns(query, PAGE_SIZE_ABORT_THRESHOLD);
+        assertPagingAborts(query, 11);
+
+        // query with limit under thresholds
+        query = "SELECT * FROM %s LIMIT 1";
+        assertPagingValid(query, 3);
+        assertPagingValid(query, PAGE_SIZE_WARN_THRESHOLD);
+        assertPagingValid(query, 6);
+        assertPagingValid(query, PAGE_SIZE_ABORT_THRESHOLD);
+        assertPagingValid(query, 11);
+    }
+
+    @Test
+    public void testExcludedUsers() throws Throwable
+    {
+        assertPagingIgnored("SELECT * FROM %s", PAGE_SIZE_WARN_THRESHOLD + 1);
+        assertPagingIgnored("SELECT * FROM %s", PAGE_SIZE_ABORT_THRESHOLD + 1);
+    }
+
+    private void assertPagingValid(String query, int pageSize) throws Throwable
+    {
+        assertValid(() -> executeWithPaging(userClientState, query, pageSize));
+    }
+
+    private void assertPagingIgnored(String query, int pageSize) throws 
Throwable
+    {
+        assertValid(() -> executeWithPaging(superClientState, query, 
pageSize));
+        assertValid(() -> executeWithPaging(systemClientState, query, 
pageSize));
+    }
+
+    private void assertPagingWarns(String query, int pageSize) throws Throwable
+    {
+        assertWarns(() -> executeWithPaging(userClientState, query, pageSize),
+                    format("Query for table %s with page size %s exceeds 
warning threshold of %s.",
+                           currentTable(), pageSize, 
PAGE_SIZE_WARN_THRESHOLD));
+    }
+
+    private void assertPagingAborts(String query, int pageSize) throws 
Throwable
+    {
+        assertAborts(() -> executeWithPaging(userClientState, query, pageSize),
+                     format("Aborting query for table %s, page size %s exceeds 
abort threshold of %s.",
+                            currentTable(), pageSize, 
PAGE_SIZE_ABORT_THRESHOLD));
+    }
+
+    private void executeWithPaging(ClientState state, String query, int 
pageSize)
+    {
+        QueryState queryState = new QueryState(state);
+
+        String formattedQuery = formatQuery(query);
+        CQLStatement statement = QueryProcessor.parseStatement(formattedQuery, 
queryState.getClientState());
+        statement.validate(state);
+
+        QueryOptions options = QueryOptions.create(ConsistencyLevel.ONE,
+                                                   Collections.emptyList(),
+                                                   false,
+                                                   pageSize,
+                                                   null,
+                                                   null,
+                                                   ProtocolVersion.CURRENT,
+                                                   KEYSPACE);
+
+        statement.executeLocally(queryState, options);
+    }
+
+    //not used by page-size guardrail tests.
+    protected long currentValue()
+    {
+        throw new UnsupportedOperationException();
+    }
+}

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

Reply via email to