CASSANDRA-21139 - Feature/guardrail for misprepare statements#4596
CASSANDRA-21139 - Feature/guardrail for misprepare statements#4596omniCoder77 wants to merge 16 commits intoapache:trunkfrom
Conversation
… CRUD, IN, and LWT operations
|
|
||
| private static void checkMispreparedGuardrail(CQLStatement statement, ClientState clientState) | ||
| { | ||
| if (clientState.isInternal) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. " + |
There was a problem hiding this comment.
can you do private static final string in this class from this string?
| return; | ||
| } | ||
|
|
||
| if (isMisprepared(statement)) |
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
conf/cassandra.yaml
Outdated
| # | ||
| # 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
conf/cassandra.yaml
Outdated
| # 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. |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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. " + |
There was a problem hiding this comment.
can you be more descriptive? "msg" is pretty non-telling. "MISPREPARED_STATEMENT_MESSAGE" is way better.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
MISPREPARED_STATEMENT_WARN_MESSAGE
| public void setMispreparedStatementsEnabled(boolean enabled) | ||
| { | ||
| DatabaseDescriptor.setMispreparedStatementsEnabled(enabled); | ||
| logger.info("Updated misprepared_statements_enabled to {}", enabled); |
There was a problem hiding this comment.
Should this logging be kept for auditing (as we removed logging from DatabaseDescriptor).
| return res; | ||
| } | ||
|
|
||
| private static void checkMispreparedGuardrail(CQLStatement statement, ClientState clientState) |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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).
| CQLStatement statement = raw.prepare(clientState); | ||
| statement.validate(clientState); | ||
|
|
||
| if (!isInternal && !clientState.isInternal) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@smiklosovic Added new test cases verifying warning behaviour (warn only when Centralized onMisprepare logic to Guardrails.java Refined test case to extract repeating statements to a function, increase readability. |
|
In my test cases I have tested
|
311028f to
44bba46
Compare
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.,
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:
Approach:
Introduced a boolean flag
use_misprepare_statements_enabled(which can be configured fromcassandra.yaml) whose default value is true (backward compatibility) and added functions toStorageServiceMBeanto enable dynamic runtime configuration.Added test cases to validate changes in
parseAndPreparefunction.