Fix nullptr resolving, no db config - #6029
Conversation
7387b69 to
1d59288
Compare
| @@ -171,7 +171,7 @@ getRowsMinMax(soci::session& session, TableType type) | |||
| bool | |||
| saveValidatedLedger( | |||
| DatabaseCon& ldgDB, | |||
| DatabaseCon& txnDB, | |||
| std::unique_ptr<DatabaseCon>& txnDB, | |||
There was a problem hiding this comment.
Sorry, I don't understand how this is solving the issue of potentially null txnDB. Instead of resolving this ptr in SQLiteDatabaseImp::saveValidatedLedger, we are now resolving it in Node::saveValidatedLedger.
Shouldn't we rather add some checks on this pointer before calling Node::saveValidatedLedger? Or maybe assert the validity of the pointer in SQLiteDatabaseImp::saveValidatedLedger (assuming ripple's version of assert will throw in release mode.)
There was a problem hiding this comment.
Please check line 255, if (app.config().useTxTables()) is a condition for db present
There was a problem hiding this comment.
Yeah, but does that also guarantee that the pointer will be valid? I believe the check only confirms that there's an entry in the config. We should ideally confirm if the database connection was successful.
There was a problem hiding this comment.
It is checked in DB connection creation, under the same condition - see node.cpp:64
There was a problem hiding this comment.
while this is correct currently - it might not be always so. This is very smelly solution. Also a reference to a unique ptr is another bad pattern.
In case you want to resolve it more a less normal, please either pass a raw pointer, or an optional<reference_wrapper...>. Check them at the point where you dereference them
There was a problem hiding this comment.
I think it'd be nice to have a helper class borrowed_ptr<T> which doesn't do anything except that it's borrowing the pointer and the ownership is managed by something else like a shared_ptr or a unique_ptr, but it sounds like a separate task.
But IMHO, I don't think raw pointers are bad at all and we shouldn't be scared of them.
There was a problem hiding this comment.
I don't understand the problem of passing unique_ptr ref. It is the same thing as passing vector / shared_ptr or any other container by ref. "Borrowing" pointer, direct pointer - why to use them, when you don't know their lifetime and even check that they are not null doesn't guarantee you anything? There is already pointer without ownership in std - weak_ptr, and there is a good reason why you can't use it directly.
There was a problem hiding this comment.
We shouldn’t expose the type of the pointer to the caller. i.e. The caller shouldn’t have to have a unique_ptr or a shared_ptr because the function only wants to take an optional value, it does not care who manages the resources and it’s leaking abstraction.
There was a problem hiding this comment.
Unique ptr is not some special container. We don't pass pointers instead of array/vector reference to maintain more abstraction. Using pointers without real need is a bad pattern.
There was a problem hiding this comment.
I don’t think a raw pointer is a bad pattern
| @@ -171,7 +171,7 @@ getRowsMinMax(soci::session& session, TableType type) | |||
| bool | |||
| saveValidatedLedger( | |||
| DatabaseCon& ldgDB, | |||
| DatabaseCon& txnDB, | |||
| std::unique_ptr<DatabaseCon>& txnDB, | |||
There was a problem hiding this comment.
while this is correct currently - it might not be always so. This is very smelly solution. Also a reference to a unique ptr is another bad pattern.
In case you want to resolve it more a less normal, please either pass a raw pointer, or an optional<reference_wrapper...>. Check them at the point where you dereference them
7291f34 to
e234fc0
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #6029 +/- ##
=======================================
Coverage 78.6% 78.6%
=======================================
Files 818 818
Lines 68981 68981
Branches 8248 8238 -10
=======================================
+ Hits 54191 54205 +14
+ Misses 14790 14776 -14
🚀 New features to boost your workflow:
|
|
I'm going to approve it as there's nothing more we can do, and it solves the problem. |
|
@vvysokikh1 please close your change request |
sorry for the delay, I thought I discarded my review back when I agreed with your point |
|
I see that other reviewers are fine with this change, so I am not going to block this. But I don't like the idea that, since one part of the code In this PR, I am not suggesting to traverse the whole repo and update code which doesn't check validity of |
I did't dig that deep, may be there is situation when txnDB can be valid DB connection, but config still doesn't allow to use txnTables, and that logic connected to config settings, not just to availability of the DB. This PR fix concrete problem with the fewest possible changes and try to don't change any other logic. |
Even then, replacing if (app.config().useTxTables())
{
auto db = txnDB->checkoutDb();with if (app.config().useTxTables() && txnDB)
{
auto db = txnDB->checkoutDb();would be much better. |
e234fc0 to
7a2a5bc
Compare
|
Added |
Thanks |
553cea4 to
700c50e
Compare
|
Change it to throw an error |
| if (!txnDB) | ||
| { | ||
| JLOG(j.fatal()) << "TxTables db isn't available"; | ||
| Throw<std::runtime_error>("TxTables db isn't available"); |
There was a problem hiding this comment.
Now a runtime error is a change in behaviour of the program. I am not sure if this should be the expected behaviour, so I will let others confirm this.
There was a problem hiding this comment.
Disagree. Adding check if (app.config().useTxTables() && txnDB) will silently continue the code which is change of the current behavior. That's why I change it to throwing error, at least it will reproduce debug behavior, cause release behavior is undefined in case of nullptr.
There was a problem hiding this comment.
Since the old code was buggy, we don't need to consider it's behaviour anymore. You now have the power to decide the new behaviour of the new code. And throwing a runtime error is such change. That's why it is important to consider if we want to crash the app and let it be relaunched, or throw then catch and then retry creating the DB connection at this point. Or even just not save the ledger state here and spit an error message(discard the transaction? Initiate a resync?). And such decisions need to be agreed upon and well documented. Sometimes such trivial looking changes could have an unexpected impact on the product.
There was a problem hiding this comment.
This additional check (if (!txnDB)) is pure placebo, it will never be reached with the current code (because of exception throwing while establishing connection to the DB). So the behavior is exactly the same as before. If someone will change behavior and will allow empty DB pointer, they will get an exception and will know that they need to make changes to this part of the code too.
In case of if (app.config().useTxTables() && txnDB) check and possible future code change - the code will silently proceed skipping initialization, and developer will miss that they introduce silent bug.
I don't quite understand what is your point ?
There was a problem hiding this comment.
My point is, just deciding to throw a runtime error is the easy way out but may not be the right way forward. Hence it is important to understand all potential routes which could lead to here(this part of the code) and then decide on potential way forward.
For now, this might be good enough. But we should definitely have a more detailed discussion about such patterns and fixing rest of the code later.
There was a problem hiding this comment.
@oleks-rip out of curiosity, is it ever possible for txnDB to be valid at first and then to become invalid over time?
I'm wondering whether the code could be changed to check txnDB on startup and throw right away when app.config().useTxTables() is enabled but txnDB is invalid. Then, each time we get here we only need to check if (!txnDB) and can remove the app.config().useTxTables() check + throw.
There was a problem hiding this comment.
The reason why it is implemented here like this is to NOT to change existing logic.
Your proposal is most straightforward check that should be implemented.
But, theoretically, txnDB can be closed and set pointer to null at any time in the program for multiple reasons (errorrs, hardware failures, db driver exceptions, some multithread collisions). And we need to check all the use cases for the DB to be ensure. Such logic improvement wasn't the purpose of this patch, which is small and intended only to fix a crash.
There was a problem hiding this comment.
Thanks - this is very helpful.
a09a72c to
26953d0
Compare
8f9635f to
a37c032
Compare
a37c032 to
a53555d
Compare
If the config disables SQL db usage, such as a validator: ``` [ledger_tx_tables] use_tx_tables = 0 ``` then the pointer to DB engine is null, but it was still resolved during startup. Although it didn't crash in Release mode, possibly due to the compiler optimizing it away, it did crash in Debug mode. This change explicitly checks for the validity of the pointer and generates a runtime error if not set.
If the config disables SQL db usage, such as a validator: ``` [ledger_tx_tables] use_tx_tables = 0 ``` then the pointer to DB engine is null, but it was still resolved during startup. Although it didn't crash in Release mode, possibly due to the compiler optimizing it away, it did crash in Debug mode. This change explicitly checks for the validity of the pointer and generates a runtime error if not set.
If the config disables SQL db usage, such as a validator: ``` [ledger_tx_tables] use_tx_tables = 0 ``` then the pointer to DB engine is null, but it was still resolved during startup. Although it didn't crash in Release mode, possibly due to the compiler optimizing it away, it did crash in Debug mode. This change explicitly checks for the validity of the pointer and generates a runtime error if not set.
If the config disables SQL db usage, such as a validator: ``` [ledger_tx_tables] use_tx_tables = 0 ``` then the pointer to DB engine is null, but it was still resolved during startup. Although it didn't crash in Release mode, possibly due to the compiler optimizing it away, it did crash in Debug mode. This change explicitly checks for the validity of the pointer and generates a runtime error if not set.
High Level Overview of Change
Fix resolving nullptr
Context of Change
If config disable sql db usage (like validator)
then pointer to DB engine is null, but still resolved during startup. I suppose that there is no crash for now because of c++ optimizer which drop the code with resolving pointer. Reproduced 100% in debug mode.
Type of Change
.gitignore, formatting, dropping support for older tooling)