-
Notifications
You must be signed in to change notification settings - Fork 698
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
[ADDED] Service api improvements #1160
Conversation
service/example_package_test.go
Outdated
// Multiple instances of the servcice with the same name can be created. | ||
// Requests to a service with the same name will be load-balanced. | ||
for i := 0; i < 5; i++ { | ||
svc, err := Add(nc, config) |
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 is just Add now? I think we might be simplifying too much here.
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 in separate package now, so the way you actually use it is by calling service.Add()
, which I think is really intuitive and pretty much a standard in Go (e.g. list.New()
or context.WithTimeout()
, http.Server()
etc.).
In that sense, service.AddService()
seems redundant.
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 thought you would hang it off alive connection. nc.AddService. This is how we register interest with a connection, and that is what a service it imo. Do you now make them pass in a nats.Connection?
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 - I want to separate services to a separate, importable package - if user does not want to use service framework, the API is less cluttered and they do not see methods they do not need. After importing service
package and typing service.
in IDE, you'll see all you need to start using interfaces. It's self-documenting and easier to use.
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 should discuss, we had stated we did not want this as a separate package IIRC because we wanted this to be the default way to do micro-services etc.
Let's schedule some time to discuss before this gets merged.
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 part where nc.AddService
may come in handy is when you have multiple services on top of the same connection and you want to be able to use the error handling on a per service basis, since nc.Opts.AsyncErrorCB
belongs to the connection, in current PR it seems that only one Service would be able to use the error handler.
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 service implementation also has ErrorHandler
, which extends AsyncErrorCB
.
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.
Not a fan of passing in nc tbh. I believe I prefer nc.AddService() tbh.
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 is the kind of package that will sprawl with dependencies like observability and more over time. Might be worth keeping in a seperate package to avoid those being in nats.go core
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.
@ripienaar You have a good point here, but unfortunately, separate packages have the same go.mod
.
We would need a multi-module setup. That brings it's own problems and complexity (especially with incompatible versions of dependencies).
service/service.go
Outdated
|
||
// Service is an interface for service management. | ||
// It exposes methods to stop/reset a service, as well as get information on a service. | ||
Service interface { |
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 change away from an interface?
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 wondering whether to better have interface or not, but my general reasoning is:
- we don't really need an interface here - unlike in JetStream simplification, where we'll probably want to support Push and Pull consumers separately (and possibly ordered consumer as well) on one interface, here we have 1 implementation
- by not using interfaces, we enable other users to add their own interfaces on top of
Service()
however they want, and we do not break anything for them if we want to add more methods - core NATS client does not rely on any interface, so while it's not 100% consistent (
JesStreamContext
is an interface), we do not break conventions
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 lately folks have pushed us more towards interfaces. I agree we should optimize for extensibility.
@ripienaar What are your thoughts?
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 you want people to wrap it or augment with additional abilities without changing code then interfaces help. So for KV for example its an interface and we can wrap a cache, codec, e2e encryption or whatever we like - those just wrap the interface and any other calling code doesnt need to change.
So if that kind of thing isnt a goal here then you can be more fluid about it.
Regarding core nats package - Msg is a great example of someting that really should have been an interface. Rather than the way things are now with a Msg struct with a ton of methods on it thats just never used by core nats
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.
interface = can change internal details or have alternative implementations = win.
I suggest we have it as a separate package, but similar to JetStream its constructor is in the core pkg. So nc.AddService(). I also think its best that service goes back to an interface. /cc @ripienaar @aricart |
service/service.go
Outdated
svc.natsHandlers.asyncErr = nc.Opts.AsyncErrorCB | ||
if nc.Opts.AsyncErrorCB != nil { | ||
nc.SetErrorHandler(func(c *nats.Conn, s *nats.Subscription, err error) { | ||
if config.ErrorHandler != nil { |
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 here you need to check whether the s *nats.Subscription
belongs to one of the subscriptions that were mounted by the Service (reqSub, etc...), as the error handler would apply to any other subscription from the client.
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.
Good catch - done.
service/service.go
Outdated
Subject: s.Subject, | ||
}) | ||
} | ||
svc.Stop() |
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.
should this one be run in a goroutine? What could be interesting here is to check whether the error is a SlowConsumer error for example to have it drain, and maybe let the service resubscribe once its buffers have caught up.
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 not sure whether it should run in a goroutine... It is added in async handler anyway, I would probably keep it synchronous within one callback.
As to detecting e.g. slow consumer error (maybe others), I would do that in a separate PR if that's ok, just to make this one works properly and then add improvements.
@derekcollison I agree that changing As to having constructor as a method on Creating a wrapper around an existing type, even reusing some kind of connection in it, by passing the connection as an argument is not uncommon in go, neither in standard library nor in other popular libraries. Some examples:
func Client(conn net.Conn, config *Config) *Conn
func NewClient(conn net.Conn, host string) (*Client, error)
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) Ultimately, we do a similar thing - we create a new layer on top on |
@piotrpio that's a good point on cyclic dependencies, it will not be possible to separate concerns into two packages and also have something like |
@wallyqs yeah, I was thinking about relying on interfaces in |
ok we can discuss, but feel it should be |
to get to |
@wallyqs Yeah, I tried that approach yesterday and while it's doable, I don't like it for a couple of reasons:
|
dc44f93
to
d3ae7e5
Compare
micro/service.go
Outdated
response, _ := json.Marshal(svc.Info()) | ||
if err := req.Respond(response); err != nil { | ||
if err := req.Error("500", fmt.Sprintf("Error handling INFO request: %s", err)); err != nil && config.ErrorHandler != nil { | ||
go config.ErrorHandler(svc, &NATSError{req.Subject, err.Error()}) |
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 would spin up as many go routines as errors appear, in the nats.go these are serialized over a channel to be able to have some control over these: https://github.com/nats-io/nats.go/blob/main/nats.go#L2769-L2792
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.
Changed implementation to use an error dispatcher per-service
micro/service.go
Outdated
response, _ := json.Marshal(ping) | ||
if err := req.Respond(response); err != nil { | ||
if err := req.Error("500", fmt.Sprintf("Error handling PING request: %s", err)); err != nil && config.ErrorHandler != nil { | ||
go config.ErrorHandler(svc, &NATSError{req.Subject, err.Error()}) |
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.
maybe could use err error
as part of the state from NATSError
? That way later on could be able to use errors.Is(err, ErrSlowConsumer)
and handle that case for example
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 leave it as it is for now, ErrSlowConsumer
would be handled internally anyway. We could add it later on if needed.
de7a8a3
to
7ae2834
Compare
Co-authored-by: Tomasz Pietrek <[email protected]>
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.
LGTM!
Changes to service API:
services
->service
)DoneHandler
andErrorHandler
nats.Msg
(Request
) to enable adding helper methodsService
to a struct (instead ofinterface
)Git diff unfortunately displays this diff as added/removed files, because of the changed package/dir name (I wanted to make it singular).