-
Notifications
You must be signed in to change notification settings - Fork 141
Expand file tree
/
Copy pathtest_kafka_header_filter.py
More file actions
154 lines (129 loc) · 5.34 KB
/
Copy pathtest_kafka_header_filter.py
File metadata and controls
154 lines (129 loc) · 5.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
"""End-to-end tests for the Kafka input connector's header filter.
Each test creates a pipeline whose Kafka input connector carries a
``header_filter``, produces messages with various headers, and checks that only
the messages satisfying the filter reach the table. A final admitted
"sentinel" message (produced last on a single-partition topic, so it is
processed last) marks completeness: once it appears, every earlier message has
been decided, so the observed row set is final.
"""
import json
import uuid
from typing import Any, Optional
from confluent_kafka import Producer
from confluent_kafka.admin import AdminClient, NewTopic
from feldera import Pipeline, PipelineBuilder
from tests import KAFKA_BOOTSTRAP, TEST_CLIENT
from tests.platform.helper import wait_for_condition
SENTINEL_ID = 100
def _random_topic(prefix: str) -> str:
return f"{prefix}-{uuid.uuid4().hex[:12]}"
def _create_topic(admin: AdminClient, topic: str) -> None:
futures = admin.create_topics(
[NewTopic(topic=topic, num_partitions=1, replication_factor=1)]
)
futures[topic].result(timeout=30)
def _delete_topic_best_effort(admin: AdminClient, topic: str) -> None:
try:
futures = admin.delete_topics([topic], operation_timeout=10)
futures[topic].result(timeout=10)
except Exception:
# Topic deletion can be disabled on some brokers; cleanup is best-effort.
pass
def _produce(
topic: str, records: list[tuple[int, Optional[list[tuple[str, bytes]]]]]
) -> None:
"""Produce ``{"id": <id>}`` messages, each with the given headers (or none)."""
producer = Producer({"bootstrap.servers": KAFKA_BOOTSTRAP})
for record_id, headers in records:
producer.produce(
topic,
value=json.dumps({"id": record_id}).encode("utf-8"),
headers=headers,
)
remaining = producer.flush(timeout=30)
assert remaining == 0, f"failed to flush Kafka messages, remaining={remaining}"
def _run_filter_test(
pipeline_name: str,
header_filter: dict[str, Any],
records: list[tuple[int, Optional[list[tuple[str, bytes]]]]],
expected_ids: set[int],
) -> None:
admin = AdminClient({"bootstrap.servers": KAFKA_BOOTSTRAP})
topic = _random_topic("header-filter-in")
_create_topic(admin, topic)
input_connector = {
"name": "kafka_in",
"transport": {
"name": "kafka_input",
"config": {
"topic": topic,
"bootstrap.servers": KAFKA_BOOTSTRAP,
"start_from": "earliest",
"header_filter": header_filter,
},
},
"format": {"name": "json", "config": {"update_format": "raw", "array": False}},
}
sql = f"""
CREATE TABLE input_t(id INT) WITH (
'connectors' = '{json.dumps([input_connector])}'
);
CREATE MATERIALIZED VIEW output_v AS SELECT * FROM input_t;
""".strip()
pipeline: Pipeline = PipelineBuilder(
TEST_CLIENT, name=pipeline_name, sql=sql
).create_or_replace()
pipeline.start()
def ingested_ids() -> set[int]:
return {row["id"] for row in pipeline.query("SELECT id FROM output_v")}
try:
# The sentinel is produced last; the filter must admit it.
assert SENTINEL_ID in expected_ids
_produce(topic, records)
wait_for_condition(
"sentinel message ingested",
lambda: SENTINEL_ID in ingested_ids(),
timeout_s=60.0,
poll_interval_s=1.0,
)
# The sentinel is last on a single-partition topic, so ingestion is now
# complete: the observed set is final and must equal the expected set.
assert ingested_ids() == expected_ids
finally:
pipeline.stop(force=True)
_delete_topic_best_effort(admin, topic)
def test_kafka_header_filter_leaf(pipeline_name):
"""A single regex leaf admits only whole-value matches; anchoring and
missing headers behave as documented."""
header_filter = {"header": {"name": "event", "pattern": "created|updated"}}
records = [
(1, [("event", b"created")]), # admit
(2, [("event", b"deleted")]), # drop: wrong value
(3, [("event", b"updated")]), # admit
(4, None), # drop: header absent
(5, [("event", b"created-x")]), # drop: anchored, no substring match
(SENTINEL_ID, [("event", b"created")]), # admit (sentinel)
]
_run_filter_test(pipeline_name, header_filter, records, {1, 3, SENTINEL_ID})
def test_kafka_header_filter_boolean(pipeline_name):
"""A boolean filter combines several header tests with and/or/not."""
header_filter = {
"and": [
{
"or": [
{"header": {"name": "env", "pattern": "prod"}},
{"header": {"name": "env", "pattern": "staging"}},
]
},
{"not": {"header": {"name": "skip", "pattern": "true"}}},
]
}
records = [
(1, [("env", b"prod")]), # admit
(2, [("env", b"dev")]), # drop: fails `or`
(3, [("env", b"staging"), ("skip", b"false")]), # admit: skip != "true"
(4, [("env", b"prod"), ("skip", b"true")]), # drop: fails `not`
(5, [("env", b"staging")]), # admit
(SENTINEL_ID, [("env", b"prod")]), # admit (sentinel)
]
_run_filter_test(pipeline_name, header_filter, records, {1, 3, 5, SENTINEL_ID})