Skip to content

Commit d52858c

Browse files
author
Ryan P
authored
Add throttle_cb support (confluentinc#237) (confluentinc#377)
1 parent b01e50b commit d52858c

File tree

9 files changed

+272
-16
lines changed

9 files changed

+272
-16
lines changed

README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,9 @@ In order to run full test suite, simply execute:
277277
**NOTE**: Requires `tox` (please install with `pip install tox`), several supported versions of Python on your path, and `librdkafka` [installed](tools/bootstrap-librdkafka.sh) into `tmp-build`.
278278

279279

280-
**Run integration tests:**
281-
282-
To run the integration tests, uncomment the following line from `tox.ini` and add the paths to your Kafka and Confluent Schema Registry instances. You can also run the integration tests outside of `tox` by running this command from the source root.
283-
284-
examples/integration_test.py <kafka-broker> [<test-topic>] [<schema-registry>]
285-
286-
**WARNING**: These tests require an active Kafka cluster and will create new topics.
280+
**Integration tests:**
287281

282+
See [tests/README.md](tests/README.md) for instructions on how to run integration tests.
288283

289284

290285

confluent_kafka/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,3 +646,25 @@ def __repr__(self):
646646

647647
def __str__(self):
648648
return "{}".format(self.partition)
649+
650+
651+
class ThrottleEvent (object):
652+
"""
653+
ThrottleEvent contains details about a throttled request.
654+
655+
This class is typically not user instantiated.
656+
657+
:ivar broker_name str: The hostname of the broker which throttled the request
658+
:ivar broker_id int: The broker id
659+
:ivar throttle_time float: The amount of time (in seconds) the broker throttled (delayed) the request
660+
"""
661+
def __init__(self, broker_name,
662+
broker_id,
663+
throttle_time):
664+
665+
self.broker_name = broker_name
666+
self.broker_id = broker_id
667+
self.throttle_time = throttle_time
668+
669+
def __str__(self):
670+
return "{}/{} throttled for {} ms".format(self.broker_name, self.broker_id, int(self.throttle_time * 1000))

confluent_kafka/src/confluent_kafka.c

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,63 @@ static void error_cb (rd_kafka_t *rk, int err, const char *reason, void *opaque)
11871187
CallState_resume(cs);
11881188
}
11891189

