Migrating from confluent-kafka
pyrula.kafka.compat.confluent mirrors the
confluent_kafka Producer,
Consumer, AdminClient and Schema Registry serializers, so existing code moves over with
an import change. What holds through the shim is on the
client guarantees page.
# beforefrom confluent_kafka import Producer, Consumer, TopicPartition, OFFSET_BEGINNING# afterfrom pyrula.kafka.compat.confluent import Producer, Consumer, TopicPartition, OFFSET_BEGINNINGNew code should use the native KafkaProducer and
KafkaConsumer, which expose the batch, Arrow and exactly-once APIs.
Config
Section titled “Config”The constructors take confluent’s string-keyed dict and translate it to KafkaConfig /
KafkaConsumerConfig. An unknown key raises KafkaException (_INVALID_ARG). A key the
shim can’t honour is refused the same way when ignoring it would change data, placement,
durability or security, and otherwise accepted with one WARNING per process.
Confluent config keys lists every key and its verdict.
An omitted key gets confluent’s default:
| Key | Shim default | Native default |
|---|---|---|
enable.auto.commit | True | False |
auto.offset.reset | latest | earliest |
session.timeout.ms | 45000 | 10000 |
partitioner | consistent (CRC32) | murmur2 |
message.max.bytes (producer) | 1000000 | 1 MiB |
To see the native config a dict becomes:
from pyrula.kafka.compat.confluent import ( consumer_config_from_confluent, producer_config_from_confluent,)
producer_config_from_confluent({"bootstrap.servers": "localhost:9092", "acks": "all"})consumer_config_from_confluent({"bootstrap.servers": "localhost:9092", "group.id": "g"})Producer
Section titled “Producer”p = Producer({"bootstrap.servers": "localhost:9092"})
def on_delivery(err, msg): if err is not None: log.error("delivery failed: %s", err) else: log.info("delivered to %s", msg.topic())
for value in values: p.produce("orders", value=value, key=b"k", callback=on_delivery)p.flush()Producer() connects in the background. Delivery callbacks and error_cb fire on poll()
and flush(), as in confluent, so a producer that never polls gets BufferError once the
queue fills. Call flush() before exit; unserved reports are dropped when the Producer is
garbage collected.
Transactions
Section titled “Transactions”p.init_transactions()p.begin_transaction()try: p.produce("orders", value=v, key=k, on_delivery=report) p.send_offsets_to_transaction(positions, consumer.consumer_group_metadata()) p.commit_transaction()except KafkaException as exc: if exc.args[0].txn_requires_abort(): p.abort_transaction() # producer stays usable else: raisecommit_transaction() flushes first and raises with txn_requires_abort() True if any
record failed. A produce() with no open transaction raises KafkaException(_STATE). A
commit, abort or init whose timeout runs out raises a retriable error and keeps going;
call it again to resume.
Consumer
Section titled “Consumer”c = Consumer({ "bootstrap.servers": "localhost:9092", "group.id": "orders-worker", "auto.offset.reset": "earliest",})c.subscribe(["orders"])while running: msg = c.poll(1.0) if msg is None: continue if msg.error(): log.error("poll error: %s", msg.error()) continue handle(msg.value()) c.commit(msg)c.close()Consumer() connects in the background. Until a broker has answered, poll() returns
None after its timeout, and connect failures go to error_cb (or the ERROR log). Poll
errors come back as a Message whose error() is a KafkaError, never as an exception.
consume(num_messages, timeout) returns a list of messages the same way.
Offsets
Section titled “Offsets”c.commit(offsets=[TopicPartition("orders", 0, 1234)]) # absolute next-read positionc.assign([TopicPartition("orders", 0, 1234), TopicPartition("orders", 1, OFFSET_BEGINNING)])c.seek(TopicPartition("orders", 0, OFFSET_END))tps = c.assignment()positions = c.position(tps)committed = c.committed(tps)With auto-commit on, commits send the offset of the last message poll() returned. With
enable.auto.offset.store=false, only store_offsets() advances commits; stage before the
next poll().
TopicPartition compares, hashes and sorts on (topic, partition) alone, as in confluent,
so TopicPartition("t", 0, 5) == TopicPartition("t", 0, 7).
Callbacks
Section titled “Callbacks”subscribe(topics, on_assign=, on_revoke=, on_lost=) takes confluent’s rebalance callbacks,
called as callback(consumer, partitions). Offsets staged with store_offsets are committed
for revoked partitions after on_revoke returns. commit() inside on_lost raises
KafkaException.
on_commit(err, partitions) reports the default asynchronous commit() and each automatic
commit, from poll(), consume() and close(). A commit(asynchronous=False) returns or
raises instead.
Partition EOF
Section titled “Partition EOF”With enable.partition.eof=true, poll() and consume() return a message with
error().code() == KafkaError._PARTITION_EOF at the end of a partition. commit(message=)
and store_offsets(message=) refuse it.
Threads
Section titled “Threads”Other threads can call commit, store_offsets, seek, pause and the rest while one
thread polls. A second concurrent poll() raises KafkaException with _CONFLICT.
close() from another thread ends the running poll with None.
from pyrula.kafka.compat.confluent.admin import AdminClient, NewTopic
admin = AdminClient({"bootstrap.servers": "localhost:9092"})futures = admin.create_topics( [NewTopic("orders", num_partitions=6, replication_factor=3, config={"cleanup.policy": "compact"})], operation_timeout=30,)for topic, future in futures.items(): try: future.result() except KafkaException as exc: if exc.args[0].code() != KafkaError.TOPIC_ALREADY_EXISTS: raisecreate_topics, delete_topics, create_partitions and list_topics return a dict of
already-resolved futures per topic. NewTopic(replica_assignment=...) raises
NotImplementedError.
Schema Registry serializers
Section titled “Schema Registry serializers”SerializingProducer, DeserializingConsumer and the Avro and JSON Schema serializers use
confluent’s import paths:
from pyrula.kafka.compat.confluent import SerializingProducer, DeserializingConsumerfrom pyrula.kafka.compat.confluent.schema_registry import SchemaRegistryClientfrom pyrula.kafka.compat.confluent.schema_registry.avro import AvroSerializer, AvroDeserializerfrom pyrula.kafka.compat.confluent.schema_registry.json_schema import JSONSerializer, JSONDeserializerfrom pyrula.kafka.compat.confluent.serialization import StringSerializer, StringDeserializer
registry = SchemaRegistryClient({"url": "http://localhost:8081"})
p = SerializingProducer({ "bootstrap.servers": "localhost:9092", "key.serializer": StringSerializer("utf_8"), "value.serializer": AvroSerializer(registry, ORDER_SCHEMA),})p.produce("orders", key="o-1", value={"id": 1, "sku": "sku-1", "qty": 2})p.flush()
c = DeserializingConsumer({ "bootstrap.servers": "localhost:9092", "group.id": "g", "key.deserializer": StringDeserializer("utf_8"), "value.deserializer": AvroDeserializer(registry, ORDER_SCHEMA),})c.subscribe(["orders"])msg = c.poll(1.0) # msg.key() -> "o-1", msg.value() -> the dictThey wrap the native AvroRegistrySerde / JsonSchemaRegistrySerde and
speak the Confluent wire format. Schemas are registered under <topic>-key /
<topic>-value on first serialize.
Not supported:
subject.name.strategyother thantopic_subject_name_strategy(NotImplementedError).auto.register.schemas=False(NotImplementedError); other serializerconfkeys such asuse.latest.versionraiseValueError.AvroDeserializer(return_record_name=True)(NotImplementedError).DeserializingConsumer.consume()(NotImplementedError); usepoll().ProtobufSerializerandProtobufDeserializer(NotImplementedErroron construction). The nativeProtobufRegistrySerdeworks.
JSONSerializer does not validate documents against the schema.
Differences from librdkafka
Section titled “Differences from librdkafka”- The default asynchronous
commit()does not raise its failure, as in confluent. A failure goes toon_commit, or is logged atWARNINGwhen there is none. msg.timestamp()never reportsTIMESTAMP_LOG_APPEND_TIME.- Keyless records are sticky under every
partitionerspelling. librdkafka sends them all to one partition underconsistentandmurmur2. request.timeout.msbounds broker requests on a consumer (30 s default); librdkafka ignores it there.- Confluent’s
fnv1a,fnv1a_randomandrandompartitioners are refused. - Partition EOF messages have
value()ofNone, where confluent’s is the error text. - Statistics carry librdkafka’s top-level counters only; per-broker, per-topic and group sections are absent.
consumer_group_metadata()isNonebefore the first connection.