-
Notifications
You must be signed in to change notification settings - Fork 649
Introduce incoming message support for envelope unwrapping #7422
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
base: master
Are you sure you want to change the base?
Conversation
This comment was marked as off-topic.
This comment was marked as off-topic.
883a7a4 to
9f82a75
Compare
9b71ba2 to
a8b908e
Compare
a62cd95 to
a48da28
Compare
| if (exception != null) | ||
| { | ||
| meterTags.Add(new KeyValuePair<string, object?>(MeterTags.ErrorType, exception.GetType().FullName)); | ||
| } | ||
|
|
||
| totalEnvelopeUnwrappingErrors.Add(succeeded ? 0 : 1, meterTags); |
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.
What's the scenario for the exception not null and succeeded true?
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.
Shouldn't be such a scenario, but I didn't want to decide the metric value based on the exception and wanted to make it explicit.
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 is no scenario and the method is called RecordEnvelopeUnwrappingError, I don't see the need for the succeeded argument, and its usage.
Thinking a bit more about it, is there a chance for an unwrapping error with no exception?
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.
Error metrics should be "always" emitted. If there is no error, then emit the metric as 0. This way you can make sure that your dashboards and alerts are configured correctly (because the metric is always there and you don't have a typo in your configuration) and that you can have additional alerts on missing metric data points.
That being said, there is a need to differentiate between "error happened" and "no error" situations.
Thinking a bit more about it, is there a chance for an unwrapping error with no exception?
Sure there is (not with the current implementation, though). .NET guidelines suggest that errors are communicated via exceptions, but there is no way to enforce that via the compiler.
| messageContext.NativeMessageId, messageContext.Headers, messageContext.Extensions, | ||
| messageContext.Body); | ||
|
|
||
| metrics.RecordEnvelopeUnwrappingError(messageContext, envelopeHandler, null, true); |
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 are we calling RecordEnvelopeUnwrappingError here? There is no error, at least not yet.
Looks like we want to record three different metrics:
- Successful unwrappings, before line 40
- Failed unwrappings due to unwrapping errors, at line 51, as we already do
- Skipped unwrappers, at line 42
Additionally:
- Do we want to count the number of messages going through as regular NServiceBus messages? (line 74/75)
- Do we want/need to report metrics about how long unwrapping took?
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 are we calling RecordEnvelopeUnwrappingError here? There is no error, at least not yet.
There is no error and we want to explicitly indicate that. As mentioned above, the metric should "always" be emitted.
Looks like we want to record three different metrics:
This only increases the observability cost and makes the configuration more cumbersome. Success vs failure should be (and currently is) captured with one metric.
Skipped unwrappers, at line 42
The unwrapper should emit the metric in that case as it knows the skipping reason (and it indeed does that now). Not sure if we want to have another "generic" metric for that.
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.
Would it be more clear if there was an RecordEnvelopeUnwrappingSuccess or simlar? (to avoid passing null as the exception, I also reacted to calling "error" when it actually succeeded)
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.
Sure, I can rename. The reasoning behind the name is to record the EnvelopeUnwrappingError metric. Given that convention, RecordEnvelopeUnwrappingSuccess would record a different metric. I personally would find that misleading.
| if (exception != null) | ||
| { | ||
| meterTags.Add(new KeyValuePair<string, object?>(MeterTags.ErrorType, exception.GetType().FullName)); | ||
| } | ||
|
|
||
| totalEnvelopeUnwrappingErrors.Add(succeeded ? 0 : 1, meterTags); |
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 is no scenario and the method is called RecordEnvelopeUnwrappingError, I don't see the need for the succeeded argument, and its usage.
Thinking a bit more about it, is there a chance for an unwrapping error with no exception?
src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Metrics/When_envelope_handler_succeeds.cs
Outdated
Show resolved
Hide resolved
# Conflicts: # src/NServiceBus.Core/Receiving/ReceiveComponent.cs
- The name seems likely to have been carried over from another file when copy/pasting.
…ixes the CI pipeline error.
- typo in a comment - convert single line methods to expression body methods. - Simplified a using statement.
|
Raised #7583 with a proposal to use the new syntax for done conditions |
| totalSentToErrorQueue.Add(1, meterTags); | ||
| } | ||
|
|
||
| public void RecordEnvelopeUnwrappingError(MessageContext messageContext, IEnvelopeHandler type, Exception? exception, bool succeeded) |
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.
Am I reading this correctly that this not only records errors, but also successful unwraps? In that case, the name of the method is confusing - perhaps should be something like "RecordEnvelopeUnwrappingResult"?
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.
(As mentioned in other comment as well) The reasoning behind the name is to record the EnvelopeUnwrappingError metric. The convention is to indicate "what metric is emitted", not "what happened". Anyway, I will rename.
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 @afprtclr has a point. The convention on the metric class is to call it RecordMetricName
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.
We can rename to EnvelopeUnwrappingErrorOnsuccess etc.
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.
What would be the concern with being super explicit and having two methods (arguments are primarily as an example):
RecordEnvelopeUnwrappingSuccess(Type unwrapperType)RecordEnvelopeUnwrappingError(Type unwrapperType, Exception? ex)
They both record the same metric with different tags, so that the caller can not be confused at all by what needs to be passed as arguments. And would not be surprised by a call to RecordEnvelopeUnwrappingError when it looks like the unwrapping succeeded.
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, they would report the same metric. The way I view the thing is that the producer, us, and the consumer, them, of the metric are completely independent. They have no idea, and don't care, what method names are used in code to report what. They only care about the metric name and its "signature."
We sit on the other side, as producers (the ones writing the code that eventually triggers the metric), I think we should favor clarity and ease of use for the API.
Something like RecordEnvelopeUnwrapping(bool success, Exception? error, someOtherParam) is harder to use from the producer perspective. For example, can I call it with RecordEnvelopeUnwrapping(true, someException, 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.
I'm okay with changing the names. I'll just add my opinion on this:
They have no idea, and don't care, what method names are used in code to report what
I disagree and I think this is the exact reason why the convention emerged in the first place.
A metric is rarely used without referring to the code when troubleshooting. When a data point is examined, the metric consumer wants to understand what caused this data point to be produced. They will look for a code that emits that metric. Since metric names are often created dynamically (e.g., by concatenating strings), it's essential to avoid false positives. The convention helps by making sure that there is exactly one place when the metric is emitted, and that the name of the method is well aligned with the metric name that the consumer sees in an external system. If the convention is followed, the consumer can find the metric emitter much easier, and trace back from there with the help of the IDE. If the metric is emitted in many places, then one never knows if they found the right entrypoint.
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.
We're not the consumer of that metric. They won't be looking at our source code. We'll be looking at logs. And if we really wanted to look at the code, we would look at the various methods and see that they are producing the same metric. IDEs are extremely good at that.
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.
Would it help to look at, for example, MS's established conventions? I think at the end it doesn't really matter which one we pick as long as it is consistent. We can then update the guidance in the platform, which currently states
A good source of "inspiration" on naming conventions, metrics and more can be found in ASP.NET Core.
A few examples
https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Core/src/SignInManagerMetrics.cs
https://github.com/dotnet/aspnetcore/blob/main/src/Hosting/Hosting/src/Internal/HostingMetrics.cs
https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Core/src/SignInManagerMetrics.cs
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 raised:
- Use a single method to report the envelope unwrapping metric counter #7586
- Use a multiple methods to report the envelope unwrapping metric counter #7587
To better explain what I mean. I prefer the latter, but it's a readability preference.
* Convert to new style of acceptance tests * Fix warning * Field --------- Co-authored-by: Daniel Marbach <daniel.marbach@openplace.net>
| { | ||
| if (Log.IsDebugEnabled) | ||
| { | ||
| Log.Debug( |
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.
Avoid interpolation, this makes it impossible to do structured logging.
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 comment also applies to the code in https://github.com/Particular/NServiceBus.Envelope.CloudEvents
| /// <param name="extensions">ContextBag of extension values provided by the transport.</param> | ||
| /// <param name="incomingBody">The raw body provided by the transport.</param> | ||
| /// <returns>Dictionary of headers and byte array of message body if the message can be unwrapped. Null or exception otherwise.</returns> | ||
| (Dictionary<string, string> headers, ReadOnlyMemory<byte> body)? UnwrapEnvelope(string nativeMessageId, IDictionary<string, string> incomingHeaders, ContextBag extensions, ReadOnlyMemory<byte> incomingBody); |
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 expected this to be more like TryUnwrapEnvelope, a user could register multiple envelope unwrappers.
I guess currently what it cannot unwrap it returns null? Maybe this can be considered to make it a bit more explicit.
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.
a user could register multiple envelope unwrappers.
They can (and this is what CloudEvents does already).
I guess currently what it cannot unwrap it returns null? Maybe this can be considered to make it a bit more explicit.
Yes, it can return null in such a case. You can see that this is documented in the line above, and also the return type is a nullable tuple.
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.
@ramonsmits This was already considered in previous iterations of the design by the TF and considered as not needed. It is by design
|
|
||
| metrics.RecordEnvelopeUnwrappingError(messageContext, envelopeHandler, null, true); | ||
|
|
||
| if (unwrappingResult.HasValue) |
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.
See other commend on TryUnwrapEnvelope
| $"Unwrapped the message (NativeID: {messageContext.NativeMessageId} using {envelopeHandler.GetType().Name}"); | ||
| } | ||
|
|
||
| return new IncomingMessage(messageContext.NativeMessageId, unwrappingResult.Value.headers, unwrappingResult.Value.body); |
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.
Current design results in a unwrapper, that then returns another body.
If the body + envelope formats share the same serialization format like Json this results in funky multi pass de-serialization/serialization which is unwanted.
Seems that deserialization both envelope + body could be done in one pass.
Which makes me wonder if the wrapper isn't invoked currently too late to deal with all variations of custom wireformats?
Is the idea the tranport passed its native headers/payload without any unwrapping where currently that is done by the transport ahead of time?
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 the body + envelope formats share the same serialization format like Json this results in funky multi pass de-serialization/serialization which is unwanted.
Multiple deserialization can be avoided in the envelope unwrapper. The properly optimized unwrapper will not result in multiple deserialization.
Seems that deserialization both envelope + body could be done in one pass.
It technically could, but IMO that would break Single Responsibility Principle. The whole process of "deserializing" the message needs to handle envelopes, custom serializers, encryption, compression, anonimization, obfuscation, and probably many other things. It's typically a pipeline-like approach to turn bytes from the wire into a fully created object (and this is what we do here).
Is the idea the tranport passed its native headers/payload without any unwrapping where currently that is done by the transport ahead of time?
We decided to not expose any envelope-related concepts to the transports. You can see our analysis in https://docs.google.com/spreadsheets/d/1Sdl-FjZkJxg_-QL0l3GxhrnUIjxYRDApsutbN_EyGGU/edit?usp=sharing ("Sending" tab)
| /// <param name="extensions">ContextBag of extension values provided by the transport.</param> | ||
| /// <param name="incomingBody">The raw body provided by the transport.</param> | ||
| /// <returns>Dictionary of headers and byte array of message body if the message can be unwrapped. Null or exception otherwise.</returns> | ||
| (Dictionary<string, string> headers, ReadOnlyMemory<byte> body)? UnwrapEnvelope(string nativeMessageId, IDictionary<string, string> incomingHeaders, ContextBag extensions, ReadOnlyMemory<byte> incomingBody); |
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 have incomingHeaders? I understand that it tries to have a common denominator among tranports but that does require us to deserializer or map from the native type header collection.
I think that can be avoided.
Did the TF intentionally decide to not make transport specific "unwrappers"? Meaning, why not have envelope unwrappers from the native message type to our Dictionary<string, string> headers, ReadOnlyMemory<byte> body abstraction?
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.
Did the TF intentionally decide to not make transport specific "unwrappers"?
Yes, we did. See the "sending" tab in https://docs.google.com/spreadsheets/d/1Sdl-FjZkJxg_-QL0l3GxhrnUIjxYRDApsutbN_EyGGU/edit?gid=1689383480#gid=1689383480
Meaning, why not have envelope unwrappers from the native message type
This would lead to custom CloudEvents unwrappers (and unwrappers for other standard) being implemented for nearly each transport separately. We wanted to avoid that.
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, we did.
👌
| /// <param name="extensions">ContextBag of extension values provided by the transport.</param> | ||
| /// <param name="incomingBody">The raw body provided by the transport.</param> | ||
| /// <returns>Dictionary of headers and byte array of message body if the message can be unwrapped. Null or exception otherwise.</returns> | ||
| (Dictionary<string, string> headers, ReadOnlyMemory<byte> body)? UnwrapEnvelope(string nativeMessageId, IDictionary<string, string> incomingHeaders, ContextBag extensions, ReadOnlyMemory<byte> incomingBody); |
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 a tuple in an result of a public interface? Why not make it a record type?
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 personally don't see much difference in this case. I imagine this could be a record struct as well, so we can change if others share similar opinion.
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.
My suggestion in #7585 changes this
| /// <param name="extensions">ContextBag of extension values provided by the transport.</param> | ||
| /// <param name="incomingBody">The raw body provided by the transport.</param> | ||
| /// <returns>Dictionary of headers and byte array of message body if the message can be unwrapped. Null or exception otherwise.</returns> | ||
| (Dictionary<string, string> headers, ReadOnlyMemory<byte> body)? UnwrapEnvelope(string nativeMessageId, IDictionary<string, string> incomingHeaders, ContextBag extensions, ReadOnlyMemory<byte> incomingBody); |
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.
Consider for clarity to name all ReadOnlyMemory<byte> before unwrapping payload, and after unwrapping body.
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.
My suggestion in #7585 changes this
| /// <param name="extensions">ContextBag of extension values provided by the transport.</param> | ||
| /// <param name="incomingBody">The raw body provided by the transport.</param> | ||
| /// <returns>Dictionary of headers and byte array of message body if the message can be unwrapped. Null or exception otherwise.</returns> | ||
| (Dictionary<string, string> headers, ReadOnlyMemory<byte> body)? UnwrapEnvelope(string nativeMessageId, IDictionary<string, string> incomingHeaders, ContextBag extensions, ReadOnlyMemory<byte> incomingBody); |
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.
Lets imagine the following:
A customer wants to use envelopes that are compressed and/or encrypted . It seems in the current design a user now needs to create a custom envelope that combines these. This shouldn't be.
Decompression and (partial) decryption task should be able to be done via behaviors.
Do we need another stage for this? Ideally the current ITransportReceiveContext stage would remain for such operations before unwrapping.
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.
A customer wants to use envelopes that are compressed and/or encrypted . It seems in the current design a user now needs to create a custom envelope that combines these. This shouldn't be.
Unwrapper is supposed to support a single specification. If the spec supports multiple features (like compression, encryption, obfuscation, etc.), then the unwrapper should handle all of that.
If the encryption and compression come from different specifications, then they probably belong to different logical pieces. Unwrappers deal with envelopes only. Dealing with body is in a different stage in NSB (in the custom serializers). That being said, I don't see a problem here.
Do we need another stage for this? Ideally the current ITransportReceiveContext stage would remain for such operations before unwrapping.
Possibly. Given that the TF comes from the exploration group and we are going toward the removability principle, we didn't want to turn the pipeline upside down.
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.
Unwrapper is supposed to support a single specification. If the spec supports multiple features (like compression, encryption, obfuscation, etc.), then the unwrapper should handle all of that.
If the encryption and compression come from different specifications, then they probably belong to different logical pieces. Unwrappers deal with envelopes only. Dealing with body is in a different stage in NSB (in the custom serializers). That being said, I don't see a problem here.
An envelope is different from that. Take gzip compression, that is not a feature of the (un)wrapper. Encryption and/or signing COULD be if the spec has nice specifications for it like fragment encryption/signing. Full payload compression like HTTP does should be able to be done before any unwrapper is invoked. IMHO that is not something the unwrapper implementation should solve.
Possibly. Given that the TF comes from the exploration group and we are going toward the removability principle, we didn't want to turn the pipeline upside down.
Which is good, and is why you went for (un)wrapping to happen in core and not in the tranport.
However, not making envelope (un)wrapping not part of the pipeline ensuring that behavior can run BEFORE unwrapping isn't really turning the pipeline upside down but would allow operations on the full payload before the envelope unwrapper is invoked.
Why couldn't unwrapping be done in IIncomingPhysicalMessageContext or StageConnector<ITransportReceiveContext, IIncomingPhysicalMessageContext> what is the requirement to have unwrapping done before invoking the pipeline?
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.
However, not making envelope (un)wrapping not part of the pipeline ensuring that behavior can run BEFORE unwrapping isn't really turning the pipeline upside down but would allow operations on the full payload before the envelope unwrapper is invoked.
Why couldn't unwrapping be done in IIncomingPhysicalMessageContext or StageConnector<ITransportReceiveContext, IIncomingPhysicalMessageContext> what is the requirement to have unwrapping done before invoking the pipeline?
One of the reasons was to avoid recreating message objects. Since envelope unwrapping may result in identifier change and similar, we didn't want to recreate the objects. I would need to go through the notes from few months ago to provide other reasons, but off the top of my head I can only say that we spent decent amount of time discussing where this thing should live.
That being said, I'm not saying that this is set in stone. If you think the unwrapping should happen somewhere else, we can return to this discussion and provide deeper explanation of why we implemented it in this spot.
|
Could potentially multiple envelopes be nested? As in a sort of envelope inception? In general there should only be 1 envelope but suddenly I got enterprise integration nostalgia where I was at a project that did that. Have a custom format wrapped in XML and then again in a SOAP envelope. |
It could. To our knowledge, it's neither known nor supported by any spec. |
Current flow stops when an unwrapper was successful in the current Second, this seems to be the only way to have multiple unwrappers applied. For example if a use would want to have separate unwrappers for (de)compression and/or (un)encryption. |
If that becomes a real requirement we could simply make our unwrappers public and then a user could decorate an unwrapper given they already said you can remove or add unwrappers, no? every time I write wrapper I silently whisper an eminem song |
This is not fully correct. It's not just unwrapper, it's envelope unwrapper. We are dealing with envelopes only. Encryption, compression, obfuscation, and other stuff - these are not envelopes. It's just like in the OSI/ISO networking model - both Layers 2 and 3 have their own envelopes, but they are not covered by one spec and are not on the same level. One can't just add encryption on Layer 3 and expect Layer 2 to deal with that. Our envelope unwrappers are meant to support some specification. If the specification supports recursive unwrapping - the unwrapper will need to handle that. When you say Jokingly, one can obviously come up with another incarnation of decorator pattern to which I can only reply with https://xkcd.com/927/ |
This pull request introduces a new extensibility point in NServiceBus for envelope unwrapping, allowing the registration of envelope handlers for incoming messages, such as CloudEvents. It adds the
IEnvelopeHandlerinterface and supporting infrastructure, integrates envelope handler registration into endpoint and feature configuration, and introduces OpenTelemetry metrics for envelope unwrapping successes and failures. Comprehensive tests are provided for both the envelope unwrapping logic and the new metrics.This new extensibility point will first be used to enable cloudevents with the Azure Service Bus and Amazon SQS transports:
Samples for will be added to the documentation with:
Envelope Handler Extensibility Infrastructure:
IEnvelopeHandlerinterface, which allows custom logic for unwrapping incoming message envelopes.EnvelopeComponentandEnvelopeUnwrapperclasses to manage and invoke registered envelope handlers. TheEnvelopeComponentcollects handler factories and creates the unwrapper at runtime.EnvelopeConfigExtensionsfor registering envelope handlers in endpoint and feature configuration via the newAddEnvelopeHandler<THandler>()extension method.Integration into Endpoint Lifecycle:
Metrics and Observability:
nservicebus.envelope.unwrapping_error(Counter), including new tags such asnservicebus.envelope.unwrapper_typeanderror.typefor observability of envelope unwrapping outcomes.Testing:
EnvelopeUnwrapperTests.These changes collectively enable advanced envelope customization and improved monitoring for message processing in NServiceBus.