Skip to content

CASSANDRA-21139 - Feature/guardrail for misprepare statements#4596

Open
omniCoder77 wants to merge 16 commits intoapache:trunkfrom
omniCoder77:feature/guardrail-for-misprepare-statements
Open

CASSANDRA-21139 - Feature/guardrail for misprepare statements#4596
omniCoder77 wants to merge 16 commits intoapache:trunkfrom
omniCoder77:feature/guardrail-for-misprepare-statements

Conversation

@omniCoder77
Copy link
Contributor

Validation is posted on Jira Ticket

Feature suggestion:
Guardrail for miss-prepared statements

Description:

We have hundreds of application teams and several dozen of them miss-prepare statements by using literals instead of bind markers.

I.e.,

// wrong 
session.prepare("select * from users where ID = 996");
session.prepare("select * from users where ID = 997");
session.prepare("select * from users where ID = 998");
session.prepare("select * from users where ID = 999");

// correct
session.prepare("select * from users where ID = ?");

The problem causes the prepared statement cache to constantly overflow, and will print a prepared statements discarded WARN message in the Cassandra log. At present, we use a wack-a-mole approach to discuss the problem with each development team individually, and hope they fix it and train the entire team on how to prepare statements correctly.

Also, finding the root cause of the issue today requires having the knowledge and access to look at the system.prepared_statements table.

Guardrails would seem a good approach here, where the guard could WARN or REJECT when a statement was prepared using a WHERE clause and no bind markers.

Note, this should not prevent users from creating prepared statements without a WHERE clause or with one or more literal values so long as there was at least one bind marker. Thus, the following would remain valid:

session.prepare("select * from users");
session.prepare("select * from users where TYPE=5 and ID = ?");

Approach:
Introduced a boolean flag use_misprepare_statements_enabled (which can be configured from cassandra.yaml) whose default value is true (backward compatibility) and added functions to StorageServiceMBean to enable dynamic runtime configuration.
Added test cases to validate changes in parseAndPrepare function.


private static void checkMispreparedGuardrail(CQLStatement statement, ClientState clientState)
{
if (clientState.isInternal)
Copy link
Contributor

@smiklosovic smiklosovic Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just move this to above if, this method will have easier job / will be more focused, logic wise. I have not check the details yet but it is interesting to see that if isInternal is false then clientState.internal can be still true. Should not be these two just aligned so only one is necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remove the clientState.isInternal check, is checkMispreparedGuardrail is called only if it's false.

at line 218 isInternal parameter parameter is false, but clientState is ClientState.forInternalCalls(), hence clientState.isInternal will be true.

}
else
{
String msg = "Performance Tip: This query contains literal values in the WHERE clause. " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you do private static final string in this class from this string?

return;
}

if (isMisprepared(statement))
Copy link
Contributor

@smiklosovic smiklosovic Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make this logic faster? If it is indeed prepared then you are evaluating it unnecessarily which would slow it down. What is the least amount of test to do to run through that "heavy" logic? It is the fact whether our guardrail is enabled in the first place, right? So base the logic on guardrail evaluation first and then we will check if it is all prepared or not - the heavy logic done only when must.

If you are not sure which approach is faster then run a stress test and profile it with async profiler.

Also, for example, superusers completely bypass guardrails so for them you would call isMisprepared completely unnecessarily.

/**
* Guardrail on mis-prepared statements.
*/
public static final EnableFlag MispreparedStatementsEnabled =
Copy link
Contributor

@smiklosovic smiklosovic Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start the guardrail name with lower-case as others are. Also, why is this (to me correctly) called "mispreparedStatementsEnabled" but the config introduces use_misprepare_statements_enabled ? Just align it, no?

boolean getVectorTypeEnabled();

