-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Change DataCache.Add() to never allow overwriting existing values #1177
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1177 +/- ##
=========================================
Coverage ? 65.15%
=========================================
Files ? 199
Lines ? 13605
Branches ? 0
=========================================
Hits ? 8864
Misses ? 4741
Partials ? 0
Continue to review full report at Codecov.
|
@@ -46,19 +46,19 @@ private bool CheckValidators(ApplicationEngine engine) | |||
internal override bool Initialize(ApplicationEngine engine) | |||
{ | |||
if (!base.Initialize(engine)) return false; | |||
engine.Snapshot.Storages.Add(CreateStorageKey(Prefix_MaxBlockSize), new StorageItem | |||
engine.Snapshot.Storages.GetAndChange(CreateStorageKey(Prefix_MaxBlockSize), () => new StorageItem |
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 change to GetAndChange()
?
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 original code relied on the overwriting behaviour of the old Add()
implementation. This now causes unit test failures because the value already exists and an exception is thrown. By using GetAndChange
we basically reproduce the old Add()
behaviour.
Alternatively I can insert some code like the following in all the affected tests to clear the backend data.
foreach (KeyValuePair<StorageKey, StorageItem> pair in snapshot.Storages.Find())
{
snapshot.Storages.DeleteInternal(pair.Key);
}
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 prefer to fix the unit test
I need help in fixing the following test neo/neo.UnitTests/UT_Culture.cs Line 17 in e53f533
It fails on e.g. UT_GasToken, but if I run that test separately it passes fine so I don't know what this specific test is doing to break it. |
If I read something like [TestInitialize]
public void TestSetup()
{
TestBlockchain.InitializeMockNeoSystem();
Store = TestBlockchain.GetStore();
} Then I expect I clearly do not know enough about C# unit testing because I can't determine what test to fix if all tests pass when running individually (see my previous screenshot). I tried adding I'm thinking somebody needs to fix the global isolation problem of the test store. |
Yes, It is a big problem |
Why is this closed? I believe the fix for |
This fix is good. But it make |
@ixje, can you give a title to this FIX? aheuahuea, something to remember the idea behind it. Another thing, could you take a look at #1318? During that tests we noticed some things strange (but I am also not an expert as @erikzhang to use all tools), however, it was noticed that:
Thus, It mad me fell that some improvements on @shargon, thank you for the tips and examples during that tests on Consensus, helping with the manipulation of Cache. |
@vncoelho is this question still relevant as I see it's already merged?
I think that's expected. You create a snapshot, you get a value, you modify it. Now for as long as you keep requesting data from that snapshot it should get your modified state. If you create a second snapshot before calling commit on the first, then the value shouldn't be changed in the second snapshot, also not after committing the first snapshot. Note that this behaviour is specific to LevelDB. Committing with say a Postgresql backend would update snapshot 2 as there a snapshot is connection based.
Each call to GetSnapshot() creates a new snapshot with new local DataCaches. At least that is how it worked before #1249 , I haven't analyzed those changes yet. |
I think it is relevant, @ixje, because I did a workaround there. Thanks for your explanation:
Thanks is strange, neither after committing the first?
In summary, I think, as you said, this is the expected, right? Thanks for your attention. |
Do you still suffer from this problem? I possible known how to fix this. |
I rebase with master for fixing, @ixje. If it is possible, it would be better if you open direct as a branch here. |
Perhaps we need to apply the same logic of Check #1343 |
@ixje, the last error was here: X TestUpdatePoolForBlockPersisted [2ms]
Error Message:
Test method Neo.UnitTests.Ledger.UT_MemoryPool.TestUpdatePoolForBlockPersisted threw exception:
System.ArgumentException: Value does not fall within the expected range.
Stack Trace:
at Neo.IO.Caching.DataCache`2.Add(TKey key, TValue value) in /home/runner/work/neo/neo/src/neo/IO/Caching/DataCache.cs:line 58
at Neo.UnitTests.Ledger.UT_MemoryPool.TestUpdatePoolForBlockPersisted() in /home/runner/work/neo/neo/tests/neo.UnitTests/Ledger/UT_MemoryPool.cs:line 490 public void TestUpdatePoolForBlockPersisted()
{
var snapshot = Blockchain.Singleton.GetSnapshot();
byte[] transactionsPerBlock = { 0x18, 0x00, 0x00, 0x00 }; // 24
byte[] feePerByte = { 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00 }; // 1048576
StorageItem item1 = new StorageItem
{
Value = transactionsPerBlock
};
StorageItem item2 = new StorageItem
{
Value = feePerByte
};
var key1 = CreateStorageKey(Prefix_MaxTransactionsPerBlock);
var key2 = CreateStorageKey(Prefix_FeePerByte);
key1.ScriptHash = NativeContract.Policy.Hash;
key2.ScriptHash = NativeContract.Policy.Hash;
snapshot.Storages.Add(key1, item1);
snapshot.Storages.Add(key2, item2); Perhaps, since the Key already exists we need to use GetAndChange now, yes? Check 29cebaf |
Another error is on In fact, the value really already exists, thus, it needs GetAndChange instead of public void TestCheckPolicy()
{
Transaction tx = Blockchain.GenesisBlock.Transactions[0];
var snapshot = Blockchain.Singleton.GetSnapshot();
StorageKey storageKey = new StorageKey
{
ScriptHash = NativeContract.Policy.Hash,
Key = new byte[sizeof(byte)]
};
storageKey.Key[0] = 15;
var sITest = snapshot.Storages.TryGet(storageKey);
Console.WriteLine($"sITest {sITest.Value.ToScriptHash()}");
snapshot.Storages.GetAndChange(storageKey, () => new StorageItem
{
Value = new UInt160[] { tx.Sender }.ToByteArray()
});
/*
snapshot.Storages.Add(storageKey, new StorageItem
{
Value = new UInt160[] { tx.Sender }.ToByteArray()
});
*/
NativeContract.Policy.CheckPolicy(tx, snapshot).Should().BeFalse();
snapshot = Blockchain.Singleton.GetSnapshot();
NativeContract.Policy.CheckPolicy(tx, snapshot).Should().BeTrue();
} However, the first assert is returning Expected NativeContract.Policy.CheckPolicy(tx, snapshot) to be false, but found True. |
In fact, it looks like that this test was wrong, I do not know why it would return False since it just checks BlockedAccounts? Any idea, @erikzhang @shargon ? |
@@ -231,21 +235,6 @@ public void TestCheckPolicy() | |||
{ | |||
Transaction tx = Blockchain.GenesisBlock.Transactions[0]; | |||
var snapshot = Blockchain.Singleton.GetSnapshot(); | |||
|
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.
@erikzhang @eryeer @shargon, double check this because perhaps this test was wrong.
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.
How about change it like this.
[TestMethod]
public void TestCheckPolicy()
{
Transaction tx = Blockchain.GenesisBlock.Transactions[0];
var snapshot = Blockchain.Singleton.GetSnapshot();
StorageKey storageKey = new StorageKey
{
ScriptHash = NativeContract.Policy.Hash,
Key = new byte[sizeof(byte)]
};
storageKey.Key[0] = 15;
var item = snapshot.Storages.GetAndChange(storageKey);
item.Value = new UInt160[] { tx.Sender }.ToByteArray();
NativeContract.Policy.CheckPolicy(tx, snapshot).Should().BeFalse();
snapshot = Blockchain.Singleton.GetSnapshot();
NativeContract.Policy.CheckPolicy(tx, snapshot).Should().BeTrue();
}
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 tried, it says True
instead of False
, that is why I believe the test was wrong. Why was it False
in any situation?
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.
Using this code, the test will pass. We need to change the return value of snapshot.Storages.GetAndChange(storageKey)
instead of passing a StorageItem creating function into it. Because the initalization will add a storageItem into storage, trackable.Item
won't be null.
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.
Anhhh, I see, but it would be better to DeleteKey, no?
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.
storageKey.Key[0] = 15;
this key is the blockedAccount key
private const byte Prefix_BlockedAccounts = 15; |
Because it is private, we can only use the 15 instead of
Prefix_BlockedAccounts
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 partially understood! Thanks for the explanation, @eryeer.
However, I still need to figure out this part:
We need to change the return value of snapshot.Storages.GetAndChange(storageKey) instead of passing a StorageItem creating function into it. Because the initalization will add a storageItem into storage, trackable.Item won't be null.
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.
😄 Here is the initalization.
neo/src/neo/SmartContract/Native/PolicyContract.cs
Lines 62 to 65 in 0f75741
engine.Snapshot.Storages.Add(CreateStorageKey(Prefix_BlockedAccounts), new StorageItem | |
{ | |
Value = new UInt160[0].ToByteArray() | |
}); |
So whenever we get the BlockedAccounts from Internal storage, the value will be new
UInt160[0].ToByteArray()
by default. We need to change the defult value instead of create a new one to add to storage.
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 think that I understood.
This would also work:
[TestMethod]
public void TestCheckPolicy()
{
Transaction tx = Blockchain.GenesisBlock.Transactions[0];
var snapshot = Blockchain.Singleton.GetSnapshot();
StorageKey storageKey = new StorageKey
{
ScriptHash = NativeContract.Policy.Hash,
Key = new byte[sizeof(byte)]
};
storageKey.Key[0] = 15;
//var item = snapshot.Storages.GetAndChange(storageKey);
//item.Value = new UInt160[] { tx.Sender }.ToByteArray();
snapshot.Storages.Delete(storageKey);
snapshot.Storages.GetAndChange(storageKey, () => new StorageItem
{
Value = new UInt160[] { tx.Sender }.ToByteArray()
});
snapshot.Commit();
NativeContract.Policy.CheckPolicy(tx, snapshot).Should().BeFalse();
snapshot = Blockchain.Singleton.GetSnapshot();
snapshot.Storages.Delete(storageKey);
snapshot.Storages.GetAndChange(storageKey, () => new StorageItem
{
Value = new UInt160[0].ToByteArray()
});
snapshot.Commit();
NativeContract.Policy.CheckPolicy(tx, snapshot).Should().BeTrue();
}
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.
Oh yes, congratulations!
@@ -487,8 +487,14 @@ public void TestUpdatePoolForBlockPersisted() | |||
var key2 = CreateStorageKey(Prefix_FeePerByte); | |||
key1.ScriptHash = NativeContract.Policy.Hash; | |||
key2.ScriptHash = NativeContract.Policy.Hash; | |||
snapshot.Storages.Add(key1, item1); | |||
snapshot.Storages.Add(key2, item2); | |||
snapshot.Storages.GetAndChange(key1, () => new StorageItem |
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.
@ixje, this change is expected, right? Since the value was previously existing.
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.
@vncoelho I don't remember, but it is most likely caused by the TestBlockchain isolation problem. I think the best way forward is to have that fixed first, reset all changes to the unit tests and try again. Some of the changes (like here) have likely been applied because of the isolation problem and might not be needed anymore once that is fixed
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.
AHeauehuaea, @ixje, this change was done by me, please check the last commits.
The question was to be sure this was consistent.
All UTs were fixed, the PR is kind of ready to merge.
Changed already done. Re-review requested.
if (dictionary.TryGetValue(key, out Trackable trackable)) | ||
{ | ||
if (trackable.State != TrackState.Deleted) | ||
throw new ArgumentException(); | ||
} | ||
else | ||
{ | ||
if (TryGetInternal(key) != null) | ||
throw new ArgumentException(); | ||
} |
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.
When we are using Add()
, we assume that the data does not exist. If we can't assume that, we should use GetAndChange()
. So, since we are using Add()
, there is no need to add another data read operation.
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 idea behind this PR (and the above code change) is to create consistent behaviour of the cache by not just validating that the value exists in cache, but to also validate that it does not exist in the real backend. You previously choose this exact solution (#1173 (comment)) instead of always overriding the data (described in the linked comment as option 1).
If we don't choose either option 1 or 2 we will have inconsistent behaviour as described in #1173
I don't mind which of the 2 we choose, but right now the feedback looks like you don't want to change anything?! 😕 I think it is wrong to assume that data does not exist and I think you agreed with that when you wrote the code initially, otherwise this check would not exists
if (trackable.State != TrackState.Deleted)
throw new ArgumentException();
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 overhead of this check is very small, so just do it by the way.
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 did not understand the point as well, @erikzhang.
The implementation here seems to be option 2 of that aforementioned issue.
Do you want to leave the code as in the original implementation?
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 don't want to change it. The purpose of Add()
is to add records when they do not exist, and save the cost of a read operation. If you are not sure that the record exists or not, you should use GetAndChange()
.
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 see, @erikzhang.
It looks like to be more efficient in the original code, yes? In the sense of avoiding a second check with if (TryGetInternal(key) != null)
.
@ixje, if that is the case, perhaps it is better to close PR and issue and open another one for commenting the code properly in order to avoid future misinterpretations.
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.
So everybody basically wasted their time in creating the PR, reviewing it and fixing unit tests (still thanks for that @vncoelho) because 1.5 months after opening the PR and the initial review we're suddenly trying to save performance on a db operation that takes < 10 microseconds (according to leveldb benchmarks for random reads) while elsewhere in the code base millisecond delays are accepted.
Don't get me wrong here. I'm all cool with keeping performance in mind for reaching the highest TPS, but how are we supposed to make a sound decision what makes sense to spend time on when performance criteria seems randomly applied?
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.
@ixje, I really apologize for that.
I really did not have enough knowledge for checking it before.
In fact, I would like to thank you for your dedication. In addition, your insights helped me to investigate further.
Perhaps, there are methods that are not being used from the DataCache
, such as GetOrAdd
.
We need to solve these questions and improve what needs to be improved.
For LevelDB not. LevelDB makes a 100% separate copy and this copy will not listen anymore to new changes happening to the real DB. Here's what it looks like for LevelDB Note how for postgresql there is a syncing between the real backend and any outstanding snapshots (=a network connection/session in their case). I think the big problem with the tests is that this TestBlockchain is using the same Blockchain.singleton everytime. It needs to be a new, clean one for each call. |
Such a nice explanation and figure, @ixje. Did you create that? |
Yes, with the free online https://www.draw.io/ platform |
@@ -47,19 +47,19 @@ private bool CheckValidators(ApplicationEngine engine) | |||
internal override bool Initialize(ApplicationEngine engine) | |||
{ | |||
if (!base.Initialize(engine)) return false; | |||
engine.Snapshot.Storages.Add(CreateStorageKey(Prefix_MaxBlockSize), new StorageItem | |||
engine.Snapshot.Storages.GetAndChange(CreateStorageKey(Prefix_MaxBlockSize), () => new StorageItem |
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 should be the same, if we already know that the item doesn't exists, why we need to call GetAndChange
?
closing according to #1177 (comment) |
close #1173