-
Notifications
You must be signed in to change notification settings - Fork 4
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
Conversation
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.
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( |
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.
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.
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 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.
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 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 😅 /
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.
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 | |
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 list of events is from the old client, but I can update it in a separate PR
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 would remove the list entirely. It's easy enough to check the code/docs.
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.
didn't know that. we should probably ditch that table in that case.
} | ||
} | ||
} | ||
|
||
func write(operation: ClientOp) throws { | ||
func write(operation: ClientOp) async throws { |
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.
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
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.
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.
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.
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 { |
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 before, I am not completely sold on this approach, @Jarema what do you think?
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.
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? { |
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.
That and the iterator are great additions! 👍
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.
thanks! sorry for the long responses above, hope it makes sense 😅
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.
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 | |
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 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( |
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 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 { |
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, 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( |
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.
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 { |
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.
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....
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:
to:
to match subscribe()