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

Async publishing and API proposals #39

Closed
wants to merge 4 commits into from
Closed

Async publishing and API proposals #39

wants to merge 4 commits into from

Conversation

mtmk
Copy link
Contributor

@mtmk mtmk commented Feb 27, 2024

See updated readme here https://github.com/nats-io/nats.swift/tree/fix-async-pub

While updating the examples I made some changes to the API to make is a little more dev friendly:

  • made publish async
  • publish subject is named as to: to match subscribe()
  • changed how options are accepted moving away from builder (this is experimental)
  • added easier to use subscripts for headers
  • updated readme and examples

@mtmk mtmk changed the title Fix async pub Docs, examples and async pub Feb 27, 2024
@mtmk mtmk requested review from Jarema and piotrpio February 27, 2024 07:36
@mtmk mtmk marked this pull request as ready for review February 27, 2024 07:36
Copy link
Contributor

@piotrpio piotrpio left a comment

Choose a reason for hiding this comment

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

Some great additions. A few questions regarding performance and the new NatsConnection class.

You can also customize your connection by specifying additional options:

```swift
let options = NatsOptions(
Copy link
Contributor

Choose a reason for hiding this comment

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

To be honest, for more advanced options setup I personally prefer builder to a large public constructor. For example, builder does not force you to pass arguments in specific order and simplifies options validation if necessary. That said, I am not strongly against this, this is just my preferred approach.

Copy link
Member

Choose a reason for hiding this comment

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

We had this approach before, and it's also easy in Swift to run into conflicts when passing different things of the same type - especially around Auth.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was assuming you can pass the params in any order same as in c#. we should double check that.

validation is a good point. it can be done in constructor and/or in the connection class in this case for example.

I see, we should cater for habits which is important, and we must if that's the case here. The idea is that gradually abandoning these patterns (builder in this case) as the languages becoming more expressive in their syntax which in turn invalidates the initial reason to have patterns in the first place.

/rant: whenever I see a .build() at the end of a call chain my eyes start burning 😅 /

Copy link
Member

Choose a reason for hiding this comment

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

The order thing surprised me a lot too. It looks so conuter intuitive and unnecesary if you have named parameters.

| dropped | The clients droped a message. Mostly because of queue length to short. |
| reconnecting | The client reconencts to the server, (Because of a called reconnect()).|
| informed | The server sends his information data successfully to the client. |
| event | description |
Copy link
Contributor

Choose a reason for hiding this comment

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

This list of events is from the old client, but I can update it in a separate PR

Copy link
Member

Choose a reason for hiding this comment

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

I would remove the list entirely. It's easy enough to check the code/docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

didn't know that. we should probably ditch that table in that case.

}
}
}

func write(operation: ClientOp) throws {
func write(operation: ClientOp) async throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity - did you compare the benchmark times before and after making publish async? I remember trying it at first and for whatever reason, the result was dramatically worse when using async

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's about 3x slower. The problem is though just writing without flushing and ignoring concurrency isn't the right use of nio api. For example in this case I've noticed the benchmark can consume literally gigabytes of memory whereas with proper async it's at a few megs. we need to find proper ways of addressing the performance issue.

Also to note, we had similar issues with c#. not using async was producing better results. in the end the problem wasn't at all to do with async/await but managing the buffers to make the socket write calls in an optimal frequency.

Copy link
Member

Choose a reason for hiding this comment

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

For me it's 10x worse performance.
However I disagree that to have proper NIO usage you have to flush after every message. You can flush whenever you feel to, and NIO should flush if buffer was filled faster. The if for flushing I added was trying to check if bufer is busy enough that it's not worth flushing and it was a nice balance between throughput and latency.

On the side of allocations - keep in mind that more 3x/10x more throughput can also bump as much the allocations.
However, I would hope that you can set the max buffer size of NIO internal buffer....

// Experimetal class to make applying options a bit more natural.
// This is also very close to how C# client is designed.
// If accepted we can refactor `Client` and `ClientOptions` types.
public class NatsConnection {
Copy link
Contributor

Choose a reason for hiding this comment

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

As mentioned before, I am not completely sold on this approach, @Jarema what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

btw just to be clear I'm not suggesting we leave this as it is. please only consider the public api i.e. options construction/building/maybe naming. we can refactor internals later. Also it would be nice to see how other libraries are solving the options issue. be nice to have a little survey of that.

btw (this is somewhat out of context) in terms of public API for c# we made the decision to prefix all public types with Nats* which I think helps when application code is using several libraries and you want to avoid clashes both for readability and avoiding namespace manipulation/aliasing/etc. overhead. This is especially helpful with overused generic names like client, stream or options (I know we don't have stream at the moment but I'm thinking future use as well).

@@ -108,4 +112,68 @@ extension HeaderMap {
}
}
}

public subscript(name: String) -> String? {
Copy link
Contributor

Choose a reason for hiding this comment

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

That and the iterator are great additions! 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks! sorry for the long responses above, hope it makes sense 😅

Copy link
Member

@Jarema Jarema left a comment

Choose a reason for hiding this comment

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

Thank you for hard work on the Readme!

I think we should split other changes into separate discussions / PRs thoufh.

| dropped | The clients droped a message. Mostly because of queue length to short. |
| reconnecting | The client reconencts to the server, (Because of a called reconnect()).|
| informed | The server sends his information data successfully to the client. |
| event | description |
Copy link
Member

Choose a reason for hiding this comment

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

I would remove the list entirely. It's easy enough to check the code/docs.

You can also customize your connection by specifying additional options:

```swift
let options = NatsOptions(
Copy link
Member

Choose a reason for hiding this comment

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

We had this approach before, and it's also easy in Swift to run into conflicts when passing different things of the same type - especially around Auth.

@@ -79,20 +79,12 @@ extension Client {

public func publish(
_ payload: Data, subject: String, reply: String? = nil, headers: HeaderMap? = nil
) throws {
) async throws {
Copy link
Member

Choose a reason for hiding this comment

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

This, at least on my system, cuts the performance by 10x.
I would avoid adding the async flag unless we find a way to make it performant.

You can also customize your connection by specifying additional options:

```swift
let options = NatsOptions(
Copy link
Member

Choose a reason for hiding this comment

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

The order thing surprised me a lot too. It looks so conuter intuitive and unnecesary if you have named parameters.

}
}
}

func write(operation: ClientOp) throws {
func write(operation: ClientOp) async throws {
Copy link
Member

Choose a reason for hiding this comment

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

For me it's 10x worse performance.
However I disagree that to have proper NIO usage you have to flush after every message. You can flush whenever you feel to, and NIO should flush if buffer was filled faster. The if for flushing I added was trying to check if bufer is busy enough that it's not worth flushing and it was a nice balance between throughput and latency.

On the side of allocations - keep in mind that more 3x/10x more throughput can also bump as much the allocations.
However, I would hope that you can set the max buffer size of NIO internal buffer....

@mtmk mtmk changed the title Docs, examples and async pub Async publishing and API proposals Feb 28, 2024
@mtmk mtmk closed this Mar 13, 2024
@mtmk mtmk deleted the fix-async-pub branch March 13, 2024 12:07
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.

3 participants