1190+
/**
1191+
* @brief librdkafka throttle callback triggered by poll() or flush(), triggers the
1192+
* corresponding Python throttle_cb
1193+
*/
1194+
static void throttle_cb (rd_kafka_t *rk, const char *broker_name, int32_t broker_id,
1195+
int throttle_time_ms, void *opaque) {
1196+
Handle *h = opaque;
1197+
PyObject *ThrottleEvent_type, *throttle_event;
1198+
PyObject *result, *kwargs, *args;
1199+
CallState *cs;
1200+
1201+
cs = CallState_get(h);
1202+
if (!h->throttle_cb) {
1203+
/* No callback defined */
1204+
goto done;
1205+
}
1206+
1207+
ThrottleEvent_type = cfl_PyObject_lookup("confluent_kafka",
1208+
"ThrottleEvent");
1209+
1210+
if (!ThrottleEvent_type) {
1211+
/* ThrottleEvent class not found */
1212+
goto err;
1213+
}
1214+
1215+
args = Py_BuildValue("(sid)", broker_name, broker_id, (double)throttle_time_ms/1000);
1216+
throttle_event = PyObject_Call(ThrottleEvent_type, args, NULL);
1217+
1218+
Py_DECREF(args);
1219+
Py_DECREF(ThrottleEvent_type);
1220+
1221+
if (!throttle_event) {
1222+
/* Failed to instantiate ThrottleEvent object */
1223+
goto err;
1224+
}
1225+
1226+
result = PyObject_CallFunctionObjArgs(h->throttle_cb, throttle_event, NULL);
1227+
1228+
Py_DECREF(throttle_event);
1229+
1230+
if (result) {
1231+
/* throttle_cb executed successfully */
1232+
Py_DECREF(result);
1233+
goto done;
1234+
}
1235+
1236+
/**
1237+
* Stop callback dispatcher, return err to application
1238+
* fall-through to unlock GIL
1239+
*/
1240+
err:
1241+
CallState_crash(cs);
1242+
rd_kafka_yield(h->rk);
1243+
done:
1244+
CallState_resume(cs);
1245+
}
1246+
11901247
static int stats_cb(rd_kafka_t *rk, char *json, size_t json_len, void *opaque) {
11911248
Handle *h = opaque;
11921249
PyObject *eo = NULL, *result = NULL;
@@ -1266,6 +1323,9 @@ void Handle_clear (Handle *h) {
12661323
if (h->error_cb)
12671324
Py_DECREF(h->error_cb);
12681325

1326+
if (h->throttle_cb)
1327+
Py_DECREF(h->throttle_cb);
1328+
12691329
if (h->stats_cb)
12701330
Py_DECREF(h->stats_cb);
12711331

@@ -1287,6 +1347,9 @@ int Handle_traverse (Handle *h, visitproc visit, void *arg) {
12871347
if (h->error_cb)
12881348
Py_VISIT(h->error_cb);
12891349

1350+
if (h->throttle_cb)
1351+
Py_VISIT(h->throttle_cb);
1352+
12901353
if (h->stats_cb)
12911354
Py_VISIT(h->stats_cb);
12921355

@@ -1620,6 +1683,28 @@ rd_kafka_conf_t *common_conf_setup (rd_kafka_type_t ktype,
16201683
Py_XDECREF(ks8);
16211684
Py_DECREF(ks);
16221685
continue;
1686+
} else if (!strcmp(k, "throttle_cb")) {
1687+
if (!PyCallable_Check(vo)) {
1688+
PyErr_SetString(PyExc_ValueError,
1689+
"expected throttle_cb property "
1690+
"as a callable function");
1691+
rd_kafka_topic_conf_destroy(tconf);
1692+
rd_kafka_conf_destroy(conf);
1693+
Py_XDECREF(ks8);
1694+
Py_DECREF(ks);
1695+
return NULL;
1696+
}
1697+
if (h->throttle_cb) {
1698+
Py_DECREF(h->throttle_cb);
1699+
h->throttle_cb = NULL;
1700+
}
1701+
if (vo != Py_None) {
1702+
h->throttle_cb = vo;
1703+
Py_INCREF(h->throttle_cb);
1704+
}
1705+
Py_XDECREF(ks8);
1706+
Py_DECREF(ks);
1707+
continue;
16231708
} else if (!strcmp(k, "stats_cb")) {
16241709
if (!PyCallable_Check(vo)) {
16251710
PyErr_SetString(PyExc_TypeError,
@@ -1719,6 +1804,9 @@ rd_kafka_conf_t *common_conf_setup (rd_kafka_type_t ktype,
17191804
if (h->error_cb)
17201805
rd_kafka_conf_set_error_cb(conf, error_cb);
17211806

1807+
if (h->throttle_cb)
1808+
rd_kafka_conf_set_throttle_cb(conf, throttle_cb);
1809+
17221810
if (h->stats_cb)
17231811
rd_kafka_conf_set_stats_cb(conf, stats_cb);
17241812

confluent_kafka/src/confluent_kafka.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ typedef struct {
180180
PyObject_HEAD
181181
rd_kafka_t *rk;
182182
PyObject *error_cb;
183+
PyObject *throttle_cb;
183184
PyObject *stats_cb;
184185
int initiated;
185186

docs/index.rst

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,17 @@ The Python bindings also provide some additional configuration properties:
8989
* ``default.topic.config``: value is a dict of topic-level configuration
9090
properties that are applied to all used topics for the instance.
9191

92-
* ``error_cb(kafka.KafkaError)``: Callback for generic/global error events. This callback is served by
93-
poll().
92+
* ``error_cb(kafka.KafkaError)``: Callback for generic/global error events. This callback is served upon calling
93+
``client.poll()`` or ``producer.flush()``.
9494

95-
* ``stats_cb(json_str)``: Callback for statistics data. This callback is triggered by poll()
95+
* ``throttle_cb(confluent_kafka.ThrottleEvent)``: Callback for throttled request reporting.
96+
This callback is served upon calling ``client.poll()`` or ``producer.flush()``.
97+
98+
* ``stats_cb(json_str)``: Callback for statistics data. This callback is triggered by poll() or flush
9699
every ``statistics.interval.ms`` (needs to be configured separately).
97100
Function argument ``json_str`` is a str instance of a JSON document containing statistics data.
101+
This callback is served upon calling ``client.poll()`` or ``producer.flush()``. See
102+
https://github.com/edenhill/librdkafka/wiki/Statistics" for more information.
98103

99104
* ``on_delivery(kafka.KafkaError, kafka.Message)`` (**Producer**): value is a Python function reference
100105
that is called once for each produced message to indicate the final
@@ -103,15 +108,15 @@ The Python bindings also provide some additional configuration properties:
103108
(or ``on_delivery=callable``) to the confluent_kafka.Producer.produce() function.
104109
Currently message headers are not supported on the message returned to the
105110
callback. The ``msg.headers()`` will return None even if the original message
106-
had headers set.
111+
had headers set. This callback is served upon calling ``producer.poll()`` or ``producer.flush()``.
107112

108113
* ``on_commit(kafka.KafkaError, list(kafka.TopicPartition))`` (**Consumer**): Callback used to indicate success or failure
109-
of commit requests.
114+
of commit requests. This callback is served upon calling ``consumer.poll()``.
110115

111116
* ``logger=logging.Handler`` kwarg: forward logs from the Kafka client to the
112117
provided ``logging.Handler`` instance.
113118
To avoid spontaneous calls from non-Python threads the log messages
114-
will only be forwarded when ``client.poll()`` is called.
119+
will only be forwarded when ``client.poll()`` or ``producer.flush()`` are called.
115120

116121
mylogger = logging.getLogger()
117122
mylogger.addHandler(logging.StreamHandler())

examples/integration_test.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
# global variable to be set by stats_cb call back function
6363
good_stats_cb_result = False
6464

65+
# global variable to be incremented by throttle_cb call back function
66+
throttled_requests = 0
67+
6568
# Shared between producer and consumer tests and used to verify
6669
# that consumed headers are what was actually produced.
6770
produce_headers = [('foo1', 'bar'),
@@ -86,6 +89,18 @@ def error_cb(err):
8689
print('Error: %s' % err)
8790

8891

92+
def throttle_cb(throttle_event):
93+
# validate argument type
94+
assert isinstance(throttle_event.broker_name, str)
95+
assert isinstance(throttle_event.broker_id, int)
96+
assert isinstance(throttle_event.throttle_time, float)
97+
98+
global throttled_requests
99+
throttled_requests += 1
100+
101+
print(throttle_event)
102+
103+
89104
class InMemorySchemaRegistry(object):
90105

91106
schemas = {}
@@ -843,6 +858,59 @@ def my_on_revoke(consumer, partitions):
843858
c.close()
844859

845860

861+
def verify_throttle_cb():
862+
""" Verify throttle_cb is invoked
863+
This test requires client quotas be configured.
864+
See tests/README.md for more information
865+
"""
866+
conf = {'bootstrap.servers': bootstrap_servers,
867+
'api.version.request': api_version_request,
868+
'linger.ms': 500,
869+
'client.id': 'throttled_client',
870+
'throttle_cb': throttle_cb}
871+
872+
p = confluent_kafka.Producer(conf)
873+
874+
msgcnt = 1000
875+
msgsize = 100
876+
msg_pattern = 'test.py throttled client'
877+
msg_payload = (msg_pattern * int(msgsize / len(msg_pattern)))[0:msgsize]
878+
879+
msgs_produced = 0
880+
msgs_backpressure = 0
881+
print('# producing %d messages to topic %s' % (msgcnt, topic))
882+
883+
if with_progress:
884+
bar = Bar('Producing', max=msgcnt)
885+
else:
886+
bar = None
887+
888+
for i in range(0, msgcnt):
889+
while True:
890+
try:
891+
p.produce(topic, value=msg_payload)
892+
break
893+
except BufferError:
894+
# Local queue is full (slow broker connection?)
895+
msgs_backpressure += 1
896+
if bar is not None and (msgs_backpressure % 1000) == 0:
897+
bar.next(n=0)
898+
p.poll(100)
899+
continue
900+
901+
if bar is not None and (msgs_produced % 5000) == 0:
902+
bar.next(n=5000)
903+
msgs_produced += 1
904+
p.poll(0)
905+
906+
if bar is not None:
907+
bar.finish()
908+
909+
p.flush()
910+
print('# %d of %d produce requests were throttled' % (throttled_requests, msgs_produced))
911+
assert throttled_requests >= 1
912+
913+
846914
def verify_stats_cb():
847915
""" Verify stats_cb """
848916

@@ -1054,7 +1122,9 @@ def verify_config(expconfig, configs):
10541122
print("Topic {} marked for deletion".format(our_topic))
10551123

10561124

1057-
all_modes = ['consumer', 'producer', 'avro', 'performance', 'admin', 'none']
1125+
# Exclude throttle since from default list
1126+
default_modes = ['consumer', 'producer', 'avro', 'performance', 'admin']
1127+
all_modes = default_modes + ['throttle', 'none']
10581128
"""All test modes"""
10591129

10601130

@@ -1095,7 +1165,7 @@ def print_usage(exitcode, reason=None):
10951165
print_usage(1)
10961166

10971167
if len(modes) == 0:
1098-
modes = all_modes
1168+
modes = default_modes
10991169

11001170
print('Using confluent_kafka module version %s (0x%x)' % confluent_kafka.version())
11011171
print('Using librdkafka version %s (0x%x)' % confluent_kafka.libversion())
@@ -1133,6 +1203,11 @@ def print_usage(exitcode, reason=None):
11331203
print('=' * 30, 'Verifying stats_cb', '=' * 30)
11341204
verify_stats_cb()
11351205

1206+
# The throttle test is utilizing the producer.
1207+
if 'throttle' in modes:
1208+
print('=' * 30, 'Verifying throttle_cb', '=' * 30)
1209+
verify_throttle_cb()
1210+
11361211
if 'avro' in modes:
11371212
print('=' * 30, 'Verifying AVRO', '=' * 30)
11381213
verify_avro()

tests/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,48 @@ From top-level directory run:
66
$ tox
77

88
**NOTE**: This requires `tox` ( please install with `pip install tox` ) and several supported versions of Python.
9+
10+
Integration tests
11+
=================
12+
13+
**WARNING**: These tests require an active Kafka cluster and will create new topics.
14+
15+
16+
To run all of the integration test `modes` uncomment the following line from `tox.ini` and add the addresses to your Kafka and Confluent Schema Registry instances.
17+
18+
#python examples/integration_test.py <bootstrap-servers> confluent-kafka-testing [<schema-registry-url>]
19+
20+
You can also run the integration tests outside of `tox` by running this command from the source root directory
21+
22+
examples/integration_test.py <kafka-broker> [<test-topic>] [<schema-registry>]
23+
24+
To run individual integration test `modes` use the following syntax
25+
26+
examples/integration_test.py --<test mode> <kafka-broker> [<test-topic>] [<schema-registry>]
27+
28+
For example:
29+
30+
examples/integration_test.py --producer <kafka-broker> [<test-topic>]
31+
32+
To get a list of modes you can run the integration test manually with the `--help` flag
33+
34+
examples/integration_tests.py --help
35+
36+
37+
The throttle_cb integration test requires an additional step and as such is not included in the default test modes.
38+
In order to execute the throttle_cb test you must first set a throttle for the client 'throttled_client' with the command below:
39+
40+
kafka-configs --zookeeper <zookeeper host>:<zookeeper port> \
41+
--alter --add-config 'request_percentage=01' \
42+
--entity-name throttled_client --entity-type clients
43+
44+
Once the throttle has been set you can proceed with the following command:
45+
46+
examples/integration_test.py --throttle <kafka-broker> [<test-topic>]
47+
48+
49+
To remove the throttle you can execute the following
50+
51+
kafka-configs --zookeeper <zookeeper host>:<zookeeper port> \
52+
--alter --delete-config 'request_percentage' \
53+
--entity-name throttled_client --entity-type clients

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy