Topic sudah menunggu; sekarang kita mengisi sisi penulisnya. Producer Kafka bertanggung jawab atas tiga hal sebelum sebuah pesan menempel di log: melakukan serialisasi nilai menjadi bytes, memilih partition tujuan, lalu menyerahkan pesan ke broker dan melaporkan hasilnya secara asinkron. Bagian ini membangun producer proyek dengan bentuk API yang mengikuti kafka-python — send() mengembalikan future, flush() memaksa pengiriman — lalu membungkusnya dalam SimulatorCheckout, tiruan halaman checkout yang menerbitkan event pesanan TokoKita. Mekanika acks dan record batch sudah dibedah pada chapter Producers dan Consumers; kode di sini adalah penerapannya.
Satu Protokol, Dua Backend #
Producer dan consumer proyek ini ditulis menghadap protokol kecil — send(topic, key, value), flush(), close() — yang dipenuhi dua implementasi: simulasi LocalProducer dan adapter KafkaProducer dari kafka-python untuk cluster KRaft sungguhan. Pabrik berikut menjadi satu-satunya titik pergantian:
def buat_producer(backend: str = "local", **kwargs):
"""Pabrik producer sesuai protokol (``send``/``flush``/``close``).
``backend="local"`` mengembalikan :class:`LocalProducer` (butuh ``broker``
berupa :class:`~src.local_kafka.LocalKafka`). ``backend="kafka"`` memuat
adapter kafka-python untuk cluster sungguhan; adapter tersebut tidak
dijalankan di lingkungan pengembangan proyek ini.
"""
if backend == "local":
return LocalProducer(**kwargs)
if backend == "kafka":
from .kafka_client_adapter import buat_producer_kafka
return buat_producer_kafka(**kwargs)
raise ValueError(f"backend tidak dikenal: {backend!r}")Adapter src/kafka_client_adapter.py menerjemahkan protokol yang sama ke pemanggilan kafka.KafkaProducer dengan value_serializer JSON UTF-8 dan acks="all"; berkas tersebut diverifikasi sintaks dengan py_compile dan tidak dijalankan di lingkungan ini karena pustaka kafka-python tidak terpasang. Berpindah ke cluster nyata nanti cukup mengganti satu argumen: buat_producer(backend="kafka", bootstrap_servers="localhost:9092").
send() dan flush() pada LocalProducer #
Inti producer ada di LocalProducer.send pada src/local_kafka.py:
def send(
self,
topic: str,
key: str | bytes | None = None,
value: str | bytes | None = None,
partition: int | None = None,
timestamp_ms: int | None = None,
) -> FutureRecordMetadata:
"""Buffer satu pesan; partition dipilih partitioner bila tidak ditentukan."""
self._broker._cek_topik(topic)
jumlah = len(self._broker.partitions_for_topic(topic))
if partition is None:
partition = self._partitioner.pilih(key, jumlah)
tp = TopicPartition(topic, partition)
future = FutureRecordMetadata()
pesan = {
"key": _baca_bytes(key, "key"),
"value": _baca_bytes(value, "value"),
"timestamp_ms": timestamp_ms,
}
self._buffer.append((tp, pesan, future))
if self.linger_ms == 0:
self.flush()
elif sum(1 for tp2, _, _ in self._buffer if tp2 == tp) >= self.batch_size:
self.flush()
return futureMari kita telaah baris-baris kode di atas (nomor mengikuti posisi di src/local_kafka.py):
- Baris 345 (
_cek_topik): producer menolak menulis ke topic yang belum dibuat — pada simulasi, topik tidak dibuat otomatis agar kesalahan konfigurasi tampak sejak dini. - Baris 347-348 (
partition = self._partitioner.pilih(...)): partition dipilih partitioner hanya bila pemanggil tidak menentukannya; pesan berkunci di-hash (Bagian 5), pesan tanpa kunci bergilir. - Baris 350-355 (
futuredanpesan):send()tidak langsung menulis; pesan masuk buffer per partition dan future dikembalikan segera — sama seperti kafka-python yang menumpuk pesan di record batch sebelum benar-benar mengirim. - Baris 357-360 (
flush()bersyarat):linger_ms=0mengirim langsung (perilaku bawaan);linger_ms>0menahan pesan sampaiflush()dipanggil eksplisit atau buffer partition mencapaibatch_size— model sederhana darilinger.msdanbatch.sizeyang menukar sedikit latensi dengan throughput. - Baris 361 (
return future): nasib pesan sesungguhnya dibaca belakangan lewatfuture.get(), yang baru terisi setelahflush()menulis ke log broker.
Simulator Checkout #
Halaman checkout web tidak tahu apa-apa soal Kafka; kode aplikasi cukup memanggil simulasi berikut. Kelas ini juga menjaga penomoran event antar panggilan sehingga hasil produksi bisa diperpanjang tanpa menimpa:
def kirim(self, jumlah: int) -> list[RecordMetadata]:
"""Kirim ``jumlah`` event baru; nomor event berlanjut antar panggilan."""
events = buat_pesanan_events(
jumlah, seed=self._seed, mulai_nomor=self._nomor + 1,
bobot_wilayah=self._bobot,
)
self._nomor += jumlah
return self.kirim_events(events)
def kirim_events(self, events: list[dict]) -> list[RecordMetadata]:
"""Kirim daftar event apa adanya; kembalikan metadata tiap pesan."""
futures = [
self._producer.send(
self.topic,
key=event.get(self._kunci),
value=serialisasi_event(event),
)
for event in events
]
self._producer.flush()
return [future.get() for future in futures]kirim_events memperlihatkan pola producer yang benar: seluruh send() ditumpuk lebih dulu, flush() mengirim semuanya, dan baru setelah itu future.get() diambil — memanggil get() sebelum flush akan menolak karena pesan masih di buffer (perilaku yang dikunci tes test_producer_send_flush_dan_future).
Menjalankan Producer #
Simulasi berikut mengirim 10 event pesanan berkunci pelanggan_id lalu memeriksa isi partition:
import json
from src.local_kafka import LocalKafka
from src.producer import SimulatorCheckout, buat_producer
broker = LocalKafka()
broker.create_topic("tokokita.pesanan", num_partitions=3)
producer = buat_producer(broker=broker, client_id="checkout-web")
checkout = SimulatorCheckout(producer, topic="tokokita.pesanan", kunci="pelanggan_id")
metadatas = checkout.kirim(10)
for m in metadatas[:4]:
print(f"terkirim: topic={m.topic} partition={m.partition} offset={m.offset}")
print(f"... total {len(metadatas)} event terkirim")
print(f"describe_topic: {broker.describe_topic('tokokita.pesanan')}")
print("isi partition 0 (kunci -> event):")
for rekam in broker.baca("tokokita.pesanan", 0):
event = json.loads(rekam.value.decode("utf-8"))
print(f" offset {rekam.offset}: {rekam.key.decode()} -> {event['event_id']}")Output yang Dihasilkan:
terkirim: topic=tokokita.pesanan partition=0 offset=0
terkirim: topic=tokokita.pesanan partition=0 offset=1
terkirim: topic=tokokita.pesanan partition=0 offset=2
terkirim: topic=tokokita.pesanan partition=2 offset=0
... total 10 event terkirim
describe_topic: {'topic': 'tokokita.pesanan', 'partitions': 3, 'total_pesan': 10}
isi partition 0 (kunci -> event):
offset 0: PLG-115 -> evt-000001
offset 1: PLG-191 -> evt-000002
offset 2: PLG-006 -> evt-000003
offset 3: PLG-124 -> evt-000005Dua hal terbaca dari output: offset bertambah monoton per partition (0, 1, 2, 3 pada partition 0), dan evt-000004 tidak ada di partition 0 karena kunci pelanggannya (misalnya PLG-019) di-hash ke partition lain — bukan karena pesan hilang. Kelima pesanan partition 0 tetap terurut sesuai urutan pengiriman; jaminan urutan inilah yang membuat kunci penting, dan kita ukur konsekuensinya pada Bagian 5.
Event sudah mengalir ke topic. Sekarang kita bangun sisi pembacanya: consumer dengan poll loop yang mengagregasi jendela per menit.