/**
* @return Whether use misprepared statements is enabled. If not enabled, misprepared statements will fail
Copy link
Contributor

@smiklosovic smiklosovic Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returns true if the usage of mis-prepared statements is enabled, false otherwise. Returns true by default.

You can also expand on what the mis-prepared statement is and when it is considered so (also what happens with batches etc, more descriptive better)

Wording might change if you rename it to "disallowMispreparedStatements" as semantics would change

#
# When true, allows the use of misprepared statements, only warns in the logs.
# When false, misprepared statements will result in an error to the client. Default is true.
# use_misprepare_statements_enabled: false
Copy link
Contributor

@smiklosovic smiklosovic Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if the name of this was "disallow_misprepared_statements: true". If you look into other guardrails, all of them follow the logic that they are set to "true".

Then you will have way easier job later in the code as you can just do

Guardrails.disallowMispreparedStatements.ensureEnabled(state) / enabled

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But enabled will still be necessary, because if disallow_misprepared_statements is false, we will have to warn against use of mis prepared statements as the ticket suggests
Although I can change the name to disallow_misprepared_statements in next PR, if needed.

# maximum_replication_factor_warn_threshold: -1
# maximum_replication_factor_fail_threshold: -1
#
# When true, allows the use of misprepared statements, only warns in the logs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allows the usage, "use" is not a noun.

public volatile boolean user_timestamps_enabled = true;
public volatile boolean alter_table_enabled = true;
public volatile boolean group_by_enabled = true;
public volatile boolean misprepared_statements_enabled = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so is it true or false? I am confused now. Should be true here, no? By default we want to just warn.


public void setMispreparedStatementsEnabled(boolean enabled)
{
updatePropertyWithLogging("use_misprepare_statements_enabled",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be called "misprepared_statements_enabled"

public static final CQLMetrics metrics = new CQLMetrics();


private static final String msg = "Performance Tip: This query contains literal values in the WHERE clause. " +
Copy link
Contributor

@smiklosovic smiklosovic Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you be more descriptive? "msg" is pretty non-telling. "MISPREPARED_STATEMENT_MESSAGE" is way better.

Copy link
Contributor Author

@omniCoder77 omniCoder77 Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, msg was way too generic. I’ve updated the variable name as per your suggestion.

Quick question on the naming: since this is technically a warning rather than a hard error, would you prefer MISPREPARED_STATEMENT_WARN_MESSAGE, or should I stick with your suggestion of MISPREPARED_STATEMENT_MESSAGE to keep it concise?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MISPREPARED_STATEMENT_WARN_MESSAGE

public void setMispreparedStatementsEnabled(boolean enabled)
{
DatabaseDescriptor.setMispreparedStatementsEnabled(enabled);
logger.info("Updated misprepared_statements_enabled to {}", enabled);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why logging

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this logging be kept for auditing (as we removed logging from DatabaseDescriptor).

return res;
}

private static void checkMispreparedGuardrail(CQLStatement statement, ClientState clientState)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not completely sure about this logic, nothing about what you did specifically ... I just need to check this more closely and I don't have time for that right now. This is very nuanced stuff.

if (user != null && user.isSuper())
return;

if (Guardrails.mispreparedStatementsEnabled.enabled(clientState))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is not the logic here actually the other way around? if it is enabled then we are going to just warn, if it is disabled (false in yaml), then we fail, "use_misprepared_statements" set to "false" means that we CAN NOT use them, so we need to fail in that situation. If it is true then we just warn.

You also do not need to use "NoSpamLogger.log". Check what Guardrail.warn is doing (EnableFlag subclass of Guardrail so you can call .warn() here too). That already warns in a non-spamming manner and does other things (diagnostic events etc).

@smiklosovic smiklosovic self-requested a review February 2, 2026 01:08
Copy link
Contributor

@smiklosovic smiklosovic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more comments

CQLStatement statement = raw.prepare(clientState);
statement.validate(clientState);

if (!isInternal && !clientState.isInternal)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if we should add the logic, conceptually, into this class. If you check close enough then you see that there is "statement.validate()" just above. If you check what, for example, SelectStatement.validate() is doing, it contains guardrails too.

We would just enrich validate methods for each respective statement kind you want to check here instead of having arbitrary logic in this class directly. I think that is way cleaner approach.

Copy link
Contributor

@smiklosovic smiklosovic Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also means that if we do not put it into "validate" method for each respective statement then calling "validate" itself would pass but in fact it would contain mis prepared statement. I do not think that is a good way do that. By putting that logic to validate method, we are centralizing it all which is better design and the logic does not "leak" outside of it.

@omniCoder77
Copy link
Contributor Author

@smiklosovic
I have addressed above comments and

Added new test cases verifying warning behaviour (warn only when misprepared_statements_enabled is true) and to bypass misprepare guardrail for system keyspaces.

Centralized onMisprepare logic to Guardrails.java

Refined test case to extract repeating statements to a function, increase readability.

@omniCoder77
Copy link
Contributor Author

In my test cases I have tested

  • System keyspaces, internal calls and super users are exempted from this guardrail check
  • Confirmed literals in the SELECT clause are allowed as long as the WHERE clause is prepared.
  • Verified use is warned if and only if
    • misprepared_statement_use is set to true and statement is misprepared
    • isn't internal call
    • user isn't super user.
  • Verified no warning when guardrail is violated, instead GuardrailViolationException
  • Verified blocks for literals in WHERE clauses, IN clauses, LWT IF conditions, and JSON inserts.
  • Confirmed that blocked queries do not enter the statement cache
  • Exempts super user and internal call from this guardrail

@omniCoder77 omniCoder77 force-pushed the feature/guardrail-for-misprepare-statements branch from 311028f to 44bba46 Compare February 4, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants