@@ -393,6 +393,107 @@ static PyObject *ShareConsumer_poll(ShareConsumerHandle *self,
393393}
394394
395395
396+ /**
397+ * @brief Acknowledge previously polled message.
398+ *
399+ * Internally delegates to rd_kafka_share_acknowledge_offset() because the
400+ * Python Message object does not retain the underlying rd_kafka_message_t
401+ * pointer (Message_new0 copies fields out and destroys the rkm)
402+ *
403+ * TODO KIP-932: Java splits ack APIs by message kind — successful records
404+ * go through acknowledge(message), error/GAP records through
405+ * acknowledge_offset(topic, partition, offset), and crossing the wires
406+ * throws. NJC clients (this one included) accept either. Revisit once the
407+ * Java-vs-NJC alignment is settled.
408+ */
409+ static PyObject * ShareConsumer_acknowledge (ShareConsumerHandle * self ,
410+ PyObject * args ,
411+ PyObject * kwargs ) {
412+ Message * msg = NULL ;
413+ int ack_type = (int )RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_ACCEPT ;
414+ PyObject * uo8 = NULL ;
415+ const char * topic = NULL ;
416+ rd_kafka_resp_err_t err ;
417+ static char * kws [] = {"message" , "ack_type" , NULL };
418+
419+ if (!self -> rkshare ) {
420+ PyErr_SetString (PyExc_RuntimeError ,
421+ ERR_MSG_SHARE_CONSUMER_CLOSED );
422+ return NULL ;
423+ }
424+
425+ if (!PyArg_ParseTupleAndKeywords (args , kwargs , "O!|i" , kws ,
426+ & MessageType , & msg , & ack_type ))
427+ return NULL ;
428+
429+ /* Validation (ack_type range, topic, partition, offset) is left to
430+ * librdkafka so every failure surfaces as KafkaException. Pass NULL
431+ * topic through for the None case rather than raising ValueError. */
432+
433+ if (msg -> topic && msg -> topic != Py_None ) {
434+ topic = cfl_PyUnistr_AsUTF8 (msg -> topic , & uo8 );
435+ if (!topic ) {
436+ Py_XDECREF (uo8 );
437+ return NULL ;
438+ }
439+ }
440+
441+ err = rd_kafka_share_acknowledge_offset (
442+ self -> rkshare , topic , msg -> partition , msg -> offset ,
443+ (rd_kafka_share_AcknowledgeType_t )ack_type );
444+
445+ Py_XDECREF (uo8 );
446+
447+ if (err ) {
448+ cfl_PyErr_Format (err , "Failed to acknowledge message: %s" ,
449+ rd_kafka_err2str (err ));
450+ return NULL ;
451+ }
452+
453+ Py_RETURN_NONE ;
454+ }
455+
456+
457+ /**
458+ * @brief Acknowledge a message by topic/partition/offset directly.
459+ */
460+ static PyObject * ShareConsumer_acknowledge_offset (ShareConsumerHandle * self ,
461+ PyObject * args ,
462+ PyObject * kwargs ) {
463+ const char * topic ;
464+ int partition ;
465+ long long offset ;
466+ int ack_type = (int )RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_ACCEPT ;
467+ rd_kafka_resp_err_t err ;
468+ static char * kws [] = {"topic" , "partition" , "offset" , "ack_type" , NULL };
469+
470+ if (!self -> rkshare ) {
471+ PyErr_SetString (PyExc_RuntimeError ,
472+ ERR_MSG_SHARE_CONSUMER_CLOSED );
473+ return NULL ;
474+ }
475+
476+ if (!PyArg_ParseTupleAndKeywords (args , kwargs , "siL|i" , kws , & topic ,
477+ & partition , & offset , & ack_type ))
478+ return NULL ;
479+
480+ /* ack_type, partition and offset are all validated by
481+ * rd_kafka_share_acknowledge_offset()*/
482+
483+ err = rd_kafka_share_acknowledge_offset (
484+ self -> rkshare , topic , (int32_t )partition , (int64_t )offset ,
485+ (rd_kafka_share_AcknowledgeType_t )ack_type );
486+
487+ if (err ) {
488+ cfl_PyErr_Format (err , "Failed to acknowledge offset: %s" ,
489+ rd_kafka_err2str (err ));
490+ return NULL ;
491+ }
492+
493+ Py_RETURN_NONE ;
494+ }
495+
496+
396497/**
397498 * @brief Close the share consumer.
398499 */
@@ -514,6 +615,65 @@ static PyMethodDef ShareConsumer_methods[] = {
514615 " :raises KeyboardInterrupt: if Ctrl+C pressed during consumption\n"
515616 "\n" },
516617
618+ /* TODO KIP-932: librdkafka error code → Python exception mapping is
619+ * provisional. Today the share consumer translates every librdkafka
620+ * error code into KafkaException via cfl_PyErr_Format(). Longer term we
621+ * want each code to map to the Python exception a user porting from
622+ * Java would expect, e.g.
623+ * _INVALID_ARG → ValueError (matches Java IllegalArgumentException)
624+ * _STATE → RuntimeError (matches Java IllegalStateException)
625+ * Open question: per-partition broker errors in commit_sync's result
626+ * dict (mirrors Java's Map<TopicIdPartition, Optional<...>>) — keep as
627+ * KafkaError, or translate as well?
628+ *
629+ * Revisit holistically once:
630+ * - librdkafka's share-consumer error surface is stable (some codes
631+ * may be redefined as work progresses), and
632+ * - the equivalent translation lands on commit_sync / commit_async /
633+ * ack-callback paths (currently TODO'd separately).
634+ */
635+ {"acknowledge" , (PyCFunction )ShareConsumer_acknowledge ,
636+ METH_VARARGS | METH_KEYWORDS ,
637+ ".. py:function:: acknowledge(message, "
638+ "[ack_type=AcknowledgeType.ACCEPT])\n"
639+ "\n"
640+ " Acknowledge a previously polled message in explicit acknowledgement\n"
641+ " mode. Tells the broker how to handle the record.\n"
642+ "\n"
643+ " :param Message message: A message returned by poll().\n"
644+ " :param AcknowledgeType ack_type: ACCEPT (default), RELEASE, or "
645+ "REJECT.\n"
646+ " :raises TypeError: if message is not a Message instance or ack_type "
647+ "is not an integer.\n"
648+ " :raises KafkaException: if the consumer is not in explicit\n"
649+ " acknowledgement mode, the message is no "
650+ "longer\n"
651+ " in-flight, ack_type is invalid, or "
652+ "message.topic() is None.\n"
653+ " :raises RuntimeError: if called on a closed share consumer.\n"
654+ "\n" },
655+
656+ {"acknowledge_offset" , (PyCFunction )ShareConsumer_acknowledge_offset ,
657+ METH_VARARGS | METH_KEYWORDS ,
658+ ".. py:function:: acknowledge_offset(topic, partition, offset, "
659+ "[ack_type=AcknowledgeType.ACCEPT])\n"
660+ "\n"
661+ " Acknowledge a message by topic/partition/offset.\n"
662+ "\n"
663+ " :param str topic: Topic name.\n"
664+ " :param int partition: Partition id.\n"
665+ " :param int offset: Offset to acknowledge.\n"
666+ " :param AcknowledgeType ack_type: ACCEPT (default), RELEASE, or "
667+ "REJECT.\n"
668+ " :raises TypeError: if topic is not a str, partition/offset are not "
669+ "integers, or ack_type is not an integer.\n"
670+ " :raises KafkaException: if the consumer is not in explicit\n"
671+ " acknowledgement mode, the offset is not\n"
672+ " in-flight, the offset is a GAP record,\n"
673+ " or ack_type is invalid.\n"
674+ " :raises RuntimeError: if called on a closed share consumer.\n"
675+ "\n" },
676+
517677 {"close" , (PyCFunction )ShareConsumer_close , METH_NOARGS ,
518678 ".. py:function:: close()\n"
519679 "\n"
0 commit comments