pax_global_header 0000666 0000000 0000000 00000000064 12716410165 0014515 g ustar 00root root 0000000 0000000 52 comment=38d579a708d2f87dd4e35342241048e361d1e66a sarama-1.9.0/ 0000775 0000000 0000000 00000000000 12716410165 0012770 5 ustar 00root root 0000000 0000000 sarama-1.9.0/.gitignore 0000664 0000000 0000000 00000000414 12716410165 0014757 0 ustar 00root root 0000000 0000000 # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so *.test # Folders _obj _test .vagrant # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe sarama-1.9.0/.travis.yml 0000664 0000000 0000000 00000001107 12716410165 0015100 0 ustar 00root root 0000000 0000000 language: go go: - 1.5.4 - 1.6.1 env: global: - KAFKA_PEERS=localhost:9091,localhost:9092,localhost:9093,localhost:9094,localhost:9095 - TOXIPROXY_ADDR=http://localhost:8474 - KAFKA_INSTALL_ROOT=/home/travis/kafka - KAFKA_HOSTNAME=localhost - DEBUG=true matrix: - KAFKA_VERSION=0.8.2.2 - KAFKA_VERSION=0.9.0.1 before_install: - export REPOSITORY_ROOT=${TRAVIS_BUILD_DIR} - vagrant/install_cluster.sh - vagrant/boot_cluster.sh - vagrant/create_topics.sh install: - make install_dependencies script: - make test - make vet - make errcheck - make fmt sudo: false sarama-1.9.0/CHANGELOG.md 0000664 0000000 0000000 00000026753 12716410165 0014616 0 ustar 00root root 0000000 0000000 # Changelog #### Version 1.9.0 (2016-05-16) New Features: - Add support for custom offset manager retention durations ([#602](https://github.com/Shopify/sarama/pull/602)). - Publish low-level mocks to enable testing of third-party producer/consumer implementations ([#570](https://github.com/Shopify/sarama/pull/570)). - Declare support for Golang 1.6 ([#611](https://github.com/Shopify/sarama/pull/611)). - Support for SASL plain-text auth ([#648](https://github.com/Shopify/sarama/pull/648)). Improvements: - Simplified broker locking scheme slightly ([#604](https://github.com/Shopify/sarama/pull/604)). - Documentation cleanup ([#605](https://github.com/Shopify/sarama/pull/605), [#621](https://github.com/Shopify/sarama/pull/621), [#654](https://github.com/Shopify/sarama/pull/654)). Bug Fixes: - Fix race condition shutting down the OffsetManager ([#658](https://github.com/Shopify/sarama/pull/658)). #### Version 1.8.0 (2016-02-01) New Features: - Full support for Kafka 0.9: - All protocol messages and fields ([#586](https://github.com/Shopify/sarama/pull/586), [#588](https://github.com/Shopify/sarama/pull/588), [#590](https://github.com/Shopify/sarama/pull/590)). - Verified that TLS support works ([#581](https://github.com/Shopify/sarama/pull/581)). - Fixed the OffsetManager compatibility ([#585](https://github.com/Shopify/sarama/pull/585)). Improvements: - Optimize for fewer system calls when reading from the network ([#584](https://github.com/Shopify/sarama/pull/584)). - Automatically retry `InvalidMessage` errors to match upstream behaviour ([#589](https://github.com/Shopify/sarama/pull/589)). #### Version 1.7.0 (2015-12-11) New Features: - Preliminary support for Kafka 0.9 ([#572](https://github.com/Shopify/sarama/pull/572)). This comes with several caveats: - Protocol-layer support is mostly in place ([#577](https://github.com/Shopify/sarama/pull/577)), however Kafka 0.9 renamed some messages and fields, which we did not in order to preserve API compatibility. - The producer and consumer work against 0.9, but the offset manager does not ([#573](https://github.com/Shopify/sarama/pull/573)). - TLS support may or may not work ([#581](https://github.com/Shopify/sarama/pull/581)). Improvements: - Don't wait for request timeouts on dead brokers, greatly speeding recovery when the TCP connection is left hanging ([#548](https://github.com/Shopify/sarama/pull/548)). - Refactored part of the producer. The new version provides a much more elegant solution to [#449](https://github.com/Shopify/sarama/pull/449). It is also slightly more efficient, and much more precise in calculating batch sizes when compression is used ([#549](https://github.com/Shopify/sarama/pull/549), [#550](https://github.com/Shopify/sarama/pull/550), [#551](https://github.com/Shopify/sarama/pull/551)). Bug Fixes: - Fix race condition in consumer test mock ([#553](https://github.com/Shopify/sarama/pull/553)). #### Version 1.6.1 (2015-09-25) Bug Fixes: - Fix panic that could occur if a user-supplied message value failed to encode ([#449](https://github.com/Shopify/sarama/pull/449)). #### Version 1.6.0 (2015-09-04) New Features: - Implementation of a consumer offset manager using the APIs introduced in Kafka 0.8.2. The API is designed mainly for integration into a future high-level consumer, not for direct use, although it is *possible* to use it directly. ([#461](https://github.com/Shopify/sarama/pull/461)). Improvements: - CRC32 calculation is much faster on machines with SSE4.2 instructions, removing a major hotspot from most profiles ([#255](https://github.com/Shopify/sarama/pull/255)). Bug Fixes: - Make protocol decoding more robust against some malformed packets generated by go-fuzz ([#523](https://github.com/Shopify/sarama/pull/523), [#525](https://github.com/Shopify/sarama/pull/525)) or found in other ways ([#528](https://github.com/Shopify/sarama/pull/528)). - Fix a potential race condition panic in the consumer on shutdown ([#529](https://github.com/Shopify/sarama/pull/529)). #### Version 1.5.0 (2015-08-17) New Features: - TLS-encrypted network connections are now supported. This feature is subject to change when Kafka releases built-in TLS support, but for now this is enough to work with TLS-terminating proxies ([#154](https://github.com/Shopify/sarama/pull/154)). Improvements: - The consumer will not block if a single partition is not drained by the user; all other partitions will continue to consume normally ([#485](https://github.com/Shopify/sarama/pull/485)). - Formatting of error strings has been much improved ([#495](https://github.com/Shopify/sarama/pull/495)). - Internal refactoring of the producer for code cleanliness and to enable future work ([#300](https://github.com/Shopify/sarama/pull/300)). Bug Fixes: - Fix a potential deadlock in the consumer on shutdown ([#475](https://github.com/Shopify/sarama/pull/475)). #### Version 1.4.3 (2015-07-21) Bug Fixes: - Don't include the partitioner in the producer's "fetch partitions" circuit-breaker ([#466](https://github.com/Shopify/sarama/pull/466)). - Don't retry messages until the broker is closed when abandoning a broker in the producer ([#468](https://github.com/Shopify/sarama/pull/468)). - Update the import path for snappy-go, it has moved again and the API has changed slightly ([#486](https://github.com/Shopify/sarama/pull/486)). #### Version 1.4.2 (2015-05-27) Bug Fixes: - Update the import path for snappy-go, it has moved from google code to github ([#456](https://github.com/Shopify/sarama/pull/456)). #### Version 1.4.1 (2015-05-25) Improvements: - Optimizations when decoding snappy messages, thanks to John Potocny ([#446](https://github.com/Shopify/sarama/pull/446)). Bug Fixes: - Fix hypothetical race conditions on producer shutdown ([#450](https://github.com/Shopify/sarama/pull/450), [#451](https://github.com/Shopify/sarama/pull/451)). #### Version 1.4.0 (2015-05-01) New Features: - The consumer now implements `Topics()` and `Partitions()` methods to enable users to dynamically choose what topics/partitions to consume without instantiating a full client ([#431](https://github.com/Shopify/sarama/pull/431)). - The partition-consumer now exposes the high water mark offset value returned by the broker via the `HighWaterMarkOffset()` method ([#339](https://github.com/Shopify/sarama/pull/339)). - Added a `kafka-console-consumer` tool capable of handling multiple partitions, and deprecated the now-obsolete `kafka-console-partitionConsumer` ([#439](https://github.com/Shopify/sarama/pull/439), [#442](https://github.com/Shopify/sarama/pull/442)). Improvements: - The producer's logging during retry scenarios is more consistent, more useful, and slightly less verbose ([#429](https://github.com/Shopify/sarama/pull/429)). - The client now shuffles its initial list of seed brokers in order to prevent thundering herd on the first broker in the list ([#441](https://github.com/Shopify/sarama/pull/441)). Bug Fixes: - The producer now correctly manages its state if retries occur when it is shutting down, fixing several instances of confusing behaviour and at least one potential deadlock ([#419](https://github.com/Shopify/sarama/pull/419)). - The consumer now handles messages for different partitions asynchronously, making it much more resilient to specific user code ordering ([#325](https://github.com/Shopify/sarama/pull/325)). #### Version 1.3.0 (2015-04-16) New Features: - The client now tracks consumer group coordinators using ConsumerMetadataRequests similar to how it tracks partition leadership using regular MetadataRequests ([#411](https://github.com/Shopify/sarama/pull/411)). This adds two methods to the client API: - `Coordinator(consumerGroup string) (*Broker, error)` - `RefreshCoordinator(consumerGroup string) error` Improvements: - ConsumerMetadataResponses now automatically create a Broker object out of the ID/address/port combination for the Coordinator; accessing the fields individually has been deprecated ([#413](https://github.com/Shopify/sarama/pull/413)). - Much improved handling of `OffsetOutOfRange` errors in the consumer. Consumers will fail to start if the provided offset is out of range ([#418](https://github.com/Shopify/sarama/pull/418)) and they will automatically shut down if the offset falls out of range ([#424](https://github.com/Shopify/sarama/pull/424)). - Small performance improvement in encoding and decoding protocol messages ([#427](https://github.com/Shopify/sarama/pull/427)). Bug Fixes: - Fix a rare race condition in the client's background metadata refresher if it happens to be activated while the client is being closed ([#422](https://github.com/Shopify/sarama/pull/422)). #### Version 1.2.0 (2015-04-07) Improvements: - The producer's behaviour when `Flush.Frequency` is set is now more intuitive ([#389](https://github.com/Shopify/sarama/pull/389)). - The producer is now somewhat more memory-efficient during and after retrying messages due to an improved queue implementation ([#396](https://github.com/Shopify/sarama/pull/396)). - The consumer produces much more useful logging output when leadership changes ([#385](https://github.com/Shopify/sarama/pull/385)). - The client's `GetOffset` method will now automatically refresh metadata and retry once in the event of stale information or similar ([#394](https://github.com/Shopify/sarama/pull/394)). - Broker connections now have support for using TCP keepalives ([#407](https://github.com/Shopify/sarama/issues/407)). Bug Fixes: - The OffsetCommitRequest message now correctly implements all three possible API versions ([#390](https://github.com/Shopify/sarama/pull/390), [#400](https://github.com/Shopify/sarama/pull/400)). #### Version 1.1.0 (2015-03-20) Improvements: - Wrap the producer's partitioner call in a circuit-breaker so that repeatedly broken topics don't choke throughput ([#373](https://github.com/Shopify/sarama/pull/373)). Bug Fixes: - Fix the producer's internal reference counting in certain unusual scenarios ([#367](https://github.com/Shopify/sarama/pull/367)). - Fix the consumer's internal reference counting in certain unusual scenarios ([#369](https://github.com/Shopify/sarama/pull/369)). - Fix a condition where the producer's internal control messages could have gotten stuck ([#368](https://github.com/Shopify/sarama/pull/368)). - Fix an issue where invalid partition lists would be cached when asking for metadata for a non-existant topic ([#372](https://github.com/Shopify/sarama/pull/372)). #### Version 1.0.0 (2015-03-17) Version 1.0.0 is the first tagged version, and is almost a complete rewrite. The primary differences with previous untagged versions are: - The producer has been rewritten; there is now a `SyncProducer` with a blocking API, and an `AsyncProducer` that is non-blocking. - The consumer has been rewritten to only open one connection per broker instead of one connection per partition. - The main types of Sarama are now interfaces to make depedency injection easy; mock implementations for `Consumer`, `SyncProducer` and `AsyncProducer` are provided in the `github.com/Shopify/sarama/mocks` package. - For most uses cases, it is no longer necessary to open a `Client`; this will be done for you. - All the configuration values have been unified in the `Config` struct. - Much improved test suite. sarama-1.9.0/CONTRIBUTING.md 0000664 0000000 0000000 00000004546 12716410165 0015232 0 ustar 00root root 0000000 0000000 # Contributing Contributions are always welcome, both reporting issues and submitting pull requests! ### Reporting issues Please make sure to include any potentially useful information in the issue, so we can pinpoint the issue faster without going back and forth. - What SHA of Sarama are you running? If this is not the latest SHA on the master branch, please try if the problem persists with the latest version. - You can set `sarama.Logger` to a [log.Logger](http://golang.org/pkg/log/#Logger) instance to capture debug output. Please include it in your issue description. - Also look at the logs of the Kafka broker you are connected to. If you see anything out of the ordinary, please include it. Also, please include the following information about your environment, so we can help you faster: - What version of Kafka are you using? - What version of Go are you using? - What are the values of your Producer/Consumer/Client configuration? ### Submitting pull requests We will gladly accept bug fixes, or additions to this library. Please fork this library, commit & push your changes, and open a pull request. Because this library is in production use by many people and applications, we code review all additions. To make the review process go as smooth as possible, please consider the following. - If you plan to work on something major, please open an issue to discuss the design first. - Don't break backwards compatibility. If you really have to, open an issue to discuss this first. - Make sure to use the `go fmt` command to format your code according to the standards. Even better, set up your editor to do this for you when saving. - Run [go vet](https://godoc.org/golang.org/x/tools/cmd/vet) to detect any suspicious constructs in your code that could be bugs. - Explicitly handle all error return values. If you really want to ignore an error value, you can assign it to `_`.You can use [errcheck](https://github.com/kisielk/errcheck) to verify whether you have handled all errors. - You may also want to run [golint](https://github.com/golang/lint) as well to detect style problems. - Add tests that cover the changes you made. Make sure to run `go test` with the `-race` argument to test for race conditions. - Make sure your code is supported by all the Go versions we support. You can rely on [Travis CI](https://travis-ci.org/Shopify/sarama) for testing older Go versions sarama-1.9.0/MIT-LICENSE 0000664 0000000 0000000 00000002035 12716410165 0014424 0 ustar 00root root 0000000 0000000 Copyright (c) 2013 Evan Huus Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sarama-1.9.0/Makefile 0000664 0000000 0000000 00000000546 12716410165 0014435 0 ustar 00root root 0000000 0000000 default: fmt vet errcheck test test: go test -v -timeout 60s -race ./... vet: go vet ./... errcheck: errcheck github.com/Shopify/sarama/... fmt: @if [ -n "$$(go fmt ./...)" ]; then echo 'Please run go fmt on your code.' && exit 1; fi install_dependencies: install_errcheck get install_errcheck: go get github.com/kisielk/errcheck get: go get -t sarama-1.9.0/README.md 0000664 0000000 0000000 00000003617 12716410165 0014256 0 ustar 00root root 0000000 0000000 sarama ====== [](https://godoc.org/github.com/Shopify/sarama) [](https://travis-ci.org/Shopify/sarama) Sarama is an MIT-licensed Go client library for [Apache Kafka](https://kafka.apache.org/) version 0.8 (and later). ### Getting started - API documentation and examples are available via [godoc](https://godoc.org/github.com/Shopify/sarama). - Mocks for testing are available in the [mocks](./mocks) subpackage. - The [examples](./examples) directory contains more elaborate example applications. - The [tools](./tools) directory contains command line tools that can be useful for testing, diagnostics, and instrumentation. ### Compatibility and API stability Sarama provides a "2 releases + 2 months" compatibility guarantee: we support the two latest stable releases of Kafka and Go, and we provide a two month grace period for older releases. This means we currently officially support Go 1.6 and 1.5, and Kafka 0.9.0 and 0.8.2, although older releases are still likely to work. Sarama follows semantic versioning and provides API stability via the gopkg.in service. You can import a version with a guaranteed stable API via http://gopkg.in/Shopify/sarama.v1. A changelog is available [here](CHANGELOG.md). ### Contributing * Get started by checking our [contribution guidelines](https://github.com/Shopify/sarama/blob/master/CONTRIBUTING.md). * Read the [Sarama wiki](https://github.com/Shopify/sarama/wiki) for more technical and design details. * The [Kafka Protocol Specification](https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol) contains a wealth of useful information. * For more general issues, there is [a google group](https://groups.google.com/forum/#!forum/kafka-clients) for Kafka client developers. * If you have any questions, just ask! sarama-1.9.0/Vagrantfile 0000664 0000000 0000000 00000000707 12716410165 0015161 0 ustar 00root root 0000000 0000000 # -*- mode: ruby -*- # vi: set ft=ruby : # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! VAGRANTFILE_API_VERSION = "2" MEMORY = 3072 Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = "ubuntu/trusty64" config.vm.provision :shell, path: "vagrant/provision.sh" config.vm.network "private_network", ip: "192.168.100.67" config.vm.provider "virtualbox" do |v| v.memory = MEMORY end end sarama-1.9.0/async_producer.go 0000664 0000000 0000000 00000061011 12716410165 0016336 0 ustar 00root root 0000000 0000000 package sarama import ( "fmt" "sync" "time" "github.com/eapache/go-resiliency/breaker" "github.com/eapache/queue" ) // AsyncProducer publishes Kafka messages using a non-blocking API. It routes messages // to the correct broker for the provided topic-partition, refreshing metadata as appropriate, // and parses responses for errors. You must read from the Errors() channel or the // producer will deadlock. You must call Close() or AsyncClose() on a producer to avoid // leaks: it will not be garbage-collected automatically when it passes out of // scope. type AsyncProducer interface { // AsyncClose triggers a shutdown of the producer, flushing any messages it may // have buffered. The shutdown has completed when both the Errors and Successes // channels have been closed. When calling AsyncClose, you *must* continue to // read from those channels in order to drain the results of any messages in // flight. AsyncClose() // Close shuts down the producer and flushes any messages it may have buffered. // You must call this function before a producer object passes out of scope, as // it may otherwise leak memory. You must call this before calling Close on the // underlying client. Close() error // Input is the input channel for the user to write messages to that they // wish to send. Input() chan<- *ProducerMessage // Successes is the success output channel back to the user when AckSuccesses is // enabled. If Return.Successes is true, you MUST read from this channel or the // Producer will deadlock. It is suggested that you send and read messages // together in a single select statement. Successes() <-chan *ProducerMessage // Errors is the error output channel back to the user. You MUST read from this // channel or the Producer will deadlock when the channel is full. Alternatively, // you can set Producer.Return.Errors in your config to false, which prevents // errors to be returned. Errors() <-chan *ProducerError } type asyncProducer struct { client Client conf *Config ownClient bool errors chan *ProducerError input, successes, retries chan *ProducerMessage inFlight sync.WaitGroup brokers map[*Broker]chan<- *ProducerMessage brokerRefs map[chan<- *ProducerMessage]int brokerLock sync.Mutex } // NewAsyncProducer creates a new AsyncProducer using the given broker addresses and configuration. func NewAsyncProducer(addrs []string, conf *Config) (AsyncProducer, error) { client, err := NewClient(addrs, conf) if err != nil { return nil, err } p, err := NewAsyncProducerFromClient(client) if err != nil { return nil, err } p.(*asyncProducer).ownClient = true return p, nil } // NewAsyncProducerFromClient creates a new Producer using the given client. It is still // necessary to call Close() on the underlying client when shutting down this producer. func NewAsyncProducerFromClient(client Client) (AsyncProducer, error) { // Check that we are not dealing with a closed Client before processing any other arguments if client.Closed() { return nil, ErrClosedClient } p := &asyncProducer{ client: client, conf: client.Config(), errors: make(chan *ProducerError), input: make(chan *ProducerMessage), successes: make(chan *ProducerMessage), retries: make(chan *ProducerMessage), brokers: make(map[*Broker]chan<- *ProducerMessage), brokerRefs: make(map[chan<- *ProducerMessage]int), } // launch our singleton dispatchers go withRecover(p.dispatcher) go withRecover(p.retryHandler) return p, nil } type flagSet int8 const ( syn flagSet = 1 << iota // first message from partitionProducer to brokerProducer fin // final message from partitionProducer to brokerProducer and back shutdown // start the shutdown process ) // ProducerMessage is the collection of elements passed to the Producer in order to send a message. type ProducerMessage struct { Topic string // The Kafka topic for this message. // The partitioning key for this message. Pre-existing Encoders include // StringEncoder and ByteEncoder. Key Encoder // The actual message to store in Kafka. Pre-existing Encoders include // StringEncoder and ByteEncoder. Value Encoder // This field is used to hold arbitrary data you wish to include so it // will be available when receiving on the Successes and Errors channels. // Sarama completely ignores this field and is only to be used for // pass-through data. Metadata interface{} // Below this point are filled in by the producer as the message is processed // Offset is the offset of the message stored on the broker. This is only // guaranteed to be defined if the message was successfully delivered and // RequiredAcks is not NoResponse. Offset int64 // Partition is the partition that the message was sent to. This is only // guaranteed to be defined if the message was successfully delivered. Partition int32 retries int flags flagSet } const producerMessageOverhead = 26 // the metadata overhead of CRC, flags, etc. func (m *ProducerMessage) byteSize() int { size := producerMessageOverhead if m.Key != nil { size += m.Key.Length() } if m.Value != nil { size += m.Value.Length() } return size } func (m *ProducerMessage) clear() { m.flags = 0 m.retries = 0 } // ProducerError is the type of error generated when the producer fails to deliver a message. // It contains the original ProducerMessage as well as the actual error value. type ProducerError struct { Msg *ProducerMessage Err error } func (pe ProducerError) Error() string { return fmt.Sprintf("kafka: Failed to produce message to topic %s: %s", pe.Msg.Topic, pe.Err) } // ProducerErrors is a type that wraps a batch of "ProducerError"s and implements the Error interface. // It can be returned from the Producer's Close method to avoid the need to manually drain the Errors channel // when closing a producer. type ProducerErrors []*ProducerError func (pe ProducerErrors) Error() string { return fmt.Sprintf("kafka: Failed to deliver %d messages.", len(pe)) } func (p *asyncProducer) Errors() <-chan *ProducerError { return p.errors } func (p *asyncProducer) Successes() <-chan *ProducerMessage { return p.successes } func (p *asyncProducer) Input() chan<- *ProducerMessage { return p.input } func (p *asyncProducer) Close() error { p.AsyncClose() if p.conf.Producer.Return.Successes { go withRecover(func() { for _ = range p.successes { } }) } var errors ProducerErrors if p.conf.Producer.Return.Errors { for event := range p.errors { errors = append(errors, event) } } if len(errors) > 0 { return errors } return nil } func (p *asyncProducer) AsyncClose() { go withRecover(p.shutdown) } // singleton // dispatches messages by topic func (p *asyncProducer) dispatcher() { handlers := make(map[string]chan<- *ProducerMessage) shuttingDown := false for msg := range p.input { if msg == nil { Logger.Println("Something tried to send a nil message, it was ignored.") continue } if msg.flags&shutdown != 0 { shuttingDown = true p.inFlight.Done() continue } else if msg.retries == 0 { if shuttingDown { // we can't just call returnError here because that decrements the wait group, // which hasn't been incremented yet for this message, and shouldn't be pErr := &ProducerError{Msg: msg, Err: ErrShuttingDown} if p.conf.Producer.Return.Errors { p.errors <- pErr } else { Logger.Println(pErr) } continue } p.inFlight.Add(1) } if msg.byteSize() > p.conf.Producer.MaxMessageBytes { p.returnError(msg, ErrMessageSizeTooLarge) continue } handler := handlers[msg.Topic] if handler == nil { handler = p.newTopicProducer(msg.Topic) handlers[msg.Topic] = handler } handler <- msg } for _, handler := range handlers { close(handler) } } // one per topic // partitions messages, then dispatches them by partition type topicProducer struct { parent *asyncProducer topic string input <-chan *ProducerMessage breaker *breaker.Breaker handlers map[int32]chan<- *ProducerMessage partitioner Partitioner } func (p *asyncProducer) newTopicProducer(topic string) chan<- *ProducerMessage { input := make(chan *ProducerMessage, p.conf.ChannelBufferSize) tp := &topicProducer{ parent: p, topic: topic, input: input, breaker: breaker.New(3, 1, 10*time.Second), handlers: make(map[int32]chan<- *ProducerMessage), partitioner: p.conf.Producer.Partitioner(topic), } go withRecover(tp.dispatch) return input } func (tp *topicProducer) dispatch() { for msg := range tp.input { if msg.retries == 0 { if err := tp.partitionMessage(msg); err != nil { tp.parent.returnError(msg, err) continue } } handler := tp.handlers[msg.Partition] if handler == nil { handler = tp.parent.newPartitionProducer(msg.Topic, msg.Partition) tp.handlers[msg.Partition] = handler } handler <- msg } for _, handler := range tp.handlers { close(handler) } } func (tp *topicProducer) partitionMessage(msg *ProducerMessage) error { var partitions []int32 err := tp.breaker.Run(func() (err error) { if tp.partitioner.RequiresConsistency() { partitions, err = tp.parent.client.Partitions(msg.Topic) } else { partitions, err = tp.parent.client.WritablePartitions(msg.Topic) } return }) if err != nil { return err } numPartitions := int32(len(partitions)) if numPartitions == 0 { return ErrLeaderNotAvailable } choice, err := tp.partitioner.Partition(msg, numPartitions) if err != nil { return err } else if choice < 0 || choice >= numPartitions { return ErrInvalidPartition } msg.Partition = partitions[choice] return nil } // one per partition per topic // dispatches messages to the appropriate broker // also responsible for maintaining message order during retries type partitionProducer struct { parent *asyncProducer topic string partition int32 input <-chan *ProducerMessage leader *Broker breaker *breaker.Breaker output chan<- *ProducerMessage // highWatermark tracks the "current" retry level, which is the only one where we actually let messages through, // all other messages get buffered in retryState[msg.retries].buf to preserve ordering // retryState[msg.retries].expectChaser simply tracks whether we've seen a fin message for a given level (and // therefore whether our buffer is complete and safe to flush) highWatermark int retryState []partitionRetryState } type partitionRetryState struct { buf []*ProducerMessage expectChaser bool } func (p *asyncProducer) newPartitionProducer(topic string, partition int32) chan<- *ProducerMessage { input := make(chan *ProducerMessage, p.conf.ChannelBufferSize) pp := &partitionProducer{ parent: p, topic: topic, partition: partition, input: input, breaker: breaker.New(3, 1, 10*time.Second), retryState: make([]partitionRetryState, p.conf.Producer.Retry.Max+1), } go withRecover(pp.dispatch) return input } func (pp *partitionProducer) dispatch() { // try to prefetch the leader; if this doesn't work, we'll do a proper call to `updateLeader` // on the first message pp.leader, _ = pp.parent.client.Leader(pp.topic, pp.partition) if pp.leader != nil { pp.output = pp.parent.getBrokerProducer(pp.leader) pp.parent.inFlight.Add(1) // we're generating a syn message; track it so we don't shut down while it's still inflight pp.output <- &ProducerMessage{Topic: pp.topic, Partition: pp.partition, flags: syn} } for msg := range pp.input { if msg.retries > pp.highWatermark { // a new, higher, retry level; handle it and then back off pp.newHighWatermark(msg.retries) time.Sleep(pp.parent.conf.Producer.Retry.Backoff) } else if pp.highWatermark > 0 { // we are retrying something (else highWatermark would be 0) but this message is not a *new* retry level if msg.retries < pp.highWatermark { // in fact this message is not even the current retry level, so buffer it for now (unless it's a just a fin) if msg.flags&fin == fin { pp.retryState[msg.retries].expectChaser = false pp.parent.inFlight.Done() // this fin is now handled and will be garbage collected } else { pp.retryState[msg.retries].buf = append(pp.retryState[msg.retries].buf, msg) } continue } else if msg.flags&fin == fin { // this message is of the current retry level (msg.retries == highWatermark) and the fin flag is set, // meaning this retry level is done and we can go down (at least) one level and flush that pp.retryState[pp.highWatermark].expectChaser = false pp.flushRetryBuffers() pp.parent.inFlight.Done() // this fin is now handled and will be garbage collected continue } } // if we made it this far then the current msg contains real data, and can be sent to the next goroutine // without breaking any of our ordering guarantees if pp.output == nil { if err := pp.updateLeader(); err != nil { pp.parent.returnError(msg, err) time.Sleep(pp.parent.conf.Producer.Retry.Backoff) continue } Logger.Printf("producer/leader/%s/%d selected broker %d\n", pp.topic, pp.partition, pp.leader.ID()) } pp.output <- msg } if pp.output != nil { pp.parent.unrefBrokerProducer(pp.leader, pp.output) } } func (pp *partitionProducer) newHighWatermark(hwm int) { Logger.Printf("producer/leader/%s/%d state change to [retrying-%d]\n", pp.topic, pp.partition, hwm) pp.highWatermark = hwm // send off a fin so that we know when everything "in between" has made it // back to us and we can safely flush the backlog (otherwise we risk re-ordering messages) pp.retryState[pp.highWatermark].expectChaser = true pp.parent.inFlight.Add(1) // we're generating a fin message; track it so we don't shut down while it's still inflight pp.output <- &ProducerMessage{Topic: pp.topic, Partition: pp.partition, flags: fin, retries: pp.highWatermark - 1} // a new HWM means that our current broker selection is out of date Logger.Printf("producer/leader/%s/%d abandoning broker %d\n", pp.topic, pp.partition, pp.leader.ID()) pp.parent.unrefBrokerProducer(pp.leader, pp.output) pp.output = nil } func (pp *partitionProducer) flushRetryBuffers() { Logger.Printf("producer/leader/%s/%d state change to [flushing-%d]\n", pp.topic, pp.partition, pp.highWatermark) for { pp.highWatermark-- if pp.output == nil { if err := pp.updateLeader(); err != nil { pp.parent.returnErrors(pp.retryState[pp.highWatermark].buf, err) goto flushDone } Logger.Printf("producer/leader/%s/%d selected broker %d\n", pp.topic, pp.partition, pp.leader.ID()) } for _, msg := range pp.retryState[pp.highWatermark].buf { pp.output <- msg } flushDone: pp.retryState[pp.highWatermark].buf = nil if pp.retryState[pp.highWatermark].expectChaser { Logger.Printf("producer/leader/%s/%d state change to [retrying-%d]\n", pp.topic, pp.partition, pp.highWatermark) break } else if pp.highWatermark == 0 { Logger.Printf("producer/leader/%s/%d state change to [normal]\n", pp.topic, pp.partition) break } } } func (pp *partitionProducer) updateLeader() error { return pp.breaker.Run(func() (err error) { if err = pp.parent.client.RefreshMetadata(pp.topic); err != nil { return err } if pp.leader, err = pp.parent.client.Leader(pp.topic, pp.partition); err != nil { return err } pp.output = pp.parent.getBrokerProducer(pp.leader) pp.parent.inFlight.Add(1) // we're generating a syn message; track it so we don't shut down while it's still inflight pp.output <- &ProducerMessage{Topic: pp.topic, Partition: pp.partition, flags: syn} return nil }) } // one per broker; also constructs an associated flusher func (p *asyncProducer) newBrokerProducer(broker *Broker) chan<- *ProducerMessage { var ( input = make(chan *ProducerMessage) bridge = make(chan *produceSet) responses = make(chan *brokerProducerResponse) ) bp := &brokerProducer{ parent: p, broker: broker, input: input, output: bridge, responses: responses, buffer: newProduceSet(p), currentRetries: make(map[string]map[int32]error), } go withRecover(bp.run) // minimal bridge to make the network response `select`able go withRecover(func() { for set := range bridge { request := set.buildRequest() response, err := broker.Produce(request) responses <- &brokerProducerResponse{ set: set, err: err, res: response, } } close(responses) }) return input } type brokerProducerResponse struct { set *produceSet err error res *ProduceResponse } // groups messages together into appropriately-sized batches for sending to the broker // handles state related to retries etc type brokerProducer struct { parent *asyncProducer broker *Broker input <-chan *ProducerMessage output chan<- *produceSet responses <-chan *brokerProducerResponse buffer *produceSet timer <-chan time.Time timerFired bool closing error currentRetries map[string]map[int32]error } func (bp *brokerProducer) run() { var output chan<- *produceSet Logger.Printf("producer/broker/%d starting up\n", bp.broker.ID()) for { select { case msg := <-bp.input: if msg == nil { bp.shutdown() return } if msg.flags&syn == syn { Logger.Printf("producer/broker/%d state change to [open] on %s/%d\n", bp.broker.ID(), msg.Topic, msg.Partition) if bp.currentRetries[msg.Topic] == nil { bp.currentRetries[msg.Topic] = make(map[int32]error) } bp.currentRetries[msg.Topic][msg.Partition] = nil bp.parent.inFlight.Done() continue } if reason := bp.needsRetry(msg); reason != nil { bp.parent.retryMessage(msg, reason) if bp.closing == nil && msg.flags&fin == fin { // we were retrying this partition but we can start processing again delete(bp.currentRetries[msg.Topic], msg.Partition) Logger.Printf("producer/broker/%d state change to [closed] on %s/%d\n", bp.broker.ID(), msg.Topic, msg.Partition) } continue } if bp.buffer.wouldOverflow(msg) { if err := bp.waitForSpace(msg); err != nil { bp.parent.retryMessage(msg, err) continue } } if err := bp.buffer.add(msg); err != nil { bp.parent.returnError(msg, err) continue } if bp.parent.conf.Producer.Flush.Frequency > 0 && bp.timer == nil { bp.timer = time.After(bp.parent.conf.Producer.Flush.Frequency) } case <-bp.timer: bp.timerFired = true case output <- bp.buffer: bp.rollOver() case response := <-bp.responses: bp.handleResponse(response) } if bp.timerFired || bp.buffer.readyToFlush() { output = bp.output } else { output = nil } } } func (bp *brokerProducer) shutdown() { for !bp.buffer.empty() { select { case response := <-bp.responses: bp.handleResponse(response) case bp.output <- bp.buffer: bp.rollOver() } } close(bp.output) for response := range bp.responses { bp.handleResponse(response) } Logger.Printf("producer/broker/%d shut down\n", bp.broker.ID()) } func (bp *brokerProducer) needsRetry(msg *ProducerMessage) error { if bp.closing != nil { return bp.closing } return bp.currentRetries[msg.Topic][msg.Partition] } func (bp *brokerProducer) waitForSpace(msg *ProducerMessage) error { Logger.Printf("producer/broker/%d maximum request accumulated, waiting for space\n", bp.broker.ID()) for { select { case response := <-bp.responses: bp.handleResponse(response) // handling a response can change our state, so re-check some things if reason := bp.needsRetry(msg); reason != nil { return reason } else if !bp.buffer.wouldOverflow(msg) { return nil } case bp.output <- bp.buffer: bp.rollOver() return nil } } } func (bp *brokerProducer) rollOver() { bp.timer = nil bp.timerFired = false bp.buffer = newProduceSet(bp.parent) } func (bp *brokerProducer) handleResponse(response *brokerProducerResponse) { if response.err != nil { bp.handleError(response.set, response.err) } else { bp.handleSuccess(response.set, response.res) } if bp.buffer.empty() { bp.rollOver() // this can happen if the response invalidated our buffer } } func (bp *brokerProducer) handleSuccess(sent *produceSet, response *ProduceResponse) { // we iterate through the blocks in the request set, not the response, so that we notice // if the response is missing a block completely sent.eachPartition(func(topic string, partition int32, msgs []*ProducerMessage) { if response == nil { // this only happens when RequiredAcks is NoResponse, so we have to assume success bp.parent.returnSuccesses(msgs) return } block := response.GetBlock(topic, partition) if block == nil { bp.parent.returnErrors(msgs, ErrIncompleteResponse) return } switch block.Err { // Success case ErrNoError: for i, msg := range msgs { msg.Offset = block.Offset + int64(i) } bp.parent.returnSuccesses(msgs) // Retriable errors case ErrInvalidMessage, ErrUnknownTopicOrPartition, ErrLeaderNotAvailable, ErrNotLeaderForPartition, ErrRequestTimedOut, ErrNotEnoughReplicas, ErrNotEnoughReplicasAfterAppend: Logger.Printf("producer/broker/%d state change to [retrying] on %s/%d because %v\n", bp.broker.ID(), topic, partition, block.Err) bp.currentRetries[topic][partition] = block.Err bp.parent.retryMessages(msgs, block.Err) bp.parent.retryMessages(bp.buffer.dropPartition(topic, partition), block.Err) // Other non-retriable errors default: bp.parent.returnErrors(msgs, block.Err) } }) } func (bp *brokerProducer) handleError(sent *produceSet, err error) { switch err.(type) { case PacketEncodingError: sent.eachPartition(func(topic string, partition int32, msgs []*ProducerMessage) { bp.parent.returnErrors(msgs, err) }) default: Logger.Printf("producer/broker/%d state change to [closing] because %s\n", bp.broker.ID(), err) bp.parent.abandonBrokerConnection(bp.broker) _ = bp.broker.Close() bp.closing = err sent.eachPartition(func(topic string, partition int32, msgs []*ProducerMessage) { bp.parent.retryMessages(msgs, err) }) bp.buffer.eachPartition(func(topic string, partition int32, msgs []*ProducerMessage) { bp.parent.retryMessages(msgs, err) }) bp.rollOver() } } // singleton // effectively a "bridge" between the flushers and the dispatcher in order to avoid deadlock // based on https://godoc.org/github.com/eapache/channels#InfiniteChannel func (p *asyncProducer) retryHandler() { var msg *ProducerMessage buf := queue.New() for { if buf.Length() == 0 { msg = <-p.retries } else { select { case msg = <-p.retries: case p.input <- buf.Peek().(*ProducerMessage): buf.Remove() continue } } if msg == nil { return } buf.Add(msg) } } // utility functions func (p *asyncProducer) shutdown() { Logger.Println("Producer shutting down.") p.inFlight.Add(1) p.input <- &ProducerMessage{flags: shutdown} p.inFlight.Wait() if p.ownClient { err := p.client.Close() if err != nil { Logger.Println("producer/shutdown failed to close the embedded client:", err) } } close(p.input) close(p.retries) close(p.errors) close(p.successes) } func (p *asyncProducer) returnError(msg *ProducerMessage, err error) { msg.clear() pErr := &ProducerError{Msg: msg, Err: err} if p.conf.Producer.Return.Errors { p.errors <- pErr } else { Logger.Println(pErr) } p.inFlight.Done() } func (p *asyncProducer) returnErrors(batch []*ProducerMessage, err error) { for _, msg := range batch { p.returnError(msg, err) } } func (p *asyncProducer) returnSuccesses(batch []*ProducerMessage) { for _, msg := range batch { if p.conf.Producer.Return.Successes { msg.clear() p.successes <- msg } p.inFlight.Done() } } func (p *asyncProducer) retryMessage(msg *ProducerMessage, err error) { if msg.retries >= p.conf.Producer.Retry.Max { p.returnError(msg, err) } else { msg.retries++ p.retries <- msg } } func (p *asyncProducer) retryMessages(batch []*ProducerMessage, err error) { for _, msg := range batch { p.retryMessage(msg, err) } } func (p *asyncProducer) getBrokerProducer(broker *Broker) chan<- *ProducerMessage { p.brokerLock.Lock() defer p.brokerLock.Unlock() bp := p.brokers[broker] if bp == nil { bp = p.newBrokerProducer(broker) p.brokers[broker] = bp p.brokerRefs[bp] = 0 } p.brokerRefs[bp]++ return bp } func (p *asyncProducer) unrefBrokerProducer(broker *Broker, bp chan<- *ProducerMessage) { p.brokerLock.Lock() defer p.brokerLock.Unlock() p.brokerRefs[bp]-- if p.brokerRefs[bp] == 0 { close(bp) delete(p.brokerRefs, bp) if p.brokers[broker] == bp { delete(p.brokers, broker) } } } func (p *asyncProducer) abandonBrokerConnection(broker *Broker) { p.brokerLock.Lock() defer p.brokerLock.Unlock() delete(p.brokers, broker) } sarama-1.9.0/async_producer_test.go 0000664 0000000 0000000 00000055764 12716410165 0017417 0 ustar 00root root 0000000 0000000 package sarama import ( "errors" "log" "os" "os/signal" "sync" "testing" "time" ) const TestMessage = "ABC THE MESSAGE" func closeProducer(t *testing.T, p AsyncProducer) { var wg sync.WaitGroup p.AsyncClose() wg.Add(2) go func() { for _ = range p.Successes() { t.Error("Unexpected message on Successes()") } wg.Done() }() go func() { for msg := range p.Errors() { t.Error(msg.Err) } wg.Done() }() wg.Wait() } func expectResults(t *testing.T, p AsyncProducer, successes, errors int) { expect := successes + errors for expect > 0 { select { case msg := <-p.Errors(): if msg.Msg.flags != 0 { t.Error("Message had flags set") } errors-- expect-- if errors < 0 { t.Error(msg.Err) } case msg := <-p.Successes(): if msg.flags != 0 { t.Error("Message had flags set") } successes-- expect-- if successes < 0 { t.Error("Too many successes") } } } if successes != 0 || errors != 0 { t.Error("Unexpected successes", successes, "or errors", errors) } } type testPartitioner chan *int32 func (p testPartitioner) Partition(msg *ProducerMessage, numPartitions int32) (int32, error) { part := <-p if part == nil { return 0, errors.New("BOOM") } return *part, nil } func (p testPartitioner) RequiresConsistency() bool { return true } func (p testPartitioner) feed(partition int32) { p <- &partition } type flakyEncoder bool func (f flakyEncoder) Length() int { return len(TestMessage) } func (f flakyEncoder) Encode() ([]byte, error) { if !bool(f) { return nil, errors.New("flaky encoding error") } return []byte(TestMessage), nil } func TestAsyncProducer(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leader.Addr(), leader.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse) prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader.Returns(prodSuccess) config := NewConfig() config.Producer.Flush.Messages = 10 config.Producer.Return.Successes = true producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage), Metadata: i} } for i := 0; i < 10; i++ { select { case msg := <-producer.Errors(): t.Error(msg.Err) if msg.Msg.flags != 0 { t.Error("Message had flags set") } case msg := <-producer.Successes(): if msg.flags != 0 { t.Error("Message had flags set") } if msg.Metadata.(int) != i { t.Error("Message metadata did not match") } } } closeProducer(t, producer) leader.Close() seedBroker.Close() } func TestAsyncProducerMultipleFlushes(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leader.Addr(), leader.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse) prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader.Returns(prodSuccess) leader.Returns(prodSuccess) leader.Returns(prodSuccess) config := NewConfig() config.Producer.Flush.Messages = 5 config.Producer.Return.Successes = true producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } for flush := 0; flush < 3; flush++ { for i := 0; i < 5; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } expectResults(t, producer, 5, 0) } closeProducer(t, producer) leader.Close() seedBroker.Close() } func TestAsyncProducerMultipleBrokers(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader0 := NewMockBroker(t, 2) leader1 := NewMockBroker(t, 3) metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leader0.Addr(), leader0.BrokerID()) metadataResponse.AddBroker(leader1.Addr(), leader1.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader0.BrokerID(), nil, nil, ErrNoError) metadataResponse.AddTopicPartition("my_topic", 1, leader1.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse) prodResponse0 := new(ProduceResponse) prodResponse0.AddTopicPartition("my_topic", 0, ErrNoError) leader0.Returns(prodResponse0) prodResponse1 := new(ProduceResponse) prodResponse1.AddTopicPartition("my_topic", 1, ErrNoError) leader1.Returns(prodResponse1) config := NewConfig() config.Producer.Flush.Messages = 5 config.Producer.Return.Successes = true config.Producer.Partitioner = NewRoundRobinPartitioner producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } expectResults(t, producer, 10, 0) closeProducer(t, producer) leader1.Close() leader0.Close() seedBroker.Close() } func TestAsyncProducerCustomPartitioner(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leader.Addr(), leader.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse) prodResponse := new(ProduceResponse) prodResponse.AddTopicPartition("my_topic", 0, ErrNoError) leader.Returns(prodResponse) config := NewConfig() config.Producer.Flush.Messages = 2 config.Producer.Return.Successes = true config.Producer.Partitioner = func(topic string) Partitioner { p := make(testPartitioner) go func() { p.feed(0) p <- nil p <- nil p <- nil p.feed(0) }() return p } producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } for i := 0; i < 5; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } expectResults(t, producer, 2, 3) closeProducer(t, producer) leader.Close() seedBroker.Close() } func TestAsyncProducerFailureRetry(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader1 := NewMockBroker(t, 2) leader2 := NewMockBroker(t, 3) metadataLeader1 := new(MetadataResponse) metadataLeader1.AddBroker(leader1.Addr(), leader1.BrokerID()) metadataLeader1.AddTopicPartition("my_topic", 0, leader1.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataLeader1) config := NewConfig() config.Producer.Flush.Messages = 10 config.Producer.Return.Successes = true config.Producer.Retry.Backoff = 0 producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } seedBroker.Close() for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } prodNotLeader := new(ProduceResponse) prodNotLeader.AddTopicPartition("my_topic", 0, ErrNotLeaderForPartition) leader1.Returns(prodNotLeader) metadataLeader2 := new(MetadataResponse) metadataLeader2.AddBroker(leader2.Addr(), leader2.BrokerID()) metadataLeader2.AddTopicPartition("my_topic", 0, leader2.BrokerID(), nil, nil, ErrNoError) leader1.Returns(metadataLeader2) prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader2.Returns(prodSuccess) expectResults(t, producer, 10, 0) leader1.Close() for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } leader2.Returns(prodSuccess) expectResults(t, producer, 10, 0) leader2.Close() closeProducer(t, producer) } func TestAsyncProducerEncoderFailures(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leader.Addr(), leader.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse) prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader.Returns(prodSuccess) leader.Returns(prodSuccess) leader.Returns(prodSuccess) config := NewConfig() config.Producer.Flush.Messages = 1 config.Producer.Return.Successes = true config.Producer.Partitioner = NewManualPartitioner producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } for flush := 0; flush < 3; flush++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: flakyEncoder(true), Value: flakyEncoder(false)} producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: flakyEncoder(false), Value: flakyEncoder(true)} producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: flakyEncoder(true), Value: flakyEncoder(true)} expectResults(t, producer, 1, 2) } closeProducer(t, producer) leader.Close() seedBroker.Close() } // If a Kafka broker becomes unavailable and then returns back in service, then // producer reconnects to it and continues sending messages. func TestAsyncProducerBrokerBounce(t *testing.T) { // Given seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) leaderAddr := leader.Addr() metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leaderAddr, leader.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse) prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) config := NewConfig() config.Producer.Flush.Messages = 1 config.Producer.Return.Successes = true config.Producer.Retry.Backoff = 0 producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} leader.Returns(prodSuccess) expectResults(t, producer, 1, 0) // When: a broker connection gets reset by a broker (network glitch, restart, you name it). leader.Close() // producer should get EOF leader = NewMockBrokerAddr(t, 2, leaderAddr) // start it up again right away for giggles seedBroker.Returns(metadataResponse) // tell it to go to broker 2 again // Then: a produced message goes through the new broker connection. producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} leader.Returns(prodSuccess) expectResults(t, producer, 1, 0) closeProducer(t, producer) seedBroker.Close() leader.Close() } func TestAsyncProducerBrokerBounceWithStaleMetadata(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader1 := NewMockBroker(t, 2) leader2 := NewMockBroker(t, 3) metadataLeader1 := new(MetadataResponse) metadataLeader1.AddBroker(leader1.Addr(), leader1.BrokerID()) metadataLeader1.AddTopicPartition("my_topic", 0, leader1.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataLeader1) config := NewConfig() config.Producer.Flush.Messages = 10 config.Producer.Return.Successes = true config.Producer.Retry.Max = 3 config.Producer.Retry.Backoff = 0 producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } leader1.Close() // producer should get EOF seedBroker.Returns(metadataLeader1) // tell it to go to leader1 again even though it's still down seedBroker.Returns(metadataLeader1) // tell it to go to leader1 again even though it's still down // ok fine, tell it to go to leader2 finally metadataLeader2 := new(MetadataResponse) metadataLeader2.AddBroker(leader2.Addr(), leader2.BrokerID()) metadataLeader2.AddTopicPartition("my_topic", 0, leader2.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataLeader2) prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader2.Returns(prodSuccess) expectResults(t, producer, 10, 0) seedBroker.Close() leader2.Close() closeProducer(t, producer) } func TestAsyncProducerMultipleRetries(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader1 := NewMockBroker(t, 2) leader2 := NewMockBroker(t, 3) metadataLeader1 := new(MetadataResponse) metadataLeader1.AddBroker(leader1.Addr(), leader1.BrokerID()) metadataLeader1.AddTopicPartition("my_topic", 0, leader1.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataLeader1) config := NewConfig() config.Producer.Flush.Messages = 10 config.Producer.Return.Successes = true config.Producer.Retry.Max = 4 config.Producer.Retry.Backoff = 0 producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } prodNotLeader := new(ProduceResponse) prodNotLeader.AddTopicPartition("my_topic", 0, ErrNotLeaderForPartition) leader1.Returns(prodNotLeader) metadataLeader2 := new(MetadataResponse) metadataLeader2.AddBroker(leader2.Addr(), leader2.BrokerID()) metadataLeader2.AddTopicPartition("my_topic", 0, leader2.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataLeader2) leader2.Returns(prodNotLeader) seedBroker.Returns(metadataLeader1) leader1.Returns(prodNotLeader) seedBroker.Returns(metadataLeader1) leader1.Returns(prodNotLeader) seedBroker.Returns(metadataLeader2) prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader2.Returns(prodSuccess) expectResults(t, producer, 10, 0) for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } leader2.Returns(prodSuccess) expectResults(t, producer, 10, 0) seedBroker.Close() leader1.Close() leader2.Close() closeProducer(t, producer) } func TestAsyncProducerOutOfRetries(t *testing.T) { t.Skip("Enable once bug #294 is fixed.") seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leader.Addr(), leader.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse) config := NewConfig() config.Producer.Flush.Messages = 10 config.Producer.Return.Successes = true config.Producer.Retry.Backoff = 0 config.Producer.Retry.Max = 0 producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } prodNotLeader := new(ProduceResponse) prodNotLeader.AddTopicPartition("my_topic", 0, ErrNotLeaderForPartition) leader.Returns(prodNotLeader) for i := 0; i < 10; i++ { select { case msg := <-producer.Errors(): if msg.Err != ErrNotLeaderForPartition { t.Error(msg.Err) } case <-producer.Successes(): t.Error("Unexpected success") } } seedBroker.Returns(metadataResponse) for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader.Returns(prodSuccess) expectResults(t, producer, 10, 0) leader.Close() seedBroker.Close() safeClose(t, producer) } func TestAsyncProducerRetryWithReferenceOpen(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) leaderAddr := leader.Addr() metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leaderAddr, leader.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader.BrokerID(), nil, nil, ErrNoError) metadataResponse.AddTopicPartition("my_topic", 1, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse) config := NewConfig() config.Producer.Return.Successes = true config.Producer.Retry.Backoff = 0 config.Producer.Retry.Max = 1 config.Producer.Partitioner = NewRoundRobinPartitioner producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } // prime partition 0 producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader.Returns(prodSuccess) expectResults(t, producer, 1, 0) // prime partition 1 producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} prodSuccess = new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 1, ErrNoError) leader.Returns(prodSuccess) expectResults(t, producer, 1, 0) // reboot the broker (the producer will get EOF on its existing connection) leader.Close() leader = NewMockBrokerAddr(t, 2, leaderAddr) // send another message on partition 0 to trigger the EOF and retry producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} // tell partition 0 to go to that broker again seedBroker.Returns(metadataResponse) // succeed this time prodSuccess = new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader.Returns(prodSuccess) expectResults(t, producer, 1, 0) // shutdown closeProducer(t, producer) seedBroker.Close() leader.Close() } func TestAsyncProducerFlusherRetryCondition(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leader.Addr(), leader.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader.BrokerID(), nil, nil, ErrNoError) metadataResponse.AddTopicPartition("my_topic", 1, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse) config := NewConfig() config.Producer.Flush.Messages = 5 config.Producer.Return.Successes = true config.Producer.Retry.Backoff = 0 config.Producer.Retry.Max = 1 config.Producer.Partitioner = NewManualPartitioner producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } // prime partitions for p := int32(0); p < 2; p++ { for i := 0; i < 5; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage), Partition: p} } prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", p, ErrNoError) leader.Returns(prodSuccess) expectResults(t, producer, 5, 0) } // send more messages on partition 0 for i := 0; i < 5; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage), Partition: 0} } prodNotLeader := new(ProduceResponse) prodNotLeader.AddTopicPartition("my_topic", 0, ErrNotLeaderForPartition) leader.Returns(prodNotLeader) time.Sleep(50 * time.Millisecond) leader.SetHandlerByMap(map[string]MockResponse{ "ProduceRequest": NewMockProduceResponse(t). SetError("my_topic", 0, ErrNoError), }) // tell partition 0 to go to that broker again seedBroker.Returns(metadataResponse) // succeed this time expectResults(t, producer, 5, 0) // put five more through for i := 0; i < 5; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage), Partition: 0} } expectResults(t, producer, 5, 0) // shutdown closeProducer(t, producer) seedBroker.Close() leader.Close() } func TestAsyncProducerRetryShutdown(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) metadataLeader := new(MetadataResponse) metadataLeader.AddBroker(leader.Addr(), leader.BrokerID()) metadataLeader.AddTopicPartition("my_topic", 0, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataLeader) config := NewConfig() config.Producer.Flush.Messages = 10 config.Producer.Return.Successes = true config.Producer.Retry.Backoff = 0 producer, err := NewAsyncProducer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } for i := 0; i < 10; i++ { producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder(TestMessage)} } producer.AsyncClose() time.Sleep(5 * time.Millisecond) // let the shutdown goroutine kick in producer.Input() <- &ProducerMessage{Topic: "FOO"} if err := <-producer.Errors(); err.Err != ErrShuttingDown { t.Error(err) } prodNotLeader := new(ProduceResponse) prodNotLeader.AddTopicPartition("my_topic", 0, ErrNotLeaderForPartition) leader.Returns(prodNotLeader) seedBroker.Returns(metadataLeader) prodSuccess := new(ProduceResponse) prodSuccess.AddTopicPartition("my_topic", 0, ErrNoError) leader.Returns(prodSuccess) expectResults(t, producer, 10, 0) seedBroker.Close() leader.Close() // wait for the async-closed producer to shut down fully for err := range producer.Errors() { t.Error(err) } } // This example shows how to use the producer while simultaneously // reading the Errors channel to know about any failures. func ExampleAsyncProducer_select() { producer, err := NewAsyncProducer([]string{"localhost:9092"}, nil) if err != nil { panic(err) } defer func() { if err := producer.Close(); err != nil { log.Fatalln(err) } }() // Trap SIGINT to trigger a shutdown. signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt) var enqueued, errors int ProducerLoop: for { select { case producer.Input() <- &ProducerMessage{Topic: "my_topic", Key: nil, Value: StringEncoder("testing 123")}: enqueued++ case err := <-producer.Errors(): log.Println("Failed to produce message", err) errors++ case <-signals: break ProducerLoop } } log.Printf("Enqueued: %d; errors: %d\n", enqueued, errors) } // This example shows how to use the producer with separate goroutines // reading from the Successes and Errors channels. Note that in order // for the Successes channel to be populated, you have to set // config.Producer.Return.Successes to true. func ExampleAsyncProducer_goroutines() { config := NewConfig() config.Producer.Return.Successes = true producer, err := NewAsyncProducer([]string{"localhost:9092"}, config) if err != nil { panic(err) } // Trap SIGINT to trigger a graceful shutdown. signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt) var ( wg sync.WaitGroup enqueued, successes, errors int ) wg.Add(1) go func() { defer wg.Done() for _ = range producer.Successes() { successes++ } }() wg.Add(1) go func() { defer wg.Done() for err := range producer.Errors() { log.Println(err) errors++ } }() ProducerLoop: for { message := &ProducerMessage{Topic: "my_topic", Value: StringEncoder("testing 123")} select { case producer.Input() <- message: enqueued++ case <-signals: producer.AsyncClose() // Trigger a shutdown of the producer. break ProducerLoop } } wg.Wait() log.Printf("Successfully produced: %d; errors: %d\n", successes, errors) } sarama-1.9.0/broker.go 0000664 0000000 0000000 00000027726 12716410165 0014621 0 ustar 00root root 0000000 0000000 package sarama import ( "crypto/tls" "encoding/binary" "fmt" "io" "net" "strconv" "sync" "sync/atomic" "time" ) // Broker represents a single Kafka broker connection. All operations on this object are entirely concurrency-safe. type Broker struct { id int32 addr string conf *Config correlationID int32 conn net.Conn connErr error lock sync.Mutex opened int32 responses chan responsePromise done chan bool } type responsePromise struct { correlationID int32 packets chan []byte errors chan error } // NewBroker creates and returns a Broker targetting the given host:port address. // This does not attempt to actually connect, you have to call Open() for that. func NewBroker(addr string) *Broker { return &Broker{id: -1, addr: addr} } // Open tries to connect to the Broker if it is not already connected or connecting, but does not block // waiting for the connection to complete. This means that any subsequent operations on the broker will // block waiting for the connection to succeed or fail. To get the effect of a fully synchronous Open call, // follow it by a call to Connected(). The only errors Open will return directly are ConfigurationError or // AlreadyConnected. If conf is nil, the result of NewConfig() is used. func (b *Broker) Open(conf *Config) error { if !atomic.CompareAndSwapInt32(&b.opened, 0, 1) { return ErrAlreadyConnected } if conf == nil { conf = NewConfig() } err := conf.Validate() if err != nil { return err } b.lock.Lock() go withRecover(func() { defer b.lock.Unlock() dialer := net.Dialer{ Timeout: conf.Net.DialTimeout, KeepAlive: conf.Net.KeepAlive, } if conf.Net.TLS.Enable { b.conn, b.connErr = tls.DialWithDialer(&dialer, "tcp", b.addr, conf.Net.TLS.Config) } else { b.conn, b.connErr = dialer.Dial("tcp", b.addr) } if b.connErr != nil { Logger.Printf("Failed to connect to broker %s: %s\n", b.addr, b.connErr) b.conn = nil atomic.StoreInt32(&b.opened, 0) return } b.conn = newBufConn(b.conn) b.conf = conf if conf.Net.SASL.Enable { b.connErr = b.sendAndReceiveSASLPlainAuth() if b.connErr != nil { err = b.conn.Close() if err == nil { Logger.Printf("Closed connection to broker %s\n", b.addr) } else { Logger.Printf("Error while closing connection to broker %s: %s\n", b.addr, err) } b.conn = nil atomic.StoreInt32(&b.opened, 0) return } } b.done = make(chan bool) b.responses = make(chan responsePromise, b.conf.Net.MaxOpenRequests-1) if b.id >= 0 { Logger.Printf("Connected to broker at %s (registered as #%d)\n", b.addr, b.id) } else { Logger.Printf("Connected to broker at %s (unregistered)\n", b.addr) } go withRecover(b.responseReceiver) }) return nil } // Connected returns true if the broker is connected and false otherwise. If the broker is not // connected but it had tried to connect, the error from that connection attempt is also returned. func (b *Broker) Connected() (bool, error) { b.lock.Lock() defer b.lock.Unlock() return b.conn != nil, b.connErr } func (b *Broker) Close() error { b.lock.Lock() defer b.lock.Unlock() if b.conn == nil { return ErrNotConnected } close(b.responses) <-b.done err := b.conn.Close() b.conn = nil b.connErr = nil b.done = nil b.responses = nil if err == nil { Logger.Printf("Closed connection to broker %s\n", b.addr) } else { Logger.Printf("Error while closing connection to broker %s: %s\n", b.addr, err) } atomic.StoreInt32(&b.opened, 0) return err } // ID returns the broker ID retrieved from Kafka's metadata, or -1 if that is not known. func (b *Broker) ID() int32 { return b.id } // Addr returns the broker address as either retrieved from Kafka's metadata or passed to NewBroker. func (b *Broker) Addr() string { return b.addr } func (b *Broker) GetMetadata(request *MetadataRequest) (*MetadataResponse, error) { response := new(MetadataResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) GetConsumerMetadata(request *ConsumerMetadataRequest) (*ConsumerMetadataResponse, error) { response := new(ConsumerMetadataResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) GetAvailableOffsets(request *OffsetRequest) (*OffsetResponse, error) { response := new(OffsetResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) Produce(request *ProduceRequest) (*ProduceResponse, error) { var response *ProduceResponse var err error if request.RequiredAcks == NoResponse { err = b.sendAndReceive(request, nil) } else { response = new(ProduceResponse) err = b.sendAndReceive(request, response) } if err != nil { return nil, err } return response, nil } func (b *Broker) Fetch(request *FetchRequest) (*FetchResponse, error) { response := new(FetchResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) CommitOffset(request *OffsetCommitRequest) (*OffsetCommitResponse, error) { response := new(OffsetCommitResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) FetchOffset(request *OffsetFetchRequest) (*OffsetFetchResponse, error) { response := new(OffsetFetchResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) JoinGroup(request *JoinGroupRequest) (*JoinGroupResponse, error) { response := new(JoinGroupResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) SyncGroup(request *SyncGroupRequest) (*SyncGroupResponse, error) { response := new(SyncGroupResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) LeaveGroup(request *LeaveGroupRequest) (*LeaveGroupResponse, error) { response := new(LeaveGroupResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) Heartbeat(request *HeartbeatRequest) (*HeartbeatResponse, error) { response := new(HeartbeatResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) ListGroups(request *ListGroupsRequest) (*ListGroupsResponse, error) { response := new(ListGroupsResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) DescribeGroups(request *DescribeGroupsRequest) (*DescribeGroupsResponse, error) { response := new(DescribeGroupsResponse) err := b.sendAndReceive(request, response) if err != nil { return nil, err } return response, nil } func (b *Broker) send(rb requestBody, promiseResponse bool) (*responsePromise, error) { b.lock.Lock() defer b.lock.Unlock() if b.conn == nil { if b.connErr != nil { return nil, b.connErr } return nil, ErrNotConnected } req := &request{correlationID: b.correlationID, clientID: b.conf.ClientID, body: rb} buf, err := encode(req) if err != nil { return nil, err } err = b.conn.SetWriteDeadline(time.Now().Add(b.conf.Net.WriteTimeout)) if err != nil { return nil, err } _, err = b.conn.Write(buf) if err != nil { return nil, err } b.correlationID++ if !promiseResponse { return nil, nil } promise := responsePromise{req.correlationID, make(chan []byte), make(chan error)} b.responses <- promise return &promise, nil } func (b *Broker) sendAndReceive(req requestBody, res decoder) error { promise, err := b.send(req, res != nil) if err != nil { return err } if promise == nil { return nil } select { case buf := <-promise.packets: return decode(buf, res) case err = <-promise.errors: return err } } func (b *Broker) decode(pd packetDecoder) (err error) { b.id, err = pd.getInt32() if err != nil { return err } host, err := pd.getString() if err != nil { return err } port, err := pd.getInt32() if err != nil { return err } b.addr = net.JoinHostPort(host, fmt.Sprint(port)) if _, _, err := net.SplitHostPort(b.addr); err != nil { return err } return nil } func (b *Broker) encode(pe packetEncoder) (err error) { host, portstr, err := net.SplitHostPort(b.addr) if err != nil { return err } port, err := strconv.Atoi(portstr) if err != nil { return err } pe.putInt32(b.id) err = pe.putString(host) if err != nil { return err } pe.putInt32(int32(port)) return nil } func (b *Broker) responseReceiver() { var dead error header := make([]byte, 8) for response := range b.responses { if dead != nil { response.errors <- dead continue } err := b.conn.SetReadDeadline(time.Now().Add(b.conf.Net.ReadTimeout)) if err != nil { dead = err response.errors <- err continue } _, err = io.ReadFull(b.conn, header) if err != nil { dead = err response.errors <- err continue } decodedHeader := responseHeader{} err = decode(header, &decodedHeader) if err != nil { dead = err response.errors <- err continue } if decodedHeader.correlationID != response.correlationID { // TODO if decoded ID < cur ID, discard until we catch up // TODO if decoded ID > cur ID, save it so when cur ID catches up we have a response dead = PacketDecodingError{fmt.Sprintf("correlation ID didn't match, wanted %d, got %d", response.correlationID, decodedHeader.correlationID)} response.errors <- dead continue } buf := make([]byte, decodedHeader.length-4) _, err = io.ReadFull(b.conn, buf) if err != nil { dead = err response.errors <- err continue } response.packets <- buf } close(b.done) } // Kafka 0.10.0 plans to support SASL Plain and Kerberos as per PR #812 (KIP-43)/(JIRA KAFKA-3149) // Some hosted kafka services such as IBM Message Hub already offer SASL/PLAIN auth with Kafka 0.9 // // In SASL Plain, Kafka expects the auth header to be in the following format // Message format (from https://tools.ietf.org/html/rfc4616): // // message = [authzid] UTF8NUL authcid UTF8NUL passwd // authcid = 1*SAFE ; MUST accept up to 255 octets // authzid = 1*SAFE ; MUST accept up to 255 octets // passwd = 1*SAFE ; MUST accept up to 255 octets // UTF8NUL = %x00 ; UTF-8 encoded NUL character // // SAFE = UTF1 / UTF2 / UTF3 / UTF4 // ;; any UTF-8 encoded Unicode character except NUL // // When credentials are valid, Kafka returns a 4 byte array of null characters. // When credentials are invalid, Kafka closes the connection. This does not seem to be the ideal way // of responding to bad credentials but thats how its being done today. func (b *Broker) sendAndReceiveSASLPlainAuth() error { length := 1 + len(b.conf.Net.SASL.User) + 1 + len(b.conf.Net.SASL.Password) authBytes := make([]byte, length+4) //4 byte length header + auth data binary.BigEndian.PutUint32(authBytes, uint32(length)) copy(authBytes[4:], []byte("\x00"+b.conf.Net.SASL.User+"\x00"+b.conf.Net.SASL.Password)) err := b.conn.SetWriteDeadline(time.Now().Add(b.conf.Net.WriteTimeout)) if err != nil { Logger.Printf("Failed to set write deadline when doing SASL auth with broker %s: %s\n", b.addr, err.Error()) return err } _, err = b.conn.Write(authBytes) if err != nil { Logger.Printf("Failed to write SASL auth header to broker %s: %s\n", b.addr, err.Error()) return err } header := make([]byte, 4) n, err := io.ReadFull(b.conn, header) // If the credentials are valid, we would get a 4 byte response filled with null characters. // Otherwise, the broker closes the connection and we get an EOF if err != nil { Logger.Printf("Failed to read response while authenticating with SASL to broker %s: %s\n", b.addr, err.Error()) return err } Logger.Printf("SASL authentication successful with broker %s:%v - %v\n", b.addr, n, header) return nil } sarama-1.9.0/broker_test.go 0000664 0000000 0000000 00000013164 12716410165 0015647 0 ustar 00root root 0000000 0000000 package sarama import ( "fmt" "testing" ) func ExampleBroker() { broker := NewBroker("localhost:9092") err := broker.Open(nil) if err != nil { panic(err) } request := MetadataRequest{Topics: []string{"myTopic"}} response, err := broker.GetMetadata(&request) if err != nil { _ = broker.Close() panic(err) } fmt.Println("There are", len(response.Topics), "topics active in the cluster.") if err = broker.Close(); err != nil { panic(err) } } type mockEncoder struct { bytes []byte } func (m mockEncoder) encode(pe packetEncoder) error { return pe.putRawBytes(m.bytes) } func TestBrokerAccessors(t *testing.T) { broker := NewBroker("abc:123") if broker.ID() != -1 { t.Error("New broker didn't have an ID of -1.") } if broker.Addr() != "abc:123" { t.Error("New broker didn't have the correct address") } broker.id = 34 if broker.ID() != 34 { t.Error("Manually setting broker ID did not take effect.") } } func TestSimpleBrokerCommunication(t *testing.T) { mb := NewMockBroker(t, 0) defer mb.Close() broker := NewBroker(mb.Addr()) err := broker.Open(nil) if err != nil { t.Fatal(err) } for _, tt := range brokerTestTable { mb.Returns(&mockEncoder{tt.response}) } for _, tt := range brokerTestTable { tt.runner(t, broker) } err = broker.Close() if err != nil { t.Error(err) } } // We're not testing encoding/decoding here, so most of the requests/responses will be empty for simplicity's sake var brokerTestTable = []struct { response []byte runner func(*testing.T, *Broker) }{ {[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := MetadataRequest{} response, err := broker.GetMetadata(&request) if err != nil { t.Error(err) } if response == nil { t.Error("Metadata request got no response!") } }}, {[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 't', 0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := ConsumerMetadataRequest{} response, err := broker.GetConsumerMetadata(&request) if err != nil { t.Error(err) } if response == nil { t.Error("Consumer Metadata request got no response!") } }}, {[]byte{}, func(t *testing.T, broker *Broker) { request := ProduceRequest{} request.RequiredAcks = NoResponse response, err := broker.Produce(&request) if err != nil { t.Error(err) } if response != nil { t.Error("Produce request with NoResponse got a response!") } }}, {[]byte{0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := ProduceRequest{} request.RequiredAcks = WaitForLocal response, err := broker.Produce(&request) if err != nil { t.Error(err) } if response == nil { t.Error("Produce request without NoResponse got no response!") } }}, {[]byte{0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := FetchRequest{} response, err := broker.Fetch(&request) if err != nil { t.Error(err) } if response == nil { t.Error("Fetch request got no response!") } }}, {[]byte{0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := OffsetFetchRequest{} response, err := broker.FetchOffset(&request) if err != nil { t.Error(err) } if response == nil { t.Error("OffsetFetch request got no response!") } }}, {[]byte{0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := OffsetCommitRequest{} response, err := broker.CommitOffset(&request) if err != nil { t.Error(err) } if response == nil { t.Error("OffsetCommit request got no response!") } }}, {[]byte{0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := OffsetRequest{} response, err := broker.GetAvailableOffsets(&request) if err != nil { t.Error(err) } if response == nil { t.Error("Offset request got no response!") } }}, {[]byte{0x00, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := JoinGroupRequest{} response, err := broker.JoinGroup(&request) if err != nil { t.Error(err) } if response == nil { t.Error("JoinGroup request got no response!") } }}, {[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := SyncGroupRequest{} response, err := broker.SyncGroup(&request) if err != nil { t.Error(err) } if response == nil { t.Error("SyncGroup request got no response!") } }}, {[]byte{0x00, 0x00}, func(t *testing.T, broker *Broker) { request := LeaveGroupRequest{} response, err := broker.LeaveGroup(&request) if err != nil { t.Error(err) } if response == nil { t.Error("LeaveGroup request got no response!") } }}, {[]byte{0x00, 0x00}, func(t *testing.T, broker *Broker) { request := HeartbeatRequest{} response, err := broker.Heartbeat(&request) if err != nil { t.Error(err) } if response == nil { t.Error("Heartbeat request got no response!") } }}, {[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := ListGroupsRequest{} response, err := broker.ListGroups(&request) if err != nil { t.Error(err) } if response == nil { t.Error("ListGroups request got no response!") } }}, {[]byte{0x00, 0x00, 0x00, 0x00}, func(t *testing.T, broker *Broker) { request := DescribeGroupsRequest{} response, err := broker.DescribeGroups(&request) if err != nil { t.Error(err) } if response == nil { t.Error("DescribeGroups request got no response!") } }}, } sarama-1.9.0/client.go 0000664 0000000 0000000 00000054575 12716410165 0014615 0 ustar 00root root 0000000 0000000 package sarama import ( "math/rand" "sort" "sync" "time" ) // Client is a generic Kafka client. It manages connections to one or more Kafka brokers. // You MUST call Close() on a client to avoid leaks, it will not be garbage-collected // automatically when it passes out of scope. It is safe to share a client amongst many // users, however Kafka will process requests from a single client strictly in serial, // so it is generally more efficient to use the default one client per producer/consumer. type Client interface { // Config returns the Config struct of the client. This struct should not be // altered after it has been created. Config() *Config // Topics returns the set of available topics as retrieved from cluster metadata. Topics() ([]string, error) // Partitions returns the sorted list of all partition IDs for the given topic. Partitions(topic string) ([]int32, error) // WritablePartitions returns the sorted list of all writable partition IDs for // the given topic, where "writable" means "having a valid leader accepting // writes". WritablePartitions(topic string) ([]int32, error) // Leader returns the broker object that is the leader of the current // topic/partition, as determined by querying the cluster metadata. Leader(topic string, partitionID int32) (*Broker, error) // Replicas returns the set of all replica IDs for the given partition. Replicas(topic string, partitionID int32) ([]int32, error) // RefreshMetadata takes a list of topics and queries the cluster to refresh the // available metadata for those topics. If no topics are provided, it will refresh // metadata for all topics. RefreshMetadata(topics ...string) error // GetOffset queries the cluster to get the most recent available offset at the // given time on the topic/partition combination. Time should be OffsetOldest for // the earliest available offset, OffsetNewest for the offset of the message that // will be produced next, or a time. GetOffset(topic string, partitionID int32, time int64) (int64, error) // Coordinator returns the coordinating broker for a consumer group. It will // return a locally cached value if it's available. You can call // RefreshCoordinator to update the cached value. This function only works on // Kafka 0.8.2 and higher. Coordinator(consumerGroup string) (*Broker, error) // RefreshCoordinator retrieves the coordinator for a consumer group and stores it // in local cache. This function only works on Kafka 0.8.2 and higher. RefreshCoordinator(consumerGroup string) error // Close shuts down all broker connections managed by this client. It is required // to call this function before a client object passes out of scope, as it will // otherwise leak memory. You must close any Producers or Consumers using a client // before you close the client. Close() error // Closed returns true if the client has already had Close called on it Closed() bool } const ( // OffsetNewest stands for the log head offset, i.e. the offset that will be // assigned to the next message that will be produced to the partition. You // can send this to a client's GetOffset method to get this offset, or when // calling ConsumePartition to start consuming new messages. OffsetNewest int64 = -1 // OffsetOldest stands for the oldest offset available on the broker for a // partition. You can send this to a client's GetOffset method to get this // offset, or when calling ConsumePartition to start consuming from the // oldest offset that is still available on the broker. OffsetOldest int64 = -2 ) type client struct { conf *Config closer, closed chan none // for shutting down background metadata updater // the broker addresses given to us through the constructor are not guaranteed to be returned in // the cluster metadata (I *think* it only returns brokers who are currently leading partitions?) // so we store them separately seedBrokers []*Broker deadSeeds []*Broker brokers map[int32]*Broker // maps broker ids to brokers metadata map[string]map[int32]*PartitionMetadata // maps topics to partition ids to metadata coordinators map[string]int32 // Maps consumer group names to coordinating broker IDs // If the number of partitions is large, we can get some churn calling cachedPartitions, // so the result is cached. It is important to update this value whenever metadata is changed cachedPartitionsResults map[string][maxPartitionIndex][]int32 lock sync.RWMutex // protects access to the maps that hold cluster state. } // NewClient creates a new Client. It connects to one of the given broker addresses // and uses that broker to automatically fetch metadata on the rest of the kafka cluster. If metadata cannot // be retrieved from any of the given broker addresses, the client is not created. func NewClient(addrs []string, conf *Config) (Client, error) { Logger.Println("Initializing new client") if conf == nil { conf = NewConfig() } if err := conf.Validate(); err != nil { return nil, err } if len(addrs) < 1 { return nil, ConfigurationError("You must provide at least one broker address") } client := &client{ conf: conf, closer: make(chan none), closed: make(chan none), brokers: make(map[int32]*Broker), metadata: make(map[string]map[int32]*PartitionMetadata), cachedPartitionsResults: make(map[string][maxPartitionIndex][]int32), coordinators: make(map[string]int32), } random := rand.New(rand.NewSource(time.Now().UnixNano())) for _, index := range random.Perm(len(addrs)) { client.seedBrokers = append(client.seedBrokers, NewBroker(addrs[index])) } // do an initial fetch of all cluster metadata by specifing an empty list of topics err := client.RefreshMetadata() switch err { case nil: break case ErrLeaderNotAvailable, ErrReplicaNotAvailable: // indicates that maybe part of the cluster is down, but is not fatal to creating the client Logger.Println(err) default: close(client.closed) // we haven't started the background updater yet, so we have to do this manually _ = client.Close() return nil, err } go withRecover(client.backgroundMetadataUpdater) Logger.Println("Successfully initialized new client") return client, nil } func (client *client) Config() *Config { return client.conf } func (client *client) Close() error { if client.Closed() { // Chances are this is being called from a defer() and the error will go unobserved // so we go ahead and log the event in this case. Logger.Printf("Close() called on already closed client") return ErrClosedClient } // shutdown and wait for the background thread before we take the lock, to avoid races close(client.closer) <-client.closed client.lock.Lock() defer client.lock.Unlock() Logger.Println("Closing Client") for _, broker := range client.brokers { safeAsyncClose(broker) } for _, broker := range client.seedBrokers { safeAsyncClose(broker) } client.brokers = nil client.metadata = nil return nil } func (client *client) Closed() bool { return client.brokers == nil } func (client *client) Topics() ([]string, error) { if client.Closed() { return nil, ErrClosedClient } client.lock.RLock() defer client.lock.RUnlock() ret := make([]string, 0, len(client.metadata)) for topic := range client.metadata { ret = append(ret, topic) } return ret, nil } func (client *client) Partitions(topic string) ([]int32, error) { if client.Closed() { return nil, ErrClosedClient } partitions := client.cachedPartitions(topic, allPartitions) if len(partitions) == 0 { err := client.RefreshMetadata(topic) if err != nil { return nil, err } partitions = client.cachedPartitions(topic, allPartitions) } if partitions == nil { return nil, ErrUnknownTopicOrPartition } return partitions, nil } func (client *client) WritablePartitions(topic string) ([]int32, error) { if client.Closed() { return nil, ErrClosedClient } partitions := client.cachedPartitions(topic, writablePartitions) // len==0 catches when it's nil (no such topic) and the odd case when every single // partition is undergoing leader election simultaneously. Callers have to be able to handle // this function returning an empty slice (which is a valid return value) but catching it // here the first time (note we *don't* catch it below where we return ErrUnknownTopicOrPartition) triggers // a metadata refresh as a nicety so callers can just try again and don't have to manually // trigger a refresh (otherwise they'd just keep getting a stale cached copy). if len(partitions) == 0 { err := client.RefreshMetadata(topic) if err != nil { return nil, err } partitions = client.cachedPartitions(topic, writablePartitions) } if partitions == nil { return nil, ErrUnknownTopicOrPartition } return partitions, nil } func (client *client) Replicas(topic string, partitionID int32) ([]int32, error) { if client.Closed() { return nil, ErrClosedClient } metadata := client.cachedMetadata(topic, partitionID) if metadata == nil { err := client.RefreshMetadata(topic) if err != nil { return nil, err } metadata = client.cachedMetadata(topic, partitionID) } if metadata == nil { return nil, ErrUnknownTopicOrPartition } if metadata.Err == ErrReplicaNotAvailable { return nil, metadata.Err } return dupeAndSort(metadata.Replicas), nil } func (client *client) Leader(topic string, partitionID int32) (*Broker, error) { if client.Closed() { return nil, ErrClosedClient } leader, err := client.cachedLeader(topic, partitionID) if leader == nil { err := client.RefreshMetadata(topic) if err != nil { return nil, err } leader, err = client.cachedLeader(topic, partitionID) } return leader, err } func (client *client) RefreshMetadata(topics ...string) error { if client.Closed() { return ErrClosedClient } // Prior to 0.8.2, Kafka will throw exceptions on an empty topic and not return a proper // error. This handles the case by returning an error instead of sending it // off to Kafka. See: https://github.com/Shopify/sarama/pull/38#issuecomment-26362310 for _, topic := range topics { if len(topic) == 0 { return ErrInvalidTopic // this is the error that 0.8.2 and later correctly return } } return client.tryRefreshMetadata(topics, client.conf.Metadata.Retry.Max) } func (client *client) GetOffset(topic string, partitionID int32, time int64) (int64, error) { if client.Closed() { return -1, ErrClosedClient } offset, err := client.getOffset(topic, partitionID, time) if err != nil { if err := client.RefreshMetadata(topic); err != nil { return -1, err } return client.getOffset(topic, partitionID, time) } return offset, err } func (client *client) Coordinator(consumerGroup string) (*Broker, error) { if client.Closed() { return nil, ErrClosedClient } coordinator := client.cachedCoordinator(consumerGroup) if coordinator == nil { if err := client.RefreshCoordinator(consumerGroup); err != nil { return nil, err } coordinator = client.cachedCoordinator(consumerGroup) } if coordinator == nil { return nil, ErrConsumerCoordinatorNotAvailable } _ = coordinator.Open(client.conf) return coordinator, nil } func (client *client) RefreshCoordinator(consumerGroup string) error { if client.Closed() { return ErrClosedClient } response, err := client.getConsumerMetadata(consumerGroup, client.conf.Metadata.Retry.Max) if err != nil { return err } client.lock.Lock() defer client.lock.Unlock() client.registerBroker(response.Coordinator) client.coordinators[consumerGroup] = response.Coordinator.ID() return nil } // private broker management helpers // registerBroker makes sure a broker received by a Metadata or Coordinator request is registered // in the brokers map. It returns the broker that is registered, which may be the provided broker, // or a previously registered Broker instance. You must hold the write lock before calling this function. func (client *client) registerBroker(broker *Broker) { if client.brokers[broker.ID()] == nil { client.brokers[broker.ID()] = broker Logger.Printf("client/brokers registered new broker #%d at %s", broker.ID(), broker.Addr()) } else if broker.Addr() != client.brokers[broker.ID()].Addr() { safeAsyncClose(client.brokers[broker.ID()]) client.brokers[broker.ID()] = broker Logger.Printf("client/brokers replaced registered broker #%d with %s", broker.ID(), broker.Addr()) } } // deregisterBroker removes a broker from the seedsBroker list, and if it's // not the seedbroker, removes it from brokers map completely. func (client *client) deregisterBroker(broker *Broker) { client.lock.Lock() defer client.lock.Unlock() if len(client.seedBrokers) > 0 && broker == client.seedBrokers[0] { client.deadSeeds = append(client.deadSeeds, broker) client.seedBrokers = client.seedBrokers[1:] } else { // we do this so that our loop in `tryRefreshMetadata` doesn't go on forever, // but we really shouldn't have to; once that loop is made better this case can be // removed, and the function generally can be renamed from `deregisterBroker` to // `nextSeedBroker` or something Logger.Printf("client/brokers deregistered broker #%d at %s", broker.ID(), broker.Addr()) delete(client.brokers, broker.ID()) } } func (client *client) resurrectDeadBrokers() { client.lock.Lock() defer client.lock.Unlock() Logger.Printf("client/brokers resurrecting %d dead seed brokers", len(client.deadSeeds)) client.seedBrokers = append(client.seedBrokers, client.deadSeeds...) client.deadSeeds = nil } func (client *client) any() *Broker { client.lock.RLock() defer client.lock.RUnlock() if len(client.seedBrokers) > 0 { _ = client.seedBrokers[0].Open(client.conf) return client.seedBrokers[0] } // not guaranteed to be random *or* deterministic for _, broker := range client.brokers { _ = broker.Open(client.conf) return broker } return nil } // private caching/lazy metadata helpers type partitionType int const ( allPartitions partitionType = iota writablePartitions // If you add any more types, update the partition cache in update() // Ensure this is the last partition type value maxPartitionIndex ) func (client *client) cachedMetadata(topic string, partitionID int32) *PartitionMetadata { client.lock.RLock() defer client.lock.RUnlock() partitions := client.metadata[topic] if partitions != nil { return partitions[partitionID] } return nil } func (client *client) cachedPartitions(topic string, partitionSet partitionType) []int32 { client.lock.RLock() defer client.lock.RUnlock() partitions, exists := client.cachedPartitionsResults[topic] if !exists { return nil } return partitions[partitionSet] } func (client *client) setPartitionCache(topic string, partitionSet partitionType) []int32 { partitions := client.metadata[topic] if partitions == nil { return nil } ret := make([]int32, 0, len(partitions)) for _, partition := range partitions { if partitionSet == writablePartitions && partition.Err == ErrLeaderNotAvailable { continue } ret = append(ret, partition.ID) } sort.Sort(int32Slice(ret)) return ret } func (client *client) cachedLeader(topic string, partitionID int32) (*Broker, error) { client.lock.RLock() defer client.lock.RUnlock() partitions := client.metadata[topic] if partitions != nil { metadata, ok := partitions[partitionID] if ok { if metadata.Err == ErrLeaderNotAvailable { return nil, ErrLeaderNotAvailable } b := client.brokers[metadata.Leader] if b == nil { return nil, ErrLeaderNotAvailable } _ = b.Open(client.conf) return b, nil } } return nil, ErrUnknownTopicOrPartition } func (client *client) getOffset(topic string, partitionID int32, time int64) (int64, error) { broker, err := client.Leader(topic, partitionID) if err != nil { return -1, err } request := &OffsetRequest{} request.AddBlock(topic, partitionID, time, 1) response, err := broker.GetAvailableOffsets(request) if err != nil { _ = broker.Close() return -1, err } block := response.GetBlock(topic, partitionID) if block == nil { _ = broker.Close() return -1, ErrIncompleteResponse } if block.Err != ErrNoError { return -1, block.Err } if len(block.Offsets) != 1 { return -1, ErrOffsetOutOfRange } return block.Offsets[0], nil } // core metadata update logic func (client *client) backgroundMetadataUpdater() { defer close(client.closed) if client.conf.Metadata.RefreshFrequency == time.Duration(0) { return } ticker := time.NewTicker(client.conf.Metadata.RefreshFrequency) defer ticker.Stop() for { select { case <-ticker.C: if err := client.RefreshMetadata(); err != nil { Logger.Println("Client background metadata update:", err) } case <-client.closer: return } } } func (client *client) tryRefreshMetadata(topics []string, attemptsRemaining int) error { retry := func(err error) error { if attemptsRemaining > 0 { Logger.Printf("client/metadata retrying after %dms... (%d attempts remaining)\n", client.conf.Metadata.Retry.Backoff/time.Millisecond, attemptsRemaining) time.Sleep(client.conf.Metadata.Retry.Backoff) return client.tryRefreshMetadata(topics, attemptsRemaining-1) } return err } for broker := client.any(); broker != nil; broker = client.any() { if len(topics) > 0 { Logger.Printf("client/metadata fetching metadata for %v from broker %s\n", topics, broker.addr) } else { Logger.Printf("client/metadata fetching metadata for all topics from broker %s\n", broker.addr) } response, err := broker.GetMetadata(&MetadataRequest{Topics: topics}) switch err.(type) { case nil: // valid response, use it if shouldRetry, err := client.updateMetadata(response); shouldRetry { Logger.Println("client/metadata found some partitions to be leaderless") return retry(err) // note: err can be nil } else { return err } case PacketEncodingError: // didn't even send, return the error return err default: // some other error, remove that broker and try again Logger.Println("client/metadata got error from broker while fetching metadata:", err) _ = broker.Close() client.deregisterBroker(broker) } } Logger.Println("client/metadata no available broker to send metadata request to") client.resurrectDeadBrokers() return retry(ErrOutOfBrokers) } // if no fatal error, returns a list of topics that need retrying due to ErrLeaderNotAvailable func (client *client) updateMetadata(data *MetadataResponse) (retry bool, err error) { client.lock.Lock() defer client.lock.Unlock() // For all the brokers we received: // - if it is a new ID, save it // - if it is an existing ID, but the address we have is stale, discard the old one and save it // - otherwise ignore it, replacing our existing one would just bounce the connection for _, broker := range data.Brokers { client.registerBroker(broker) } for _, topic := range data.Topics { delete(client.metadata, topic.Name) delete(client.cachedPartitionsResults, topic.Name) switch topic.Err { case ErrNoError: break case ErrInvalidTopic, ErrTopicAuthorizationFailed: // don't retry, don't store partial results err = topic.Err continue case ErrUnknownTopicOrPartition: // retry, do not store partial partition results err = topic.Err retry = true continue case ErrLeaderNotAvailable: // retry, but store partial partition results retry = true break default: // don't retry, don't store partial results Logger.Printf("Unexpected topic-level metadata error: %s", topic.Err) err = topic.Err continue } client.metadata[topic.Name] = make(map[int32]*PartitionMetadata, len(topic.Partitions)) for _, partition := range topic.Partitions { client.metadata[topic.Name][partition.ID] = partition if partition.Err == ErrLeaderNotAvailable { retry = true } } var partitionCache [maxPartitionIndex][]int32 partitionCache[allPartitions] = client.setPartitionCache(topic.Name, allPartitions) partitionCache[writablePartitions] = client.setPartitionCache(topic.Name, writablePartitions) client.cachedPartitionsResults[topic.Name] = partitionCache } return } func (client *client) cachedCoordinator(consumerGroup string) *Broker { client.lock.RLock() defer client.lock.RUnlock() if coordinatorID, ok := client.coordinators[consumerGroup]; ok { return client.brokers[coordinatorID] } return nil } func (client *client) getConsumerMetadata(consumerGroup string, attemptsRemaining int) (*ConsumerMetadataResponse, error) { retry := func(err error) (*ConsumerMetadataResponse, error) { if attemptsRemaining > 0 { Logger.Printf("client/coordinator retrying after %dms... (%d attempts remaining)\n", client.conf.Metadata.Retry.Backoff/time.Millisecond, attemptsRemaining) time.Sleep(client.conf.Metadata.Retry.Backoff) return client.getConsumerMetadata(consumerGroup, attemptsRemaining-1) } return nil, err } for broker := client.any(); broker != nil; broker = client.any() { Logger.Printf("client/coordinator requesting coordinator for consumergoup %s from %s\n", consumerGroup, broker.Addr()) request := new(ConsumerMetadataRequest) request.ConsumerGroup = consumerGroup response, err := broker.GetConsumerMetadata(request) if err != nil { Logger.Printf("client/coordinator request to broker %s failed: %s\n", broker.Addr(), err) switch err.(type) { case PacketEncodingError: return nil, err default: _ = broker.Close() client.deregisterBroker(broker) continue } } switch response.Err { case ErrNoError: Logger.Printf("client/coordinator coordinator for consumergoup %s is #%d (%s)\n", consumerGroup, response.Coordinator.ID(), response.Coordinator.Addr()) return response, nil case ErrConsumerCoordinatorNotAvailable: Logger.Printf("client/coordinator coordinator for consumer group %s is not available\n", consumerGroup) // This is very ugly, but this scenario will only happen once per cluster. // The __consumer_offsets topic only has to be created one time. // The number of partitions not configurable, but partition 0 should always exist. if _, err := client.Leader("__consumer_offsets", 0); err != nil { Logger.Printf("client/coordinator the __consumer_offsets topic is not initialized completely yet. Waiting 2 seconds...\n") time.Sleep(2 * time.Second) } return retry(ErrConsumerCoordinatorNotAvailable) default: return nil, response.Err } } Logger.Println("client/coordinator no available broker to send consumer metadata request to") client.resurrectDeadBrokers() return retry(ErrOutOfBrokers) } sarama-1.9.0/client_test.go 0000664 0000000 0000000 00000041360 12716410165 0015640 0 ustar 00root root 0000000 0000000 package sarama import ( "io" "sync" "testing" "time" ) func safeClose(t testing.TB, c io.Closer) { err := c.Close() if err != nil { t.Error(err) } } func TestSimpleClient(t *testing.T) { seedBroker := NewMockBroker(t, 1) seedBroker.Returns(new(MetadataResponse)) client, err := NewClient([]string{seedBroker.Addr()}, nil) if err != nil { t.Fatal(err) } seedBroker.Close() safeClose(t, client) } func TestCachedPartitions(t *testing.T) { seedBroker := NewMockBroker(t, 1) replicas := []int32{3, 1, 5} isr := []int32{5, 1} metadataResponse := new(MetadataResponse) metadataResponse.AddBroker("localhost:12345", 2) metadataResponse.AddTopicPartition("my_topic", 0, 2, replicas, isr, ErrNoError) metadataResponse.AddTopicPartition("my_topic", 1, 2, replicas, isr, ErrLeaderNotAvailable) seedBroker.Returns(metadataResponse) config := NewConfig() config.Metadata.Retry.Max = 0 c, err := NewClient([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } client := c.(*client) // Verify they aren't cached the same allP := client.cachedPartitionsResults["my_topic"][allPartitions] writeP := client.cachedPartitionsResults["my_topic"][writablePartitions] if len(allP) == len(writeP) { t.Fatal("Invalid lengths!") } tmp := client.cachedPartitionsResults["my_topic"] // Verify we actually use the cache at all! tmp[allPartitions] = []int32{1, 2, 3, 4} client.cachedPartitionsResults["my_topic"] = tmp if 4 != len(client.cachedPartitions("my_topic", allPartitions)) { t.Fatal("Not using the cache!") } seedBroker.Close() safeClose(t, client) } func TestClientDoesntCachePartitionsForTopicsWithErrors(t *testing.T) { seedBroker := NewMockBroker(t, 1) replicas := []int32{seedBroker.BrokerID()} metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(seedBroker.Addr(), seedBroker.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 1, replicas[0], replicas, replicas, ErrNoError) metadataResponse.AddTopicPartition("my_topic", 2, replicas[0], replicas, replicas, ErrNoError) seedBroker.Returns(metadataResponse) config := NewConfig() config.Metadata.Retry.Max = 0 client, err := NewClient([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } metadataResponse = new(MetadataResponse) metadataResponse.AddTopic("unknown", ErrUnknownTopicOrPartition) seedBroker.Returns(metadataResponse) partitions, err := client.Partitions("unknown") if err != ErrUnknownTopicOrPartition { t.Error("Expected ErrUnknownTopicOrPartition, found", err) } if partitions != nil { t.Errorf("Should return nil as partition list, found %v", partitions) } // Should still use the cache of a known topic partitions, err = client.Partitions("my_topic") if err != nil { t.Errorf("Expected no error, found %v", err) } metadataResponse = new(MetadataResponse) metadataResponse.AddTopic("unknown", ErrUnknownTopicOrPartition) seedBroker.Returns(metadataResponse) // Should not use cache for unknown topic partitions, err = client.Partitions("unknown") if err != ErrUnknownTopicOrPartition { t.Error("Expected ErrUnknownTopicOrPartition, found", err) } if partitions != nil { t.Errorf("Should return nil as partition list, found %v", partitions) } seedBroker.Close() safeClose(t, client) } func TestClientSeedBrokers(t *testing.T) { seedBroker := NewMockBroker(t, 1) metadataResponse := new(MetadataResponse) metadataResponse.AddBroker("localhost:12345", 2) seedBroker.Returns(metadataResponse) client, err := NewClient([]string{seedBroker.Addr()}, nil) if err != nil { t.Fatal(err) } seedBroker.Close() safeClose(t, client) } func TestClientMetadata(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 5) replicas := []int32{3, 1, 5} isr := []int32{5, 1} metadataResponse := new(MetadataResponse) metadataResponse.AddBroker(leader.Addr(), leader.BrokerID()) metadataResponse.AddTopicPartition("my_topic", 0, leader.BrokerID(), replicas, isr, ErrNoError) metadataResponse.AddTopicPartition("my_topic", 1, leader.BrokerID(), replicas, isr, ErrLeaderNotAvailable) seedBroker.Returns(metadataResponse) config := NewConfig() config.Metadata.Retry.Max = 0 client, err := NewClient([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } topics, err := client.Topics() if err != nil { t.Error(err) } else if len(topics) != 1 || topics[0] != "my_topic" { t.Error("Client returned incorrect topics:", topics) } parts, err := client.Partitions("my_topic") if err != nil { t.Error(err) } else if len(parts) != 2 || parts[0] != 0 || parts[1] != 1 { t.Error("Client returned incorrect partitions for my_topic:", parts) } parts, err = client.WritablePartitions("my_topic") if err != nil { t.Error(err) } else if len(parts) != 1 || parts[0] != 0 { t.Error("Client returned incorrect writable partitions for my_topic:", parts) } tst, err := client.Leader("my_topic", 0) if err != nil { t.Error(err) } else if tst.ID() != 5 { t.Error("Leader for my_topic had incorrect ID.") } replicas, err = client.Replicas("my_topic", 0) if err != nil { t.Error(err) } else if replicas[0] != 1 { t.Error("Incorrect (or unsorted) replica") } else if replicas[1] != 3 { t.Error("Incorrect (or unsorted) replica") } else if replicas[2] != 5 { t.Error("Incorrect (or unsorted) replica") } leader.Close() seedBroker.Close() safeClose(t, client) } func TestClientGetOffset(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 2) leaderAddr := leader.Addr() metadata := new(MetadataResponse) metadata.AddTopicPartition("foo", 0, leader.BrokerID(), nil, nil, ErrNoError) metadata.AddBroker(leaderAddr, leader.BrokerID()) seedBroker.Returns(metadata) client, err := NewClient([]string{seedBroker.Addr()}, nil) if err != nil { t.Fatal(err) } offsetResponse := new(OffsetResponse) offsetResponse.AddTopicPartition("foo", 0, 123) leader.Returns(offsetResponse) offset, err := client.GetOffset("foo", 0, OffsetNewest) if err != nil { t.Error(err) } if offset != 123 { t.Error("Unexpected offset, got ", offset) } leader.Close() seedBroker.Returns(metadata) leader = NewMockBrokerAddr(t, 2, leaderAddr) offsetResponse = new(OffsetResponse) offsetResponse.AddTopicPartition("foo", 0, 456) leader.Returns(offsetResponse) offset, err = client.GetOffset("foo", 0, OffsetNewest) if err != nil { t.Error(err) } if offset != 456 { t.Error("Unexpected offset, got ", offset) } seedBroker.Close() leader.Close() safeClose(t, client) } func TestClientReceivingUnknownTopic(t *testing.T) { seedBroker := NewMockBroker(t, 1) metadataResponse1 := new(MetadataResponse) seedBroker.Returns(metadataResponse1) config := NewConfig() config.Metadata.Retry.Max = 1 config.Metadata.Retry.Backoff = 0 client, err := NewClient([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } metadataUnknownTopic := new(MetadataResponse) metadataUnknownTopic.AddTopic("new_topic", ErrUnknownTopicOrPartition) seedBroker.Returns(metadataUnknownTopic) seedBroker.Returns(metadataUnknownTopic) if err := client.RefreshMetadata("new_topic"); err != ErrUnknownTopicOrPartition { t.Error("ErrUnknownTopicOrPartition expected, got", err) } // If we are asking for the leader of a partition of the non-existing topic. // we will request metadata again. seedBroker.Returns(metadataUnknownTopic) seedBroker.Returns(metadataUnknownTopic) if _, err = client.Leader("new_topic", 1); err != ErrUnknownTopicOrPartition { t.Error("Expected ErrUnknownTopicOrPartition, got", err) } safeClose(t, client) seedBroker.Close() } func TestClientReceivingPartialMetadata(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 5) metadataResponse1 := new(MetadataResponse) metadataResponse1.AddBroker(leader.Addr(), leader.BrokerID()) seedBroker.Returns(metadataResponse1) config := NewConfig() config.Metadata.Retry.Max = 0 client, err := NewClient([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } replicas := []int32{leader.BrokerID(), seedBroker.BrokerID()} metadataPartial := new(MetadataResponse) metadataPartial.AddTopic("new_topic", ErrLeaderNotAvailable) metadataPartial.AddTopicPartition("new_topic", 0, leader.BrokerID(), replicas, replicas, ErrNoError) metadataPartial.AddTopicPartition("new_topic", 1, -1, replicas, []int32{}, ErrLeaderNotAvailable) seedBroker.Returns(metadataPartial) if err := client.RefreshMetadata("new_topic"); err != nil { t.Error("ErrLeaderNotAvailable should not make RefreshMetadata respond with an error") } // Even though the metadata was incomplete, we should be able to get the leader of a partition // for which we did get a useful response, without doing additional requests. partition0Leader, err := client.Leader("new_topic", 0) if err != nil { t.Error(err) } else if partition0Leader.Addr() != leader.Addr() { t.Error("Unexpected leader returned", partition0Leader.Addr()) } // If we are asking for the leader of a partition that didn't have a leader before, // we will do another metadata request. seedBroker.Returns(metadataPartial) // Still no leader for the partition, so asking for it should return an error. _, err = client.Leader("new_topic", 1) if err != ErrLeaderNotAvailable { t.Error("Expected ErrLeaderNotAvailable, got", err) } safeClose(t, client) seedBroker.Close() leader.Close() } func TestClientRefreshBehaviour(t *testing.T) { seedBroker := NewMockBroker(t, 1) leader := NewMockBroker(t, 5) metadataResponse1 := new(MetadataResponse) metadataResponse1.AddBroker(leader.Addr(), leader.BrokerID()) seedBroker.Returns(metadataResponse1) metadataResponse2 := new(MetadataResponse) metadataResponse2.AddTopicPartition("my_topic", 0xb, leader.BrokerID(), nil, nil, ErrNoError) seedBroker.Returns(metadataResponse2) client, err := NewClient([]string{seedBroker.Addr()}, nil) if err != nil { t.Fatal(err) } parts, err := client.Partitions("my_topic") if err != nil { t.Error(err) } else if len(parts) != 1 || parts[0] != 0xb { t.Error("Client returned incorrect partitions for my_topic:", parts) } tst, err := client.Leader("my_topic", 0xb) if err != nil { t.Error(err) } else if tst.ID() != 5 { t.Error("Leader for my_topic had incorrect ID.") } leader.Close() seedBroker.Close() safeClose(t, client) } func TestClientResurrectDeadSeeds(t *testing.T) { initialSeed := NewMockBroker(t, 0) emptyMetadata := new(MetadataResponse) initialSeed.Returns(emptyMetadata) conf := NewConfig() conf.Metadata.Retry.Backoff = 0 conf.Metadata.RefreshFrequency = 0 c, err := NewClient([]string{initialSeed.Addr()}, conf) if err != nil { t.Fatal(err) } initialSeed.Close() client := c.(*client) seed1 := NewMockBroker(t, 1) seed2 := NewMockBroker(t, 2) seed3 := NewMockBroker(t, 3) addr1 := seed1.Addr() addr2 := seed2.Addr() addr3 := seed3.Addr() // Overwrite the seed brokers with a fixed ordering to make this test deterministic. safeClose(t, client.seedBrokers[0]) client.seedBrokers = []*Broker{NewBroker(addr1), NewBroker(addr2), NewBroker(addr3)} client.deadSeeds = []*Broker{} wg := sync.WaitGroup{} wg.Add(1) go func() { if err := client.RefreshMetadata(); err != nil { t.Error(err) } wg.Done() }() seed1.Close() seed2.Close() seed1 = NewMockBrokerAddr(t, 1, addr1) seed2 = NewMockBrokerAddr(t, 2, addr2) seed3.Close() seed1.Close() seed2.Returns(emptyMetadata) wg.Wait() if len(client.seedBrokers) != 2 { t.Error("incorrect number of live seeds") } if len(client.deadSeeds) != 1 { t.Error("incorrect number of dead seeds") } safeClose(t, c) } func TestClientCoordinatorWithConsumerOffsetsTopic(t *testing.T) { seedBroker := NewMockBroker(t, 1) staleCoordinator := NewMockBroker(t, 2) freshCoordinator := NewMockBroker(t, 3) replicas := []int32{staleCoordinator.BrokerID(), freshCoordinator.BrokerID()} metadataResponse1 := new(MetadataResponse) metadataResponse1.AddBroker(staleCoordinator.Addr(), staleCoordinator.BrokerID()) metadataResponse1.AddBroker(freshCoordinator.Addr(), freshCoordinator.BrokerID()) metadataResponse1.AddTopicPartition("__consumer_offsets", 0, replicas[0], replicas, replicas, ErrNoError) seedBroker.Returns(metadataResponse1) client, err := NewClient([]string{seedBroker.Addr()}, nil) if err != nil { t.Fatal(err) } coordinatorResponse1 := new(ConsumerMetadataResponse) coordinatorResponse1.Err = ErrConsumerCoordinatorNotAvailable seedBroker.Returns(coordinatorResponse1) coordinatorResponse2 := new(ConsumerMetadataResponse) coordinatorResponse2.CoordinatorID = staleCoordinator.BrokerID() coordinatorResponse2.CoordinatorHost = "127.0.0.1" coordinatorResponse2.CoordinatorPort = staleCoordinator.Port() seedBroker.Returns(coordinatorResponse2) broker, err := client.Coordinator("my_group") if err != nil { t.Error(err) } if staleCoordinator.Addr() != broker.Addr() { t.Errorf("Expected coordinator to have address %s, found %s", staleCoordinator.Addr(), broker.Addr()) } if staleCoordinator.BrokerID() != broker.ID() { t.Errorf("Expected coordinator to have ID %d, found %d", staleCoordinator.BrokerID(), broker.ID()) } // Grab the cached value broker2, err := client.Coordinator("my_group") if err != nil { t.Error(err) } if broker2.Addr() != broker.Addr() { t.Errorf("Expected the coordinator to be the same, but found %s vs. %s", broker2.Addr(), broker.Addr()) } coordinatorResponse3 := new(ConsumerMetadataResponse) coordinatorResponse3.CoordinatorID = freshCoordinator.BrokerID() coordinatorResponse3.CoordinatorHost = "127.0.0.1" coordinatorResponse3.CoordinatorPort = freshCoordinator.Port() seedBroker.Returns(coordinatorResponse3) // Refresh the locally cahced value because it's stale if err := client.RefreshCoordinator("my_group"); err != nil { t.Error(err) } // Grab the fresh value broker3, err := client.Coordinator("my_group") if err != nil { t.Error(err) } if broker3.Addr() != freshCoordinator.Addr() { t.Errorf("Expected the freshCoordinator to be returned, but found %s.", broker3.Addr()) } freshCoordinator.Close() staleCoordinator.Close() seedBroker.Close() safeClose(t, client) } func TestClientCoordinatorWithoutConsumerOffsetsTopic(t *testing.T) { seedBroker := NewMockBroker(t, 1) coordinator := NewMockBroker(t, 2) metadataResponse1 := new(MetadataResponse) seedBroker.Returns(metadataResponse1) config := NewConfig() config.Metadata.Retry.Max = 1 config.Metadata.Retry.Backoff = 0 client, err := NewClient([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } coordinatorResponse1 := new(ConsumerMetadataResponse) coordinatorResponse1.Err = ErrConsumerCoordinatorNotAvailable seedBroker.Returns(coordinatorResponse1) metadataResponse2 := new(MetadataResponse) metadataResponse2.AddTopic("__consumer_offsets", ErrUnknownTopicOrPartition) seedBroker.Returns(metadataResponse2) replicas := []int32{coordinator.BrokerID()} metadataResponse3 := new(MetadataResponse) metadataResponse3.AddTopicPartition("__consumer_offsets", 0, replicas[0], replicas, replicas, ErrNoError) seedBroker.Returns(metadataResponse3) coordinatorResponse2 := new(ConsumerMetadataResponse) coordinatorResponse2.CoordinatorID = coordinator.BrokerID() coordinatorResponse2.CoordinatorHost = "127.0.0.1" coordinatorResponse2.CoordinatorPort = coordinator.Port() seedBroker.Returns(coordinatorResponse2) broker, err := client.Coordinator("my_group") if err != nil { t.Error(err) } if coordinator.Addr() != broker.Addr() { t.Errorf("Expected coordinator to have address %s, found %s", coordinator.Addr(), broker.Addr()) } if coordinator.BrokerID() != broker.ID() { t.Errorf("Expected coordinator to have ID %d, found %d", coordinator.BrokerID(), broker.ID()) } coordinator.Close() seedBroker.Close() safeClose(t, client) } func TestClientAutorefreshShutdownRace(t *testing.T) { seedBroker := NewMockBroker(t, 1) metadataResponse := new(MetadataResponse) seedBroker.Returns(metadataResponse) conf := NewConfig() conf.Metadata.RefreshFrequency = 100 * time.Millisecond client, err := NewClient([]string{seedBroker.Addr()}, conf) if err != nil { t.Fatal(err) } // Wait for the background refresh to kick in time.Sleep(110 * time.Millisecond) done := make(chan none) go func() { // Close the client if err := client.Close(); err != nil { t.Fatal(err) } close(done) }() // Wait for the Close to kick in time.Sleep(10 * time.Millisecond) // Then return some metadata to the still-running background thread leader := NewMockBroker(t, 2) metadataResponse.AddBroker(leader.Addr(), leader.BrokerID()) metadataResponse.AddTopicPartition("foo", 0, leader.BrokerID(), []int32{2}, []int32{2}, ErrNoError) seedBroker.Returns(metadataResponse) <-done seedBroker.Close() // give the update time to happen so we get a panic if it's still running (which it shouldn't) time.Sleep(10 * time.Millisecond) } sarama-1.9.0/config.go 0000664 0000000 0000000 00000040012 12716410165 0014561 0 ustar 00root root 0000000 0000000 package sarama import ( "crypto/tls" "regexp" "time" ) var validID *regexp.Regexp = regexp.MustCompile(`\A[A-Za-z0-9._-]*\z`) // Config is used to pass multiple configuration options to Sarama's constructors. type Config struct { // Net is the namespace for network-level properties used by the Broker, and // shared by the Client/Producer/Consumer. Net struct { // How many outstanding requests a connection is allowed to have before // sending on it blocks (default 5). MaxOpenRequests int // All three of the below configurations are similar to the // `socket.timeout.ms` setting in JVM kafka. All of them default // to 30 seconds. DialTimeout time.Duration // How long to wait for the initial connection. ReadTimeout time.Duration // How long to wait for a response. WriteTimeout time.Duration // How long to wait for a transmit. TLS struct { // Whether or not to use TLS when connecting to the broker // (defaults to false). Enable bool // The TLS configuration to use for secure connections if // enabled (defaults to nil). Config *tls.Config } // SASL based authentication with broker. While there are multiple SASL authentication methods // the current implementation is limited to plaintext (SASL/PLAIN) authentication SASL struct { // Whether or not to use SASL authentication when connecting to the broker // (defaults to false). Enable bool //username and password for SASL/PLAIN authentication User string Password string } // KeepAlive specifies the keep-alive period for an active network connection. // If zero, keep-alives are disabled. (default is 0: disabled). KeepAlive time.Duration } // Metadata is the namespace for metadata management properties used by the // Client, and shared by the Producer/Consumer. Metadata struct { Retry struct { // The total number of times to retry a metadata request when the // cluster is in the middle of a leader election (default 3). Max int // How long to wait for leader election to occur before retrying // (default 250ms). Similar to the JVM's `retry.backoff.ms`. Backoff time.Duration } // How frequently to refresh the cluster metadata in the background. // Defaults to 10 minutes. Set to 0 to disable. Similar to // `topic.metadata.refresh.interval.ms` in the JVM version. RefreshFrequency time.Duration } // Producer is the namespace for configuration related to producing messages, // used by the Producer. Producer struct { // The maximum permitted size of a message (defaults to 1000000). Should be // set equal to or smaller than the broker's `message.max.bytes`. MaxMessageBytes int // The level of acknowledgement reliability needed from the broker (defaults // to WaitForLocal). Equivalent to the `request.required.acks` setting of the // JVM producer. RequiredAcks RequiredAcks // The maximum duration the broker will wait the receipt of the number of // RequiredAcks (defaults to 10 seconds). This is only relevant when // RequiredAcks is set to WaitForAll or a number > 1. Only supports // millisecond resolution, nanoseconds will be truncated. Equivalent to // the JVM producer's `request.timeout.ms` setting. Timeout time.Duration // The type of compression to use on messages (defaults to no compression). // Similar to `compression.codec` setting of the JVM producer. Compression CompressionCodec // Generates partitioners for choosing the partition to send messages to // (defaults to hashing the message key). Similar to the `partitioner.class` // setting for the JVM producer. Partitioner PartitionerConstructor // Return specifies what channels will be populated. If they are set to true, // you must read from the respective channels to prevent deadlock. Return struct { // If enabled, successfully delivered messages will be returned on the // Successes channel (default disabled). Successes bool // If enabled, messages that failed to deliver will be returned on the // Errors channel, including error (default enabled). Errors bool } // The following config options control how often messages are batched up and // sent to the broker. By default, messages are sent as fast as possible, and // all messages received while the current batch is in-flight are placed // into the subsequent batch. Flush struct { // The best-effort number of bytes needed to trigger a flush. Use the // global sarama.MaxRequestSize to set a hard upper limit. Bytes int // The best-effort number of messages needed to trigger a flush. Use // `MaxMessages` to set a hard upper limit. Messages int // The best-effort frequency of flushes. Equivalent to // `queue.buffering.max.ms` setting of JVM producer. Frequency time.Duration // The maximum number of messages the producer will send in a single // broker request. Defaults to 0 for unlimited. Similar to // `queue.buffering.max.messages` in the JVM producer. MaxMessages int } Retry struct { // The total number of times to retry sending a message (default 3). // Similar to the `message.send.max.retries` setting of the JVM producer. Max int // How long to wait for the cluster to settle between retries // (default 100ms). Similar to the `retry.backoff.ms` setting of the // JVM producer. Backoff time.Duration } } // Consumer is the namespace for configuration related to consuming messages, // used by the Consumer. // // Note that Sarama's Consumer type does not currently support automatic // consumer-group rebalancing and offset tracking. For Zookeeper-based // tracking (Kafka 0.8.2 and earlier), the https://github.com/wvanbergen/kafka // library builds on Sarama to add this support. For Kafka-based tracking // (Kafka 0.9 and later), the https://github.com/bsm/sarama-cluster library // builds on Sarama to add this support. Consumer struct { Retry struct { // How long to wait after a failing to read from a partition before // trying again (default 2s). Backoff time.Duration } // Fetch is the namespace for controlling how many bytes are retrieved by any // given request. Fetch struct { // The minimum number of message bytes to fetch in a request - the broker // will wait until at least this many are available. The default is 1, // as 0 causes the consumer to spin when no messages are available. // Equivalent to the JVM's `fetch.min.bytes`. Min int32 // The default number of message bytes to fetch from the broker in each // request (default 32768). This should be larger than the majority of // your messages, or else the consumer will spend a lot of time // negotiating sizes and not actually consuming. Similar to the JVM's // `fetch.message.max.bytes`. Default int32 // The maximum number of message bytes to fetch from the broker in a // single request. Messages larger than this will return // ErrMessageTooLarge and will not be consumable, so you must be sure // this is at least as large as your largest message. Defaults to 0 // (no limit). Similar to the JVM's `fetch.message.max.bytes`. The // global `sarama.MaxResponseSize` still applies. Max int32 } // The maximum amount of time the broker will wait for Consumer.Fetch.Min // bytes to become available before it returns fewer than that anyways. The // default is 250ms, since 0 causes the consumer to spin when no events are // available. 100-500ms is a reasonable range for most cases. Kafka only // supports precision up to milliseconds; nanoseconds will be truncated. // Equivalent to the JVM's `fetch.wait.max.ms`. MaxWaitTime time.Duration // The maximum amount of time the consumer expects a message takes to process // for the user. If writing to the Messages channel takes longer than this, // that partition will stop fetching more messages until it can proceed again. // Note that, since the Messages channel is buffered, the actual grace time is // (MaxProcessingTime * ChanneBufferSize). Defaults to 100ms. MaxProcessingTime time.Duration // Return specifies what channels will be populated. If they are set to true, // you must read from them to prevent deadlock. Return struct { // If enabled, any errors that occured while consuming are returned on // the Errors channel (default disabled). Errors bool } // Offsets specifies configuration for how and when to commit consumed // offsets. This currently requires the manual use of an OffsetManager // but will eventually be automated. Offsets struct { // How frequently to commit updated offsets. Defaults to 1s. CommitInterval time.Duration // The initial offset to use if no offset was previously committed. // Should be OffsetNewest or OffsetOldest. Defaults to OffsetNewest. Initial int64 // The retention duration for committed offsets. If zero, disabled // (in which case the `offsets.retention.minutes` option on the // broker will be used). Kafka only supports precision up to // milliseconds; nanoseconds will be truncated. Requires Kafka // broker version 0.9.0 or later. // (default is 0: disabled). Retention time.Duration } } // A user-provided string sent with every request to the brokers for logging, // debugging, and auditing purposes. Defaults to "sarama", but you should // probably set it to something specific to your application. ClientID string // The number of events to buffer in internal and external channels. This // permits the producer and consumer to continue processing some messages // in the background while user code is working, greatly improving throughput. // Defaults to 256. ChannelBufferSize int } // NewConfig returns a new configuration instance with sane defaults. func NewConfig() *Config { c := &Config{} c.Net.MaxOpenRequests = 5 c.Net.DialTimeout = 30 * time.Second c.Net.ReadTimeout = 30 * time.Second c.Net.WriteTimeout = 30 * time.Second c.Metadata.Retry.Max = 3 c.Metadata.Retry.Backoff = 250 * time.Millisecond c.Metadata.RefreshFrequency = 10 * time.Minute c.Producer.MaxMessageBytes = 1000000 c.Producer.RequiredAcks = WaitForLocal c.Producer.Timeout = 10 * time.Second c.Producer.Partitioner = NewHashPartitioner c.Producer.Retry.Max = 3 c.Producer.Retry.Backoff = 100 * time.Millisecond c.Producer.Return.Errors = true c.Consumer.Fetch.Min = 1 c.Consumer.Fetch.Default = 32768 c.Consumer.Retry.Backoff = 2 * time.Second c.Consumer.MaxWaitTime = 250 * time.Millisecond c.Consumer.MaxProcessingTime = 100 * time.Millisecond c.Consumer.Return.Errors = false c.Consumer.Offsets.CommitInterval = 1 * time.Second c.Consumer.Offsets.Initial = OffsetNewest c.ChannelBufferSize = 256 return c } // Validate checks a Config instance. It will return a // ConfigurationError if the specified values don't make sense. func (c *Config) Validate() error { // some configuration values should be warned on but not fail completely, do those first if c.Net.TLS.Enable == false && c.Net.TLS.Config != nil { Logger.Println("Net.TLS is disabled but a non-nil configuration was provided.") } if c.Net.SASL.Enable == false { if c.Net.SASL.User != "" { Logger.Println("Net.SASL is disabled but a non-empty username was provided.") } if c.Net.SASL.Password != "" { Logger.Println("Net.SASL is disabled but a non-empty password was provided.") } } if c.Producer.RequiredAcks > 1 { Logger.Println("Producer.RequiredAcks > 1 is deprecated and will raise an exception with kafka >= 0.8.2.0.") } if c.Producer.MaxMessageBytes >= int(MaxRequestSize) { Logger.Println("Producer.MaxMessageBytes is larger than MaxRequestSize; it will be ignored.") } if c.Producer.Flush.Bytes >= int(MaxRequestSize) { Logger.Println("Producer.Flush.Bytes is larger than MaxRequestSize; it will be ignored.") } if c.Producer.Timeout%time.Millisecond != 0 { Logger.Println("Producer.Timeout only supports millisecond resolution; nanoseconds will be truncated.") } if c.Consumer.MaxWaitTime < 100*time.Millisecond { Logger.Println("Consumer.MaxWaitTime is very low, which can cause high CPU and network usage. See documentation for details.") } if c.Consumer.MaxWaitTime%time.Millisecond != 0 { Logger.Println("Consumer.MaxWaitTime only supports millisecond precision; nanoseconds will be truncated.") } if c.Consumer.Offsets.Retention%time.Millisecond != 0 { Logger.Println("Consumer.Offsets.Retention only supports millisecond precision; nanoseconds will be truncated.") } if c.ClientID == "sarama" { Logger.Println("ClientID is the default of 'sarama', you should consider setting it to something application-specific.") } // validate Net values switch { case c.Net.MaxOpenRequests <= 0: return ConfigurationError("Net.MaxOpenRequests must be > 0") case c.Net.DialTimeout <= 0: return ConfigurationError("Net.DialTimeout must be > 0") case c.Net.ReadTimeout <= 0: return ConfigurationError("Net.ReadTimeout must be > 0") case c.Net.WriteTimeout <= 0: return ConfigurationError("Net.WriteTimeout must be > 0") case c.Net.KeepAlive < 0: return ConfigurationError("Net.KeepAlive must be >= 0") case c.Net.SASL.Enable == true && c.Net.SASL.User == "": return ConfigurationError("Net.SASL.User must not be empty when SASL is enabled") case c.Net.SASL.Enable == true && c.Net.SASL.Password == "": return ConfigurationError("Net.SASL.Password must not be empty when SASL is enabled") } // validate the Metadata values switch { case c.Metadata.Retry.Max < 0: return ConfigurationError("Metadata.Retry.Max must be >= 0") case c.Metadata.Retry.Backoff < 0: return ConfigurationError("Metadata.Retry.Backoff must be >= 0") case c.Metadata.RefreshFrequency < 0: return ConfigurationError("Metadata.RefreshFrequency must be >= 0") } // validate the Producer values switch { case c.Producer.MaxMessageBytes <= 0: return ConfigurationError("Producer.MaxMessageBytes must be > 0") case c.Producer.RequiredAcks < -1: return ConfigurationError("Producer.RequiredAcks must be >= -1") case c.Producer.Timeout <= 0: return ConfigurationError("Producer.Timeout must be > 0") case c.Producer.Partitioner == nil: return ConfigurationError("Producer.Partitioner must not be nil") case c.Producer.Flush.Bytes < 0: return ConfigurationError("Producer.Flush.Bytes must be >= 0") case c.Producer.Flush.Messages < 0: return ConfigurationError("Producer.Flush.Messages must be >= 0") case c.Producer.Flush.Frequency < 0: return ConfigurationError("Producer.Flush.Frequency must be >= 0") case c.Producer.Flush.MaxMessages < 0: return ConfigurationError("Producer.Flush.MaxMessages must be >= 0") case c.Producer.Flush.MaxMessages > 0 && c.Producer.Flush.MaxMessages < c.Producer.Flush.Messages: return ConfigurationError("Producer.Flush.MaxMessages must be >= Producer.Flush.Messages when set") case c.Producer.Retry.Max < 0: return ConfigurationError("Producer.Retry.Max must be >= 0") case c.Producer.Retry.Backoff < 0: return ConfigurationError("Producer.Retry.Backoff must be >= 0") } // validate the Consumer values switch { case c.Consumer.Fetch.Min <= 0: return ConfigurationError("Consumer.Fetch.Min must be > 0") case c.Consumer.Fetch.Default <= 0: return ConfigurationError("Consumer.Fetch.Default must be > 0") case c.Consumer.Fetch.Max < 0: return ConfigurationError("Consumer.Fetch.Max must be >= 0") case c.Consumer.MaxWaitTime < 1*time.Millisecond: return ConfigurationError("Consumer.MaxWaitTime must be >= 1ms") case c.Consumer.MaxProcessingTime <= 0: return ConfigurationError("Consumer.MaxProcessingTime must be > 0") case c.Consumer.Retry.Backoff < 0: return ConfigurationError("Consumer.Retry.Backoff must be >= 0") case c.Consumer.Offsets.CommitInterval <= 0: return ConfigurationError("Consumer.Offsets.CommitInterval must be > 0") case c.Consumer.Offsets.Initial != OffsetOldest && c.Consumer.Offsets.Initial != OffsetNewest: return ConfigurationError("Consumer.Offsets.Initial must be OffsetOldest or OffsetNewest") } // validate misc shared values switch { case c.ChannelBufferSize < 0: return ConfigurationError("ChannelBufferSize must be >= 0") case !validID.MatchString(c.ClientID): return ConfigurationError("ClientID is invalid") } return nil } sarama-1.9.0/config_test.go 0000664 0000000 0000000 00000000631 12716410165 0015623 0 ustar 00root root 0000000 0000000 package sarama import "testing" func TestDefaultConfigValidates(t *testing.T) { config := NewConfig() if err := config.Validate(); err != nil { t.Error(err) } } func TestClientIDValidates(t *testing.T) { config := NewConfig() config.ClientID = "foo:bar" if err := config.Validate(); string(err.(ConfigurationError)) != "ClientID is invalid" { t.Error("Expected invalid ClientID, got ", err) } } sarama-1.9.0/consumer.go 0000664 0000000 0000000 00000047530 12716410165 0015163 0 ustar 00root root 0000000 0000000 package sarama import ( "errors" "fmt" "sync" "sync/atomic" "time" ) // ConsumerMessage encapsulates a Kafka message returned by the consumer. type ConsumerMessage struct { Key, Value []byte Topic string Partition int32 Offset int64 } // ConsumerError is what is provided to the user when an error occurs. // It wraps an error and includes the topic and partition. type ConsumerError struct { Topic string Partition int32 Err error } func (ce ConsumerError) Error() string { return fmt.Sprintf("kafka: error while consuming %s/%d: %s", ce.Topic, ce.Partition, ce.Err) } // ConsumerErrors is a type that wraps a batch of errors and implements the Error interface. // It can be returned from the PartitionConsumer's Close methods to avoid the need to manually drain errors // when stopping. type ConsumerErrors []*ConsumerError func (ce ConsumerErrors) Error() string { return fmt.Sprintf("kafka: %d errors while consuming", len(ce)) } // Consumer manages PartitionConsumers which process Kafka messages from brokers. You MUST call Close() // on a consumer to avoid leaks, it will not be garbage-collected automatically when it passes out of // scope. // // Sarama's Consumer type does not currently support automatic consumer-group rebalancing and offset tracking. // For Zookeeper-based tracking (Kafka 0.8.2 and earlier), the https://github.com/wvanbergen/kafka library // builds on Sarama to add this support. For Kafka-based tracking (Kafka 0.9 and later), the // https://github.com/bsm/sarama-cluster library builds on Sarama to add this support. type Consumer interface { // Topics returns the set of available topics as retrieved from the cluster // metadata. This method is the same as Client.Topics(), and is provided for // convenience. Topics() ([]string, error) // Partitions returns the sorted list of all partition IDs for the given topic. // This method is the same as Client.Partitions(), and is provided for convenience. Partitions(topic string) ([]int32, error) // ConsumePartition creates a PartitionConsumer on the given topic/partition with // the given offset. It will return an error if this Consumer is already consuming // on the given topic/partition. Offset can be a literal offset, or OffsetNewest // or OffsetOldest ConsumePartition(topic string, partition int32, offset int64) (PartitionConsumer, error) // Close shuts down the consumer. It must be called after all child // PartitionConsumers have already been closed. Close() error } type consumer struct { client Client conf *Config ownClient bool lock sync.Mutex children map[string]map[int32]*partitionConsumer brokerConsumers map[*Broker]*brokerConsumer } // NewConsumer creates a new consumer using the given broker addresses and configuration. func NewConsumer(addrs []string, config *Config) (Consumer, error) { client, err := NewClient(addrs, config) if err != nil { return nil, err } c, err := NewConsumerFromClient(client) if err != nil { return nil, err } c.(*consumer).ownClient = true return c, nil } // NewConsumerFromClient creates a new consumer using the given client. It is still // necessary to call Close() on the underlying client when shutting down this consumer. func NewConsumerFromClient(client Client) (Consumer, error) { // Check that we are not dealing with a closed Client before processing any other arguments if client.Closed() { return nil, ErrClosedClient } c := &consumer{ client: client, conf: client.Config(), children: make(map[string]map[int32]*partitionConsumer), brokerConsumers: make(map[*Broker]*brokerConsumer), } return c, nil } func (c *consumer) Close() error { if c.ownClient { return c.client.Close() } return nil } func (c *consumer) Topics() ([]string, error) { return c.client.Topics() } func (c *consumer) Partitions(topic string) ([]int32, error) { return c.client.Partitions(topic) } func (c *consumer) ConsumePartition(topic string, partition int32, offset int64) (PartitionConsumer, error) { child := &partitionConsumer{ consumer: c, conf: c.conf, topic: topic, partition: partition, messages: make(chan *ConsumerMessage, c.conf.ChannelBufferSize), errors: make(chan *ConsumerError, c.conf.ChannelBufferSize), feeder: make(chan *FetchResponse, 1), trigger: make(chan none, 1), dying: make(chan none), fetchSize: c.conf.Consumer.Fetch.Default, } if err := child.chooseStartingOffset(offset); err != nil { return nil, err } var leader *Broker var err error if leader, err = c.client.Leader(child.topic, child.partition); err != nil { return nil, err } if err := c.addChild(child); err != nil { return nil, err } go withRecover(child.dispatcher) go withRecover(child.responseFeeder) child.broker = c.refBrokerConsumer(leader) child.broker.input <- child return child, nil } func (c *consumer) addChild(child *partitionConsumer) error { c.lock.Lock() defer c.lock.Unlock() topicChildren := c.children[child.topic] if topicChildren == nil { topicChildren = make(map[int32]*partitionConsumer) c.children[child.topic] = topicChildren } if topicChildren[child.partition] != nil { return ConfigurationError("That topic/partition is already being consumed") } topicChildren[child.partition] = child return nil } func (c *consumer) removeChild(child *partitionConsumer) { c.lock.Lock() defer c.lock.Unlock() delete(c.children[child.topic], child.partition) } func (c *consumer) refBrokerConsumer(broker *Broker) *brokerConsumer { c.lock.Lock() defer c.lock.Unlock() bc := c.brokerConsumers[broker] if bc == nil { bc = c.newBrokerConsumer(broker) c.brokerConsumers[broker] = bc } bc.refs++ return bc } func (c *consumer) unrefBrokerConsumer(brokerWorker *brokerConsumer) { c.lock.Lock() defer c.lock.Unlock() brokerWorker.refs-- if brokerWorker.refs == 0 { close(brokerWorker.input) if c.brokerConsumers[brokerWorker.broker] == brokerWorker { delete(c.brokerConsumers, brokerWorker.broker) } } } func (c *consumer) abandonBrokerConsumer(brokerWorker *brokerConsumer) { c.lock.Lock() defer c.lock.Unlock() delete(c.brokerConsumers, brokerWorker.broker) } // PartitionConsumer // PartitionConsumer processes Kafka messages from a given topic and partition. You MUST call Close() // or AsyncClose() on a PartitionConsumer to avoid leaks, it will not be garbage-collected automatically // when it passes out of scope. // // The simplest way of using a PartitionConsumer is to loop over its Messages channel using a for/range // loop. The PartitionConsumer will only stop itself in one case: when the offset being consumed is reported // as out of range by the brokers. In this case you should decide what you want to do (try a different offset, // notify a human, etc) and handle it appropriately. For all other error cases, it will just keep retrying. // By default, it logs these errors to sarama.Logger; if you want to be notified directly of all errors, set // your config's Consumer.Return.Errors to true and read from the Errors channel, using a select statement // or a separate goroutine. Check out the Consumer examples to see implementations of these different approaches. type PartitionConsumer interface { // AsyncClose initiates a shutdown of the PartitionConsumer. This method will // return immediately, after which you should wait until the 'messages' and // 'errors' channel are drained. It is required to call this function, or // Close before a consumer object passes out of scope, as it will otherwise // leak memory. You must call this before calling Close on the underlying client. AsyncClose() // Close stops the PartitionConsumer from fetching messages. It is required to // call this function (or AsyncClose) before a consumer object passes out of // scope, as it will otherwise leak memory. You must call this before calling // Close on the underlying client. Close() error // Messages returns the read channel for the messages that are returned by // the broker. Messages() <-chan *ConsumerMessage // Errors returns a read channel of errors that occured during consuming, if // enabled. By default, errors are logged and not returned over this channel. // If you want to implement any custom error handling, set your config's // Consumer.Return.Errors setting to true, and read from this channel. Errors() <-chan *ConsumerError // HighWaterMarkOffset returns the high water mark offset of the partition, // i.e. the offset that will be used for the next message that will be produced. // You can use this to determine how far behind the processing is. HighWaterMarkOffset() int64 } type partitionConsumer struct { consumer *consumer conf *Config topic string partition int32 broker *brokerConsumer messages chan *ConsumerMessage errors chan *ConsumerError feeder chan *FetchResponse trigger, dying chan none responseResult error fetchSize int32 offset int64 highWaterMarkOffset int64 } var errTimedOut = errors.New("timed out feeding messages to the user") // not user-facing func (child *partitionConsumer) sendError(err error) { cErr := &ConsumerError{ Topic: child.topic, Partition: child.partition, Err: err, } if child.conf.Consumer.Return.Errors { child.errors <- cErr } else { Logger.Println(cErr) } } func (child *partitionConsumer) dispatcher() { for _ = range child.trigger { select { case <-child.dying: close(child.trigger) case <-time.After(child.conf.Consumer.Retry.Backoff): if child.broker != nil { child.consumer.unrefBrokerConsumer(child.broker) child.broker = nil } Logger.Printf("consumer/%s/%d finding new broker\n", child.topic, child.partition) if err := child.dispatch(); err != nil { child.sendError(err) child.trigger <- none{} } } } if child.broker != nil { child.consumer.unrefBrokerConsumer(child.broker) } child.consumer.removeChild(child) close(child.feeder) } func (child *partitionConsumer) dispatch() error { if err := child.consumer.client.RefreshMetadata(child.topic); err != nil { return err } var leader *Broker var err error if leader, err = child.consumer.client.Leader(child.topic, child.partition); err != nil { return err } child.broker = child.consumer.refBrokerConsumer(leader) child.broker.input <- child return nil } func (child *partitionConsumer) chooseStartingOffset(offset int64) error { newestOffset, err := child.consumer.client.GetOffset(child.topic, child.partition, OffsetNewest) if err != nil { return err } oldestOffset, err := child.consumer.client.GetOffset(child.topic, child.partition, OffsetOldest) if err != nil { return err } switch { case offset == OffsetNewest: child.offset = newestOffset case offset == OffsetOldest: child.offset = oldestOffset case offset >= oldestOffset && offset <= newestOffset: child.offset = offset default: return ErrOffsetOutOfRange } return nil } func (child *partitionConsumer) Messages() <-chan *ConsumerMessage { return child.messages } func (child *partitionConsumer) Errors() <-chan *ConsumerError { return child.errors } func (child *partitionConsumer) AsyncClose() { // this triggers whatever broker owns this child to abandon it and close its trigger channel, which causes // the dispatcher to exit its loop, which removes it from the consumer then closes its 'messages' and // 'errors' channel (alternatively, if the child is already at the dispatcher for some reason, that will // also just close itself) close(child.dying) } func (child *partitionConsumer) Close() error { child.AsyncClose() go withRecover(func() { for _ = range child.messages { // drain } }) var errors ConsumerErrors for err := range child.errors { errors = append(errors, err) } if len(errors) > 0 { return errors } return nil } func (child *partitionConsumer) HighWaterMarkOffset() int64 { return atomic.LoadInt64(&child.highWaterMarkOffset) } func (child *partitionConsumer) responseFeeder() { var msgs []*ConsumerMessage feederLoop: for response := range child.feeder { msgs, child.responseResult = child.parseResponse(response) for i, msg := range msgs { select { case child.messages <- msg: case <-time.After(child.conf.Consumer.MaxProcessingTime): child.responseResult = errTimedOut child.broker.acks.Done() for _, msg = range msgs[i:] { child.messages <- msg } child.broker.input <- child continue feederLoop } } child.broker.acks.Done() } close(child.messages) close(child.errors) } func (child *partitionConsumer) parseResponse(response *FetchResponse) ([]*ConsumerMessage, error) { block := response.GetBlock(child.topic, child.partition) if block == nil { return nil, ErrIncompleteResponse } if block.Err != ErrNoError { return nil, block.Err } if len(block.MsgSet.Messages) == 0 { // We got no messages. If we got a trailing one then we need to ask for more data. // Otherwise we just poll again and wait for one to be produced... if block.MsgSet.PartialTrailingMessage { if child.conf.Consumer.Fetch.Max > 0 && child.fetchSize == child.conf.Consumer.Fetch.Max { // we can't ask for more data, we've hit the configured limit child.sendError(ErrMessageTooLarge) child.offset++ // skip this one so we can keep processing future messages } else { child.fetchSize *= 2 if child.conf.Consumer.Fetch.Max > 0 && child.fetchSize > child.conf.Consumer.Fetch.Max { child.fetchSize = child.conf.Consumer.Fetch.Max } } } return nil, nil } // we got messages, reset our fetch size in case it was increased for a previous request child.fetchSize = child.conf.Consumer.Fetch.Default atomic.StoreInt64(&child.highWaterMarkOffset, block.HighWaterMarkOffset) incomplete := false prelude := true var messages []*ConsumerMessage for _, msgBlock := range block.MsgSet.Messages { for _, msg := range msgBlock.Messages() { if prelude && msg.Offset < child.offset { continue } prelude = false if msg.Offset >= child.offset { messages = append(messages, &ConsumerMessage{ Topic: child.topic, Partition: child.partition, Key: msg.Msg.Key, Value: msg.Msg.Value, Offset: msg.Offset, }) child.offset = msg.Offset + 1 } else { incomplete = true } } } if incomplete || len(messages) == 0 { return nil, ErrIncompleteResponse } return messages, nil } // brokerConsumer type brokerConsumer struct { consumer *consumer broker *Broker input chan *partitionConsumer newSubscriptions chan []*partitionConsumer wait chan none subscriptions map[*partitionConsumer]none acks sync.WaitGroup refs int } func (c *consumer) newBrokerConsumer(broker *Broker) *brokerConsumer { bc := &brokerConsumer{ consumer: c, broker: broker, input: make(chan *partitionConsumer), newSubscriptions: make(chan []*partitionConsumer), wait: make(chan none), subscriptions: make(map[*partitionConsumer]none), refs: 0, } go withRecover(bc.subscriptionManager) go withRecover(bc.subscriptionConsumer) return bc } func (bc *brokerConsumer) subscriptionManager() { var buffer []*partitionConsumer // The subscriptionManager constantly accepts new subscriptions on `input` (even when the main subscriptionConsumer // goroutine is in the middle of a network request) and batches it up. The main worker goroutine picks // up a batch of new subscriptions between every network request by reading from `newSubscriptions`, so we give // it nil if no new subscriptions are available. We also write to `wait` only when new subscriptions is available, // so the main goroutine can block waiting for work if it has none. for { if len(buffer) > 0 { select { case event, ok := <-bc.input: if !ok { goto done } buffer = append(buffer, event) case bc.newSubscriptions <- buffer: buffer = nil case bc.wait <- none{}: } } else { select { case event, ok := <-bc.input: if !ok { goto done } buffer = append(buffer, event) case bc.newSubscriptions <- nil: } } } done: close(bc.wait) if len(buffer) > 0 { bc.newSubscriptions <- buffer } close(bc.newSubscriptions) } func (bc *brokerConsumer) subscriptionConsumer() { <-bc.wait // wait for our first piece of work // the subscriptionConsumer ensures we will get nil right away if no new subscriptions is available for newSubscriptions := range bc.newSubscriptions { bc.updateSubscriptions(newSubscriptions) if len(bc.subscriptions) == 0 { // We're about to be shut down or we're about to receive more subscriptions. // Either way, the signal just hasn't propagated to our goroutine yet. <-bc.wait continue } response, err := bc.fetchNewMessages() if err != nil { Logger.Printf("consumer/broker/%d disconnecting due to error processing FetchRequest: %s\n", bc.broker.ID(), err) bc.abort(err) return } bc.acks.Add(len(bc.subscriptions)) for child := range bc.subscriptions { child.feeder <- response } bc.acks.Wait() bc.handleResponses() } } func (bc *brokerConsumer) updateSubscriptions(newSubscriptions []*partitionConsumer) { for _, child := range newSubscriptions { bc.subscriptions[child] = none{} Logger.Printf("consumer/broker/%d added subscription to %s/%d\n", bc.broker.ID(), child.topic, child.partition) } for child := range bc.subscriptions { select { case <-child.dying: Logger.Printf("consumer/broker/%d closed dead subscription to %s/%d\n", bc.broker.ID(), child.topic, child.partition) close(child.trigger) delete(bc.subscriptions, child) default: break } } } func (bc *brokerConsumer) handleResponses() { // handles the response codes left for us by our subscriptions, and abandons ones that have been closed for child := range bc.subscriptions { result := child.responseResult child.responseResult = nil switch result { case nil: break case errTimedOut: Logger.Printf("consumer/broker/%d abandoned subscription to %s/%d because consuming was taking too long\n", bc.broker.ID(), child.topic, child.partition) delete(bc.subscriptions, child) case ErrOffsetOutOfRange: // there's no point in retrying this it will just fail the same way again // shut it down and force the user to choose what to do child.sendError(result) Logger.Printf("consumer/%s/%d shutting down because %s\n", child.topic, child.partition, result) close(child.trigger) delete(bc.subscriptions, child) case ErrUnknownTopicOrPartition, ErrNotLeaderForPartition, ErrLeaderNotAvailable, ErrReplicaNotAvailable: // not an error, but does need redispatching Logger.Printf("consumer/broker/%d abandoned subscription to %s/%d because %s\n", bc.broker.ID(), child.topic, child.partition, result) child.trigger <- none{} delete(bc.subscriptions, child) default: // dunno, tell the user and try redispatching child.sendError(result) Logger.Printf("consumer/broker/%d abandoned subscription to %s/%d because %s\n", bc.broker.ID(), child.topic, child.partition, result) child.trigger <- none{} delete(bc.subscriptions, child) } } } func (bc *brokerConsumer) abort(err error) { bc.consumer.abandonBrokerConsumer(bc) _ = bc.broker.Close() // we don't care about the error this might return, we already have one for child := range bc.subscriptions { child.sendError(err) child.trigger <- none{} } for newSubscription := range bc.newSubscriptions { for _, child := range newSubscription { child.sendError(err) child.trigger <- none{} } } } func (bc *brokerConsumer) fetchNewMessages() (*FetchResponse, error) { request := &FetchRequest{ MinBytes: bc.consumer.conf.Consumer.Fetch.Min, MaxWaitTime: int32(bc.consumer.conf.Consumer.MaxWaitTime / time.Millisecond), } for child := range bc.subscriptions { request.AddBlock(child.topic, child.partition, child.offset, child.fetchSize) } return bc.broker.Fetch(request) } sarama-1.9.0/consumer_group_members.go 0000664 0000000 0000000 00000003305 12716410165 0020101 0 ustar 00root root 0000000 0000000 package sarama type ConsumerGroupMemberMetadata struct { Version int16 Topics []string UserData []byte } func (m *ConsumerGroupMemberMetadata) encode(pe packetEncoder) error { pe.putInt16(m.Version) if err := pe.putStringArray(m.Topics); err != nil { return err } if err := pe.putBytes(m.UserData); err != nil { return err } return nil } func (m *ConsumerGroupMemberMetadata) decode(pd packetDecoder) (err error) { if m.Version, err = pd.getInt16(); err != nil { return } if m.Topics, err = pd.getStringArray(); err != nil { return } if m.UserData, err = pd.getBytes(); err != nil { return } return nil } type ConsumerGroupMemberAssignment struct { Version int16 Topics map[string][]int32 UserData []byte } func (m *ConsumerGroupMemberAssignment) encode(pe packetEncoder) error { pe.putInt16(m.Version) if err := pe.putArrayLength(len(m.Topics)); err != nil { return err } for topic, partitions := range m.Topics { if err := pe.putString(topic); err != nil { return err } if err := pe.putInt32Array(partitions); err != nil { return err } } if err := pe.putBytes(m.UserData); err != nil { return err } return nil } func (m *ConsumerGroupMemberAssignment) decode(pd packetDecoder) (err error) { if m.Version, err = pd.getInt16(); err != nil { return } var topicLen int if topicLen, err = pd.getArrayLength(); err != nil { return } m.Topics = make(map[string][]int32, topicLen) for i := 0; i < topicLen; i++ { var topic string if topic, err = pd.getString(); err != nil { return } if m.Topics[topic], err = pd.getInt32Array(); err != nil { return } } if m.UserData, err = pd.getBytes(); err != nil { return } return nil } sarama-1.9.0/consumer_group_members_test.go 0000664 0000000 0000000 00000003663 12716410165 0021147 0 ustar 00root root 0000000 0000000 package sarama import ( "bytes" "reflect" "testing" ) var ( groupMemberMetadata = []byte{ 0, 1, // Version 0, 0, 0, 2, // Topic array length 0, 3, 'o', 'n', 'e', // Topic one 0, 3, 't', 'w', 'o', // Topic two 0, 0, 0, 3, 0x01, 0x02, 0x03, // Userdata } groupMemberAssignment = []byte{ 0, 1, // Version 0, 0, 0, 1, // Topic array length 0, 3, 'o', 'n', 'e', // Topic one 0, 0, 0, 3, // Topic one, partition array length 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4, // 0, 2, 4 0, 0, 0, 3, 0x01, 0x02, 0x03, // Userdata } ) func TestConsumerGroupMemberMetadata(t *testing.T) { meta := &ConsumerGroupMemberMetadata{ Version: 1, Topics: []string{"one", "two"}, UserData: []byte{0x01, 0x02, 0x03}, } buf, err := encode(meta) if err != nil { t.Error("Failed to encode data", err) } else if !bytes.Equal(groupMemberMetadata, buf) { t.Errorf("Encoded data does not match expectation\nexpected: %v\nactual: %v", groupMemberMetadata, buf) } meta2 := new(ConsumerGroupMemberMetadata) err = decode(buf, meta2) if err != nil { t.Error("Failed to decode data", err) } else if !reflect.DeepEqual(meta, meta2) { t.Errorf("Encoded data does not match expectation\nexpected: %v\nactual: %v", meta, meta2) } } func TestConsumerGroupMemberAssignment(t *testing.T) { amt := &ConsumerGroupMemberAssignment{ Version: 1, Topics: map[string][]int32{ "one": []int32{0, 2, 4}, }, UserData: []byte{0x01, 0x02, 0x03}, } buf, err := encode(amt) if err != nil { t.Error("Failed to encode data", err) } else if !bytes.Equal(groupMemberAssignment, buf) { t.Errorf("Encoded data does not match expectation\nexpected: %v\nactual: %v", groupMemberAssignment, buf) } amt2 := new(ConsumerGroupMemberAssignment) err = decode(buf, amt2) if err != nil { t.Error("Failed to decode data", err) } else if !reflect.DeepEqual(amt, amt2) { t.Errorf("Encoded data does not match expectation\nexpected: %v\nactual: %v", amt, amt2) } } sarama-1.9.0/consumer_metadata_request.go 0000664 0000000 0000000 00000000670 12716410165 0020565 0 ustar 00root root 0000000 0000000 package sarama type ConsumerMetadataRequest struct { ConsumerGroup string } func (r *ConsumerMetadataRequest) encode(pe packetEncoder) error { return pe.putString(r.ConsumerGroup) } func (r *ConsumerMetadataRequest) decode(pd packetDecoder) (err error) { r.ConsumerGroup, err = pd.getString() return err } func (r *ConsumerMetadataRequest) key() int16 { return 10 } func (r *ConsumerMetadataRequest) version() int16 { return 0 } sarama-1.9.0/consumer_metadata_request_test.go 0000664 0000000 0000000 00000000704 12716410165 0021622 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( consumerMetadataRequestEmpty = []byte{ 0x00, 0x00} consumerMetadataRequestString = []byte{ 0x00, 0x06, 'f', 'o', 'o', 'b', 'a', 'r'} ) func TestConsumerMetadataRequest(t *testing.T) { request := new(ConsumerMetadataRequest) testRequest(t, "empty string", request, consumerMetadataRequestEmpty) request.ConsumerGroup = "foobar" testRequest(t, "with string", request, consumerMetadataRequestString) } sarama-1.9.0/consumer_metadata_response.go 0000664 0000000 0000000 00000003156 12716410165 0020735 0 ustar 00root root 0000000 0000000 package sarama import ( "net" "strconv" ) type ConsumerMetadataResponse struct { Err KError Coordinator *Broker CoordinatorID int32 // deprecated: use Coordinator.ID() CoordinatorHost string // deprecated: use Coordinator.Addr() CoordinatorPort int32 // deprecated: use Coordinator.Addr() } func (r *ConsumerMetadataResponse) decode(pd packetDecoder) (err error) { tmp, err := pd.getInt16() if err != nil { return err } r.Err = KError(tmp) coordinator := new(Broker) if err := coordinator.decode(pd); err != nil { return err } if coordinator.addr == ":0" { return nil } r.Coordinator = coordinator // this can all go away in 2.0, but we have to fill in deprecated fields to maintain // backwards compatibility host, portstr, err := net.SplitHostPort(r.Coordinator.Addr()) if err != nil { return err } port, err := strconv.ParseInt(portstr, 10, 32) if err != nil { return err } r.CoordinatorID = r.Coordinator.ID() r.CoordinatorHost = host r.CoordinatorPort = int32(port) return nil } func (r *ConsumerMetadataResponse) encode(pe packetEncoder) error { pe.putInt16(int16(r.Err)) if r.Coordinator != nil { host, portstr, err := net.SplitHostPort(r.Coordinator.Addr()) if err != nil { return err } port, err := strconv.ParseInt(portstr, 10, 32) if err != nil { return err } pe.putInt32(r.Coordinator.ID()) if err := pe.putString(host); err != nil { return err } pe.putInt32(int32(port)) return nil } pe.putInt32(r.CoordinatorID) if err := pe.putString(r.CoordinatorHost); err != nil { return err } pe.putInt32(r.CoordinatorPort) return nil } sarama-1.9.0/consumer_metadata_response_test.go 0000664 0000000 0000000 00000001533 12716410165 0021771 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( consumerMetadataResponseError = []byte{ 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} consumerMetadataResponseSuccess = []byte{ 0x00, 0x00, 0x00, 0x00, 0x00, 0xAB, 0x00, 0x03, 'f', 'o', 'o', 0x00, 0x00, 0xCC, 0xDD} ) func TestConsumerMetadataResponseError(t *testing.T) { response := ConsumerMetadataResponse{Err: ErrOffsetsLoadInProgress} testResponse(t, "error", &response, consumerMetadataResponseError) } func TestConsumerMetadataResponseSuccess(t *testing.T) { broker := NewBroker("foo:52445") broker.id = 0xAB response := ConsumerMetadataResponse{ Coordinator: broker, CoordinatorID: 0xAB, CoordinatorHost: "foo", CoordinatorPort: 0xCCDD, Err: ErrNoError, } testResponse(t, "success", &response, consumerMetadataResponseSuccess) } sarama-1.9.0/consumer_test.go 0000664 0000000 0000000 00000061364 12716410165 0016223 0 ustar 00root root 0000000 0000000 package sarama import ( "log" "os" "os/signal" "sync" "testing" "time" ) var testMsg = StringEncoder("Foo") // If a particular offset is provided then messages are consumed starting from // that offset. func TestConsumerOffsetManual(t *testing.T) { // Given broker0 := NewMockBroker(t, 0) mockFetchResponse := NewMockFetchResponse(t, 1) for i := 0; i < 10; i++ { mockFetchResponse.SetMessage("my_topic", 0, int64(i+1234), testMsg) } broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetOldest, 0). SetOffset("my_topic", 0, OffsetNewest, 2345), "FetchRequest": mockFetchResponse, }) // When master, err := NewConsumer([]string{broker0.Addr()}, nil) if err != nil { t.Fatal(err) } consumer, err := master.ConsumePartition("my_topic", 0, 1234) if err != nil { t.Fatal(err) } // Then: messages starting from offset 1234 are consumed. for i := 0; i < 10; i++ { select { case message := <-consumer.Messages(): assertMessageOffset(t, message, int64(i+1234)) case err := <-consumer.Errors(): t.Error(err) } } safeClose(t, consumer) safeClose(t, master) broker0.Close() } // If `OffsetNewest` is passed as the initial offset then the first consumed // message is indeed corresponds to the offset that broker claims to be the // newest in its metadata response. func TestConsumerOffsetNewest(t *testing.T) { // Given broker0 := NewMockBroker(t, 0) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetNewest, 10). SetOffset("my_topic", 0, OffsetOldest, 7), "FetchRequest": NewMockFetchResponse(t, 1). SetMessage("my_topic", 0, 9, testMsg). SetMessage("my_topic", 0, 10, testMsg). SetMessage("my_topic", 0, 11, testMsg). SetHighWaterMark("my_topic", 0, 14), }) master, err := NewConsumer([]string{broker0.Addr()}, nil) if err != nil { t.Fatal(err) } // When consumer, err := master.ConsumePartition("my_topic", 0, OffsetNewest) if err != nil { t.Fatal(err) } // Then assertMessageOffset(t, <-consumer.Messages(), 10) if hwmo := consumer.HighWaterMarkOffset(); hwmo != 14 { t.Errorf("Expected high water mark offset 14, found %d", hwmo) } safeClose(t, consumer) safeClose(t, master) broker0.Close() } // It is possible to close a partition consumer and create the same anew. func TestConsumerRecreate(t *testing.T) { // Given broker0 := NewMockBroker(t, 0) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetOldest, 0). SetOffset("my_topic", 0, OffsetNewest, 1000), "FetchRequest": NewMockFetchResponse(t, 1). SetMessage("my_topic", 0, 10, testMsg), }) c, err := NewConsumer([]string{broker0.Addr()}, nil) if err != nil { t.Fatal(err) } pc, err := c.ConsumePartition("my_topic", 0, 10) if err != nil { t.Fatal(err) } assertMessageOffset(t, <-pc.Messages(), 10) // When safeClose(t, pc) pc, err = c.ConsumePartition("my_topic", 0, 10) if err != nil { t.Fatal(err) } // Then assertMessageOffset(t, <-pc.Messages(), 10) safeClose(t, pc) safeClose(t, c) broker0.Close() } // An attempt to consume the same partition twice should fail. func TestConsumerDuplicate(t *testing.T) { // Given broker0 := NewMockBroker(t, 0) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetOldest, 0). SetOffset("my_topic", 0, OffsetNewest, 1000), "FetchRequest": NewMockFetchResponse(t, 1), }) config := NewConfig() config.ChannelBufferSize = 0 c, err := NewConsumer([]string{broker0.Addr()}, config) if err != nil { t.Fatal(err) } pc1, err := c.ConsumePartition("my_topic", 0, 0) if err != nil { t.Fatal(err) } // When pc2, err := c.ConsumePartition("my_topic", 0, 0) // Then if pc2 != nil || err != ConfigurationError("That topic/partition is already being consumed") { t.Fatal("A partition cannot be consumed twice at the same time") } safeClose(t, pc1) safeClose(t, c) broker0.Close() } // If consumer fails to refresh metadata it keeps retrying with frequency // specified by `Config.Consumer.Retry.Backoff`. func TestConsumerLeaderRefreshError(t *testing.T) { // Given broker0 := NewMockBroker(t, 100) // Stage 1: my_topic/0 served by broker0 Logger.Printf(" STAGE 1") broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetOldest, 123). SetOffset("my_topic", 0, OffsetNewest, 1000), "FetchRequest": NewMockFetchResponse(t, 1). SetMessage("my_topic", 0, 123, testMsg), }) config := NewConfig() config.Net.ReadTimeout = 100 * time.Millisecond config.Consumer.Retry.Backoff = 200 * time.Millisecond config.Consumer.Return.Errors = true config.Metadata.Retry.Max = 0 c, err := NewConsumer([]string{broker0.Addr()}, config) if err != nil { t.Fatal(err) } pc, err := c.ConsumePartition("my_topic", 0, OffsetOldest) if err != nil { t.Fatal(err) } assertMessageOffset(t, <-pc.Messages(), 123) // Stage 2: broker0 says that it is no longer the leader for my_topic/0, // but the requests to retrieve metadata fail with network timeout. Logger.Printf(" STAGE 2") fetchResponse2 := &FetchResponse{} fetchResponse2.AddError("my_topic", 0, ErrNotLeaderForPartition) broker0.SetHandlerByMap(map[string]MockResponse{ "FetchRequest": NewMockWrapper(fetchResponse2), }) if consErr := <-pc.Errors(); consErr.Err != ErrOutOfBrokers { t.Errorf("Unexpected error: %v", consErr.Err) } // Stage 3: finally the metadata returned by broker0 tells that broker1 is // a new leader for my_topic/0. Consumption resumes. Logger.Printf(" STAGE 3") broker1 := NewMockBroker(t, 101) broker1.SetHandlerByMap(map[string]MockResponse{ "FetchRequest": NewMockFetchResponse(t, 1). SetMessage("my_topic", 0, 124, testMsg), }) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetBroker(broker1.Addr(), broker1.BrokerID()). SetLeader("my_topic", 0, broker1.BrokerID()), }) assertMessageOffset(t, <-pc.Messages(), 124) safeClose(t, pc) safeClose(t, c) broker1.Close() broker0.Close() } func TestConsumerInvalidTopic(t *testing.T) { // Given broker0 := NewMockBroker(t, 100) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()), }) c, err := NewConsumer([]string{broker0.Addr()}, nil) if err != nil { t.Fatal(err) } // When pc, err := c.ConsumePartition("my_topic", 0, OffsetOldest) // Then if pc != nil || err != ErrUnknownTopicOrPartition { t.Errorf("Should fail with, err=%v", err) } safeClose(t, c) broker0.Close() } // Nothing bad happens if a partition consumer that has no leader assigned at // the moment is closed. func TestConsumerClosePartitionWithoutLeader(t *testing.T) { // Given broker0 := NewMockBroker(t, 100) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetOldest, 123). SetOffset("my_topic", 0, OffsetNewest, 1000), "FetchRequest": NewMockFetchResponse(t, 1). SetMessage("my_topic", 0, 123, testMsg), }) config := NewConfig() config.Net.ReadTimeout = 100 * time.Millisecond config.Consumer.Retry.Backoff = 100 * time.Millisecond config.Consumer.Return.Errors = true config.Metadata.Retry.Max = 0 c, err := NewConsumer([]string{broker0.Addr()}, config) if err != nil { t.Fatal(err) } pc, err := c.ConsumePartition("my_topic", 0, OffsetOldest) if err != nil { t.Fatal(err) } assertMessageOffset(t, <-pc.Messages(), 123) // broker0 says that it is no longer the leader for my_topic/0, but the // requests to retrieve metadata fail with network timeout. fetchResponse2 := &FetchResponse{} fetchResponse2.AddError("my_topic", 0, ErrNotLeaderForPartition) broker0.SetHandlerByMap(map[string]MockResponse{ "FetchRequest": NewMockWrapper(fetchResponse2), }) // When if consErr := <-pc.Errors(); consErr.Err != ErrOutOfBrokers { t.Errorf("Unexpected error: %v", consErr.Err) } // Then: the partition consumer can be closed without any problem. safeClose(t, pc) safeClose(t, c) broker0.Close() } // If the initial offset passed on partition consumer creation is out of the // actual offset range for the partition, then the partition consumer stops // immediately closing its output channels. func TestConsumerShutsDownOutOfRange(t *testing.T) { // Given broker0 := NewMockBroker(t, 0) fetchResponse := new(FetchResponse) fetchResponse.AddError("my_topic", 0, ErrOffsetOutOfRange) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetNewest, 1234). SetOffset("my_topic", 0, OffsetOldest, 7), "FetchRequest": NewMockWrapper(fetchResponse), }) master, err := NewConsumer([]string{broker0.Addr()}, nil) if err != nil { t.Fatal(err) } // When consumer, err := master.ConsumePartition("my_topic", 0, 101) if err != nil { t.Fatal(err) } // Then: consumer should shut down closing its messages and errors channels. if _, ok := <-consumer.Messages(); ok { t.Error("Expected the consumer to shut down") } safeClose(t, consumer) safeClose(t, master) broker0.Close() } // If a fetch response contains messages with offsets that are smaller then // requested, then such messages are ignored. func TestConsumerExtraOffsets(t *testing.T) { // Given broker0 := NewMockBroker(t, 0) fetchResponse1 := &FetchResponse{} fetchResponse1.AddMessage("my_topic", 0, nil, testMsg, 1) fetchResponse1.AddMessage("my_topic", 0, nil, testMsg, 2) fetchResponse1.AddMessage("my_topic", 0, nil, testMsg, 3) fetchResponse1.AddMessage("my_topic", 0, nil, testMsg, 4) fetchResponse2 := &FetchResponse{} fetchResponse2.AddError("my_topic", 0, ErrNoError) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetNewest, 1234). SetOffset("my_topic", 0, OffsetOldest, 0), "FetchRequest": NewMockSequence(fetchResponse1, fetchResponse2), }) master, err := NewConsumer([]string{broker0.Addr()}, nil) if err != nil { t.Fatal(err) } // When consumer, err := master.ConsumePartition("my_topic", 0, 3) if err != nil { t.Fatal(err) } // Then: messages with offsets 1 and 2 are not returned even though they // are present in the response. assertMessageOffset(t, <-consumer.Messages(), 3) assertMessageOffset(t, <-consumer.Messages(), 4) safeClose(t, consumer) safeClose(t, master) broker0.Close() } // It is fine if offsets of fetched messages are not sequential (although // strictly increasing!). func TestConsumerNonSequentialOffsets(t *testing.T) { // Given broker0 := NewMockBroker(t, 0) fetchResponse1 := &FetchResponse{} fetchResponse1.AddMessage("my_topic", 0, nil, testMsg, 5) fetchResponse1.AddMessage("my_topic", 0, nil, testMsg, 7) fetchResponse1.AddMessage("my_topic", 0, nil, testMsg, 11) fetchResponse2 := &FetchResponse{} fetchResponse2.AddError("my_topic", 0, ErrNoError) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetNewest, 1234). SetOffset("my_topic", 0, OffsetOldest, 0), "FetchRequest": NewMockSequence(fetchResponse1, fetchResponse2), }) master, err := NewConsumer([]string{broker0.Addr()}, nil) if err != nil { t.Fatal(err) } // When consumer, err := master.ConsumePartition("my_topic", 0, 3) if err != nil { t.Fatal(err) } // Then: messages with offsets 1 and 2 are not returned even though they // are present in the response. assertMessageOffset(t, <-consumer.Messages(), 5) assertMessageOffset(t, <-consumer.Messages(), 7) assertMessageOffset(t, <-consumer.Messages(), 11) safeClose(t, consumer) safeClose(t, master) broker0.Close() } // If leadership for a partition is changing then consumer resolves the new // leader and switches to it. func TestConsumerRebalancingMultiplePartitions(t *testing.T) { // initial setup seedBroker := NewMockBroker(t, 10) leader0 := NewMockBroker(t, 0) leader1 := NewMockBroker(t, 1) seedBroker.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(leader0.Addr(), leader0.BrokerID()). SetBroker(leader1.Addr(), leader1.BrokerID()). SetLeader("my_topic", 0, leader0.BrokerID()). SetLeader("my_topic", 1, leader1.BrokerID()), }) mockOffsetResponse1 := NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetOldest, 0). SetOffset("my_topic", 0, OffsetNewest, 1000). SetOffset("my_topic", 1, OffsetOldest, 0). SetOffset("my_topic", 1, OffsetNewest, 1000) leader0.SetHandlerByMap(map[string]MockResponse{ "OffsetRequest": mockOffsetResponse1, "FetchRequest": NewMockFetchResponse(t, 1), }) leader1.SetHandlerByMap(map[string]MockResponse{ "OffsetRequest": mockOffsetResponse1, "FetchRequest": NewMockFetchResponse(t, 1), }) // launch test goroutines config := NewConfig() config.Consumer.Retry.Backoff = 50 master, err := NewConsumer([]string{seedBroker.Addr()}, config) if err != nil { t.Fatal(err) } // we expect to end up (eventually) consuming exactly ten messages on each partition var wg sync.WaitGroup for i := int32(0); i < 2; i++ { consumer, err := master.ConsumePartition("my_topic", i, 0) if err != nil { t.Error(err) } go func(c PartitionConsumer) { for err := range c.Errors() { t.Error(err) } }(consumer) wg.Add(1) go func(partition int32, c PartitionConsumer) { for i := 0; i < 10; i++ { message := <-consumer.Messages() if message.Offset != int64(i) { t.Error("Incorrect message offset!", i, partition, message.Offset) } if message.Partition != partition { t.Error("Incorrect message partition!") } } safeClose(t, consumer) wg.Done() }(i, consumer) } time.Sleep(50 * time.Millisecond) Logger.Printf(" STAGE 1") // Stage 1: // * my_topic/0 -> leader0 serves 4 messages // * my_topic/1 -> leader1 serves 0 messages mockFetchResponse := NewMockFetchResponse(t, 1) for i := 0; i < 4; i++ { mockFetchResponse.SetMessage("my_topic", 0, int64(i), testMsg) } leader0.SetHandlerByMap(map[string]MockResponse{ "FetchRequest": mockFetchResponse, }) time.Sleep(50 * time.Millisecond) Logger.Printf(" STAGE 2") // Stage 2: // * leader0 says that it is no longer serving my_topic/0 // * seedBroker tells that leader1 is serving my_topic/0 now // seed broker tells that the new partition 0 leader is leader1 seedBroker.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetLeader("my_topic", 0, leader1.BrokerID()). SetLeader("my_topic", 1, leader1.BrokerID()), }) // leader0 says no longer leader of partition 0 fetchResponse := new(FetchResponse) fetchResponse.AddError("my_topic", 0, ErrNotLeaderForPartition) leader0.SetHandlerByMap(map[string]MockResponse{ "FetchRequest": NewMockWrapper(fetchResponse), }) time.Sleep(50 * time.Millisecond) Logger.Printf(" STAGE 3") // Stage 3: // * my_topic/0 -> leader1 serves 3 messages // * my_topic/1 -> leader1 server 8 messages // leader1 provides 3 message on partition 0, and 8 messages on partition 1 mockFetchResponse2 := NewMockFetchResponse(t, 2) for i := 4; i < 7; i++ { mockFetchResponse2.SetMessage("my_topic", 0, int64(i), testMsg) } for i := 0; i < 8; i++ { mockFetchResponse2.SetMessage("my_topic", 1, int64(i), testMsg) } leader1.SetHandlerByMap(map[string]MockResponse{ "FetchRequest": mockFetchResponse2, }) time.Sleep(50 * time.Millisecond) Logger.Printf(" STAGE 4") // Stage 4: // * my_topic/0 -> leader1 serves 3 messages // * my_topic/1 -> leader1 tells that it is no longer the leader // * seedBroker tells that leader0 is a new leader for my_topic/1 // metadata assigns 0 to leader1 and 1 to leader0 seedBroker.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetLeader("my_topic", 0, leader1.BrokerID()). SetLeader("my_topic", 1, leader0.BrokerID()), }) // leader1 provides three more messages on partition0, says no longer leader of partition1 mockFetchResponse3 := NewMockFetchResponse(t, 3). SetMessage("my_topic", 0, int64(7), testMsg). SetMessage("my_topic", 0, int64(8), testMsg). SetMessage("my_topic", 0, int64(9), testMsg) fetchResponse4 := new(FetchResponse) fetchResponse4.AddError("my_topic", 1, ErrNotLeaderForPartition) leader1.SetHandlerByMap(map[string]MockResponse{ "FetchRequest": NewMockSequence(mockFetchResponse3, fetchResponse4), }) // leader0 provides two messages on partition 1 mockFetchResponse4 := NewMockFetchResponse(t, 2) for i := 8; i < 10; i++ { mockFetchResponse4.SetMessage("my_topic", 1, int64(i), testMsg) } leader0.SetHandlerByMap(map[string]MockResponse{ "FetchRequest": mockFetchResponse4, }) wg.Wait() safeClose(t, master) leader1.Close() leader0.Close() seedBroker.Close() } // When two partitions have the same broker as the leader, if one partition // consumer channel buffer is full then that does not affect the ability to // read messages by the other consumer. func TestConsumerInterleavedClose(t *testing.T) { // Given broker0 := NewMockBroker(t, 0) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()). SetLeader("my_topic", 1, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetOldest, 1000). SetOffset("my_topic", 0, OffsetNewest, 1100). SetOffset("my_topic", 1, OffsetOldest, 2000). SetOffset("my_topic", 1, OffsetNewest, 2100), "FetchRequest": NewMockFetchResponse(t, 1). SetMessage("my_topic", 0, 1000, testMsg). SetMessage("my_topic", 0, 1001, testMsg). SetMessage("my_topic", 0, 1002, testMsg). SetMessage("my_topic", 1, 2000, testMsg), }) config := NewConfig() config.ChannelBufferSize = 0 master, err := NewConsumer([]string{broker0.Addr()}, config) if err != nil { t.Fatal(err) } c0, err := master.ConsumePartition("my_topic", 0, 1000) if err != nil { t.Fatal(err) } c1, err := master.ConsumePartition("my_topic", 1, 2000) if err != nil { t.Fatal(err) } // When/Then: we can read from partition 0 even if nobody reads from partition 1 assertMessageOffset(t, <-c0.Messages(), 1000) assertMessageOffset(t, <-c0.Messages(), 1001) assertMessageOffset(t, <-c0.Messages(), 1002) safeClose(t, c1) safeClose(t, c0) safeClose(t, master) broker0.Close() } func TestConsumerBounceWithReferenceOpen(t *testing.T) { broker0 := NewMockBroker(t, 0) broker0Addr := broker0.Addr() broker1 := NewMockBroker(t, 1) mockMetadataResponse := NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetBroker(broker1.Addr(), broker1.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()). SetLeader("my_topic", 1, broker1.BrokerID()) mockOffsetResponse := NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetOldest, 1000). SetOffset("my_topic", 0, OffsetNewest, 1100). SetOffset("my_topic", 1, OffsetOldest, 2000). SetOffset("my_topic", 1, OffsetNewest, 2100) mockFetchResponse := NewMockFetchResponse(t, 1) for i := 0; i < 10; i++ { mockFetchResponse.SetMessage("my_topic", 0, int64(1000+i), testMsg) mockFetchResponse.SetMessage("my_topic", 1, int64(2000+i), testMsg) } broker0.SetHandlerByMap(map[string]MockResponse{ "OffsetRequest": mockOffsetResponse, "FetchRequest": mockFetchResponse, }) broker1.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": mockMetadataResponse, "OffsetRequest": mockOffsetResponse, "FetchRequest": mockFetchResponse, }) config := NewConfig() config.Consumer.Return.Errors = true config.Consumer.Retry.Backoff = 100 * time.Millisecond config.ChannelBufferSize = 1 master, err := NewConsumer([]string{broker1.Addr()}, config) if err != nil { t.Fatal(err) } c0, err := master.ConsumePartition("my_topic", 0, 1000) if err != nil { t.Fatal(err) } c1, err := master.ConsumePartition("my_topic", 1, 2000) if err != nil { t.Fatal(err) } // read messages from both partition to make sure that both brokers operate // normally. assertMessageOffset(t, <-c0.Messages(), 1000) assertMessageOffset(t, <-c1.Messages(), 2000) // Simulate broker shutdown. Note that metadata response does not change, // that is the leadership does not move to another broker. So partition // consumer will keep retrying to restore the connection with the broker. broker0.Close() // Make sure that while the partition/0 leader is down, consumer/partition/1 // is capable of pulling messages from broker1. for i := 1; i < 7; i++ { offset := (<-c1.Messages()).Offset if offset != int64(2000+i) { t.Errorf("Expected offset %d from consumer/partition/1", int64(2000+i)) } } // Bring broker0 back to service. broker0 = NewMockBrokerAddr(t, 0, broker0Addr) broker0.SetHandlerByMap(map[string]MockResponse{ "FetchRequest": mockFetchResponse, }) // Read the rest of messages from both partitions. for i := 7; i < 10; i++ { assertMessageOffset(t, <-c1.Messages(), int64(2000+i)) } for i := 1; i < 10; i++ { assertMessageOffset(t, <-c0.Messages(), int64(1000+i)) } select { case <-c0.Errors(): default: t.Errorf("Partition consumer should have detected broker restart") } safeClose(t, c1) safeClose(t, c0) safeClose(t, master) broker0.Close() broker1.Close() } func TestConsumerOffsetOutOfRange(t *testing.T) { // Given broker0 := NewMockBroker(t, 2) broker0.SetHandlerByMap(map[string]MockResponse{ "MetadataRequest": NewMockMetadataResponse(t). SetBroker(broker0.Addr(), broker0.BrokerID()). SetLeader("my_topic", 0, broker0.BrokerID()), "OffsetRequest": NewMockOffsetResponse(t). SetOffset("my_topic", 0, OffsetNewest, 1234). SetOffset("my_topic", 0, OffsetOldest, 2345), }) master, err := NewConsumer([]string{broker0.Addr()}, nil) if err != nil { t.Fatal(err) } // When/Then if _, err := master.ConsumePartition("my_topic", 0, 0); err != ErrOffsetOutOfRange { t.Fatal("Should return ErrOffsetOutOfRange, got:", err) } if _, err := master.ConsumePartition("my_topic", 0, 3456); err != ErrOffsetOutOfRange { t.Fatal("Should return ErrOffsetOutOfRange, got:", err) } if _, err := master.ConsumePartition("my_topic", 0, -3); err != ErrOffsetOutOfRange { t.Fatal("Should return ErrOffsetOutOfRange, got:", err) } safeClose(t, master) broker0.Close() } func assertMessageOffset(t *testing.T, msg *ConsumerMessage, expectedOffset int64) { if msg.Offset != expectedOffset { t.Errorf("Incorrect message offset: expected=%d, actual=%d", expectedOffset, msg.Offset) } } // This example shows how to use the consumer to read messages // from a single partition. func ExampleConsumer() { consumer, err := NewConsumer([]string{"localhost:9092"}, nil) if err != nil { panic(err) } defer func() { if err := consumer.Close(); err != nil { log.Fatalln(err) } }() partitionConsumer, err := consumer.ConsumePartition("my_topic", 0, OffsetNewest) if err != nil { panic(err) } defer func() { if err := partitionConsumer.Close(); err != nil { log.Fatalln(err) } }() // Trap SIGINT to trigger a shutdown. signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt) consumed := 0 ConsumerLoop: for { select { case msg := <-partitionConsumer.Messages(): log.Printf("Consumed message offset %d\n", msg.Offset) consumed++ case <-signals: break ConsumerLoop } } log.Printf("Consumed: %d\n", consumed) } sarama-1.9.0/crc32_field.go 0000664 0000000 0000000 00000001407 12716410165 0015400 0 ustar 00root root 0000000 0000000 package sarama import ( "encoding/binary" "github.com/klauspost/crc32" ) // crc32Field implements the pushEncoder and pushDecoder interfaces for calculating CRC32s. type crc32Field struct { startOffset int } func (c *crc32Field) saveOffset(in int) { c.startOffset = in } func (c *crc32Field) reserveLength() int { return 4 } func (c *crc32Field) run(curOffset int, buf []byte) error { crc := crc32.ChecksumIEEE(buf[c.startOffset+4 : curOffset]) binary.BigEndian.PutUint32(buf[c.startOffset:], crc) return nil } func (c *crc32Field) check(curOffset int, buf []byte) error { crc := crc32.ChecksumIEEE(buf[c.startOffset+4 : curOffset]) if crc != binary.BigEndian.Uint32(buf[c.startOffset:]) { return PacketDecodingError{"CRC didn't match"} } return nil } sarama-1.9.0/describe_groups_request.go 0000664 0000000 0000000 00000001001 12716410165 0020236 0 ustar 00root root 0000000 0000000 package sarama type DescribeGroupsRequest struct { Groups []string } func (r *DescribeGroupsRequest) encode(pe packetEncoder) error { return pe.putStringArray(r.Groups) } func (r *DescribeGroupsRequest) decode(pd packetDecoder) (err error) { r.Groups, err = pd.getStringArray() return } func (r *DescribeGroupsRequest) key() int16 { return 15 } func (r *DescribeGroupsRequest) version() int16 { return 0 } func (r *DescribeGroupsRequest) AddGroup(group string) { r.Groups = append(r.Groups, group) } sarama-1.9.0/describe_groups_request_test.go 0000664 0000000 0000000 00000001501 12716410165 0021302 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( emptyDescribeGroupsRequest = []byte{0, 0, 0, 0} singleDescribeGroupsRequest = []byte{ 0, 0, 0, 1, // 1 group 0, 3, 'f', 'o', 'o', // group name: foo } doubleDescribeGroupsRequest = []byte{ 0, 0, 0, 2, // 2 groups 0, 3, 'f', 'o', 'o', // group name: foo 0, 3, 'b', 'a', 'r', // group name: foo } ) func TestDescribeGroupsRequest(t *testing.T) { var request *DescribeGroupsRequest request = new(DescribeGroupsRequest) testRequest(t, "no groups", request, emptyDescribeGroupsRequest) request = new(DescribeGroupsRequest) request.AddGroup("foo") testRequest(t, "one group", request, singleDescribeGroupsRequest) request = new(DescribeGroupsRequest) request.AddGroup("foo") request.AddGroup("bar") testRequest(t, "two groups", request, doubleDescribeGroupsRequest) } sarama-1.9.0/describe_groups_response.go 0000664 0000000 0000000 00000006230 12716410165 0020415 0 ustar 00root root 0000000 0000000 package sarama type DescribeGroupsResponse struct { Groups []*GroupDescription } func (r *DescribeGroupsResponse) encode(pe packetEncoder) error { if err := pe.putArrayLength(len(r.Groups)); err != nil { return err } for _, groupDescription := range r.Groups { if err := groupDescription.encode(pe); err != nil { return err } } return nil } func (r *DescribeGroupsResponse) decode(pd packetDecoder) (err error) { n, err := pd.getArrayLength() if err != nil { return err } r.Groups = make([]*GroupDescription, n) for i := 0; i < n; i++ { r.Groups[i] = new(GroupDescription) if err := r.Groups[i].decode(pd); err != nil { return err } } return nil } type GroupDescription struct { Err KError GroupId string State string ProtocolType string Protocol string Members map[string]*GroupMemberDescription } func (gd *GroupDescription) encode(pe packetEncoder) error { pe.putInt16(int16(gd.Err)) if err := pe.putString(gd.GroupId); err != nil { return err } if err := pe.putString(gd.State); err != nil { return err } if err := pe.putString(gd.ProtocolType); err != nil { return err } if err := pe.putString(gd.Protocol); err != nil { return err } if err := pe.putArrayLength(len(gd.Members)); err != nil { return err } for memberId, groupMemberDescription := range gd.Members { if err := pe.putString(memberId); err != nil { return err } if err := groupMemberDescription.encode(pe); err != nil { return err } } return nil } func (gd *GroupDescription) decode(pd packetDecoder) (err error) { if kerr, err := pd.getInt16(); err != nil { return err } else { gd.Err = KError(kerr) } if gd.GroupId, err = pd.getString(); err != nil { return } if gd.State, err = pd.getString(); err != nil { return } if gd.ProtocolType, err = pd.getString(); err != nil { return } if gd.Protocol, err = pd.getString(); err != nil { return } n, err := pd.getArrayLength() if err != nil { return err } if n == 0 { return nil } gd.Members = make(map[string]*GroupMemberDescription) for i := 0; i < n; i++ { memberId, err := pd.getString() if err != nil { return err } gd.Members[memberId] = new(GroupMemberDescription) if err := gd.Members[memberId].decode(pd); err != nil { return err } } return nil } type GroupMemberDescription struct { ClientId string ClientHost string MemberMetadata []byte MemberAssignment []byte } func (gmd *GroupMemberDescription) encode(pe packetEncoder) error { if err := pe.putString(gmd.ClientId); err != nil { return err } if err := pe.putString(gmd.ClientHost); err != nil { return err } if err := pe.putBytes(gmd.MemberMetadata); err != nil { return err } if err := pe.putBytes(gmd.MemberAssignment); err != nil { return err } return nil } func (gmd *GroupMemberDescription) decode(pd packetDecoder) (err error) { if gmd.ClientId, err = pd.getString(); err != nil { return } if gmd.ClientHost, err = pd.getString(); err != nil { return } if gmd.MemberMetadata, err = pd.getBytes(); err != nil { return } if gmd.MemberAssignment, err = pd.getBytes(); err != nil { return } return nil } sarama-1.9.0/describe_groups_response_test.go 0000664 0000000 0000000 00000005313 12716410165 0021455 0 ustar 00root root 0000000 0000000 package sarama import ( "reflect" "testing" ) var ( describeGroupsResponseEmpty = []byte{ 0, 0, 0, 0, // no groups } describeGroupsResponsePopulated = []byte{ 0, 0, 0, 2, // 2 groups 0, 0, // no error 0, 3, 'f', 'o', 'o', // Group ID 0, 3, 'b', 'a', 'r', // State 0, 8, 'c', 'o', 'n', 's', 'u', 'm', 'e', 'r', // ConsumerProtocol type 0, 3, 'b', 'a', 'z', // Protocol name 0, 0, 0, 1, // 1 member 0, 2, 'i', 'd', // Member ID 0, 6, 's', 'a', 'r', 'a', 'm', 'a', // Client ID 0, 9, 'l', 'o', 'c', 'a', 'l', 'h', 'o', 's', 't', // Client Host 0, 0, 0, 3, 0x01, 0x02, 0x03, // MemberMetadata 0, 0, 0, 3, 0x04, 0x05, 0x06, // MemberAssignment 0, 30, // ErrGroupAuthorizationFailed 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, } ) func TestDescribeGroupsResponse(t *testing.T) { var response *DescribeGroupsResponse response = new(DescribeGroupsResponse) testDecodable(t, "empty", response, describeGroupsResponseEmpty) if len(response.Groups) != 0 { t.Error("Expected no groups") } response = new(DescribeGroupsResponse) testDecodable(t, "populated", response, describeGroupsResponsePopulated) if len(response.Groups) != 2 { t.Error("Expected two groups") } group0 := response.Groups[0] if group0.Err != ErrNoError { t.Error("Unxpected groups[0].Err, found", group0.Err) } if group0.GroupId != "foo" { t.Error("Unxpected groups[0].GroupId, found", group0.GroupId) } if group0.State != "bar" { t.Error("Unxpected groups[0].State, found", group0.State) } if group0.ProtocolType != "consumer" { t.Error("Unxpected groups[0].ProtocolType, found", group0.ProtocolType) } if group0.Protocol != "baz" { t.Error("Unxpected groups[0].Protocol, found", group0.Protocol) } if len(group0.Members) != 1 { t.Error("Unxpected groups[0].Members, found", group0.Members) } if group0.Members["id"].ClientId != "sarama" { t.Error("Unxpected groups[0].Members[id].ClientId, found", group0.Members["id"].ClientId) } if group0.Members["id"].ClientHost != "localhost" { t.Error("Unxpected groups[0].Members[id].ClientHost, found", group0.Members["id"].ClientHost) } if !reflect.DeepEqual(group0.Members["id"].MemberMetadata, []byte{0x01, 0x02, 0x03}) { t.Error("Unxpected groups[0].Members[id].MemberMetadata, found", group0.Members["id"].MemberMetadata) } if !reflect.DeepEqual(group0.Members["id"].MemberAssignment, []byte{0x04, 0x05, 0x06}) { t.Error("Unxpected groups[0].Members[id].MemberAssignment, found", group0.Members["id"].MemberAssignment) } group1 := response.Groups[1] if group1.Err != ErrGroupAuthorizationFailed { t.Error("Unxpected groups[1].Err, found", group0.Err) } if len(group1.Members) != 0 { t.Error("Unxpected groups[1].Members, found", group0.Members) } } sarama-1.9.0/dev.yml 0000664 0000000 0000000 00000000141 12716410165 0014265 0 ustar 00root root 0000000 0000000 name: sarama up: - go: 1.6.2 commands: test: run: make test desc: 'run unit tests' sarama-1.9.0/encoder_decoder.go 0000664 0000000 0000000 00000002571 12716410165 0016430 0 ustar 00root root 0000000 0000000 package sarama import "fmt" // Encoder is the interface that wraps the basic Encode method. // Anything implementing Encoder can be turned into bytes using Kafka's encoding rules. type encoder interface { encode(pe packetEncoder) error } // Encode takes an Encoder and turns it into bytes. func encode(e encoder) ([]byte, error) { if e == nil { return nil, nil } var prepEnc prepEncoder var realEnc realEncoder err := e.encode(&prepEnc) if err != nil { return nil, err } if prepEnc.length < 0 || prepEnc.length > int(MaxRequestSize) { return nil, PacketEncodingError{fmt.Sprintf("invalid request size (%d)", prepEnc.length)} } realEnc.raw = make([]byte, prepEnc.length) err = e.encode(&realEnc) if err != nil { return nil, err } return realEnc.raw, nil } // Decoder is the interface that wraps the basic Decode method. // Anything implementing Decoder can be extracted from bytes using Kafka's encoding rules. type decoder interface { decode(pd packetDecoder) error } // Decode takes bytes and a Decoder and fills the fields of the decoder from the bytes, // interpreted using Kafka's encoding rules. func decode(buf []byte, in decoder) error { if buf == nil { return nil } helper := realDecoder{raw: buf} err := in.decode(&helper) if err != nil { return err } if helper.off != len(buf) { return PacketDecodingError{"invalid length"} } return nil } sarama-1.9.0/errors.go 0000664 0000000 0000000 00000021737 12716410165 0014645 0 ustar 00root root 0000000 0000000 package sarama import ( "errors" "fmt" ) // ErrOutOfBrokers is the error returned when the client has run out of brokers to talk to because all of them errored // or otherwise failed to respond. var ErrOutOfBrokers = errors.New("kafka: client has run out of available brokers to talk to (Is your cluster reachable?)") // ErrClosedClient is the error returned when a method is called on a client that has been closed. var ErrClosedClient = errors.New("kafka: tried to use a client that was closed") // ErrIncompleteResponse is the error returned when the server returns a syntactically valid response, but it does // not contain the expected information. var ErrIncompleteResponse = errors.New("kafka: response did not contain all the expected topic/partition blocks") // ErrInvalidPartition is the error returned when a partitioner returns an invalid partition index // (meaning one outside of the range [0...numPartitions-1]). var ErrInvalidPartition = errors.New("kafka: partitioner returned an invalid partition index") // ErrAlreadyConnected is the error returned when calling Open() on a Broker that is already connected or connecting. var ErrAlreadyConnected = errors.New("kafka: broker connection already initiated") // ErrNotConnected is the error returned when trying to send or call Close() on a Broker that is not connected. var ErrNotConnected = errors.New("kafka: broker not connected") // ErrInsufficientData is returned when decoding and the packet is truncated. This can be expected // when requesting messages, since as an optimization the server is allowed to return a partial message at the end // of the message set. var ErrInsufficientData = errors.New("kafka: insufficient data to decode packet, more bytes expected") // ErrShuttingDown is returned when a producer receives a message during shutdown. var ErrShuttingDown = errors.New("kafka: message received by producer in process of shutting down") // ErrMessageTooLarge is returned when the next message to consume is larger than the configured Consumer.Fetch.Max var ErrMessageTooLarge = errors.New("kafka: message is larger than Consumer.Fetch.Max") // PacketEncodingError is returned from a failure while encoding a Kafka packet. This can happen, for example, // if you try to encode a string over 2^15 characters in length, since Kafka's encoding rules do not permit that. type PacketEncodingError struct { Info string } func (err PacketEncodingError) Error() string { return fmt.Sprintf("kafka: error encoding packet: %s", err.Info) } // PacketDecodingError is returned when there was an error (other than truncated data) decoding the Kafka broker's response. // This can be a bad CRC or length field, or any other invalid value. type PacketDecodingError struct { Info string } func (err PacketDecodingError) Error() string { return fmt.Sprintf("kafka: error decoding packet: %s", err.Info) } // ConfigurationError is the type of error returned from a constructor (e.g. NewClient, or NewConsumer) // when the specified configuration is invalid. type ConfigurationError string func (err ConfigurationError) Error() string { return "kafka: invalid configuration (" + string(err) + ")" } // KError is the type of error that can be returned directly by the Kafka broker. // See https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes type KError int16 // Numeric error codes returned by the Kafka server. const ( ErrNoError KError = 0 ErrUnknown KError = -1 ErrOffsetOutOfRange KError = 1 ErrInvalidMessage KError = 2 ErrUnknownTopicOrPartition KError = 3 ErrInvalidMessageSize KError = 4 ErrLeaderNotAvailable KError = 5 ErrNotLeaderForPartition KError = 6 ErrRequestTimedOut KError = 7 ErrBrokerNotAvailable KError = 8 ErrReplicaNotAvailable KError = 9 ErrMessageSizeTooLarge KError = 10 ErrStaleControllerEpochCode KError = 11 ErrOffsetMetadataTooLarge KError = 12 ErrOffsetsLoadInProgress KError = 14 ErrConsumerCoordinatorNotAvailable KError = 15 ErrNotCoordinatorForConsumer KError = 16 ErrInvalidTopic KError = 17 ErrMessageSetSizeTooLarge KError = 18 ErrNotEnoughReplicas KError = 19 ErrNotEnoughReplicasAfterAppend KError = 20 ErrInvalidRequiredAcks KError = 21 ErrIllegalGeneration KError = 22 ErrInconsistentGroupProtocol KError = 23 ErrInvalidGroupId KError = 24 ErrUnknownMemberId KError = 25 ErrInvalidSessionTimeout KError = 26 ErrRebalanceInProgress KError = 27 ErrInvalidCommitOffsetSize KError = 28 ErrTopicAuthorizationFailed KError = 29 ErrGroupAuthorizationFailed KError = 30 ErrClusterAuthorizationFailed KError = 31 ) func (err KError) Error() string { // Error messages stolen/adapted from // https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol switch err { case ErrNoError: return "kafka server: Not an error, why are you printing me?" case ErrUnknown: return "kafka server: Unexpected (unknown?) server error." case ErrOffsetOutOfRange: return "kafka server: The requested offset is outside the range of offsets maintained by the server for the given topic/partition." case ErrInvalidMessage: return "kafka server: Message contents does not match its CRC." case ErrUnknownTopicOrPartition: return "kafka server: Request was for a topic or partition that does not exist on this broker." case ErrInvalidMessageSize: return "kafka server: The message has a negative size." case ErrLeaderNotAvailable: return "kafka server: In the middle of a leadership election, there is currently no leader for this partition and hence it is unavailable for writes." case ErrNotLeaderForPartition: return "kafka server: Tried to send a message to a replica that is not the leader for some partition. Your metadata is out of date." case ErrRequestTimedOut: return "kafka server: Request exceeded the user-specified time limit in the request." case ErrBrokerNotAvailable: return "kafka server: Broker not available. Not a client facing error, we should never receive this!!!" case ErrReplicaNotAvailable: return "kafka server: Replica infomation not available, one or more brokers are down." case ErrMessageSizeTooLarge: return "kafka server: Message was too large, server rejected it to avoid allocation error." case ErrStaleControllerEpochCode: return "kafka server: StaleControllerEpochCode (internal error code for broker-to-broker communication)." case ErrOffsetMetadataTooLarge: return "kafka server: Specified a string larger than the configured maximum for offset metadata." case ErrOffsetsLoadInProgress: return "kafka server: The broker is still loading offsets after a leader change for that offset's topic partition." case ErrConsumerCoordinatorNotAvailable: return "kafka server: Offset's topic has not yet been created." case ErrNotCoordinatorForConsumer: return "kafka server: Request was for a consumer group that is not coordinated by this broker." case ErrInvalidTopic: return "kafka server: The request attempted to perform an operation on an invalid topic." case ErrMessageSetSizeTooLarge: return "kafka server: The request included message batch larger than the configured segment size on the server." case ErrNotEnoughReplicas: return "kafka server: Messages are rejected since there are fewer in-sync replicas than required." case ErrNotEnoughReplicasAfterAppend: return "kafka server: Messages are written to the log, but to fewer in-sync replicas than required." case ErrInvalidRequiredAcks: return "kafka server: The number of required acks is invalid (should be either -1, 0, or 1)." case ErrIllegalGeneration: return "kafka server: The provided generation id is not the current generation." case ErrInconsistentGroupProtocol: return "kafka server: The provider group protocol type is incompatible with the other members." case ErrInvalidGroupId: return "kafka server: The provided group id was empty." case ErrUnknownMemberId: return "kafka server: The provided member is not known in the current generation." case ErrInvalidSessionTimeout: return "kafka server: The provided session timeout is outside the allowed range." case ErrRebalanceInProgress: return "kafka server: A rebalance for the group is in progress. Please re-join the group." case ErrInvalidCommitOffsetSize: return "kafka server: The provided commit metadata was too large." case ErrTopicAuthorizationFailed: return "kafka server: The client is not authorized to access this topic." case ErrGroupAuthorizationFailed: return "kafka server: The client is not authorized to access this group." case ErrClusterAuthorizationFailed: return "kafka server: The client is not authorized to send this request type." } return fmt.Sprintf("Unknown error, how did this happen? Error code = %d", err) } sarama-1.9.0/examples/ 0000775 0000000 0000000 00000000000 12716410165 0014606 5 ustar 00root root 0000000 0000000 sarama-1.9.0/examples/README.md 0000664 0000000 0000000 00000001510 12716410165 0016062 0 ustar 00root root 0000000 0000000 # Sarama examples This folder contains example applications to demonstrate the use of Sarama. For code snippet examples on how to use the different types in Sarama, see [Sarams's API documentation on godoc.org](https://godoc.org/github.com/Shopify/sarama) In these examples, we use `github.com/Shopify/sarama` as import path. We do this to ensure all the examples are up to date with the latest changes in Sarama. For your own applications, you may want to use `gopkg.in/Shopify/sarama.v1` to lock into a stable API version. #### HTTP server [http_server](./http_server) is a simple HTTP server uses both the sync producer to produce data as part of the request handling cycle, as well as the async producer to maintain an access log. It also uses the [mocks subpackage](https://godoc.org/github.com/Shopify/sarama/mocks) to test both. sarama-1.9.0/examples/http_server/ 0000775 0000000 0000000 00000000000 12716410165 0017153 5 ustar 00root root 0000000 0000000 sarama-1.9.0/examples/http_server/.gitignore 0000664 0000000 0000000 00000000035 12716410165 0021141 0 ustar 00root root 0000000 0000000 http_server http_server.test sarama-1.9.0/examples/http_server/README.md 0000664 0000000 0000000 00000002055 12716410165 0020434 0 ustar 00root root 0000000 0000000 # HTTP server example This HTTP server example shows you how to use the AsyncProducer and SyncProducer, and how to test them using mocks. The server simply sends the data of the HTTP request's query string to Kafka, and send a 200 result if that succeeds. For every request, it will send an access log entry to Kafka as well in the background. If you need to know whether a message was successfully sent to the Kafka cluster before you can send your HTTP response, using the `SyncProducer` is probably the simplest way to achieve this. If you don't care, e.g. for the access log, using the `AsyncProducer` will let you fire and forget. You can send the HTTP response, while the message is being produced in the background. One important thing to note is that both the `SyncProducer` and `AsyncProducer` are **thread-safe**. Go's `http.Server` handles requests concurrently in different goroutines, but you can use a single producer safely. This will actually achieve efficiency gains as the producer will be able to batch messages from concurrent requests together. sarama-1.9.0/examples/http_server/http_server.go 0000664 0000000 0000000 00000015440 12716410165 0022053 0 ustar 00root root 0000000 0000000 package main import ( "github.com/Shopify/sarama" "crypto/tls" "crypto/x509" "encoding/json" "flag" "fmt" "io/ioutil" "log" "net/http" "os" "strings" "time" ) var ( addr = flag.String("addr", ":8080", "The address to bind to") brokers = flag.String("brokers", os.Getenv("KAFKA_PEERS"), "The Kafka brokers to connect to, as a comma separated list") verbose = flag.Bool("verbose", false, "Turn on Sarama logging") certFile = flag.String("certificate", "", "The optional certificate file for client authentication") keyFile = flag.String("key", "", "The optional key file for client authentication") caFile = flag.String("ca", "", "The optional certificate authority file for TLS client authentication") verifySsl = flag.Bool("verify", false, "Optional verify ssl certificates chain") ) func main() { flag.Parse() if *verbose { sarama.Logger = log.New(os.Stdout, "[sarama] ", log.LstdFlags) } if *brokers == "" { flag.PrintDefaults() os.Exit(1) } brokerList := strings.Split(*brokers, ",") log.Printf("Kafka brokers: %s", strings.Join(brokerList, ", ")) server := &Server{ DataCollector: newDataCollector(brokerList), AccessLogProducer: newAccessLogProducer(brokerList), } defer func() { if err := server.Close(); err != nil { log.Println("Failed to close server", err) } }() log.Fatal(server.Run(*addr)) } func createTlsConfiguration() (t *tls.Config) { if *certFile != "" && *keyFile != "" && *caFile != "" { cert, err := tls.LoadX509KeyPair(*certFile, *keyFile) if err != nil { log.Fatal(err) } caCert, err := ioutil.ReadFile(*caFile) if err != nil { log.Fatal(err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) t = &tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: caCertPool, InsecureSkipVerify: *verifySsl, } } // will be nil by default if nothing is provided return t } type Server struct { DataCollector sarama.SyncProducer AccessLogProducer sarama.AsyncProducer } func (s *Server) Close() error { if err := s.DataCollector.Close(); err != nil { log.Println("Failed to shut down data collector cleanly", err) } if err := s.AccessLogProducer.Close(); err != nil { log.Println("Failed to shut down access log producer cleanly", err) } return nil } func (s *Server) Handler() http.Handler { return s.withAccessLog(s.collectQueryStringData()) } func (s *Server) Run(addr string) error { httpServer := &http.Server{ Addr: addr, Handler: s.Handler(), } log.Printf("Listening for requests on %s...\n", addr) return httpServer.ListenAndServe() } func (s *Server) collectQueryStringData() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } // We are not setting a message key, which means that all messages will // be distributed randomly over the different partitions. partition, offset, err := s.DataCollector.SendMessage(&sarama.ProducerMessage{ Topic: "important", Value: sarama.StringEncoder(r.URL.RawQuery), }) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Failed to store your data:, %s", err) } else { // The tuple (topic, partition, offset) can be used as a unique identifier // for a message in a Kafka cluster. fmt.Fprintf(w, "Your data is stored with unique identifier important/%d/%d", partition, offset) } }) } type accessLogEntry struct { Method string `json:"method"` Host string `json:"host"` Path string `json:"path"` IP string `json:"ip"` ResponseTime float64 `json:"response_time"` encoded []byte err error } func (ale *accessLogEntry) ensureEncoded() { if ale.encoded == nil && ale.err == nil { ale.encoded, ale.err = json.Marshal(ale) } } func (ale *accessLogEntry) Length() int { ale.ensureEncoded() return len(ale.encoded) } func (ale *accessLogEntry) Encode() ([]byte, error) { ale.ensureEncoded() return ale.encoded, ale.err } func (s *Server) withAccessLog(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { started := time.Now() next.ServeHTTP(w, r) entry := &accessLogEntry{ Method: r.Method, Host: r.Host, Path: r.RequestURI, IP: r.RemoteAddr, ResponseTime: float64(time.Since(started)) / float64(time.Second), } // We will use the client's IP address as key. This will cause // all the access log entries of the same IP address to end up // on the same partition. s.AccessLogProducer.Input() <- &sarama.ProducerMessage{ Topic: "access_log", Key: sarama.StringEncoder(r.RemoteAddr), Value: entry, } }) } func newDataCollector(brokerList []string) sarama.SyncProducer { // For the data collector, we are looking for strong consistency semantics. // Because we don't change the flush settings, sarama will try to produce messages // as fast as possible to keep latency low. config := sarama.NewConfig() config.Producer.RequiredAcks = sarama.WaitForAll // Wait for all in-sync replicas to ack the message config.Producer.Retry.Max = 10 // Retry up to 10 times to produce the message tlsConfig := createTlsConfiguration() if tlsConfig != nil { config.Net.TLS.Config = tlsConfig config.Net.TLS.Enable = true } // On the broker side, you may want to change the following settings to get // stronger consistency guarantees: // - For your broker, set `unclean.leader.election.enable` to false // - For the topic, you could increase `min.insync.replicas`. producer, err := sarama.NewSyncProducer(brokerList, config) if err != nil { log.Fatalln("Failed to start Sarama producer:", err) } return producer } func newAccessLogProducer(brokerList []string) sarama.AsyncProducer { // For the access log, we are looking for AP semantics, with high throughput. // By creating batches of compressed messages, we reduce network I/O at a cost of more latency. config := sarama.NewConfig() tlsConfig := createTlsConfiguration() if tlsConfig != nil { config.Net.TLS.Enable = true config.Net.TLS.Config = tlsConfig } config.Producer.RequiredAcks = sarama.WaitForLocal // Only wait for the leader to ack config.Producer.Compression = sarama.CompressionSnappy // Compress messages config.Producer.Flush.Frequency = 500 * time.Millisecond // Flush batches every 500ms producer, err := sarama.NewAsyncProducer(brokerList, config) if err != nil { log.Fatalln("Failed to start Sarama producer:", err) } // We will just log to STDOUT if we're not able to produce messages. // Note: messages will only be returned here after all retry attempts are exhausted. go func() { for err := range producer.Errors() { log.Println("Failed to write access log entry:", err) } }() return producer } sarama-1.9.0/examples/http_server/http_server_test.go 0000664 0000000 0000000 00000005630 12716410165 0023112 0 ustar 00root root 0000000 0000000 package main import ( "io" "net/http" "net/http/httptest" "testing" "github.com/Shopify/sarama" "github.com/Shopify/sarama/mocks" ) // In normal operation, we expect one access log entry, // and one data collector entry. Let's assume both will succeed. // We should return a HTTP 200 status. func TestCollectSuccessfully(t *testing.T) { dataCollectorMock := mocks.NewSyncProducer(t, nil) dataCollectorMock.ExpectSendMessageAndSucceed() accessLogProducerMock := mocks.NewAsyncProducer(t, nil) accessLogProducerMock.ExpectInputAndSucceed() // Now, use dependency injection to use the mocks. s := &Server{ DataCollector: dataCollectorMock, AccessLogProducer: accessLogProducerMock, } // The Server's Close call is important; it will call Close on // the two mock producers, which will then validate whether all // expectations are resolved. defer safeClose(t, s) req, err := http.NewRequest("GET", "http://example.com/?data", nil) if err != nil { t.Fatal(err) } res := httptest.NewRecorder() s.Handler().ServeHTTP(res, req) if res.Code != 200 { t.Errorf("Expected HTTP status 200, found %d", res.Code) } if string(res.Body.Bytes()) != "Your data is stored with unique identifier important/0/1" { t.Error("Unexpected response body", res.Body) } } // Now, let's see if we handle the case of not being able to produce // to the data collector properly. In this case we should return a 500 status. func TestCollectionFailure(t *testing.T) { dataCollectorMock := mocks.NewSyncProducer(t, nil) dataCollectorMock.ExpectSendMessageAndFail(sarama.ErrRequestTimedOut) accessLogProducerMock := mocks.NewAsyncProducer(t, nil) accessLogProducerMock.ExpectInputAndSucceed() s := &Server{ DataCollector: dataCollectorMock, AccessLogProducer: accessLogProducerMock, } defer safeClose(t, s) req, err := http.NewRequest("GET", "http://example.com/?data", nil) if err != nil { t.Fatal(err) } res := httptest.NewRecorder() s.Handler().ServeHTTP(res, req) if res.Code != 500 { t.Errorf("Expected HTTP status 500, found %d", res.Code) } } // We don't expect any data collector calls because the path is wrong, // so we are not setting any expectations on the dataCollectorMock. It // will still generate an access log entry though. func TestWrongPath(t *testing.T) { dataCollectorMock := mocks.NewSyncProducer(t, nil) accessLogProducerMock := mocks.NewAsyncProducer(t, nil) accessLogProducerMock.ExpectInputAndSucceed() s := &Server{ DataCollector: dataCollectorMock, AccessLogProducer: accessLogProducerMock, } defer safeClose(t, s) req, err := http.NewRequest("GET", "http://example.com/wrong?data", nil) if err != nil { t.Fatal(err) } res := httptest.NewRecorder() s.Handler().ServeHTTP(res, req) if res.Code != 404 { t.Errorf("Expected HTTP status 404, found %d", res.Code) } } func safeClose(t *testing.T, o io.Closer) { if err := o.Close(); err != nil { t.Error(err) } } sarama-1.9.0/fetch_request.go 0000664 0000000 0000000 00000005027 12716410165 0016164 0 ustar 00root root 0000000 0000000 package sarama type fetchRequestBlock struct { fetchOffset int64 maxBytes int32 } func (f *fetchRequestBlock) encode(pe packetEncoder) error { pe.putInt64(f.fetchOffset) pe.putInt32(f.maxBytes) return nil } func (f *fetchRequestBlock) decode(pd packetDecoder) (err error) { if f.fetchOffset, err = pd.getInt64(); err != nil { return err } if f.maxBytes, err = pd.getInt32(); err != nil { return err } return nil } type FetchRequest struct { MaxWaitTime int32 MinBytes int32 blocks map[string]map[int32]*fetchRequestBlock } func (f *FetchRequest) encode(pe packetEncoder) (err error) { pe.putInt32(-1) // replica ID is always -1 for clients pe.putInt32(f.MaxWaitTime) pe.putInt32(f.MinBytes) err = pe.putArrayLength(len(f.blocks)) if err != nil { return err } for topic, blocks := range f.blocks { err = pe.putString(topic) if err != nil { return err } err = pe.putArrayLength(len(blocks)) if err != nil { return err } for partition, block := range blocks { pe.putInt32(partition) err = block.encode(pe) if err != nil { return err } } } return nil } func (f *FetchRequest) decode(pd packetDecoder) (err error) { if _, err = pd.getInt32(); err != nil { return err } if f.MaxWaitTime, err = pd.getInt32(); err != nil { return err } if f.MinBytes, err = pd.getInt32(); err != nil { return err } topicCount, err := pd.getArrayLength() if err != nil { return err } if topicCount == 0 { return nil } f.blocks = make(map[string]map[int32]*fetchRequestBlock) for i := 0; i < topicCount; i++ { topic, err := pd.getString() if err != nil { return err } partitionCount, err := pd.getArrayLength() if err != nil { return err } f.blocks[topic] = make(map[int32]*fetchRequestBlock) for j := 0; j < partitionCount; j++ { partition, err := pd.getInt32() if err != nil { return err } fetchBlock := &fetchRequestBlock{} if err = fetchBlock.decode(pd); err != nil { return nil } f.blocks[topic][partition] = fetchBlock } } return nil } func (f *FetchRequest) key() int16 { return 1 } func (f *FetchRequest) version() int16 { return 0 } func (f *FetchRequest) AddBlock(topic string, partitionID int32, fetchOffset int64, maxBytes int32) { if f.blocks == nil { f.blocks = make(map[string]map[int32]*fetchRequestBlock) } if f.blocks[topic] == nil { f.blocks[topic] = make(map[int32]*fetchRequestBlock) } tmp := new(fetchRequestBlock) tmp.maxBytes = maxBytes tmp.fetchOffset = fetchOffset f.blocks[topic][partitionID] = tmp } sarama-1.9.0/fetch_request_test.go 0000664 0000000 0000000 00000001773 12716410165 0017227 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( fetchRequestNoBlocks = []byte{ 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} fetchRequestWithProperties = []byte{ 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xEF, 0x00, 0x00, 0x00, 0x00} fetchRequestOneBlock = []byte{ 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 't', 'o', 'p', 'i', 'c', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x56} ) func TestFetchRequest(t *testing.T) { request := new(FetchRequest) testRequest(t, "no blocks", request, fetchRequestNoBlocks) request.MaxWaitTime = 0x20 request.MinBytes = 0xEF testRequest(t, "with properties", request, fetchRequestWithProperties) request.MaxWaitTime = 0 request.MinBytes = 0 request.AddBlock("topic", 0x12, 0x34, 0x56) testRequest(t, "one block", request, fetchRequestOneBlock) } sarama-1.9.0/fetch_response.go 0000664 0000000 0000000 00000006547 12716410165 0016342 0 ustar 00root root 0000000 0000000 package sarama type FetchResponseBlock struct { Err KError HighWaterMarkOffset int64 MsgSet MessageSet } func (pr *FetchResponseBlock) decode(pd packetDecoder) (err error) { tmp, err := pd.getInt16() if err != nil { return err } pr.Err = KError(tmp) pr.HighWaterMarkOffset, err = pd.getInt64() if err != nil { return err } msgSetSize, err := pd.getInt32() if err != nil { return err } msgSetDecoder, err := pd.getSubset(int(msgSetSize)) if err != nil { return err } err = (&pr.MsgSet).decode(msgSetDecoder) return err } type FetchResponse struct { Blocks map[string]map[int32]*FetchResponseBlock } func (pr *FetchResponseBlock) encode(pe packetEncoder) (err error) { pe.putInt16(int16(pr.Err)) pe.putInt64(pr.HighWaterMarkOffset) pe.push(&lengthField{}) err = pr.MsgSet.encode(pe) if err != nil { return err } return pe.pop() } func (fr *FetchResponse) decode(pd packetDecoder) (err error) { numTopics, err := pd.getArrayLength() if err != nil { return err } fr.Blocks = make(map[string]map[int32]*FetchResponseBlock, numTopics) for i := 0; i < numTopics; i++ { name, err := pd.getString() if err != nil { return err } numBlocks, err := pd.getArrayLength() if err != nil { return err } fr.Blocks[name] = make(map[int32]*FetchResponseBlock, numBlocks) for j := 0; j < numBlocks; j++ { id, err := pd.getInt32() if err != nil { return err } block := new(FetchResponseBlock) err = block.decode(pd) if err != nil { return err } fr.Blocks[name][id] = block } } return nil } func (fr *FetchResponse) encode(pe packetEncoder) (err error) { err = pe.putArrayLength(len(fr.Blocks)) if err != nil { return err } for topic, partitions := range fr.Blocks { err = pe.putString(topic) if err != nil { return err } err = pe.putArrayLength(len(partitions)) if err != nil { return err } for id, block := range partitions { pe.putInt32(id) err = block.encode(pe) if err != nil { return err } } } return nil } func (fr *FetchResponse) GetBlock(topic string, partition int32) *FetchResponseBlock { if fr.Blocks == nil { return nil } if fr.Blocks[topic] == nil { return nil } return fr.Blocks[topic][partition] } func (fr *FetchResponse) AddError(topic string, partition int32, err KError) { if fr.Blocks == nil { fr.Blocks = make(map[string]map[int32]*FetchResponseBlock) } partitions, ok := fr.Blocks[topic] if !ok { partitions = make(map[int32]*FetchResponseBlock) fr.Blocks[topic] = partitions } frb, ok := partitions[partition] if !ok { frb = new(FetchResponseBlock) partitions[partition] = frb } frb.Err = err } func (fr *FetchResponse) AddMessage(topic string, partition int32, key, value Encoder, offset int64) { if fr.Blocks == nil { fr.Blocks = make(map[string]map[int32]*FetchResponseBlock) } partitions, ok := fr.Blocks[topic] if !ok { partitions = make(map[int32]*FetchResponseBlock) fr.Blocks[topic] = partitions } frb, ok := partitions[partition] if !ok { frb = new(FetchResponseBlock) partitions[partition] = frb } var kb []byte var vb []byte if key != nil { kb, _ = key.Encode() } if value != nil { vb, _ = value.Encode() } msg := &Message{Key: kb, Value: vb} msgBlock := &MessageBlock{Msg: msg, Offset: offset} frb.MsgSet.Messages = append(frb.MsgSet.Messages, msgBlock) } sarama-1.9.0/fetch_response_test.go 0000664 0000000 0000000 00000004216 12716410165 0017370 0 ustar 00root root 0000000 0000000 package sarama import ( "bytes" "testing" ) var ( emptyFetchResponse = []byte{ 0x00, 0x00, 0x00, 0x00} oneMessageFetchResponse = []byte{ 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 't', 'o', 'p', 'i', 'c', 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x1C, // messageSet 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // message 0x23, 0x96, 0x4a, 0xf7, // CRC 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0xEE} ) func TestEmptyFetchResponse(t *testing.T) { response := FetchResponse{} testDecodable(t, "empty", &response, emptyFetchResponse) if len(response.Blocks) != 0 { t.Error("Decoding produced topic blocks where there were none.") } } func TestOneMessageFetchResponse(t *testing.T) { response := FetchResponse{} testDecodable(t, "one message", &response, oneMessageFetchResponse) if len(response.Blocks) != 1 { t.Fatal("Decoding produced incorrect number of topic blocks.") } if len(response.Blocks["topic"]) != 1 { t.Fatal("Decoding produced incorrect number of partition blocks for topic.") } block := response.GetBlock("topic", 5) if block == nil { t.Fatal("GetBlock didn't return block.") } if block.Err != ErrOffsetOutOfRange { t.Error("Decoding didn't produce correct error code.") } if block.HighWaterMarkOffset != 0x10101010 { t.Error("Decoding didn't produce correct high water mark offset.") } if block.MsgSet.PartialTrailingMessage { t.Error("Decoding detected a partial trailing message where there wasn't one.") } if len(block.MsgSet.Messages) != 1 { t.Fatal("Decoding produced incorrect number of messages.") } msgBlock := block.MsgSet.Messages[0] if msgBlock.Offset != 0x550000 { t.Error("Decoding produced incorrect message offset.") } msg := msgBlock.Msg if msg.Codec != CompressionNone { t.Error("Decoding produced incorrect message compression.") } if msg.Key != nil { t.Error("Decoding produced message key where there was none.") } if !bytes.Equal(msg.Value, []byte{0x00, 0xEE}) { t.Error("Decoding produced incorrect message value.") } } sarama-1.9.0/functional_client_test.go 0000664 0000000 0000000 00000004152 12716410165 0020060 0 ustar 00root root 0000000 0000000 package sarama import ( "fmt" "testing" "time" ) func TestFuncConnectionFailure(t *testing.T) { setupFunctionalTest(t) defer teardownFunctionalTest(t) Proxies["kafka1"].Enabled = false SaveProxy(t, "kafka1") config := NewConfig() config.Metadata.Retry.Max = 1 _, err := NewClient([]string{kafkaBrokers[0]}, config) if err != ErrOutOfBrokers { t.Fatal("Expected returned error to be ErrOutOfBrokers, but was: ", err) } } func TestFuncClientMetadata(t *testing.T) { setupFunctionalTest(t) defer teardownFunctionalTest(t) config := NewConfig() config.Metadata.Retry.Max = 1 config.Metadata.Retry.Backoff = 10 * time.Millisecond client, err := NewClient(kafkaBrokers, config) if err != nil { t.Fatal(err) } if err := client.RefreshMetadata("unknown_topic"); err != ErrUnknownTopicOrPartition { t.Error("Expected ErrUnknownTopicOrPartition, got", err) } if _, err := client.Leader("unknown_topic", 0); err != ErrUnknownTopicOrPartition { t.Error("Expected ErrUnknownTopicOrPartition, got", err) } if _, err := client.Replicas("invalid/topic", 0); err != ErrUnknownTopicOrPartition { t.Error("Expected ErrUnknownTopicOrPartition, got", err) } partitions, err := client.Partitions("test.4") if err != nil { t.Error(err) } if len(partitions) != 4 { t.Errorf("Expected test.4 topic to have 4 partitions, found %v", partitions) } partitions, err = client.Partitions("test.1") if err != nil { t.Error(err) } if len(partitions) != 1 { t.Errorf("Expected test.1 topic to have 1 partitions, found %v", partitions) } safeClose(t, client) } func TestFuncClientCoordinator(t *testing.T) { checkKafkaVersion(t, "0.8.2") setupFunctionalTest(t) defer teardownFunctionalTest(t) client, err := NewClient(kafkaBrokers, nil) if err != nil { t.Fatal(err) } for i := 0; i < 10; i++ { broker, err := client.Coordinator(fmt.Sprintf("another_new_consumer_group_%d", i)) if err != nil { t.Error(err) } if connected, err := broker.Connected(); !connected || err != nil { t.Errorf("Expected to coordinator %s broker to be properly connected.", broker.Addr()) } } safeClose(t, client) } sarama-1.9.0/functional_consumer_test.go 0000664 0000000 0000000 00000002445 12716410165 0020440 0 ustar 00root root 0000000 0000000 package sarama import ( "math" "testing" ) func TestFuncConsumerOffsetOutOfRange(t *testing.T) { setupFunctionalTest(t) defer teardownFunctionalTest(t) consumer, err := NewConsumer(kafkaBrokers, nil) if err != nil { t.Fatal(err) } if _, err := consumer.ConsumePartition("test.1", 0, -10); err != ErrOffsetOutOfRange { t.Error("Expected ErrOffsetOutOfRange, got:", err) } if _, err := consumer.ConsumePartition("test.1", 0, math.MaxInt64); err != ErrOffsetOutOfRange { t.Error("Expected ErrOffsetOutOfRange, got:", err) } safeClose(t, consumer) } func TestConsumerHighWaterMarkOffset(t *testing.T) { setupFunctionalTest(t) defer teardownFunctionalTest(t) p, err := NewSyncProducer(kafkaBrokers, nil) if err != nil { t.Fatal(err) } defer safeClose(t, p) _, offset, err := p.SendMessage(&ProducerMessage{Topic: "test.1", Value: StringEncoder("Test")}) if err != nil { t.Fatal(err) } c, err := NewConsumer(kafkaBrokers, nil) if err != nil { t.Fatal(err) } defer safeClose(t, c) pc, err := c.ConsumePartition("test.1", 0, OffsetOldest) if err != nil { t.Fatal(err) } <-pc.Messages() if hwmo := pc.HighWaterMarkOffset(); hwmo != offset+1 { t.Logf("Last produced offset %d; high water mark should be one higher but found %d.", offset, hwmo) } safeClose(t, pc) } sarama-1.9.0/functional_offset_manager_test.go 0000664 0000000 0000000 00000001654 12716410165 0021566 0 ustar 00root root 0000000 0000000 package sarama import ( "testing" ) func TestFuncOffsetManager(t *testing.T) { checkKafkaVersion(t, "0.8.2") setupFunctionalTest(t) defer teardownFunctionalTest(t) client, err := NewClient(kafkaBrokers, nil) if err != nil { t.Fatal(err) } offsetManager, err := NewOffsetManagerFromClient("sarama.TestFuncOffsetManager", client) if err != nil { t.Fatal(err) } pom1, err := offsetManager.ManagePartition("test.1", 0) if err != nil { t.Fatal(err) } pom1.MarkOffset(10, "test metadata") safeClose(t, pom1) pom2, err := offsetManager.ManagePartition("test.1", 0) if err != nil { t.Fatal(err) } offset, metadata := pom2.NextOffset() if offset != 10+1 { t.Errorf("Expected the next offset to be 11, found %d.", offset) } if metadata != "test metadata" { t.Errorf("Expected metadata to be 'test metadata', found %s.", metadata) } safeClose(t, pom2) safeClose(t, offsetManager) safeClose(t, client) } sarama-1.9.0/functional_producer_test.go 0000664 0000000 0000000 00000011526 12716410165 0020430 0 ustar 00root root 0000000 0000000 package sarama import ( "fmt" "sync" "testing" "time" ) const TestBatchSize = 1000 func TestFuncProducing(t *testing.T) { config := NewConfig() testProducingMessages(t, config) } func TestFuncProducingGzip(t *testing.T) { config := NewConfig() config.Producer.Compression = CompressionGZIP testProducingMessages(t, config) } func TestFuncProducingSnappy(t *testing.T) { config := NewConfig() config.Producer.Compression = CompressionSnappy testProducingMessages(t, config) } func TestFuncProducingNoResponse(t *testing.T) { config := NewConfig() config.Producer.RequiredAcks = NoResponse testProducingMessages(t, config) } func TestFuncProducingFlushing(t *testing.T) { config := NewConfig() config.Producer.Flush.Messages = TestBatchSize / 8 config.Producer.Flush.Frequency = 250 * time.Millisecond testProducingMessages(t, config) } func TestFuncMultiPartitionProduce(t *testing.T) { setupFunctionalTest(t) defer teardownFunctionalTest(t) config := NewConfig() config.ChannelBufferSize = 20 config.Producer.Flush.Frequency = 50 * time.Millisecond config.Producer.Flush.Messages = 200 config.Producer.Return.Successes = true producer, err := NewSyncProducer(kafkaBrokers, config) if err != nil { t.Fatal(err) } var wg sync.WaitGroup wg.Add(TestBatchSize) for i := 1; i <= TestBatchSize; i++ { go func(i int) { defer wg.Done() msg := &ProducerMessage{Topic: "test.64", Key: nil, Value: StringEncoder(fmt.Sprintf("hur %d", i))} if _, _, err := producer.SendMessage(msg); err != nil { t.Error(i, err) } }(i) } wg.Wait() if err := producer.Close(); err != nil { t.Error(err) } } func TestFuncProducingToInvalidTopic(t *testing.T) { setupFunctionalTest(t) defer teardownFunctionalTest(t) producer, err := NewSyncProducer(kafkaBrokers, nil) if err != nil { t.Fatal(err) } if _, _, err := producer.SendMessage(&ProducerMessage{Topic: "in/valid"}); err != ErrUnknownTopicOrPartition { t.Error("Expected ErrUnknownTopicOrPartition, found", err) } if _, _, err := producer.SendMessage(&ProducerMessage{Topic: "in/valid"}); err != ErrUnknownTopicOrPartition { t.Error("Expected ErrUnknownTopicOrPartition, found", err) } safeClose(t, producer) } func testProducingMessages(t *testing.T, config *Config) { setupFunctionalTest(t) defer teardownFunctionalTest(t) config.Producer.Return.Successes = true config.Consumer.Return.Errors = true client, err := NewClient(kafkaBrokers, config) if err != nil { t.Fatal(err) } master, err := NewConsumerFromClient(client) if err != nil { t.Fatal(err) } consumer, err := master.ConsumePartition("test.1", 0, OffsetNewest) if err != nil { t.Fatal(err) } producer, err := NewAsyncProducerFromClient(client) if err != nil { t.Fatal(err) } expectedResponses := TestBatchSize for i := 1; i <= TestBatchSize; { msg := &ProducerMessage{Topic: "test.1", Key: nil, Value: StringEncoder(fmt.Sprintf("testing %d", i))} select { case producer.Input() <- msg: i++ case ret := <-producer.Errors(): t.Fatal(ret.Err) case <-producer.Successes(): expectedResponses-- } } for expectedResponses > 0 { select { case ret := <-producer.Errors(): t.Fatal(ret.Err) case <-producer.Successes(): expectedResponses-- } } safeClose(t, producer) for i := 1; i <= TestBatchSize; i++ { select { case <-time.After(10 * time.Second): t.Fatal("Not received any more events in the last 10 seconds.") case err := <-consumer.Errors(): t.Error(err) case message := <-consumer.Messages(): if string(message.Value) != fmt.Sprintf("testing %d", i) { t.Fatalf("Unexpected message with index %d: %s", i, message.Value) } } } safeClose(t, consumer) safeClose(t, client) } // Benchmarks func BenchmarkProducerSmall(b *testing.B) { benchmarkProducer(b, nil, "test.64", ByteEncoder(make([]byte, 128))) } func BenchmarkProducerMedium(b *testing.B) { benchmarkProducer(b, nil, "test.64", ByteEncoder(make([]byte, 1024))) } func BenchmarkProducerLarge(b *testing.B) { benchmarkProducer(b, nil, "test.64", ByteEncoder(make([]byte, 8192))) } func BenchmarkProducerSmallSinglePartition(b *testing.B) { benchmarkProducer(b, nil, "test.1", ByteEncoder(make([]byte, 128))) } func BenchmarkProducerMediumSnappy(b *testing.B) { conf := NewConfig() conf.Producer.Compression = CompressionSnappy benchmarkProducer(b, conf, "test.1", ByteEncoder(make([]byte, 1024))) } func benchmarkProducer(b *testing.B, conf *Config, topic string, value Encoder) { setupFunctionalTest(b) defer teardownFunctionalTest(b) producer, err := NewAsyncProducer(kafkaBrokers, conf) if err != nil { b.Fatal(err) } b.ResetTimer() for i := 1; i <= b.N; { msg := &ProducerMessage{Topic: topic, Key: StringEncoder(fmt.Sprintf("%d", i)), Value: value} select { case producer.Input() <- msg: i++ case ret := <-producer.Errors(): b.Fatal(ret.Err) } } safeClose(b, producer) } sarama-1.9.0/functional_test.go 0000664 0000000 0000000 00000006652 12716410165 0016531 0 ustar 00root root 0000000 0000000 package sarama import ( "log" "math/rand" "net" "os" "strconv" "strings" "testing" "time" toxiproxy "github.com/Shopify/toxiproxy/client" ) const ( VagrantToxiproxy = "http://192.168.100.67:8474" VagrantKafkaPeers = "192.168.100.67:9091,192.168.100.67:9092,192.168.100.67:9093,192.168.100.67:9094,192.168.100.67:9095" VagrantZookeeperPeers = "192.168.100.67:2181,192.168.100.67:2182,192.168.100.67:2183,192.168.100.67:2184,192.168.100.67:2185" ) var ( kafkaAvailable, kafkaRequired bool kafkaBrokers []string proxyClient *toxiproxy.Client Proxies map[string]*toxiproxy.Proxy ZKProxies = []string{"zk1", "zk2", "zk3", "zk4", "zk5"} KafkaProxies = []string{"kafka1", "kafka2", "kafka3", "kafka4", "kafka5"} ) func init() { if os.Getenv("DEBUG") == "true" { Logger = log.New(os.Stdout, "[sarama] ", log.LstdFlags) } seed := time.Now().UTC().UnixNano() if tmp := os.Getenv("TEST_SEED"); tmp != "" { seed, _ = strconv.ParseInt(tmp, 0, 64) } Logger.Println("Using random seed:", seed) rand.Seed(seed) proxyAddr := os.Getenv("TOXIPROXY_ADDR") if proxyAddr == "" { proxyAddr = VagrantToxiproxy } proxyClient = toxiproxy.NewClient(proxyAddr) kafkaPeers := os.Getenv("KAFKA_PEERS") if kafkaPeers == "" { kafkaPeers = VagrantKafkaPeers } kafkaBrokers = strings.Split(kafkaPeers, ",") if c, err := net.DialTimeout("tcp", kafkaBrokers[0], 5*time.Second); err == nil { if err = c.Close(); err == nil { kafkaAvailable = true } } kafkaRequired = os.Getenv("CI") != "" } func checkKafkaAvailability(t testing.TB) { if !kafkaAvailable { if kafkaRequired { t.Fatalf("Kafka broker is not available on %s. Set KAFKA_PEERS to connect to Kafka on a different location.", kafkaBrokers[0]) } else { t.Skipf("Kafka broker is not available on %s. Set KAFKA_PEERS to connect to Kafka on a different location.", kafkaBrokers[0]) } } } func checkKafkaVersion(t testing.TB, requiredVersion string) { kafkaVersion := os.Getenv("KAFKA_VERSION") if kafkaVersion == "" { t.Logf("No KAFKA_VERSION set. This test requires Kafka version %s or higher. Continuing...", requiredVersion) } else { available := parseKafkaVersion(kafkaVersion) required := parseKafkaVersion(requiredVersion) if !available.satisfies(required) { t.Skipf("Kafka version %s is required for this test; you have %s. Skipping...", requiredVersion, kafkaVersion) } } } func resetProxies(t testing.TB) { if err := proxyClient.ResetState(); err != nil { t.Error(err) } Proxies = nil } func fetchProxies(t testing.TB) { var err error Proxies, err = proxyClient.Proxies() if err != nil { t.Fatal(err) } } func SaveProxy(t *testing.T, px string) { if err := Proxies[px].Save(); err != nil { t.Fatal(err) } } func setupFunctionalTest(t testing.TB) { checkKafkaAvailability(t) resetProxies(t) fetchProxies(t) } func teardownFunctionalTest(t testing.TB) { resetProxies(t) } type kafkaVersion []int func (kv kafkaVersion) satisfies(other kafkaVersion) bool { var ov int for index, v := range kv { if len(other) <= index { ov = 0 } else { ov = other[index] } if v < ov { return false } else if v > ov { return true } } return true } func parseKafkaVersion(version string) kafkaVersion { numbers := strings.Split(version, ".") result := make(kafkaVersion, 0, len(numbers)) for _, number := range numbers { nr, _ := strconv.Atoi(number) result = append(result, nr) } return result } sarama-1.9.0/heartbeat_request.go 0000664 0000000 0000000 00000001337 12716410165 0017032 0 ustar 00root root 0000000 0000000 package sarama type HeartbeatRequest struct { GroupId string GenerationId int32 MemberId string } func (r *HeartbeatRequest) encode(pe packetEncoder) error { if err := pe.putString(r.GroupId); err != nil { return err } pe.putInt32(r.GenerationId) if err := pe.putString(r.MemberId); err != nil { return err } return nil } func (r *HeartbeatRequest) decode(pd packetDecoder) (err error) { if r.GroupId, err = pd.getString(); err != nil { return } if r.GenerationId, err = pd.getInt32(); err != nil { return } if r.MemberId, err = pd.getString(); err != nil { return } return nil } func (r *HeartbeatRequest) key() int16 { return 12 } func (r *HeartbeatRequest) version() int16 { return 0 } sarama-1.9.0/heartbeat_request_test.go 0000664 0000000 0000000 00000000671 12716410165 0020071 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( basicHeartbeatRequest = []byte{ 0, 3, 'f', 'o', 'o', // Group ID 0x00, 0x01, 0x02, 0x03, // Generatiuon ID 0, 3, 'b', 'a', 'z', // Member ID } ) func TestHeartbeatRequest(t *testing.T) { var request *HeartbeatRequest request = new(HeartbeatRequest) request.GroupId = "foo" request.GenerationId = 66051 request.MemberId = "baz" testRequest(t, "basic", request, basicHeartbeatRequest) } sarama-1.9.0/heartbeat_response.go 0000664 0000000 0000000 00000000520 12716410165 0017171 0 ustar 00root root 0000000 0000000 package sarama type HeartbeatResponse struct { Err KError } func (r *HeartbeatResponse) encode(pe packetEncoder) error { pe.putInt16(int16(r.Err)) return nil } func (r *HeartbeatResponse) decode(pd packetDecoder) error { if kerr, err := pd.getInt16(); err != nil { return err } else { r.Err = KError(kerr) } return nil } sarama-1.9.0/heartbeat_response_test.go 0000664 0000000 0000000 00000000603 12716410165 0020232 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( heartbeatResponseNoError = []byte{ 0x00, 0x00} ) func TestHeartbeatResponse(t *testing.T) { var response *HeartbeatResponse response = new(HeartbeatResponse) testDecodable(t, "no error", response, heartbeatResponseNoError) if response.Err != ErrNoError { t.Error("Decoding error failed: no error expected but found", response.Err) } } sarama-1.9.0/join_group_request.go 0000664 0000000 0000000 00000003646 12716410165 0017253 0 ustar 00root root 0000000 0000000 package sarama type JoinGroupRequest struct { GroupId string SessionTimeout int32 MemberId string ProtocolType string GroupProtocols map[string][]byte } func (r *JoinGroupRequest) encode(pe packetEncoder) error { if err := pe.putString(r.GroupId); err != nil { return err } pe.putInt32(r.SessionTimeout) if err := pe.putString(r.MemberId); err != nil { return err } if err := pe.putString(r.ProtocolType); err != nil { return err } if err := pe.putArrayLength(len(r.GroupProtocols)); err != nil { return err } for name, metadata := range r.GroupProtocols { if err := pe.putString(name); err != nil { return err } if err := pe.putBytes(metadata); err != nil { return err } } return nil } func (r *JoinGroupRequest) decode(pd packetDecoder) (err error) { if r.GroupId, err = pd.getString(); err != nil { return } if r.SessionTimeout, err = pd.getInt32(); err != nil { return } if r.MemberId, err = pd.getString(); err != nil { return } if r.ProtocolType, err = pd.getString(); err != nil { return } n, err := pd.getArrayLength() if err != nil { return err } if n == 0 { return nil } r.GroupProtocols = make(map[string][]byte) for i := 0; i < n; i++ { name, err := pd.getString() if err != nil { return err } metadata, err := pd.getBytes() if err != nil { return err } r.GroupProtocols[name] = metadata } return nil } func (r *JoinGroupRequest) key() int16 { return 11 } func (r *JoinGroupRequest) version() int16 { return 0 } func (r *JoinGroupRequest) AddGroupProtocol(name string, metadata []byte) { if r.GroupProtocols == nil { r.GroupProtocols = make(map[string][]byte) } r.GroupProtocols[name] = metadata } func (r *JoinGroupRequest) AddGroupProtocolMetadata(name string, metadata *ConsumerGroupMemberMetadata) error { bin, err := encode(metadata) if err != nil { return err } r.AddGroupProtocol(name, bin) return nil } sarama-1.9.0/join_group_request_test.go 0000664 0000000 0000000 00000002406 12716410165 0020303 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( joinGroupRequestNoProtocols = []byte{ 0, 9, 'T', 'e', 's', 't', 'G', 'r', 'o', 'u', 'p', // Group ID 0, 0, 0, 100, // Session timeout 0, 0, // Member ID 0, 8, 'c', 'o', 'n', 's', 'u', 'm', 'e', 'r', // Protocol Type 0, 0, 0, 0, // 0 protocol groups } joinGroupRequestOneProtocol = []byte{ 0, 9, 'T', 'e', 's', 't', 'G', 'r', 'o', 'u', 'p', // Group ID 0, 0, 0, 100, // Session timeout 0, 11, 'O', 'n', 'e', 'P', 'r', 'o', 't', 'o', 'c', 'o', 'l', // Member ID 0, 8, 'c', 'o', 'n', 's', 'u', 'm', 'e', 'r', // Protocol Type 0, 0, 0, 1, // 1 group protocol 0, 3, 'o', 'n', 'e', // Protocol name 0, 0, 0, 3, 0x01, 0x02, 0x03, // protocol metadata } ) func TestJoinGroupRequest(t *testing.T) { var request *JoinGroupRequest request = new(JoinGroupRequest) request.GroupId = "TestGroup" request.SessionTimeout = 100 request.ProtocolType = "consumer" testRequest(t, "no protocols", request, joinGroupRequestNoProtocols) request = new(JoinGroupRequest) request.GroupId = "TestGroup" request.SessionTimeout = 100 request.MemberId = "OneProtocol" request.ProtocolType = "consumer" request.AddGroupProtocol("one", []byte{0x01, 0x02, 0x03}) testRequest(t, "one protocol", request, joinGroupRequestOneProtocol) } sarama-1.9.0/join_group_response.go 0000664 0000000 0000000 00000003631 12716410165 0017413 0 ustar 00root root 0000000 0000000 package sarama type JoinGroupResponse struct { Err KError GenerationId int32 GroupProtocol string LeaderId string MemberId string Members map[string][]byte } func (r *JoinGroupResponse) GetMembers() (map[string]ConsumerGroupMemberMetadata, error) { members := make(map[string]ConsumerGroupMemberMetadata, len(r.Members)) for id, bin := range r.Members { meta := new(ConsumerGroupMemberMetadata) if err := decode(bin, meta); err != nil { return nil, err } members[id] = *meta } return members, nil } func (r *JoinGroupResponse) encode(pe packetEncoder) error { pe.putInt16(int16(r.Err)) pe.putInt32(r.GenerationId) if err := pe.putString(r.GroupProtocol); err != nil { return err } if err := pe.putString(r.LeaderId); err != nil { return err } if err := pe.putString(r.MemberId); err != nil { return err } if err := pe.putArrayLength(len(r.Members)); err != nil { return err } for memberId, memberMetadata := range r.Members { if err := pe.putString(memberId); err != nil { return err } if err := pe.putBytes(memberMetadata); err != nil { return err } } return nil } func (r *JoinGroupResponse) decode(pd packetDecoder) (err error) { if kerr, err := pd.getInt16(); err != nil { return err } else { r.Err = KError(kerr) } if r.GenerationId, err = pd.getInt32(); err != nil { return } if r.GroupProtocol, err = pd.getString(); err != nil { return } if r.LeaderId, err = pd.getString(); err != nil { return } if r.MemberId, err = pd.getString(); err != nil { return } n, err := pd.getArrayLength() if err != nil { return err } if n == 0 { return nil } r.Members = make(map[string][]byte) for i := 0; i < n; i++ { memberId, err := pd.getString() if err != nil { return err } memberMetadata, err := pd.getBytes() if err != nil { return err } r.Members[memberId] = memberMetadata } return nil } sarama-1.9.0/join_group_response_test.go 0000664 0000000 0000000 00000006044 12716410165 0020453 0 ustar 00root root 0000000 0000000 package sarama import ( "reflect" "testing" ) var ( joinGroupResponseNoError = []byte{ 0x00, 0x00, // No error 0x00, 0x01, 0x02, 0x03, // Generation ID 0, 8, 'p', 'r', 'o', 't', 'o', 'c', 'o', 'l', // Protocol name chosen 0, 3, 'f', 'o', 'o', // Leader ID 0, 3, 'b', 'a', 'r', // Member ID 0, 0, 0, 0, // No member info } joinGroupResponseWithError = []byte{ 0, 23, // Error: inconsistent group protocol 0x00, 0x00, 0x00, 0x00, // Generation ID 0, 0, // Protocol name chosen 0, 0, // Leader ID 0, 0, // Member ID 0, 0, 0, 0, // No member info } joinGroupResponseLeader = []byte{ 0x00, 0x00, // No error 0x00, 0x01, 0x02, 0x03, // Generation ID 0, 8, 'p', 'r', 'o', 't', 'o', 'c', 'o', 'l', // Protocol name chosen 0, 3, 'f', 'o', 'o', // Leader ID 0, 3, 'f', 'o', 'o', // Member ID == Leader ID 0, 0, 0, 1, // 1 member 0, 3, 'f', 'o', 'o', // Member ID 0, 0, 0, 3, 0x01, 0x02, 0x03, // Member metadata } ) func TestJoinGroupResponse(t *testing.T) { var response *JoinGroupResponse response = new(JoinGroupResponse) testDecodable(t, "no error", response, joinGroupResponseNoError) if response.Err != ErrNoError { t.Error("Decoding Err failed: no error expected but found", response.Err) } if response.GenerationId != 66051 { t.Error("Decoding GenerationId failed, found:", response.GenerationId) } if response.LeaderId != "foo" { t.Error("Decoding LeaderId failed, found:", response.LeaderId) } if response.MemberId != "bar" { t.Error("Decoding MemberId failed, found:", response.MemberId) } if len(response.Members) != 0 { t.Error("Decoding Members failed, found:", response.Members) } response = new(JoinGroupResponse) testDecodable(t, "with error", response, joinGroupResponseWithError) if response.Err != ErrInconsistentGroupProtocol { t.Error("Decoding Err failed: ErrInconsistentGroupProtocol expected but found", response.Err) } if response.GenerationId != 0 { t.Error("Decoding GenerationId failed, found:", response.GenerationId) } if response.LeaderId != "" { t.Error("Decoding LeaderId failed, found:", response.LeaderId) } if response.MemberId != "" { t.Error("Decoding MemberId failed, found:", response.MemberId) } if len(response.Members) != 0 { t.Error("Decoding Members failed, found:", response.Members) } response = new(JoinGroupResponse) testDecodable(t, "with error", response, joinGroupResponseLeader) if response.Err != ErrNoError { t.Error("Decoding Err failed: ErrNoError expected but found", response.Err) } if response.GenerationId != 66051 { t.Error("Decoding GenerationId failed, found:", response.GenerationId) } if response.LeaderId != "foo" { t.Error("Decoding LeaderId failed, found:", response.LeaderId) } if response.MemberId != "foo" { t.Error("Decoding MemberId failed, found:", response.MemberId) } if len(response.Members) != 1 { t.Error("Decoding Members failed, found:", response.Members) } if !reflect.DeepEqual(response.Members["foo"], []byte{0x01, 0x02, 0x03}) { t.Error("Decoding foo member failed, found:", response.Members["foo"]) } } sarama-1.9.0/leave_group_request.go 0000664 0000000 0000000 00000001147 12716410165 0017402 0 ustar 00root root 0000000 0000000 package sarama type LeaveGroupRequest struct { GroupId string MemberId string } func (r *LeaveGroupRequest) encode(pe packetEncoder) error { if err := pe.putString(r.GroupId); err != nil { return err } if err := pe.putString(r.MemberId); err != nil { return err } return nil } func (r *LeaveGroupRequest) decode(pd packetDecoder) (err error) { if r.GroupId, err = pd.getString(); err != nil { return } if r.MemberId, err = pd.getString(); err != nil { return } return nil } func (r *LeaveGroupRequest) key() int16 { return 13 } func (r *LeaveGroupRequest) version() int16 { return 0 } sarama-1.9.0/leave_group_request_test.go 0000664 0000000 0000000 00000000533 12716410165 0020437 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( basicLeaveGroupRequest = []byte{ 0, 3, 'f', 'o', 'o', 0, 3, 'b', 'a', 'r', } ) func TestLeaveGroupRequest(t *testing.T) { var request *LeaveGroupRequest request = new(LeaveGroupRequest) request.GroupId = "foo" request.MemberId = "bar" testRequest(t, "basic", request, basicLeaveGroupRequest) } sarama-1.9.0/leave_group_response.go 0000664 0000000 0000000 00000000531 12716410165 0017544 0 ustar 00root root 0000000 0000000 package sarama type LeaveGroupResponse struct { Err KError } func (r *LeaveGroupResponse) encode(pe packetEncoder) error { pe.putInt16(int16(r.Err)) return nil } func (r *LeaveGroupResponse) decode(pd packetDecoder) (err error) { if kerr, err := pd.getInt16(); err != nil { return err } else { r.Err = KError(kerr) } return nil } sarama-1.9.0/leave_group_response_test.go 0000664 0000000 0000000 00000001244 12716410165 0020605 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( leaveGroupResponseNoError = []byte{0x00, 0x00} leaveGroupResponseWithError = []byte{0, 25} ) func TestLeaveGroupResponse(t *testing.T) { var response *LeaveGroupResponse response = new(LeaveGroupResponse) testDecodable(t, "no error", response, leaveGroupResponseNoError) if response.Err != ErrNoError { t.Error("Decoding error failed: no error expected but found", response.Err) } response = new(LeaveGroupResponse) testDecodable(t, "with error", response, leaveGroupResponseWithError) if response.Err != ErrUnknownMemberId { t.Error("Decoding error failed: ErrUnknownMemberId expected but found", response.Err) } } sarama-1.9.0/length_field.go 0000664 0000000 0000000 00000001266 12716410165 0015750 0 ustar 00root root 0000000 0000000 package sarama import "encoding/binary" // LengthField implements the PushEncoder and PushDecoder interfaces for calculating 4-byte lengths. type lengthField struct { startOffset int } func (l *lengthField) saveOffset(in int) { l.startOffset = in } func (l *lengthField) reserveLength() int { return 4 } func (l *lengthField) run(curOffset int, buf []byte) error { binary.BigEndian.PutUint32(buf[l.startOffset:], uint32(curOffset-l.startOffset-4)) return nil } func (l *lengthField) check(curOffset int, buf []byte) error { if uint32(curOffset-l.startOffset-4) != binary.BigEndian.Uint32(buf[l.startOffset:]) { return PacketDecodingError{"length field invalid"} } return nil } sarama-1.9.0/list_groups_request.go 0000664 0000000 0000000 00000000503 12716410165 0017437 0 ustar 00root root 0000000 0000000 package sarama type ListGroupsRequest struct { } func (r *ListGroupsRequest) encode(pe packetEncoder) error { return nil } func (r *ListGroupsRequest) decode(pd packetDecoder) (err error) { return nil } func (r *ListGroupsRequest) key() int16 { return 16 } func (r *ListGroupsRequest) version() int16 { return 0 } sarama-1.9.0/list_groups_request_test.go 0000664 0000000 0000000 00000000224 12716410165 0020476 0 ustar 00root root 0000000 0000000 package sarama import "testing" func TestListGroupsRequest(t *testing.T) { testRequest(t, "ListGroupsRequest", &ListGroupsRequest{}, []byte{}) } sarama-1.9.0/list_groups_response.go 0000664 0000000 0000000 00000001712 12716410165 0017610 0 ustar 00root root 0000000 0000000 package sarama type ListGroupsResponse struct { Err KError Groups map[string]string } func (r *ListGroupsResponse) encode(pe packetEncoder) error { pe.putInt16(int16(r.Err)) if err := pe.putArrayLength(len(r.Groups)); err != nil { return err } for groupId, protocolType := range r.Groups { if err := pe.putString(groupId); err != nil { return err } if err := pe.putString(protocolType); err != nil { return err } } return nil } func (r *ListGroupsResponse) decode(pd packetDecoder) error { if kerr, err := pd.getInt16(); err != nil { return err } else { r.Err = KError(kerr) } n, err := pd.getArrayLength() if err != nil { return err } if n == 0 { return nil } r.Groups = make(map[string]string) for i := 0; i < n; i++ { groupId, err := pd.getString() if err != nil { return err } protocolType, err := pd.getString() if err != nil { return err } r.Groups[groupId] = protocolType } return nil } sarama-1.9.0/list_groups_response_test.go 0000664 0000000 0000000 00000002613 12716410165 0020650 0 ustar 00root root 0000000 0000000 package sarama import ( "testing" ) var ( listGroupsResponseEmpty = []byte{ 0, 0, // no error 0, 0, 0, 0, // no groups } listGroupsResponseError = []byte{ 0, 31, // no error 0, 0, 0, 0, // ErrClusterAuthorizationFailed } listGroupsResponseWithConsumer = []byte{ 0, 0, // no error 0, 0, 0, 1, // 1 group 0, 3, 'f', 'o', 'o', // group name 0, 8, 'c', 'o', 'n', 's', 'u', 'm', 'e', 'r', // protocol type } ) func TestListGroupsResponse(t *testing.T) { var response *ListGroupsResponse response = new(ListGroupsResponse) testDecodable(t, "no error", response, listGroupsResponseEmpty) if response.Err != ErrNoError { t.Error("Expected no gerror, found:", response.Err) } if len(response.Groups) != 0 { t.Error("Expected no groups") } response = new(ListGroupsResponse) testDecodable(t, "no error", response, listGroupsResponseError) if response.Err != ErrClusterAuthorizationFailed { t.Error("Expected no gerror, found:", response.Err) } if len(response.Groups) != 0 { t.Error("Expected no groups") } response = new(ListGroupsResponse) testDecodable(t, "no error", response, listGroupsResponseWithConsumer) if response.Err != ErrNoError { t.Error("Expected no gerror, found:", response.Err) } if len(response.Groups) != 1 { t.Error("Expected one group") } if response.Groups["foo"] != "consumer" { t.Error("Expected foo group to use consumer protocol") } } sarama-1.9.0/message.go 0000664 0000000 0000000 00000006611 12716410165 0014747 0 ustar 00root root 0000000 0000000 package sarama import ( "bytes" "compress/gzip" "fmt" "io/ioutil" ) // CompressionCodec represents the various compression codecs recognized by Kafka in messages. type CompressionCodec int8 // only the last two bits are really used const compressionCodecMask int8 = 0x03 const ( CompressionNone CompressionCodec = 0 CompressionGZIP CompressionCodec = 1 CompressionSnappy CompressionCodec = 2 ) // The spec just says: "This is a version id used to allow backwards compatible evolution of the message // binary format." but it doesn't say what the current value is, so presumably 0... const messageFormat int8 = 0 type Message struct { Codec CompressionCodec // codec used to compress the message contents Key []byte // the message key, may be nil Value []byte // the message contents Set *MessageSet // the message set a message might wrap compressedCache []byte } func (m *Message) encode(pe packetEncoder) error { pe.push(&crc32Field{}) pe.putInt8(messageFormat) attributes := int8(m.Codec) & compressionCodecMask pe.putInt8(attributes) err := pe.putBytes(m.Key) if err != nil { return err } var payload []byte if m.compressedCache != nil { payload = m.compressedCache m.compressedCache = nil } else { switch m.Codec { case CompressionNone: payload = m.Value case CompressionGZIP: var buf bytes.Buffer writer := gzip.NewWriter(&buf) if _, err = writer.Write(m.Value); err != nil { return err } if err = writer.Close(); err != nil { return err } m.compressedCache = buf.Bytes() payload = m.compressedCache case CompressionSnappy: tmp := snappyEncode(m.Value) m.compressedCache = tmp payload = m.compressedCache default: return PacketEncodingError{fmt.Sprintf("unsupported compression codec (%d)", m.Codec)} } } if err = pe.putBytes(payload); err != nil { return err } return pe.pop() } func (m *Message) decode(pd packetDecoder) (err error) { err = pd.push(&crc32Field{}) if err != nil { return err } format, err := pd.getInt8() if err != nil { return err } if format != messageFormat { return PacketDecodingError{"unexpected messageFormat"} } attribute, err := pd.getInt8() if err != nil { return err } m.Codec = CompressionCodec(attribute & compressionCodecMask) m.Key, err = pd.getBytes() if err != nil { return err } m.Value, err = pd.getBytes() if err != nil { return err } switch m.Codec { case CompressionNone: // nothing to do case CompressionGZIP: if m.Value == nil { return PacketDecodingError{"GZIP compression specified, but no data to uncompress"} } reader, err := gzip.NewReader(bytes.NewReader(m.Value)) if err != nil { return err } if m.Value, err = ioutil.ReadAll(reader); err != nil { return err } if err := m.decodeSet(); err != nil { return err } case CompressionSnappy: if m.Value == nil { return PacketDecodingError{"Snappy compression specified, but no data to uncompress"} } if m.Value, err = snappyDecode(m.Value); err != nil { return err } if err := m.decodeSet(); err != nil { return err } default: return PacketDecodingError{fmt.Sprintf("invalid compression specified (%d)", m.Codec)} } return pd.pop() } // decodes a message set from a previousy encoded bulk-message func (m *Message) decodeSet() (err error) { pd := realDecoder{raw: m.Value} m.Set = &MessageSet{} return m.Set.decode(&pd) } sarama-1.9.0/message_set.go 0000664 0000000 0000000 00000003517 12716410165 0015624 0 ustar 00root root 0000000 0000000 package sarama type MessageBlock struct { Offset int64 Msg *Message } // Messages convenience helper which returns either all the // messages that are wrapped in this block func (msb *MessageBlock) Messages() []*MessageBlock { if msb.Msg.Set != nil { return msb.Msg.Set.Messages } return []*MessageBlock{msb} } func (msb *MessageBlock) encode(pe packetEncoder) error { pe.putInt64(msb.Offset) pe.push(&lengthField{}) err := msb.Msg.encode(pe) if err != nil { return err } return pe.pop() } func (msb *MessageBlock) decode(pd packetDecoder) (err error) { if msb.Offset, err = pd.getInt64(); err != nil { return err } if err = pd.push(&lengthField{}); err != nil { return err } msb.Msg = new(Message) if err = msb.Msg.decode(pd); err != nil { return err } if err = pd.pop(); err != nil { return err } return nil } type MessageSet struct { PartialTrailingMessage bool // whether the set on the wire contained an incomplete trailing MessageBlock Messages []*MessageBlock } func (ms *MessageSet) encode(pe packetEncoder) error { for i := range ms.Messages { err := ms.Messages[i].encode(pe) if err != nil { return err } } return nil } func (ms *MessageSet) decode(pd packetDecoder) (err error) { ms.Messages = nil for pd.remaining() > 0 { msb := new(MessageBlock) err = msb.decode(pd) switch err { case nil: ms.Messages = append(ms.Messages, msb) case ErrInsufficientData: // As an optimization the server is allowed to return a partial message at the // end of the message set. Clients should handle this case. So we just ignore such things. ms.PartialTrailingMessage = true return nil default: return err } } return nil } func (ms *MessageSet) addMessage(msg *Message) { block := new(MessageBlock) block.Msg = msg ms.Messages = append(ms.Messages, block) } sarama-1.9.0/message_test.go 0000664 0000000 0000000 00000007262 12716410165 0016011 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( emptyMessage = []byte{ 167, 236, 104, 3, // CRC 0x00, // magic version byte 0x00, // attribute flags 0xFF, 0xFF, 0xFF, 0xFF, // key 0xFF, 0xFF, 0xFF, 0xFF} // value emptyGzipMessage = []byte{ 97, 79, 149, 90, //CRC 0x00, // magic version byte 0x01, // attribute flags 0xFF, 0xFF, 0xFF, 0xFF, // key // value 0x00, 0x00, 0x00, 0x17, 0x1f, 0x8b, 0x08, 0, 0, 9, 110, 136, 0, 255, 1, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0} emptyBulkSnappyMessage = []byte{ 180, 47, 53, 209, //CRC 0x00, // magic version byte 0x02, // attribute flags 0xFF, 0xFF, 0xFF, 0xFF, // key 0, 0, 0, 42, 130, 83, 78, 65, 80, 80, 89, 0, // SNAPPY magic 0, 0, 0, 1, // min version 0, 0, 0, 1, // default version 0, 0, 0, 22, 52, 0, 0, 25, 1, 16, 14, 227, 138, 104, 118, 25, 15, 13, 1, 8, 1, 0, 0, 62, 26, 0} emptyBulkGzipMessage = []byte{ 139, 160, 63, 141, //CRC 0x00, // magic version byte 0x01, // attribute flags 0xFF, 0xFF, 0xFF, 0xFF, // key 0x00, 0x00, 0x00, 0x27, // len 0x1f, 0x8b, // Gzip Magic 0x08, // deflate compressed 0, 0, 0, 0, 0, 0, 0, 99, 96, 128, 3, 190, 202, 112, 143, 7, 12, 12, 255, 129, 0, 33, 200, 192, 136, 41, 3, 0, 199, 226, 155, 70, 52, 0, 0, 0} ) func TestMessageEncoding(t *testing.T) { message := Message{} testEncodable(t, "empty", &message, emptyMessage) message.Value = []byte{} message.Codec = CompressionGZIP testEncodable(t, "empty gzip", &message, emptyGzipMessage) } func TestMessageDecoding(t *testing.T) { message := Message{} testDecodable(t, "empty", &message, emptyMessage) if message.Codec != CompressionNone { t.Error("Decoding produced compression codec where there was none.") } if message.Key != nil { t.Error("Decoding produced key where there was none.") } if message.Value != nil { t.Error("Decoding produced value where there was none.") } if message.Set != nil { t.Error("Decoding produced set where there was none.") } testDecodable(t, "empty gzip", &message, emptyGzipMessage) if message.Codec != CompressionGZIP { t.Error("Decoding produced incorrect compression codec (was gzip).") } if message.Key != nil { t.Error("Decoding produced key where there was none.") } if message.Value == nil || len(message.Value) != 0 { t.Error("Decoding produced nil or content-ful value where there was an empty array.") } } func TestMessageDecodingBulkSnappy(t *testing.T) { message := Message{} testDecodable(t, "bulk snappy", &message, emptyBulkSnappyMessage) if message.Codec != CompressionSnappy { t.Errorf("Decoding produced codec %d, but expected %d.", message.Codec, CompressionSnappy) } if message.Key != nil { t.Errorf("Decoding produced key %+v, but none was expected.", message.Key) } if message.Set == nil { t.Error("Decoding produced no set, but one was expected.") } else if len(message.Set.Messages) != 2 { t.Errorf("Decoding produced a set with %d messages, but 2 were expected.", len(message.Set.Messages)) } } func TestMessageDecodingBulkGzip(t *testing.T) { message := Message{} testDecodable(t, "bulk gzip", &message, emptyBulkGzipMessage) if message.Codec != CompressionGZIP { t.Errorf("Decoding produced codec %d, but expected %d.", message.Codec, CompressionGZIP) } if message.Key != nil { t.Errorf("Decoding produced key %+v, but none was expected.", message.Key) } if message.Set == nil { t.Error("Decoding produced no set, but one was expected.") } else if len(message.Set.Messages) != 2 { t.Errorf("Decoding produced a set with %d messages, but 2 were expected.", len(message.Set.Messages)) } } sarama-1.9.0/metadata_request.go 0000664 0000000 0000000 00000001412 12716410165 0016645 0 ustar 00root root 0000000 0000000 package sarama type MetadataRequest struct { Topics []string } func (mr *MetadataRequest) encode(pe packetEncoder) error { err := pe.putArrayLength(len(mr.Topics)) if err != nil { return err } for i := range mr.Topics { err = pe.putString(mr.Topics[i]) if err != nil { return err } } return nil } func (mr *MetadataRequest) decode(pd packetDecoder) error { topicCount, err := pd.getArrayLength() if err != nil { return err } if topicCount == 0 { return nil } mr.Topics = make([]string, topicCount) for i := range mr.Topics { topic, err := pd.getString() if err != nil { return err } mr.Topics[i] = topic } return nil } func (mr *MetadataRequest) key() int16 { return 3 } func (mr *MetadataRequest) version() int16 { return 0 } sarama-1.9.0/metadata_request_test.go 0000664 0000000 0000000 00000001320 12716410165 0017702 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( metadataRequestNoTopics = []byte{ 0x00, 0x00, 0x00, 0x00} metadataRequestOneTopic = []byte{ 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, 't', 'o', 'p', 'i', 'c', '1'} metadataRequestThreeTopics = []byte{ 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 'f', 'o', 'o', 0x00, 0x03, 'b', 'a', 'r', 0x00, 0x03, 'b', 'a', 'z'} ) func TestMetadataRequest(t *testing.T) { request := new(MetadataRequest) testRequest(t, "no topics", request, metadataRequestNoTopics) request.Topics = []string{"topic1"} testRequest(t, "one topic", request, metadataRequestOneTopic) request.Topics = []string{"foo", "bar", "baz"} testRequest(t, "three topics", request, metadataRequestThreeTopics) } sarama-1.9.0/metadata_response.go 0000664 0000000 0000000 00000007406 12716410165 0017024 0 ustar 00root root 0000000 0000000 package sarama type PartitionMetadata struct { Err KError ID int32 Leader int32 Replicas []int32 Isr []int32 } func (pm *PartitionMetadata) decode(pd packetDecoder) (err error) { tmp, err := pd.getInt16() if err != nil { return err } pm.Err = KError(tmp) pm.ID, err = pd.getInt32() if err != nil { return err } pm.Leader, err = pd.getInt32() if err != nil { return err } pm.Replicas, err = pd.getInt32Array() if err != nil { return err } pm.Isr, err = pd.getInt32Array() if err != nil { return err } return nil } func (pm *PartitionMetadata) encode(pe packetEncoder) (err error) { pe.putInt16(int16(pm.Err)) pe.putInt32(pm.ID) pe.putInt32(pm.Leader) err = pe.putInt32Array(pm.Replicas) if err != nil { return err } err = pe.putInt32Array(pm.Isr) if err != nil { return err } return nil } type TopicMetadata struct { Err KError Name string Partitions []*PartitionMetadata } func (tm *TopicMetadata) decode(pd packetDecoder) (err error) { tmp, err := pd.getInt16() if err != nil { return err } tm.Err = KError(tmp) tm.Name, err = pd.getString() if err != nil { return err } n, err := pd.getArrayLength() if err != nil { return err } tm.Partitions = make([]*PartitionMetadata, n) for i := 0; i < n; i++ { tm.Partitions[i] = new(PartitionMetadata) err = tm.Partitions[i].decode(pd) if err != nil { return err } } return nil } func (tm *TopicMetadata) encode(pe packetEncoder) (err error) { pe.putInt16(int16(tm.Err)) err = pe.putString(tm.Name) if err != nil { return err } err = pe.putArrayLength(len(tm.Partitions)) if err != nil { return err } for _, pm := range tm.Partitions { err = pm.encode(pe) if err != nil { return err } } return nil } type MetadataResponse struct { Brokers []*Broker Topics []*TopicMetadata } func (m *MetadataResponse) decode(pd packetDecoder) (err error) { n, err := pd.getArrayLength() if err != nil { return err } m.Brokers = make([]*Broker, n) for i := 0; i < n; i++ { m.Brokers[i] = new(Broker) err = m.Brokers[i].decode(pd) if err != nil { return err } } n, err = pd.getArrayLength() if err != nil { return err } m.Topics = make([]*TopicMetadata, n) for i := 0; i < n; i++ { m.Topics[i] = new(TopicMetadata) err = m.Topics[i].decode(pd) if err != nil { return err } } return nil } func (m *MetadataResponse) encode(pe packetEncoder) error { err := pe.putArrayLength(len(m.Brokers)) if err != nil { return err } for _, broker := range m.Brokers { err = broker.encode(pe) if err != nil { return err } } err = pe.putArrayLength(len(m.Topics)) if err != nil { return err } for _, tm := range m.Topics { err = tm.encode(pe) if err != nil { return err } } return nil } // testing API func (m *MetadataResponse) AddBroker(addr string, id int32) { m.Brokers = append(m.Brokers, &Broker{id: id, addr: addr}) } func (m *MetadataResponse) AddTopic(topic string, err KError) *TopicMetadata { var tmatch *TopicMetadata for _, tm := range m.Topics { if tm.Name == topic { tmatch = tm goto foundTopic } } tmatch = new(TopicMetadata) tmatch.Name = topic m.Topics = append(m.Topics, tmatch) foundTopic: tmatch.Err = err return tmatch } func (m *MetadataResponse) AddTopicPartition(topic string, partition, brokerID int32, replicas, isr []int32, err KError) { tmatch := m.AddTopic(topic, ErrNoError) var pmatch *PartitionMetadata for _, pm := range tmatch.Partitions { if pm.ID == partition { pmatch = pm goto foundPartition } } pmatch = new(PartitionMetadata) pmatch.ID = partition tmatch.Partitions = append(tmatch.Partitions, pmatch) foundPartition: pmatch.Leader = brokerID pmatch.Replicas = replicas pmatch.Isr = isr pmatch.Err = err } sarama-1.9.0/metadata_response_test.go 0000664 0000000 0000000 00000007561 12716410165 0020065 0 ustar 00root root 0000000 0000000 package sarama import "testing" var ( emptyMetadataResponse = []byte{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} brokersNoTopicsMetadataResponse = []byte{ 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xab, 0xff, 0x00, 0x09, 'l', 'o', 'c', 'a', 'l', 'h', 'o', 's', 't', 0x00, 0x00, 0x00, 0x33, 0x00, 0x01, 0x02, 0x03, 0x00, 0x0a, 'g', 'o', 'o', 'g', 'l', 'e', '.', 'c', 'o', 'm', 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00} topicsNoBrokersMetadataResponse = []byte{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 'f', 'o', 'o', 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 'b', 'a', 'r', 0x00, 0x00, 0x00, 0x00} ) func TestEmptyMetadataResponse(t *testing.T) { response := MetadataResponse{} testDecodable(t, "empty", &response, emptyMetadataResponse) if len(response.Brokers) != 0 { t.Error("Decoding produced", len(response.Brokers), "brokers where there were none!") } if len(response.Topics) != 0 { t.Error("Decoding produced", len(response.Topics), "topics where there were none!") } } func TestMetadataResponseWithBrokers(t *testing.T) { response := MetadataResponse{} testDecodable(t, "brokers, no topics", &response, brokersNoTopicsMetadataResponse) if len(response.Brokers) != 2 { t.Fatal("Decoding produced", len(response.Brokers), "brokers where there were two!") } if response.Brokers[0].id != 0xabff { t.Error("Decoding produced invalid broker 0 id.") } if response.Brokers[0].addr != "localhost:51" { t.Error("Decoding produced invalid broker 0 address.") } if response.Brokers[1].id != 0x010203 { t.Error("Decoding produced invalid broker 1 id.") } if response.Brokers[1].addr != "google.com:273" { t.Error("Decoding produced invalid broker 1 address.") } if len(response.Topics) != 0 { t.Error("Decoding produced", len(response.Topics), "topics where there were none!") } } func TestMetadataResponseWithTopics(t *testing.T) { response := MetadataResponse{} testDecodable(t, "topics, no brokers", &response, topicsNoBrokersMetadataResponse) if len(response.Brokers) != 0 { t.Error("Decoding produced", len(response.Brokers), "brokers where there were none!") } if len(response.Topics) != 2 { t.Fatal("Decoding produced", len(response.Topics), "topics where there were two!") } if response.Topics[0].Err != ErrNoError { t.Error("Decoding produced invalid topic 0 error.") } if response.Topics[0].Name != "foo" { t.Error("Decoding produced invalid topic 0 name.") } if len(response.Topics[0].Partitions) != 1 { t.Fatal("Decoding produced invalid partition count for topic 0.") } if response.Topics[0].Partitions[0].Err != ErrInvalidMessageSize { t.Error("Decoding produced invalid topic 0 partition 0 error.") } if response.Topics[0].Partitions[0].ID != 0x01 { t.Error("Decoding produced invalid topic 0 partition 0 id.") } if response.Topics[0].Partitions[0].Leader != 0x07 { t.Error("Decoding produced invalid topic 0 partition 0 leader.") } if len(response.Topics[0].Partitions[0].Replicas) != 3 { t.Fatal("Decoding produced invalid topic 0 partition 0 replicas.") } for i := 0; i < 3; i++ { if response.Topics[0].Partitions[0].Replicas[i] != int32(i+1) { t.Error("Decoding produced invalid topic 0 partition 0 replica", i) } } if len(response.Topics[0].Partitions[0].Isr) != 0 { t.Error("Decoding produced invalid topic 0 partition 0 isr length.") } if response.Topics[1].Err != ErrNoError { t.Error("Decoding produced invalid topic 1 error.") } if response.Topics[1].Name != "bar" { t.Error("Decoding produced invalid topic 0 name.") } if len(response.Topics[1].Partitions) != 0 { t.Error("Decoding produced invalid partition count for topic 1.") } } sarama-1.9.0/mockbroker.go 0000664 0000000 0000000 00000020457 12716410165 0015465 0 ustar 00root root 0000000 0000000 package sarama import ( "bytes" "encoding/binary" "fmt" "io" "net" "reflect" "strconv" "sync" "time" "github.com/davecgh/go-spew/spew" ) const ( expectationTimeout = 500 * time.Millisecond ) type requestHandlerFunc func(req *request) (res encoder) // MockBroker is a mock Kafka broker that is used in unit tests. It is exposed // to facilitate testing of higher level or specialized consumers and producers // built on top of Sarama. Note that it does not 'mimic' the Kafka API protocol, // but rather provides a facility to do that. It takes care of the TCP // transport, request unmarshaling, response marshaling, and makes it the test // writer responsibility to program correct according to the Kafka API protocol // MockBroker behaviour. // // MockBroker is implemented as a TCP server listening on a kernel-selected // localhost port that can accept many connections. It reads Kafka requests // from that connection and returns responses programmed by the SetHandlerByMap // function. If a MockBroker receives a request that it has no programmed // response for, then it returns nothing and the request times out. // // A set of MockRequest builders to define mappings used by MockBroker is // provided by Sarama. But users can develop MockRequests of their own and use // them along with or instead of the standard ones. // // When running tests with MockBroker it is strongly recommended to specify // a timeout to `go test` so that if the broker hangs waiting for a response, // the test panics. // // It is not necessary to prefix message length or correlation ID to your // response bytes, the server does that automatically as a convenience. type MockBroker struct { brokerID int32 port int32 closing chan none stopper chan none expectations chan encoder listener net.Listener t TestReporter latency time.Duration handler requestHandlerFunc history []RequestResponse lock sync.Mutex } // RequestResponse represents a Request/Response pair processed by MockBroker. type RequestResponse struct { Request requestBody Response encoder } // SetLatency makes broker pause for the specified period every time before // replying. func (b *MockBroker) SetLatency(latency time.Duration) { b.latency = latency } // SetHandlerByMap defines mapping of Request types to MockResponses. When a // request is received by the broker, it looks up the request type in the map // and uses the found MockResponse instance to generate an appropriate reply. // If the request type is not found in the map then nothing is sent. func (b *MockBroker) SetHandlerByMap(handlerMap map[string]MockResponse) { b.setHandler(func(req *request) (res encoder) { reqTypeName := reflect.TypeOf(req.body).Elem().Name() mockResponse := handlerMap[reqTypeName] if mockResponse == nil { return nil } return mockResponse.For(req.body) }) } // BrokerID returns broker ID assigned to the broker. func (b *MockBroker) BrokerID() int32 { return b.brokerID } // History returns a slice of RequestResponse pairs in the order they were // processed by the broker. Note that in case of multiple connections to the // broker the order expected by a test can be different from the order recorded // in the history, unless some synchronization is implemented in the test. func (b *MockBroker) History() []RequestResponse { b.lock.Lock() history := make([]RequestResponse, len(b.history)) copy(history, b.history) b.lock.Unlock() return history } // Port returns the TCP port number the broker is listening for requests on. func (b *MockBroker) Port() int32 { return b.port } // Addr returns the broker connection string in the form "
: