blob: 2493ec0ee664801ddd4d90dc894844a4b64bea24 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use iggy::prelude::{
ConsumerPosition as RustConsumerPosition, IggyMessage as RustReceiveMessage,
PartitionContext as RustPartitionContext, PollingStrategy as RustPollingStrategy,
ReceivedMessage,
};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyclass_complex_enum, gen_stub_pymethods};
use crate::user_headers::{UserHeaders, rust_user_headers_to_py};
/// The partition incarnation and consumer group owner a poll was served under.
///
/// Keep it with an offset to continue or commit under the same partition later: the
/// server refuses a context whose partition was deleted and created again, or whose
/// consumer group owner changed, instead of applying the offset to the new partition.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[gen_stub_pyclass]
#[pyclass(eq, frozen, hash, skip_from_py_object)]
pub struct PartitionContext {
/// Identifies one creation of the partition. It changes when a deleted partition id is
/// created again.
#[pyo3(get)]
pub incarnation: u64,
/// The consumer group owner generation, or `0` outside a consumer group.
#[pyo3(get)]
pub owner_generation: u64,
/// The metadata operation the server must have applied before it serves a request with
/// this context.
#[pyo3(get)]
pub metadata_op: u64,
}
impl From<RustPartitionContext> for PartitionContext {
fn from(context: RustPartitionContext) -> Self {
Self {
incarnation: context.incarnation,
owner_generation: context.owner_generation,
metadata_op: context.metadata_op,
}
}
}
impl From<PartitionContext> for RustPartitionContext {
fn from(context: PartitionContext) -> Self {
Self {
incarnation: context.incarnation,
owner_generation: context.owner_generation,
metadata_op: context.metadata_op,
}
}
}
#[gen_stub_pymethods]
#[pymethods]
impl PartitionContext {
#[new]
fn new(incarnation: u64, owner_generation: u64, metadata_op: u64) -> Self {
Self {
incarnation,
owner_generation,
metadata_op,
}
}
fn __repr__(&self) -> String {
format!(
"PartitionContext(incarnation={}, owner_generation={}, metadata_op={})",
self.incarnation, self.owner_generation, self.metadata_op
)
}
}
/// The position of a received message: its partition, its offset and the context of
/// the poll that delivered it. Commit it with `IggyConsumer.store_position()`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[gen_stub_pyclass]
#[pyclass(eq, frozen, hash, skip_from_py_object)]
pub struct ConsumerPosition {
/// The partition the message was read from.
#[pyo3(get)]
pub partition_id: u32,
/// The offset of the message. Committing it marks the message as consumed.
#[pyo3(get)]
pub offset: u64,
pub context: PartitionContext,
}
impl From<ConsumerPosition> for RustConsumerPosition {
fn from(position: ConsumerPosition) -> Self {
Self {
partition_id: position.partition_id,
offset: position.offset,
context: position.context.into(),
}
}
}
#[gen_stub_pymethods]
#[pymethods]
impl ConsumerPosition {
#[new]
fn new(partition_id: u32, offset: u64, context: &PartitionContext) -> Self {
Self {
partition_id,
offset,
context: *context,
}
}
// A getter, not `#[pyo3(get)]`: pyo3-stub-gen writes an undefined type name for a
// pyclass-typed field.
/// The partition incarnation and owner the poll was served under.
#[getter]
fn context(&self) -> PartitionContext {
self.context
}
fn __repr__(&self) -> String {
format!(
"ConsumerPosition(partition_id={}, offset={}, context={})",
self.partition_id,
self.offset,
self.context.__repr__()
)
}
}
/// A Python class representing a received message.
/// It provides access to the message payload and offset.
#[pyclass]
#[gen_stub_pyclass]
pub struct ReceiveMessage {
pub(crate) inner: RustReceiveMessage,
pub(crate) partition_id: u32,
pub(crate) context: PartitionContext,
}
impl From<ReceivedMessage> for ReceiveMessage {
fn from(received: ReceivedMessage) -> Self {
Self {
inner: received.message,
partition_id: received.partition_id,
context: received.context.into(),
}
}
}
#[gen_stub_pymethods]
#[pymethods]
impl ReceiveMessage {
/// Retrieves the payload of the received message.
/// The payload is returned as a Python bytes object.
pub fn payload<'a>(&self, py: Python<'a>) -> Bound<'a, PyBytes> {
PyBytes::new(py, &self.inner.payload)
}
/// Retrieves the offset of the received message.
/// The offset represents the position of the message within its topic.
pub fn offset(&self) -> u64 {
self.inner.header.offset
}
/// Retrieves the timestamp of the received message.
/// The timestamp represents the time of the message within its topic.
pub fn timestamp(&self) -> u64 {
self.inner.header.timestamp
}
/// Retrieves the origin timestamp of the received message.
/// The origin timestamp represents when the message was originally created.
pub fn origin_timestamp(&self) -> u64 {
self.inner.header.origin_timestamp
}
/// Retrieves the id of the received message.
/// The id represents unique identifier of the message within its topic.
pub fn id(&self) -> u128 {
self.inner.header.id
}
/// Retrieves the checksum of the received message.
/// The checksum represents the integrity of the message within its topic.
pub fn checksum(&self) -> u64 {
self.inner.header.checksum
}
/// Retrieves the length of the received message.
/// The length represents the length of the payload.
pub fn length(&self) -> u32 {
self.inner.header.payload_length
}
/// Retrieves the partition this message belongs to.
pub fn partition_id(&self) -> u32 {
self.partition_id
}
/// Retrieves the partition incarnation and owner the poll that delivered this message
/// was served under.
pub fn context(&self) -> PartitionContext {
self.context
}
/// The position to commit for this message with `IggyConsumer.store_position()`.
/// It keeps the context that delivered the message, so a commit after the partition
/// was deleted and created again, or after its consumer group owner changed, is
/// refused instead of landing on the new partition.
pub fn position(&self) -> ConsumerPosition {
ConsumerPosition {
partition_id: self.partition_id,
offset: self.inner.header.offset,
context: self.context,
}
}
/// Retrieves user headers attached to the received message.
///
/// Returns `None` when no headers are present or when the headers
/// on the wire are structurally malformed (those errors are logged
/// internally). Only known semantic decode errors raise `ValueError`.
#[gen_stub(override_return_type(type_repr = "UserHeaders | None"))]
pub fn user_headers<'a>(&self, py: Python<'a>) -> PyResult<Option<Bound<'a, UserHeaders>>> {
let Some(headers) = self
.inner
.user_headers_map()
.map_err(|e| PyValueError::new_err(e.to_string()))?
else {
return Ok(None);
};
rust_user_headers_to_py(py, headers).map(Some)
}
}
#[derive(Clone, Copy)]
#[gen_stub_pyclass_complex_enum]
#[pyclass(from_py_object)]
pub enum PollingStrategy {
Offset { value: u64 },
Timestamp { value: u64 },
First {},
Last {},
Next {},
}
impl From<&PollingStrategy> for RustPollingStrategy {
fn from(value: &PollingStrategy) -> Self {
match value {
PollingStrategy::Offset { value } => RustPollingStrategy::offset(value.to_owned()),
PollingStrategy::Timestamp { value } => {
RustPollingStrategy::timestamp(value.to_owned().into())
}
PollingStrategy::First {} => RustPollingStrategy::first(),
PollingStrategy::Last {} => RustPollingStrategy::last(),
PollingStrategy::Next {} => RustPollingStrategy::next(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
const PARTITION_ID: u32 = 3;
const OFFSET: u64 = 42;
const CONTEXT: RustPartitionContext = RustPartitionContext {
incarnation: 7,
owner_generation: 11,
metadata_op: 13,
};
fn received_message() -> ReceivedMessage {
let mut message = RustReceiveMessage::from_str("payload").expect("build message");
message.header.offset = OFFSET;
ReceivedMessage::new(message, OFFSET, PARTITION_ID, CONTEXT)
}
#[test]
fn given_received_message_when_converted_should_keep_its_context() {
let message = ReceiveMessage::from(received_message());
assert_eq!(RustPartitionContext::from(message.context()), CONTEXT);
}
#[test]
fn given_received_message_when_reading_position_should_match_the_rust_position() {
let received = received_message();
let expected = received.position();
let message = ReceiveMessage::from(received);
assert_eq!(RustConsumerPosition::from(message.position()), expected);
}
}