This outlines the backwards incompatible changes that were made to the public API after the
v0.3.7
stable release, and and how to migrate existing legacy codebases.
The original go-nsq
codebase is some of our earliest Go code, and one of our first attempts at a
public Go library.
We've learned a lot over the last 2 years and we wanted go-nsq
to reflect the experiences we've
had working with the library as well as the general Go conventions and best practices we picked up
along the way.
The diff can be seen via: https://github.com/nsqio/go-nsq/compare/v0.3.7...HEAD
The bulk of the refactoring came via: nsqio#30
Previously, the high-level types we exposed were named nsq.Reader
and nsq.Writer
. These
reflected internal naming conventions we had used at bitly for some time but conflated semantics
with what a typical Go developer would expect (they obviously did not implement io.Reader
and
io.Writer
).
We renamed these types to nsq.Consumer
and nsq.Producer
, which more effectively communicate
their purpose and is consistent with the NSQ documentation.
In the previous API there were inconsistent and confusing ways to configure your clients.
Now, configuration is performed before creating an nsq.Consumer
or nsq.Producer
by creating
an nsq.Config
struct. The only valid way to do this is via nsq.NewConfig
(i.e. using a struct
literal will panic due to invalid internal state).
The nsq.Config
struct has exported variables that can be set directly in a type-safe manner. You
can also call cfg.Validate()
to check that the values are correct and within range.
nsq.Config
also exposes a convenient helper method Set(k string, v interface{})
that can set
options by coercing the supplied interface{}
value.
This is incredibly convenient if you're reading options from a config file or in a serialized format that does not exactly match the native types.
It is both flexible and forgiving.
go-nsq
attempts to make writing the common use case consumer incredibly easy.
You specify a type that implements the nsq.Handler
interface, the interface method is called per
message, and the return value of said method indicates to the library what the response to nsqd
should be (FIN
or REQ
), all the while managing flow control and backoff.
However, more advanced use cases require the ability to respond to a message later
("asynchronously", if you will). Our original API provided a second message handler interface
called nsq.AsyncHandler
.
Unfortunately, it was never obvious from the name alone (or even the documentation) how to properly use this form. The API was needlessly complex, involving the garbage creation of wrapping structs to track state and respond to messages.
We originally had the same problem in pynsq
, our Python client library, and we were able to
resolve the tension and expose an API that was robust and supported all use cases.
The new go-nsq
message handler interface exposes only nsq.Handler
, and its HandleMessage
method remains identical (specifically, nsq.AsyncHandler
has been removed).
Additionally, the API to configure handlers has been improved to provide better first-class support
for common operations. We've added AddConcurrentHandlers
(for quickly spawning multiple handler
goroutines).
For the most common use case, where you want go-nsq
to respond to messages on your behalf, there
are no changes required! In fact, we've made it even easier to implement the nsq.Handler
interface for simple functions by providing the nsq.HandlerFunc
type (in the spirit of the Go
standard library's http.HandlerFunc
):
r, err := nsq.NewConsumer("test_topic", "test_channel", nsq.NewConfig())
if err != nil {
log.Fatalf(err.Error())
}
r.AddHandler(nsq.HandlerFunc(func(m *nsq.Message) error {
return doSomeWork(m)
})
err := r.ConnectToNSQD(nsqdAddr)
if err != nil {
log.Fatalf(err.Error())
}
<-r.StopChan
In the new API, we've made the nsq.Message
struct more robust, giving it the ability to proxy
responses. If you want to usurp control of the message from go-nsq
, you simply call
msg.DisableAutoResponse()
.
This is effectively the same as if you had used nsq.AsyncHandler
, only you don't need to manage
nsq.FinishedMessage
structs or implement a separate interface. Instead you just keep/pass
references to the nsq.Message
itself, and when you're ready to respond you call msg.Finish()
,
msg.Requeue(<duration>)
or msg.Touch(<duration>)
. Additionally, this means you can make this
decision on a per-message basis rather than for the lifetime of the handler.
Here is an example:
type myHandler struct {}
func (h *myHandler) HandleMessage(m *nsq.Message) error {
m.DisableAutoResponse()
workerChan <- m
return nil
}
go func() {
for m := range workerChan {
err := doSomeWork(m)
if err != nil {
m.Requeue(-1)
continue
}
m.Finish()
}
}()
cfg := nsq.NewConfig()
cfg.MaxInFlight = 1000
r, err := nsq.NewConsumer("test_topic", "test_channel", cfg)
if err != nil {
log.Fatalf(err.Error())
}
r.AddConcurrentHandlers(&myHandler{}, 20)
err := r.ConnectToNSQD(nsqdAddr)
if err != nil {
log.Fatalf(err.Error())
}
<-r.StopChan
As a side effect of the message handler restructuring above, it is now trivial to respond to a
message without triggering a backoff state in nsq.Consumer
(which was not possible in the
previous API).
The nsq.Message
type now has a msg.RequeueWithoutBackoff()
method for this purpose.
Previously, Writer
(now Producer
) returned a triplicate of frameType
, responseBody
, and
error
from calls to *Publish
.
This required the caller to check both error
and frameType
to confirm success. Producer
publish methods now return only error
.
One of the challenges library implementors face is how to provide feedback via logging, while exposing an interface that follows the standard library and still provides a means to control and configure the output.
In the new API, we've provided a method on Consumer
and Producer
called SetLogger
that takes
an interface compatible with the Go standard library log.Logger
(which can be instantiated via
log.NewLogger
) and a traditional log level integer nsq.LogLevel{Debug,Info,Warning,Error}
:
Output(maxdepth int, s string) error
This gives the user the flexibility to control the format, destination, and verbosity while still conforming to standard library logging conventions.
Un-exported NewDeadlineTransport
and ApiRequest
, which never should have been exported in the
first place.
nsq.Message
serialization switched away from binary.{Read,Write}
for performance and
nsq.Message
now implements the io.WriterTo
interface.