Consumer¶
What is Consumer¶
The Consumer is a program that continuously pulls records from a stream system and processes the data, either sequentially or in parallel. Typically, we require a ‘pointer’ that tells the consumer where to start pulling the data. In Kafka, it is referred to as an ‘offset’; in AWS Kinesis Stream, it is known as a ‘shard iterator’; and in Pulsar, it is called a ‘message id’.
In the previous document, we introduced the concept of Checkpoint. A consumer program essentially leverages the checkpoint, updating the processing status before and after executing processing logic, and handling errors appropriately. It also persists the checkpoint data to the storage backend every time it changes.
Who Implements What¶
get_records()andnew()— Plugin/backend developers implement these to pull records from a specific streaming backend (e.g. Kinesisget_records, Kafka poll).process_record(record)— End users must implement this with their business logic. Raise an exception to indicate failure; the framework handles retries automatically.process_failed_record(record)— End users may override this to send failed records to a DLQ. Default is no-op.process_batch(),run()— End users call these to start consuming. Already implemented inBaseConsumer.
What is Dead-Letter-Queue (DLQ)¶
Some records may still fail after multiple retries. Typically, we aim to ensure smooth data processing without blocking it. In business-critical applications, it’s common practice to route failed data to a dedicated location, often a message queue or another stream system. This allows for debugging and later reprocessing.
In certain use cases, it’s critical to process records strictly in order. If a preceding processing attempt fails, we must stop from processing subsequent records. In such scenarios, we should halt processing and trigger a notification for immediate investigation. In any case, a Dead-Letter Queue (DLQ) serves as an additional fault-tolerant layer for business-critical use cases.
Simple Consumer Example¶
Below is the sample usage of SimpleConsumer, a simple consumer that read data from the output of SimpleProducer.
simple_consumer.py
1# -*- coding: utf-8 -*-
2
3import random
4import time
5import shutil
6import dataclasses
7from pathlib import Path
8
9from unistream.api import (
10 DataClassRecord,
11 SimpleCheckpoint,
12 SimpleConsumer,
13)
14
15
16def rand_value() -> int:
17 return random.randint(1, 100)
18
19
20@dataclasses.dataclass(frozen=True)
21class MyRecord(DataClassRecord):
22 value: int = dataclasses.field(default_factory=rand_value)
23
24
25class RandomError(Exception):
26 pass
27
28
29@dataclasses.dataclass
30class MyConsumer(SimpleConsumer):
31 path_target: Path = dataclasses.field(init=False)
32
33 def process_record(self, record: MyRecord) -> str:
34 s = record.serialize()
35 if random.randint(1, 100) <= 50:
36 print(f"❌ {s}")
37 raise RandomError(f"random error at record_id = {record.id}")
38 else:
39 with self.path_target.open("a") as f:
40 f.write(f"{s}\n")
41 print(f"✅ {s}")
42 return s
43
44 def process_failed_record(self, record: MyRecord) -> str:
45 s = record.serialize()
46 if random.randint(1, 100) <= 0:
47 print(f"❌ DLQ:{s}")
48 raise RandomError(f"{s}")
49 else:
50 with self.path_dlq.open("a") as f:
51 f.write(f"{s}\n")
52 print(f"✅ DLQ: {s}")
53 return s
54
55
56dir_here = Path(__file__).absolute().parent
57dir_demo = dir_here.joinpath("simple_consumer_demo")
58shutil.rmtree(dir_demo, ignore_errors=True)
59dir_demo.mkdir(exist_ok=True)
60
61consumer_id = "simple_consumer_1"
62path_checkpoint = dir_demo.joinpath(f"{consumer_id}.checkpoint.json")
63path_records = dir_demo.joinpath(f"{consumer_id}.records.json")
64path_target = dir_demo.joinpath(f"{consumer_id}.target.txt")
65path_dlq = dir_demo.joinpath(f"{consumer_id}.dlq.txt")
66
67checkpoint = SimpleCheckpoint.load(
68 checkpoint_file=str(path_checkpoint),
69 records_file=str(path_records),
70)
71
72consumer = MyConsumer.new(
73 record_class=MyRecord,
74 checkpoint=checkpoint,
75 path_source=dir_here.joinpath(
76 "simple_producer_demo", "simple_producer_history.log"
77 ),
78 path_dlq=path_dlq,
79 limit=3,
80 delay=1,
81)
82consumer.path_target = path_target
83
84
85# --- method 1 ---
86# consumer.run()
87
88# --- method 2 ---
89def run():
90 i = 0
91 while 1:
92 i += 1
93 print(f"--- {i} th pull ---")
94 consumer.process_batch()
95 if consumer.delay:
96 time.sleep(consumer.delay)
97
98run()
Vendor-Specific Consumers¶
Note
Vendor-specific consumers (AWS Kinesis Stream, Apache Kafka, etc.) are released as separate plugin packages — for example unistream-aws-kinesis, unistream-kafka. Each plugin ships its own concrete Consumer and (when relevant) DLQ variants. Refer to those plugins for installation and usage examples.