Skip to content
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

RavenDB-22261: Inconsistency in Tree external API #18422

Open
wants to merge 16 commits into
base: v6.0
Choose a base branch
from

Conversation

redknightlois
Copy link
Member

@redknightlois redknightlois commented Apr 18, 2024

Issue link

https://issues.hibernatingrhinos.com/issue/RavenDB-22261

Additional description

This is a series of commits aimed at homogenizing the external API of Voron data structures in order to simplify the code and avoid edge cases that arise from inconsistent usage patterns along the codebase.

Type of change

  • Bug fix
  • Regression bug fix
  • Optimization
  • New feature

How risky is the change?

  • Low
  • Moderate
  • High
  • Not relevant

Backward compatibility

  • Non breaking change
  • Ensured. Please explain how has it been implemented?
  • Breaking change
  • Not relevant

Is it platform specific issue?

  • Yes. Please list the affected platforms.
  • No

Documentation update

  • This change requires a documentation update. Please mark the issue on YouTrack using Documentation Required tag.
  • No documentation update is needed

Testing by Contributor

  • Tests have been added that prove the fix is effective or that the feature works
  • Internal classes added to the test class (e.g. entity or index definition classes) have the lowest possible access modifier (preferable private)
  • It has been verified by manual testing

Testing by RavenDB QA team

  • This change requires a special QA testing due to possible performance or resources usage implications (CPU, memory, IO). Please mark the issue on YouTrack using QA Required tag.
  • No special testing by RavenDB QA team is needed

Is there any existing behavior change of other features due to this change?

  • Yes. Please list the affected features/subsystems and provide appropriate explanation
  • No

UI work

  • It requires further work in the Studio. Please mark the issue on YouTrack using Studio Required tag.
  • No UI work is needed

@redknightlois
Copy link
Member Author

Look at the last commit, this is more of an exploration for standarization of the logic inside Voron. I have a few other places where this approach can simplify a lot the current code. For example, as of right now I identified 10s of places where for example we assume implicitly non-nullness when in fact creation methods can return null among other issues.

cc @ayende @arekpalinski

@redknightlois redknightlois requested review from ayende and arekpalinski and removed request for ayende April 18, 2024 22:44
src/Voron/Data/Fixed/FixedSizeTree.cs Outdated Show resolved Hide resolved
if (header == null)
return;
return false;
Copy link
Member

Choose a reason for hiding this comment

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

Do you think it's a valid scenario to not read it? Shouldn't we revert the already introduced changes to tree object? For example we have already changed the name - tree._treeName = ...;

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, in fact I thought exactly as you the first time. Maybe I should add more comments. It can happen that we try to repurpose the instance but the instance does not exist / or does not have any item in it. BUT, we should still have repurposed the instance for a new tree. This is one of the big inconsistencies I have to address but I am not addressing it yet.

Copy link
Member

Choose a reason for hiding this comment

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

Any additional thoughts on this after spending more time on it?

if (tree._tx.Flags == TransactionFlags.ReadWrite && (tree._parent.State.Header.Flags & TreeFlags.FixedSizeTrees) != TreeFlags.FixedSizeTrees)
{
ref var state = ref tree._parent.State.Modify();
state.Flags |= TreeFlags.FixedSizeTrees;
Copy link
Member

Choose a reason for hiding this comment

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

I have serious doubts if this is the place to handle that. It's only about repurposing the instance. IMO it's tricky that we modify the state here. I'd prefer more explicit approach

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree, but that is the only way we can be backward compatible with already messed trees.

Copy link
Member

Choose a reason for hiding this comment

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

But in other places we explicitly apply TreeFlags.FixedSizeTrees flag

Copy link
Member

Choose a reason for hiding this comment

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

Why not to apply TreeFlags.FixedSizeTrees explicitly?

src/Voron/Data/Fixed/FixedSizeTree.cs Show resolved Hide resolved
src/Voron/Impl/Transaction.cs Show resolved Hide resolved
src/Voron/VoronExceptions.cs Show resolved Hide resolved
src/Voron/Data/BTrees/Tree.cs Outdated Show resolved Hide resolved
src/Voron/VoronExceptions.cs Show resolved Hide resolved
if (offset + count > dest.Length)
throw new ArgumentOutOfRangeException(nameof(from), "Cannot copy data after the end of the buffer");

ThrowIfOnDebug<InvalidOperationException>(HasValue == false, $"{nameof(HasValue)} is false.");
EnsureIsNotBadPointer();
Copy link
Member

Choose a reason for hiding this comment

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

Is EnsureIsNotBadPointer left as a placeholder for future debugging? Since the method is empty.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a conditional implementation. This is the VALIDATE version of it:

        [Conditional("VALIDATE")]
        internal readonly void EnsureIsNotBadPointer()
        {
            if (_pointer->Ptr == null)
                throw new InvalidOperationException("The inner storage pointer is not initialized. This is a defect on the implementation of the ByteStringContext class");

            if (_pointer->Key == ByteStringStorage.NullKey)
                throw new InvalidOperationException("The memory referenced has already being released. This is a dangling pointer. Check your .Release() statements and aliases in the calling code.");

            if ( this.Key != _pointer->Key)
            {
                if (this.Key >> 16 != _pointer->Key >> 16)
                    throw new InvalidOperationException("The owner context for the ByteString and the unmanaged storage are different. Make sure you havent killed the allocator and kept a reference to the ByteString outside of its scope.");

                Debug.Assert((this.Key & 0x0000000FFFFFFFF) != (_pointer->Key & 0x0000000FFFFFFFF), "(this.Key & 0x0000000FFFFFFFF) != (_pointer->Key & 0x0000000FFFFFFFF)");
                throw new InvalidOperationException("The key for the ByteString and the unmanaged storage are different. This is a dangling pointer. Check your .Release() statements and aliases in the calling code.");                                    
            }
        }

@@ -1424,14 +1391,16 @@ private SegmentInformation AllocateSegment(int size)
private void ThrowInvalidMemorySegmentOnAllocation()
{
AllocationFailed?.Invoke();
throw new InvalidOperationException("Allocate gave us a segment that was already disposed.");
PortableExceptions.Throw<InvalidOperationException>("Allocate gave us a segment that was already disposed.");
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
PortableExceptions.Throw<InvalidOperationException>("Allocate gave us a segment that was already disposed.");
Throw<InvalidOperationException>("Allocate gave us a segment that was already disposed.");

if (_disposed)
ThrowObjectDisposed();

PortableExceptions.ThrowIfNull(value._pointer);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
PortableExceptions.ThrowIfNull(value._pointer);
ThrowIfNull(value._pointer);

Comment on lines 296 to 297
ThrowIf<InvalidOperationException>(required > ArenaMemoryAllocator.MaxArenaSize,
$"Tried to allocate {new Size(required, SizeUnit.Bytes)}, which exceeds maximum allocation size of {new Size(ArenaMemoryAllocator.MaxArenaSize, SizeUnit.Bytes)}");
Copy link
Member

Choose a reason for hiding this comment

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

Don't we allocate to generate string exception every time even if we do not throw?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good point. I will have to look into what the JIT does on that one. Most likely case is that it will get cancelled because the underlying Throw of the method is marked as [DoesNotReturn], but we should look into it anyways.

@@ -36,19 +42,18 @@ public TreeIterator(Tree tree, LowLevelTransaction tx, bool prefetch)

public int GetCurrentDataSize()
{
if (_disposed)
throw new ObjectDisposedException("TreeIterator " + _tree.Name);
ThrowIfDisposedOnDebug(this, "TreeIterator " + _tree.Name);
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to throw only in debug?

Copy link
Member

Choose a reason for hiding this comment

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

It wasn't the case in the previous code. @redknightlois?


if (_currentPage == null)
throw new InvalidOperationException("No current page was set");
ThrowIfDisposedOnDebug(this, "TreeIterator " + _tree.Name);
Copy link
Member

Choose a reason for hiding this comment

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

Same here.

Copy link
Member

Choose a reason for hiding this comment

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

@@ -8,6 +8,7 @@
using Xunit;
using Voron.Global;
using Xunit.Abstractions;
using Elastic.Clients.Elasticsearch;
Copy link
Member

Choose a reason for hiding this comment

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

Unintended?

Copy link
Member Author

Choose a reason for hiding this comment

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

I hate resharper and it's automatic additions. Yes.

Copy link
Member

Choose a reason for hiding this comment

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

it's still here

@redknightlois redknightlois force-pushed the RavenDB-22261 branch 2 times, most recently from 81b3aa2 to 17b5d13 Compare May 10, 2024 19:29
@redknightlois
Copy link
Member Author

Test this in DEBUG

@arekpalinski
Copy link
Member

test this in debug please

@arekpalinski
Copy link
Member

@redknightlois redknightlois force-pushed the RavenDB-22261 branch 3 times, most recently from 9ec7724 to d45f744 Compare May 15, 2024 18:19
@arekpalinski
Copy link
Member

test this in debug please

@@ -8,6 +8,7 @@
using Xunit;
using Voron.Global;
using Xunit.Abstractions;
using Elastic.Clients.Elasticsearch;
Copy link
Member

Choose a reason for hiding this comment

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

it's still here

@@ -212,7 +212,9 @@ internal Slice EncodeAndApplyAnalyzer(in FieldMetadata binding, Analyzer analyze
//Function used to generate Slice from query parameters.
//We cannot dispose them before the whole query is executed because they are an integral part of IQueryMatch.
//We know that the Slices are automatically disposed when the transaction is closed so we don't need to track them.
#if !DEBUG
Copy link
Member

Choose a reason for hiding this comment

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

Why only if not debug? I'm worried it might be a potential source of odd issues

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason why you want it in debug is because if you dont do so, you can't debug this method as Visual Studio will trip into not initialized memory and kill the process. (It sucks big time).

Copy link
Member

Choose a reason for hiding this comment

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

Ouch :/ Okay I understand, although I'm still a bit concerned about having that only in RElease

[InterpolatedStringHandler]
public readonly ref struct ConditionalThrowInterpolatedStringHandler
{
private readonly StringBuilder? _builder;
Copy link
Member

Choose a reason for hiding this comment

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

Can we use #nullable enable in this file? So we won't need to enable it globally but use a file-by-file approach

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, but at least for Voron, Sparrow and Corax expect to see in the next few month added usage of those annotations.

@@ -36,19 +42,18 @@ public TreeIterator(Tree tree, LowLevelTransaction tx, bool prefetch)

public int GetCurrentDataSize()
{
if (_disposed)
throw new ObjectDisposedException("TreeIterator " + _tree.Name);
ThrowIfDisposedOnDebug(this, "TreeIterator " + _tree.Name);
Copy link
Member

Choose a reason for hiding this comment

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

It wasn't the case in the previous code. @redknightlois?


if (_currentPage == null)
throw new InvalidOperationException("No current page was set");
ThrowIfDisposedOnDebug(this, "TreeIterator " + _tree.Name);
Copy link
Member

Choose a reason for hiding this comment

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

if (header == null)
return;
return false;
Copy link
Member

Choose a reason for hiding this comment

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

Any additional thoughts on this after spending more time on it?

if (tree._tx.Flags == TransactionFlags.ReadWrite && (tree._parent.State.Header.Flags & TreeFlags.FixedSizeTrees) != TreeFlags.FixedSizeTrees)
{
ref var state = ref tree._parent.State.Modify();
state.Flags |= TreeFlags.FixedSizeTrees;
Copy link
Member

Choose a reason for hiding this comment

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

Why not to apply TreeFlags.FixedSizeTrees explicitly?

@redknightlois
Copy link
Member Author

redknightlois commented May 21, 2024

I don't understand why it does not allow me to comment on your comments. So I will go one by one with quotes, but first thing first. I hate resharper and it adding back stuff I explicitly remove.

Why not to apply TreeFlags.FixedSizeTrees explicitly?

Because one of the important changes is to make the ability to modify the state explicit. In fact, this uncovered a bug that had no impact but it was already there. It was an inconsistency on what we stored at the level of the parent.

Any additional thoughts on this after spending more time on it?

Yes, we are handling it explicitly on the caller now.

It wasn't the case in the previous code. @redknightlois?

I inspected every single call manually, there is no case where it makes sense to do that check on runtime. I still leave the check on Debug for 2 reasons.

  • It is an invariant check which I am fine with it being checked.
  • I will be moving more checks that meet the same criteria while we start modifying this files. Already did that for JsonOperationContext not long ago.

@arekpalinski
Copy link
Member

test this in debug please

@@ -53,7 +51,7 @@ public void CanRenameTree()
}
}

[Fact]
[RavenFact(RavenTestCategory.Voron)]
Copy link
Member

Choose a reason for hiding this comment

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

Since you switched to RavenFact please adjust - AllTestsShouldUseRavenFactOrRavenTheoryAttributes test

@arekpalinski
Copy link
Member

@redknightlois what is the status here?

  • Are you going to make any more changes / profiling etc?
  • What about:
  • This change requires a special QA testing due to possible performance or resources usage implications (CPU, memory, IO). Please mark the issue on YouTrack using QA Required tag.

?
I believe this needs to be verified before we merge this PR CC @garayx

@redknightlois
Copy link
Member Author

We need to do a performance run to avoid regressions, correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants