-
Notifications
You must be signed in to change notification settings - Fork 972
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove mutable result from TransactionFrame into test-only classes #3391
Conversation
@@ -984,7 985,8 @@ applyFuzzOperations(LedgerTxn& ltx, PublicKey const& sourceAccount, | |||
{ | |||
auto txFramePtr = createFuzzTransactionFrame(ltx, sourceAccount, begin, end, | |||
app.getNetworkID()); | |||
txFramePtr->attemptApplication(app, ltx); | |||
TransactionResult txRes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the result is not used anywhere, would it make sense to just allocate txRes
in attemptApplication
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
attemptApplication
is also being used in applySetupOperations
where txRes
is used.
|
||
virtual StellarMessage toStellarMessage() const = 0; | ||
|
||
#ifdef BUILD_TESTS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't examined each usage of this, but it seems like you create test tx frames using the special method (makeTestTransactionFromWire
). Would it be possible then to not have these methods in the public interface and only declare/implement them in the respective test-only classes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be difficult, as many of the existing tests are calling those public methods via the base class pointer. To remove those test-only functions from the base class to the "Test" classes would require updating all (hundreds of) callers in the test code to pass in the additional parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not entirely accurate.
See my other comment on TransactionTestFrame
: I think that we should just use TransactionTestFrame
in all test code instead of returning TransactionFrameBase
or TransactionFrame
. The change should be fairly transparent as we usually do things like
auto b1 = root.create("b1", paymentAmount);
(that would return a TransactionTestFrame
instead).
That way we can also move the other "mutators" from TransactionFrame, like addSignature
that should only be used in test/simulation code.
@@ -735,25 747,27 @@ TransactionFrame::insertKeysForTxApply(UnorderedSet<LedgerKey>& keys) const | |||
} | |||
|
|||
void | |||
TransactionFrame::markResultFailed() | |||
TransactionFrame::markResultFailed(TransactionResult& txResult) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this still need to be a member method of TransactionFrame
? Seems like could be just a free internal function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also being called in FuzzTransactionFrame::attemptApplication
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not a big issue, but I think in general it's better for classes to have less responsibilities when possible. Here it seems like the method no longer has anything to do with the TransactionFrame
and could be either a public free function (given that it's reused), a static function or even removed altogether, given that it's one line and for some reason it's the only 'special' case for setting the result code (others seem to be just set in-place). In any case, that's up to your judgement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do see your point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am inclined to remove the method, since it is just one line of setting the result code, and we are already directly setting it for other kinds of code (other than txFAILED
). The only thing I'm concerned is the comments in this function, which points out to some perf implication that caller might need to know. So I'm gonna keep this function for now and maybe think of a better place to put it later.
@@ -300,28 300,28 @@ TransactionFrame::makeOperation(Operation const& op, OperationResult& res, | |||
|
|||
void | |||
TransactionFrame::resetResults(LedgerHeader const& header, int64_t baseFee, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this method still mutates TransactionFrame
(it has mOperations.clear()
call). Not sure if that's in the scope of this PR, just wanted to make sure this doesn't escape our attention.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I also noticed that, I agree this is one place that needs attention.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is because the change in this PR is incomplete: if you look at OperationFrame
, it has a mResult
that gets updated in a bunch of places.
Like when "apply" is called -> it ends up calling OperationFrame::doApply
that mutates the result with things like innerResult().code(CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST);
I think it's just that doApply
is missing a parameter so that it uses the result passed to TransactionFrame::apply
(same thing in other places like doCheckValid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After you remove mResult from
OperationFrame
mOperations` should be set once and be immutable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the reason this is incomplete and unsafe is that OperationFrame::mResult
ends up caching a reference to the Result that you use, this in turn can have catastrophic implications in production code if the the local variable goes out of scope before the TransactionFrame gets deleted.
e0e2539
to
7ee03bc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work. This PR removes mResult
from TransactionFrame
without requiring many test changes, but it does mix in more #ifdef BUILD_TESTS
code into non-test code, which isn't ideal.
I think Nico had an idea to use a wrapper class in tests (instead of using inheritance) where we replace TransactionFramePtr
with a class that holds the TransactionFramePtr
and txResult
. The wrapper could imitate the TransactionFrame
interface to keep test changes to a minimum, and the txResult
member can be passed in wherever you need This would allow you to remove the new #ifdef BUILD_TESTS
blocks. I'm not sure yet if there are issues with this approach, but it's something we should discuss.
src/ledger/LedgerManagerImpl.cpp
Outdated
{ | ||
LedgerTxn ltxTx(ltx); | ||
tx->processFeeSeqNum(ltxTx, baseFee); | ||
txs[i]->processFeeSeqNum(ltxTx, baseFee, txResults[i]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method makes the assumption that txResults
is the same size as txs
, but doesn't check that. I would use txResults.at(i)
instead for bounds checking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is a good catch. Also, I realize there already exists index
that does what i
does so I'm using that instead.
src/ledger/LedgerManagerImpl.cpp
Outdated
{ | ||
ZoneNamedN(txZone, "applyTransaction", true); | ||
auto txTime = mTransactionApply.TimeScope(); | ||
TransactionMeta tm(2); | ||
auto& tx = txs[i]; | ||
auto& txRes = txResults[i]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above about txResults.at(i)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
Although I realize this code is not in a try...catch
block like processFeesSeqNums
is and if out-of-bound exception did occur we are not handling it. I wonder if we should catch and throw here.
@@ -24,4 28,24 @@ TransactionFrameBase::makeTransactionFromWire(Hash const& networkID, | |||
abort(); | |||
} | |||
} | |||
#ifdef BUILD_TESTS | |||
TransactionFrameBasePtr | |||
TransactionFrameBase::makeTestTransactionFromWire( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this in TransactionFrameBase
? Can't you put this in a test file? Maybe TxTests
?
f6b537b
to
2f606ad
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good start, but the changes are only partial. I put a few recommendations in the comments that should allow to get this one over the line
namespace stellar | ||
{ | ||
|
||
class TestTransactionFrame : public TransactionFrame |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class can be reworked to avoid more copy/paste down the line: you can just add a member std::shared_ptr<TransactionFrameBase> mTx
and derive from TransactionFrameBase
-> you can implement a proxy pattern and update mResult
as needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
update: I am not even sure you should derive from TransactionFrameBase
.
- test code needs the version of the methods that don't have a "result"
- the whole
syncResult
thing is misleading and does not catch all bugs if people are mixing using a local variable and using this class' member, so there is no real point in trying to keep those things in sync - for the few places where you need a
TransactionFrameBase
, you can just get aconst
reference tomTx
.
@@ -300,28 300,28 @@ TransactionFrame::makeOperation(Operation const& op, OperationResult& res, | |||
|
|||
void | |||
TransactionFrame::resetResults(LedgerHeader const& header, int64_t baseFee, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is because the change in this PR is incomplete: if you look at OperationFrame
, it has a mResult
that gets updated in a bunch of places.
Like when "apply" is called -> it ends up calling OperationFrame::doApply
that mutates the result with things like innerResult().code(CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST);
I think it's just that doApply
is missing a parameter so that it uses the result passed to TransactionFrame::apply
(same thing in other places like doCheckValid
@@ -300,28 300,28 @@ TransactionFrame::makeOperation(Operation const& op, OperationResult& res, | |||
|
|||
void | |||
TransactionFrame::resetResults(LedgerHeader const& header, int64_t baseFee, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After you remove mResult from
OperationFrame
mOperations` should be set once and be immutable
|
||
virtual StellarMessage toStellarMessage() const = 0; | ||
|
||
#ifdef BUILD_TESTS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not entirely accurate.
See my other comment on TransactionTestFrame
: I think that we should just use TransactionTestFrame
in all test code instead of returning TransactionFrameBase
or TransactionFrame
. The change should be fairly transparent as we usually do things like
auto b1 = root.create("b1", paymentAmount);
(that would return a TransactionTestFrame
instead).
That way we can also move the other "mutators" from TransactionFrame, like addSignature
that should only be used in test/simulation code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a few clarifying comments
{ | ||
|
||
void | ||
TestTransactionFrame::resetResults(LedgerHeader const& header, int64_t baseFee, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm adding a few more comments
namespace stellar | ||
{ | ||
|
||
class TestTransactionFrame : public TransactionFrame |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
update: I am not even sure you should derive from TransactionFrameBase
.
- test code needs the version of the methods that don't have a "result"
- the whole
syncResult
thing is misleading and does not catch all bugs if people are mixing using a local variable and using this class' member, so there is no real point in trying to keep those things in sync - for the few places where you need a
TransactionFrameBase
, you can just get aconst
reference tomTx
.
StellarMessage toStellarMessage() const override; | ||
|
||
static TransactionEnvelope | ||
convertInnerTxToV1(TransactionEnvelope const& envelope); | ||
|
||
#ifdef BUILD_TESTS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: you don't need any of this we directly use TestTransactionFrame
in test code
@@ -300,28 300,28 @@ TransactionFrame::makeOperation(Operation const& op, OperationResult& res, | |||
|
|||
void | |||
TransactionFrame::resetResults(LedgerHeader const& header, int64_t baseFee, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the reason this is incomplete and unsafe is that OperationFrame::mResult
ends up caching a reference to the Result that you use, this in turn can have catastrophic implications in production code if the the local variable goes out of scope before the TransactionFrame gets deleted.
@@ -27,11 27,12 @@ class TransactionFrameBase | |||
TransactionEnvelope const& env); | |||
|
|||
virtual bool apply(Application& app, AbstractLedgerTxn& ltx, | |||
TransactionMeta& meta) = 0; | |||
TransactionMeta& meta, TransactionResult& txResult) = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all methods exposed here should now be const
(this would allow you to easily find remaining bugs related to mutating the object/caching)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are any non const methods left in derived classes, it probably means they belong to TestTransactionFrame
(or there is something we missed outside of "result", in which case let me know!)
closing: stale and unlikely to land anytime soon |
Description
Resolves "TransactionFrame" part of #2163
Refactor for
TransactionFrame
class-family (includingTransactionFrame
,TransactionFrameBase
,FeeBumpTransactionFrame
,Fuzzer*TransactionFrame
,TxSim*TransactionFrame
).Make the class immutable by removing the member
TransactionResult
, instead pass them externally.Separate test code from production code by creating
TestTransactionFrame
andTestFeeBumpTransactionFrame
that contains the old (mutable) behavior. This is to save from changing boilerplate code in tests everywhere that interacts withTransactionFrameBase
(and its derived classes).Checklist
clang-format
v8.0.0 (viamake format
or the Visual Studio extension)