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

Merge common code bases for TdsParserStateObject.cs (4) #2254

Merged
merged 19 commits into from Jan 17, 2024

Conversation

panoskj
Copy link
Contributor

@panoskj panoskj commented Dec 4, 2023

More methods of TdsParserStateObject merged (#2168 follow-up).

The first two commits merge some identical methods. Then there is a series of commits refactoring netcore/netfx TdsParserStateObject in order to make more methods identical, without modifying their behavior. The last commit simply merges these methods.

…ject.netcore.cs (similarly to TdsParser.ReliabilitySection.Assert). Add "missing" calls to these two methods.
…cs. Note IntPtr.Zero is equivalent to default(IntPtr).
…ReleasePacket in TdsParserStateObject.netfx.cs.
@panoskj
Copy link
Contributor Author

panoskj commented Dec 4, 2023

Force pushed to remove some whitespace changes I had accidentally included in the originally pushed commits - should make reviewing them a little bit easier when whitespaces comparison is on.

@panoskj panoskj changed the title Merge common code bases for TdsParserStateObject.cs (3) Merge common code bases for TdsParserStateObject.cs (4) Dec 4, 2023
Copy link

codecov bot commented Dec 4, 2023

Codecov Report

Attention: 35 lines in your changes are missing coverage. Please review.

Comparison is base (416afcd) 72.45% compared to head (619bb4c) 72.44%.
Report is 24 commits behind head on main.

Files Patch % Lines
...c/Microsoft/Data/SqlClient/TdsParserStateObject.cs 85.98% 30 Missing ⚠️
...osoft/Data/SqlClient/TdsParserStateObject.netfx.cs 88.46% 3 Missing ⚠️
...oft/Data/SqlClient/TdsParserStateObject.netcore.cs 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2254      +/-   ##
==========================================
- Coverage   72.45%   72.44%   -0.02%     
==========================================
  Files         309      310       +1     
  Lines       61960    61868      -92     
==========================================
- Hits        44895    44818      -77     
+ Misses      17065    17050      -15     
Flag Coverage Δ
addons 92.88% <ø> (ø)
netcore 76.57% <85.71%> (-0.14%) ⬇️
netfx 69.94% <86.63%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

if (readPacket != IntPtr.Zero)
{
// Be sure to release packet, otherwise it will be leaked by native.
SNINativeMethodWrapper.SNIPacketRelease(readPacket);
Copy link
Contributor Author

@panoskj panoskj Dec 12, 2023

Choose a reason for hiding this comment

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

Since this code is unreachable, the enclosing try/finally serves no purpose other than whatever RuntimeHelpers.PrepareConstrainedRegions does. I have preserved the try/finally (by adding it in .net core) but I suspect it can be removed, because according to the documentation (emphasis mine):

The PrepareConstrainedRegions method must immediately precede a try block and marks catch, finally, and fault blocks as constrained execution regions.

If I understand correctly, RuntimeHelpers.PrepareConstrainedRegions only affects catch/finally and has no effect if they are empty.

@@ -13,6 +13,11 @@

namespace Microsoft.Data.SqlClient
{
#if NETFRAMEWORK
using PacketHandle = IntPtr;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

An alternative would be to add a struct wrapping IntPtr instead of an alias, but would require refactoring a few more lines of code.

The advantage of using a struct is that the alias has to be defined in every file it is used, rather than once.

@Wraith2
Copy link
Contributor

Wraith2 commented Jan 10, 2024

For context on reliability see comments on #2168

My notes:

UnsafeCreateTimer - moved in the same file, no changes
SessionHandle - added in netfx with only a NativeHandle implementation because there is no managed equivalent in netfx
NullBitmap.TryInitialize - moved to shared, no changes
TrySkipLongBytes - moved to shared, no changes
TrySkipBytes - moved to shared, no changes
TryReadNetworkPacket - moved to shared, no changes. adds RuntimeHelpers.PrepareConstrainedRegion() stub implementation to netcore
PrepareReplaySnapshot - moved to shared, no changes
ReadSniSyncOverAsync - netcore version using handles is moved to shared, prepare and constrained region code added
OnConnectionClosed - moved to shared. the only change is in MemoryBarrier class and as previous discussed they are identical in functionality
SetTimeoutStateStopped - moved to shared, no changes
IsTimeoutStateExpired - moved to shared, no changes
OnTimeoutAsync - moved to shared, no changes
OnTimeoutSync - moved to shared, no changes
OnTimeoutCore - moved to shared. the only difference is a PrepareConstrainedRegion() call which is stubbed in netcore
ReadSni - netcore version using handled moved to shared and the try-finally and constrained region code added in. i think this is correct but please review it carefully.
IsConnectionAlive - merged code. carefully review. It looks like the readPacket variable is no longer used.

netfx only:
ReadSniError - changed to use packet handle instead of IntPtr
ProcessSniPacket - changed to use packet handle instead of IntPtr
ReadAsyncCallback - changed to use packet handle instead of IntPtr
WriteAsyncCallback - changed to use packet handle instead of IntPtr, changed to use interlocked memory barrier
SNIWritePacket - changed to use packet handle instead of IntPtr
IsValidPacket - added
EmptyReadPacket - added

netcore only:
ReadSniError - added reliability calls
WriteSecureString - added reliability calls
WriteByte - added reliability calls

over to you @David-Engel

Copy link
Contributor

@David-Engel David-Engel left a comment

Choose a reason for hiding this comment

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

LGTM. Much thanks for this!

@David-Engel
Copy link
Contributor

Thanks @panoskj for the work. Thanks @Wraith2 for the notes. I went through it commit by commit and the merge looks nice and clean from that respect.

CC: @DavoudEshtehari , @JRahnama

@panoskj
Copy link
Contributor Author

panoskj commented Jan 12, 2024

Thanks for the positive feedback. I have some questions/notes:

  1. I would like to know what you think about removing this try/finally and CER (constrained execution region). It looks like there used to be another implementation that would read a packet which then had to be freed in the finally block and that's why the CER was needed. If it was solely up to me, I would remove it.

  2. As a side note, I should point out the CERs place some constraints on the code being called (e.g. no boxing) in order to guarantee execution of the catch/finally blocks (even when the system is out of memory for example). It seems to me it is impossible to test the CERs honor these constraints and actually work as intended. The code being called inside CERs could have been modified to violate the CER constraints without anyone noticing. This is just a side note for future PRs - this PR is safe in this regard.

  3. I was going to include the ValidateSNIConnection method in this PR. However, if you review it carefully, you will notice merging it is not that simple:

    1. When called by IsConnectionAlive, the CheckConnection method that I added in netfx doesn't check for null SNIHandle before calling the native library (it simply calls it forwarding the null pointer).
    2. But when called by ValidateSNIConnection, the netfx CheckConnection method should check if SNIHandle is null. If SNIHandle is null, CheckConnection should return SNI_SUCCESS resulting in ValidateSNIConnection returning true.
    3. The netcore version checks if SNIHandle is null and returns SNI_SUCCESS if it is in both cases (when using the native wrapper that is).
    4. It looks like SNIHandle will not be null in netfx, unless the TdsParserStateObject has been disposed.

    Does anyone know what SNICheckConnection (native) does when given a null pointer? Is there any documentation about it? If it does something other than returning SNI_SUCCESS, then the netcore version behaves differently in this case (implying one of the two versions netcore/netfx is wrong). Unless this execution path is unreachable (e.g. if this method is never called on disposed TdsParserStateObjects due to other checks performed before calling it). Anyway, the options I can think of are:

    1. Go with netcore logic (checking SNIHandle in both cases) in netfx too.
    2. Use an #if directive to add the check prior to calling CheckConnection from ValidateSNIConnection for netfx.
    3. Add a second CheckConnection method that is the same as the first in netcore but implements the slight difference in netfx.
    4. Don't merge one of the two methods.

    Which option would you choose?

  4. Last but not least, where should I ask questions like the third one when I am preparing a new PR so that you guys (@Wraith2, @David-Engel and the rest of the team) can see them? I was think of either opening a draft PR or an issue mentioning you but none of these feels right - on the one hand I haven't made any commits to create a new PR yet (and the new PR would require this one as its base) and on the other hand this question isn't really an issue either.

@Wraith2
Copy link
Contributor

Wraith2 commented Jan 12, 2024

  1. and 2) I think you should include but otherwise leave CER's alone unless it is causing a clear problem. While M.D.SqlClient is unlikely to ever be able to run in-proc in sql server someone somewhere may have a dependency on the exact behaviour of the CER containing code.

  2. I'd need to dig into it in detail to even understand the question properly. I'd say open a specific small PR for that issue that changes it how you think is best and then we can discuss it there.

  3. draft issue seems like the best idea, i've opened them on small changes in the past to get info and then closed if they weren't needed.

@panoskj
Copy link
Contributor Author

panoskj commented Jan 12, 2024

The only problem about this CER is the finally clause is completely empty and (I think) it has no effect. Keeping it around can be confusing. Maybe we should consult someone who is more knowledgeable on the subject and either remove the unnecessary code or document why it is necessary, that's why I am asking.

As for the other question, it isn't really as complicated as it may seem when you read it, but you would have to read the method's code and maybe try to merge it in order to understand it. Looks like I should make a small draft PR so you can easily see what I am talking about.

@David-Engel
Copy link
Contributor

1 and 2: I think we should try to keep the CERs. Wraith is correct in that MDS doesn't run inproc in SQL, but if SQL ever wants to (somehow?) move to current .NET in the SQL proc in the future, it's not going to be with SDS. So someone will have to do the big job of making sure MDS works in that context. It seems like removing the CERs could make that even harder. We can always decide to remove them later.

  1. I'm not a C++ expert. But from what I do know and see in the native SNI code, the result will be unpredicatable. Thus I'm pretty sure you were correct here:

Unless this execution path is unreachable (e.g. if this method is never called on disposed TdsParserStateObjects due to other checks performed before calling it).

I'd go with option i.

  1. Ditto on the draft PR. It will tell us exactly what you are considering in the code. If it's purely theoretical and pre-code, an issue is fine.

@panoskj
Copy link
Contributor Author

panoskj commented Jan 13, 2024

Thanks for the feedback. In general I also agree we should keep CERs around, I was only referring to this specific case where it would not used even if running in-proc - that is dead code - but similar cases could be found later.

@David-Engel regarding the native SNI code, is it open-source? It would be a useful reference.

But if "this execution path is unreachable (e.g. if this method is never called on disposed TdsParserStateObjects due to other checks performed before calling it)" then the null check we are talking about is actually redundant, since the variable will never be null in this context. This would be better documented if an exception was thrown instead of returning SNI_SUCCESS from CheckConnection or at least if a debug assertion was in place.

This is what the managed CheckConnection does, but in a funny way: it calls GetSessionSNIHandleHandleOrThrow which returns a non-nullable reference or throws, then casts the returned reference to nullable and checks it against null (in which case it would return SNI_SUCCESS).

I guess my point is the code could be improved to cause less confusion and document any assumptions better. This isn't in the scope of these PRs, but in some cases like this, the netcore version has diverged from netfx and I have to understand what's going on anyway, so I could as well refactor the code to make it more self-documenting given the opportunity.

@DavoudEshtehari DavoudEshtehari added the ➕ Code Health Changes related to source code improvements label Jan 15, 2024
@DavoudEshtehari DavoudEshtehari added this to In progress in SqlClient v5.2 via automation Jan 15, 2024
Comment on lines 2450 to 2476
try
{
TdsParser.ReliabilitySection.Assert("unreliable call to IsConnectionAlive"); // you need to setup for a thread abort somewhere before you call this method

SniContext = SniContext.Snix_Connect;

error = CheckConnection();
if ((error != TdsEnums.SNI_SUCCESS) && (error != TdsEnums.SNI_WAIT_TIMEOUT))
{
// Connection is dead
SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObject.IsConnectionAlive | Info | State Object Id {0}, received error {1} on idle connection", _objectID, (int)error);
isAlive = false;
if (throwOnException)
{
// Get the error from SNI so that we can throw the correct exception
AddError(_parser.ProcessSNIError(this));
ThrowExceptionAndWarning();
}
}
else
{
_lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks;
}
}
finally
{
}
Copy link
Member

Choose a reason for hiding this comment

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

If you intend to eliminate SNIPacketRelease method call, I don't see any reason for retaining the "try-finally" block.

Copy link
Contributor Author

@panoskj panoskj Jan 17, 2024

Choose a reason for hiding this comment

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

I agree, but note this change will require that the call to RuntimeHelpers.PrepareConstrainedRegions prior to it is removed too. This is what I was asking @Wraith2 and @David-Engel earlier but they didn't seem fond of the idea. Let me know how to proceed on this one.

EDIT: I figured I will remove the try-finally and the call to RuntimeHelpers.PrepareConstrainedRegions and if there is any objection I can revert the change. Otherwise the PR will be ready to be merged.

Copy link
Contributor

Choose a reason for hiding this comment

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

No objections.

Comment on lines +18 to +27
private struct RuntimeHelpers
{
/// <summary>
/// This is a no-op in netcore version. Only needed for merging with netfx codebase.
/// </summary>
[Conditional("NETFRAMEWORK")]
internal static void PrepareConstrainedRegions()
{
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I like the way you addressed the code differences. 👏🏼

SqlClient v5.2 automation moved this from In progress to Reviewer approved Jan 16, 2024
@JRahnama JRahnama self-requested a review January 16, 2024 23:22
@JRahnama
Copy link
Member

LGTM, waiting for comments to be addressed and resolved.

panoskj and others added 4 commits January 17, 2024 10:01
…arserStateObject.cs

Co-authored-by: DavoudEshtehari <61173489+DavoudEshtehari@users.noreply.github.com>
…arserStateObject.cs

Co-authored-by: DavoudEshtehari <61173489+DavoudEshtehari@users.noreply.github.com>
…arserStateObject.cs

Co-authored-by: DavoudEshtehari <61173489+DavoudEshtehari@users.noreply.github.com>
@panoskj
Copy link
Contributor Author

panoskj commented Jan 17, 2024

I have addressed all comments. Along with the empty try-finally block, I have removed the PrepareConstrainedRegions call above it.

Copy link
Member

@DavoudEshtehari DavoudEshtehari left a comment

Choose a reason for hiding this comment

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

Kudus @panoskj 🎉

@DavoudEshtehari DavoudEshtehari merged commit 900d051 into dotnet:main Jan 17, 2024
148 checks passed
SqlClient v5.2 automation moved this from Reviewer approved to Done Jan 17, 2024
@panoskj
Copy link
Contributor Author

panoskj commented Jan 17, 2024

Thanks guys, I will prepare the next one.

@Wraith2
Copy link
Contributor

Wraith2 commented Jan 17, 2024

Slightly smaller please, reviewing is hard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
➕ Code Health Changes related to source code improvements
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

None yet

5 participants