|
|
|
/**
|
|
|
|
* Copyright 2016 Confluent Inc.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Package kafka provides high-level Apache Kafka producer and consumers
|
|
|
|
// using bindings on-top of the librdkafka C library.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// High-level Consumer
|
|
|
|
//
|
|
|
|
// * Decide if you want to read messages and events by calling `.Poll()` or
|
|
|
|
// the deprecated option of using the `.Events()` channel. (If you want to use
|
|
|
|
// `.Events()` channel then set `"go.events.channel.enable": true`).
|
|
|
|
//
|
|
|
|
// * Create a Consumer with `kafka.NewConsumer()` providing at
|
|
|
|
// least the `bootstrap.servers` and `group.id` configuration properties.
|
|
|
|
//
|
|
|
|
// * Call `.Subscribe()` or (`.SubscribeTopics()` to subscribe to multiple topics)
|
|
|
|
// to join the group with the specified subscription set.
|
|
|
|
// Subscriptions are atomic, calling `.Subscribe*()` again will leave
|
|
|
|
// the group and rejoin with the new set of topics.
|
|
|
|
//
|
|
|
|
// * Start reading events and messages from either the `.Events` channel
|
|
|
|
// or by calling `.Poll()`.
|
|
|
|
//
|
|
|
|
// * When the group has rebalanced each client member is assigned a
|
|
|
|
// (sub-)set of topic+partitions.
|
|
|
|
// By default the consumer will start fetching messages for its assigned
|
|
|
|
// partitions at this point, but your application may enable rebalance
|
|
|
|
// events to get an insight into what the assigned partitions where
|
|
|
|
// as well as set the initial offsets. To do this you need to pass
|
|
|
|
// `"go.application.rebalance.enable": true` to the `NewConsumer()` call
|
|
|
|
// mentioned above. You will (eventually) see a `kafka.AssignedPartitions` event
|
|
|
|
// with the assigned partition set. You can optionally modify the initial
|
|
|
|
// offsets (they'll default to stored offsets and if there are no previously stored
|
|
|
|
// offsets it will fall back to `"auto.offset.reset"`
|
|
|
|
// which defaults to the `latest` message) and then call `.Assign(partitions)`
|
|
|
|
// to start consuming. If you don't need to modify the initial offsets you will
|
|
|
|
// not need to call `.Assign()`, the client will do so automatically for you if
|
|
|
|
// you dont, unless you are using the channel-based consumer in which case
|
|
|
|
// you MUST call `.Assign()` when receiving the `AssignedPartitions` and
|
|
|
|
// `RevokedPartitions` events.
|
|
|
|
//
|
|
|
|
// * As messages are fetched they will be made available on either the
|
|
|
|
// `.Events` channel or by calling `.Poll()`, look for event type `*kafka.Message`.
|
|
|
|
//
|
|
|
|
// * Handle messages, events and errors to your liking.
|
|
|
|
//
|
|
|
|
// * When you are done consuming call `.Close()` to commit final offsets
|
|
|
|
// and leave the consumer group.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Producer
|
|
|
|
//
|
|
|
|
// * Create a Producer with `kafka.NewProducer()` providing at least
|
|
|
|
// the `bootstrap.servers` configuration properties.
|
|
|
|
//
|
|
|
|
// * Messages may now be produced either by sending a `*kafka.Message`
|
|
|
|
// on the `.ProduceChannel` or by calling `.Produce()`.
|
|
|
|
//
|
|
|
|
// * Producing is an asynchronous operation so the client notifies the application
|
|
|
|
// of per-message produce success or failure through something called delivery reports.
|
|
|
|
// Delivery reports are by default emitted on the `.Events()` channel as `*kafka.Message`
|
|
|
|
// and you should check `msg.TopicPartition.Error` for `nil` to find out if the message
|
|
|
|
// was succesfully delivered or not.
|
|
|
|
// It is also possible to direct delivery reports to alternate channels
|
|
|
|
// by providing a non-nil `chan Event` channel to `.Produce()`.
|
|
|
|
// If no delivery reports are wanted they can be completely disabled by
|
|
|
|
// setting configuration property `"go.delivery.reports": false`.
|
|
|
|
//
|
|
|
|
// * When you are done producing messages you will need to make sure all messages
|
|
|
|
// are indeed delivered to the broker (or failed), remember that this is
|
|
|
|
// an asynchronous client so some of your messages may be lingering in internal
|
|
|
|
// channels or tranmission queues.
|
|
|
|
// To do this you can either keep track of the messages you've produced
|
|
|
|
// and wait for their corresponding delivery reports, or call the convenience
|
|
|
|
// function `.Flush()` that will block until all message deliveries are done
|
|
|
|
// or the provided timeout elapses.
|
|
|
|
//
|
|
|
|
// * Finally call `.Close()` to decommission the producer.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Transactional producer API
|
|
|
|
//
|
|
|
|
// The transactional producer operates on top of the idempotent producer,
|
|
|
|
// and provides full exactly-once semantics (EOS) for Apache Kafka when used
|
|
|
|
// with the transaction aware consumer (`isolation.level=read_committed`).
|
|
|
|
//
|
|
|
|
// A producer instance is configured for transactions by setting the
|
|
|
|
// `transactional.id` to an identifier unique for the application. This
|
|
|
|
// id will be used to fence stale transactions from previous instances of
|
|
|
|
// the application, typically following an outage or crash.
|
|
|
|
//
|
|
|
|
// After creating the transactional producer instance using `NewProducer()`
|
|
|
|
// the transactional state must be initialized by calling
|
|
|
|
// `InitTransactions()`. This is a blocking call that will
|
|
|
|
// acquire a runtime producer id from the transaction coordinator broker
|
|
|
|
// as well as abort any stale transactions and fence any still running producer
|
|
|
|
// instances with the same `transactional.id`.
|
|
|
|
//
|
|
|
|
// Once transactions are initialized the application may begin a new
|
|
|
|
// transaction by calling `BeginTransaction()`.
|
|
|
|
// A producer instance may only have one single on-going transaction.
|
|
|
|
//
|
|
|
|
// Any messages produced after the transaction has been started will
|
|
|
|
// belong to the ongoing transaction and will be committed or aborted
|
|
|
|
// atomically.
|
|
|
|
// It is not permitted to produce messages outside a transaction
|
|
|
|
// boundary, e.g., before `BeginTransaction()` or after `CommitTransaction()`,
|
|
|
|
// `AbortTransaction()` or if the current transaction has failed.
|
|
|
|
//
|
|
|
|
// If consumed messages are used as input to the transaction, the consumer
|
|
|
|
// instance must be configured with `enable.auto.commit` set to `false`.
|
|
|
|
// To commit the consumed offsets along with the transaction pass the
|
|
|
|
// list of consumed partitions and the last offset processed + 1 to
|
|
|
|
// `SendOffsetsToTransaction()` prior to committing the transaction.
|
|
|
|
// This allows an aborted transaction to be restarted using the previously
|
|
|
|
// committed offsets.
|
|
|
|
//
|
|
|
|
// To commit the produced messages, and any consumed offsets, to the
|
|
|
|
// current transaction, call `CommitTransaction()`.
|
|
|
|
// This call will block until the transaction has been fully committed or
|
|
|
|
// failed (typically due to fencing by a newer producer instance).
|
|
|
|
//
|
|
|
|
// Alternatively, if processing fails, or an abortable transaction error is
|
|
|
|
// raised, the transaction needs to be aborted by calling
|
|
|
|
// `AbortTransaction()` which marks any produced messages and
|
|
|
|
// offset commits as aborted.
|
|
|
|
//
|
|
|
|
// After the current transaction has been committed or aborted a new
|
|
|
|
// transaction may be started by calling `BeginTransaction()` again.
|
|
|
|
//
|
|
|
|
// Retriable errors:
|
|
|
|
// Some error cases allow the attempted operation to be retried, this is
|
|
|
|
// indicated by the error object having the retriable flag set which can
|
|
|
|
// be detected by calling `err.(kafka.Error).IsRetriable()`.
|
|
|
|
// When this flag is set the application may retry the operation immediately
|
|
|
|
// or preferably after a shorter grace period (to avoid busy-looping).
|
|
|
|
// Retriable errors include timeouts, broker transport failures, etc.
|
|
|
|
//
|
|
|
|
// Abortable errors:
|
|
|
|
// An ongoing transaction may fail permanently due to various errors,
|
|
|
|
// such as transaction coordinator becoming unavailable, write failures to the
|
|
|
|
// Apache Kafka log, under-replicated partitions, etc.
|
|
|
|
// At this point the producer application must abort the current transaction
|
|
|
|
// using `AbortTransaction()` and optionally start a new transaction
|
|
|
|
// by calling `BeginTransaction()`.
|
|
|
|
// Whether an error is abortable or not is detected by calling
|
|
|
|
// `err.(kafka.Error).TxnRequiresAbort()` on the returned error object.
|
|
|
|
//
|
|
|
|
// Fatal errors:
|
|
|
|
// While the underlying idempotent producer will typically only raise
|
|
|
|
// fatal errors for unrecoverable cluster errors where the idempotency
|
|
|
|
// guarantees can't be maintained, most of these are treated as abortable by
|
|
|
|
// the transactional producer since transactions may be aborted and retried
|
|
|
|
// in their entirety;
|
|
|
|
// The transactional producer on the other hand introduces a set of additional
|
|
|
|
// fatal errors which the application needs to handle by shutting down the
|
|
|
|
// producer and terminate. There is no way for a producer instance to recover
|
|
|
|
// from fatal errors.
|
|
|
|
// Whether an error is fatal or not is detected by calling
|
|
|
|
// `err.(kafka.Error).IsFatal()` on the returned error object or by checking
|
|
|
|
// the global `GetFatalError()`.
|
|
|
|
//
|
|
|
|
// Handling of other errors:
|
|
|
|
// For errors that have neither retriable, abortable or the fatal flag set
|
|
|
|
// it is not always obvious how to handle them. While some of these errors
|
|
|
|
// may be indicative of bugs in the application code, such as when
|
|
|
|
// an invalid parameter is passed to a method, other errors might originate
|
|
|
|
// from the broker and be passed thru as-is to the application.
|
|
|
|
// The general recommendation is to treat these errors, that have
|
|
|
|
// neither the retriable or abortable flags set, as fatal.
|
|
|
|
//
|
|
|
|
// Error handling example:
|
|
|
|
// retry:
|
|
|
|
//
|
|
|
|
// err := producer.CommitTransaction(...)
|
|
|
|
// if err == nil {
|
|
|
|
// return nil
|
|
|
|
// } else if err.(kafka.Error).TxnRequiresAbort() {
|
|
|
|
// do_abort_transaction_and_reset_inputs()
|
|
|
|
// } else if err.(kafka.Error).IsRetriable() {
|
|
|
|
// goto retry
|
|
|
|
// } else { // treat all other errors as fatal errors
|
|
|
|
// panic(err)
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Events
|
|
|
|
//
|
|
|
|
// Apart from emitting messages and delivery reports the client also communicates
|
|
|
|
// with the application through a number of different event types.
|
|
|
|
// An application may choose to handle or ignore these events.
|
|
|
|
//
|
|
|
|
// Consumer events
|
|
|
|
//
|
|
|
|
// * `*kafka.Message` - a fetched message.
|
|
|
|
//
|
|
|
|
// * `AssignedPartitions` - The assigned partition set for this client following a rebalance.
|
|
|
|
// Requires `go.application.rebalance.enable`
|
|
|
|
//
|
|
|
|
// * `RevokedPartitions` - The counter part to `AssignedPartitions` following a rebalance.
|
|
|
|
// `AssignedPartitions` and `RevokedPartitions` are symmetrical.
|
|
|
|
// Requires `go.application.rebalance.enable`
|
|
|
|
//
|
|
|
|
// * `PartitionEOF` - Consumer has reached the end of a partition.
|
|
|
|
// NOTE: The consumer will keep trying to fetch new messages for the partition.
|
|
|
|
//
|
|
|
|
// * `OffsetsCommitted` - Offset commit results (when `enable.auto.commit` is enabled).
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Producer events
|
|
|
|
//
|
|
|
|
// * `*kafka.Message` - delivery report for produced message.
|
|
|
|
// Check `.TopicPartition.Error` for delivery result.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Generic events for both Consumer and Producer
|
|
|
|
//
|
|
|
|
// * `KafkaError` - client (error codes are prefixed with _) or broker error.
|
|
|
|
// These errors are normally just informational since the
|
|
|
|
// client will try its best to automatically recover (eventually).
|
|
|
|
//
|
|
|
|
// * `OAuthBearerTokenRefresh` - retrieval of a new SASL/OAUTHBEARER token is required.
|
|
|
|
// This event only occurs with sasl.mechanism=OAUTHBEARER.
|
|
|
|
// Be sure to invoke SetOAuthBearerToken() on the Producer/Consumer/AdminClient
|
|
|
|
// instance when a successful token retrieval is completed, otherwise be sure to
|
|
|
|
// invoke SetOAuthBearerTokenFailure() to indicate that retrieval failed (or
|
|
|
|
// if setting the token failed, which could happen if an extension doesn't meet
|
|
|
|
// the required regular expression); invoking SetOAuthBearerTokenFailure() will
|
|
|
|
// schedule a new event for 10 seconds later so another retrieval can be attempted.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Hint: If your application registers a signal notification
|
|
|
|
// (signal.Notify) makes sure the signals channel is buffered to avoid
|
|
|
|
// possible complications with blocking Poll() calls.
|
|
|
|
//
|
|
|
|
// Note: The Confluent Kafka Go client is safe for concurrent use.
|
|
|
|
package kafka
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
// Make sure librdkafka_vendor/ sub-directory is included in vendor pulls.
|
|
|
|
_ "github.com/confluentinc/confluent-kafka-go/kafka/librdkafka_vendor"
|
|
|
|
"unsafe"
|
|
|
|
)
|
|
|
|
|
|
|
|
/*
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include "select_rdkafka.h"
|
|
|
|
|
|
|
|
static rd_kafka_topic_partition_t *_c_rdkafka_topic_partition_list_entry(rd_kafka_topic_partition_list_t *rktparlist, int idx) {
|
|
|
|
return idx < rktparlist->cnt ? &rktparlist->elems[idx] : NULL;
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
import "C"
|
|
|
|
|
|
|
|
// PartitionAny represents any partition (for partitioning),
|
|
|
|
// or unspecified value (for all other cases)
|
|
|
|
const PartitionAny = int32(C.RD_KAFKA_PARTITION_UA)
|
|
|
|
|
|
|
|
// TopicPartition is a generic placeholder for a Topic+Partition and optionally Offset.
|
|
|
|
type TopicPartition struct {
|
|
|
|
Topic *string
|
|
|
|
Partition int32
|
|
|
|
Offset Offset
|
|
|
|
Metadata *string
|
|
|
|
Error error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p TopicPartition) String() string {
|
|
|
|
topic := "<null>"
|
|
|
|
if p.Topic != nil {
|
|
|
|
topic = *p.Topic
|
|
|
|
}
|
|
|
|
if p.Error != nil {
|
|
|
|
return fmt.Sprintf("%s[%d]@%s(%s)",
|
|
|
|
topic, p.Partition, p.Offset, p.Error)
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%s[%d]@%s",
|
|
|
|
topic, p.Partition, p.Offset)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TopicPartitions is a slice of TopicPartitions that also implements
|
|
|
|
// the sort interface
|
|
|
|
type TopicPartitions []TopicPartition
|
|
|
|
|
|
|
|
func (tps TopicPartitions) Len() int {
|
|
|
|
return len(tps)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tps TopicPartitions) Less(i, j int) bool {
|
|
|
|
if *tps[i].Topic < *tps[j].Topic {
|
|
|
|
return true
|
|
|
|
} else if *tps[i].Topic > *tps[j].Topic {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return tps[i].Partition < tps[j].Partition
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tps TopicPartitions) Swap(i, j int) {
|
|
|
|
tps[i], tps[j] = tps[j], tps[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
// new_cparts_from_TopicPartitions creates a new C rd_kafka_topic_partition_list_t
|
|
|
|
// from a TopicPartition array.
|
|
|
|
func newCPartsFromTopicPartitions(partitions []TopicPartition) (cparts *C.rd_kafka_topic_partition_list_t) {
|
|
|
|
cparts = C.rd_kafka_topic_partition_list_new(C.int(len(partitions)))
|
|
|
|
for _, part := range partitions {
|
|
|
|
ctopic := C.CString(*part.Topic)
|
|
|
|
defer C.free(unsafe.Pointer(ctopic))
|
|
|
|
rktpar := C.rd_kafka_topic_partition_list_add(cparts, ctopic, C.int32_t(part.Partition))
|
|
|
|
rktpar.offset = C.int64_t(part.Offset)
|
|
|
|
|
|
|
|
if part.Metadata != nil {
|
|
|
|
cmetadata := C.CString(*part.Metadata)
|
|
|
|
rktpar.metadata = unsafe.Pointer(cmetadata)
|
|
|
|
rktpar.metadata_size = C.size_t(len(*part.Metadata))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cparts
|
|
|
|
}
|
|
|
|
|
|
|
|
func setupTopicPartitionFromCrktpar(partition *TopicPartition, crktpar *C.rd_kafka_topic_partition_t) {
|
|
|
|
|
|
|
|
topic := C.GoString(crktpar.topic)
|
|
|
|
partition.Topic = &topic
|
|
|
|
partition.Partition = int32(crktpar.partition)
|
|
|
|
partition.Offset = Offset(crktpar.offset)
|
|
|
|
if crktpar.metadata_size > 0 {
|
|
|
|
size := C.int(crktpar.metadata_size)
|
|
|
|
cstr := (*C.char)(unsafe.Pointer(crktpar.metadata))
|
|
|
|
metadata := C.GoStringN(cstr, size)
|
|
|
|
partition.Metadata = &metadata
|
|
|
|
}
|
|
|
|
if crktpar.err != C.RD_KAFKA_RESP_ERR_NO_ERROR {
|
|
|
|
partition.Error = newError(crktpar.err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func newTopicPartitionsFromCparts(cparts *C.rd_kafka_topic_partition_list_t) (partitions []TopicPartition) {
|
|
|
|
|
|
|
|
partcnt := int(cparts.cnt)
|
|
|
|
|
|
|
|
partitions = make([]TopicPartition, partcnt)
|
|
|
|
for i := 0; i < partcnt; i++ {
|
|
|
|
crktpar := C._c_rdkafka_topic_partition_list_entry(cparts, C.int(i))
|
|
|
|
setupTopicPartitionFromCrktpar(&partitions[i], crktpar)
|
|
|
|
}
|
|
|
|
|
|
|
|
return partitions
|
|
|
|
}
|
|
|
|
|
|
|
|
// LibraryVersion returns the underlying librdkafka library version as a
|
|
|
|
// (version_int, version_str) tuple.
|
|
|
|
func LibraryVersion() (int, string) {
|
|
|
|
ver := (int)(C.rd_kafka_version())
|
|
|
|
verstr := C.GoString(C.rd_kafka_version_str())
|
|
|
|
return ver, verstr
|
|
|
|
}
|