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

Add flags to NatsMsg and JetStream Request #652

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft

Add flags to NatsMsg and JetStream Request #652

wants to merge 13 commits into from

Conversation

mtmk
Copy link
Collaborator

@mtmk mtmk commented Oct 12, 2024

This is part of preparing to reduce exception usage as stated in #581.

Note that a similar attempt was made in #275 but due to NatsMsg struct size getting larger it wasn't merged. With this PR we're using the message size fields left most two bits to set the flags relying on the fact a 30-bit field would still be large enough to hold message size int.

JetStream request changed to handle API error responses without throwing exceptions.

@mtmk mtmk changed the title Add flags to NatsMsg Add flags to NatsMsg and JetStream Request Oct 12, 2024
{
Subject = subject;
ReplyTo = replyTo;
Size = size;
Copy link
Collaborator

@to11mtm to11mtm Oct 12, 2024

Choose a reason for hiding this comment

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

Is it worth setting _flagsAndSize directly here? (Sharplab is not cooperating so I can't check whether it gets optimized as is or not)

Edit: Got it working, do double check my bitshift but the ctor on NatsMsg2 looks way cleaner

Copy link
Collaborator

@to11mtm to11mtm left a comment

Choose a reason for hiding this comment

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

Just some comments/etc.


namespace NATS.Client.JetStream.Internal;

internal sealed class NatsJSForcedJsonDocumentSerializer<T> : INatsDeserialize<T>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Worth making this internal sealed class NatsJSForcedJsonDocumentSerializer : INatsDeserialize<JsonDocument>?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yea much better

}

[Fact]
public void Check_struct_size()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Awesome to see this added.


public bool IsEmpty => (Flags & NatsMsgFlags.Empty) == NatsMsgFlags.Empty;

public bool HasNoResponders => (Flags & NatsMsgFlags.NoResponders) == NatsMsgFlags.NoResponders;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Super cool to note , IsEmpty reduces to very simple ASM even as-is on modern runtimes.

NatsMsg`1[[System.Int32, System.Private.CoreLib]].get_IsEmpty()
    L0000: xor eax, eax
    L0002: test dword ptr [rcx+0x20], 0x40000000
    L0009: setne al
    L000c: ret

Strangely, HasNoResponders does reduce quite as well; I wonder if it's due to it's spot being a sign bit? 🤔
It's fine as is but still an interesting curiosity.

NatsMsg`1[[System.Int32, System.Private.CoreLib]].get_HasNoResponders()
    L0000: mov eax, [rcx+0x20]
    L0003: shr eax, 0x1e
    L0006: test al, 2
    L0008: setne al
    L000b: movzx eax, al
    L000e: ret

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

interesting. if I changed getter to:

public bool HasNoResponders => (_flagsAndSize & 0x80000000) != 0;

I get the same code:

NATS.Client.Core.dll!NATS.Client.Core.NatsMsg<T>.HasNoResponders.get():
00007FF8841C5B80  xor         eax,eax  
00007FF8841C5B82  test        dword ptr [rcx+20h],80000000h  
00007FF8841C5B89  setne       al

my guess is after inlined

public bool IsEmpty => ((_flagsAndSize >> 30) & 1) == 1;
public bool HasNoResponders => ((_flagsAndSize >> 30) & 2) == 2;

there must be an optimization for x & 1 == 1 only.

if (payloadBuffer.Length == 0)
{
flags |= NatsMsgFlags.Empty;
if (NatsSubBase.IsHeader503(headersBuffer))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be unnested?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's pretty specific where server sets the 503 header for no responders and Go client seems to check for data being empty too. tbh I think no-responders is the only usage of 503 so i suppose it would be ok to move it out idk.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh nvm I guess I didn't realize they noresponders only came with empty 😅

Reordered error checks in `NatsJSContext.cs` to prioritize handling `HasNoResponders` and `null` data conditions early. Modified flag checks in `NatsMsg.cs` for `IsEmpty` and `HasNoResponders` by directly using bitwise operations instead of enum comparisons.
// .NET 6 new APIs to the rescue: we can read the buffer once
// by deserializing into a document, inspect and using the new
// API deserialize to the final type from the document.
var jsonDocument = msg.Data;
Copy link
Collaborator

Choose a reason for hiding this comment

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

There may be a concern here, due to the fact that JsonDocument is IDisposable and based on how the Parse function for ReadOnlySequence looks it is a case where we are likely pulling in a buffer.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

first just to clarify; the idea/assumption is that there may be two different types of JSON data coming in (the expected type like StreamConfig or an Error) and we are able to parse the binary data once, detecting if it's error, then convert the JsonDocument into the requested type. So all that said if there is a better way we can do this, we should try. Also this is assumption. I will see if I can benchmark this.

I kind of see what you mean with the extra buffer (need to look closer).

another option is that the good thing is the type information seems to be always at the front. with a short look-ahead maybe we can identify the document then use the correct json context to parse it.

I wonder if there is a solution for this already?

Copy link
Collaborator

Choose a reason for hiding this comment

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

TBH I think we are fine as long as we dispose our jsondocument after getting our part, but will check if I have time this weekend.

@mtmk mtmk self-assigned this Nov 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants