From ae9d9b97128ba87fb345581fd6dcd1194150f7c9 Mon Sep 17 00:00:00 2001 From: Alexander Bushnev Date: Tue, 9 Jun 2026 15:39:27 +0200 Subject: [PATCH 01/20] Add scripts and CI workflows to build debian package (#733) * Add scripts and CI workflows to build debian package * fix: remove arch-specific glibc version from deb description * fix: add argument validation and include dist-info in deb package * fix: guard against multiple wheels matching glob in build-debian job * fix: use array for dist-info lookup and enable nullglob for wheel glob * fix: keep RECORD in dist-info and derive versioned libc6 dep from wheel filename * fix: add INSTALLER marker and quote version in workflow * fix: use eclipse-zenoh/ci debian publication workflow * fix: zip deb artifact before upload for publish-crates-debian compatibility --- .github/workflows/release.yml | 66 ++++++++++++++++++++++++++++ ci/scripts/wheel-to-deb.sh | 82 +++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100755 ci/scripts/wheel-to-deb.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aed06819..cf3b74dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -205,6 +205,60 @@ jobs: name: wheels-linux-armv6 path: dist + build-debian: + needs: [tag, build-linux, build-linux-aarch64] + runs-on: ubuntu-latest + strategy: + matrix: + include: + - artifact: wheels-linux-x86_64 + arch: amd64 + target: x86_64-unknown-linux-gnu + - artifact: wheels-linux-i686 + arch: i386 + target: i686-unknown-linux-gnu + - artifact: wheels-linux-armv7 + arch: armhf + target: armv7-unknown-linux-gnueabihf + - artifact: wheels-linux-aarch64 + arch: arm64 + target: aarch64-unknown-linux-gnu + steps: + - name: Checkout this repository + uses: actions/checkout@v4 + with: + ref: ${{ needs.tag.outputs.branch }} + + - name: Download wheel + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.artifact }} + path: dist + + - name: Convert wheel to Debian package + run: | + shopt -s nullglob + wheels=(dist/eclipse_zenoh-*.whl) + if [[ ${#wheels[@]} -ne 1 ]]; then + echo "Expected exactly one wheel, found: ${#wheels[@]} (${wheels[*]})" >&2 + exit 1 + fi + bash ci/scripts/wheel-to-deb.sh \ + "${wheels[0]}" \ + python3-eclipse-zenoh \ + "${{ needs.tag.outputs.version }}" \ + "${{ matrix.arch }}" + + - name: Package Debian artifact as zip + run: | + zip "zenoh-python-${{ needs.tag.outputs.version }}-${{ matrix.target }}-debian.zip" *.deb + + - name: Upload Debian package artifact + uses: actions/upload-artifact@v4 + with: + name: zenoh-python-${{ needs.tag.outputs.version }}-${{ matrix.target }}-debian.zip + path: "zenoh-python-${{ needs.tag.outputs.version }}-${{ matrix.target }}-debian.zip" + publish-pypi: needs: [ @@ -253,3 +307,15 @@ jobs: branch: ${{ needs.tag.outputs.branch }} github-token: ${{ secrets.BOT_TOKEN_WORKFLOW }} archive-patterns: "^$" + + publish-debian: + needs: [tag, build-debian] + name: Publish Debian packages + uses: eclipse-zenoh/ci/.github/workflows/release-crates-debian.yml@main + with: + no-build: true + live-run: ${{ inputs.live-run || false }} + version: ${{ needs.tag.outputs.version }} + repo: ${{ github.repository }} + branch: ${{ needs.tag.outputs.branch }} + secrets: inherit diff --git a/ci/scripts/wheel-to-deb.sh b/ci/scripts/wheel-to-deb.sh new file mode 100755 index 00000000..73dd12e5 --- /dev/null +++ b/ci/scripts/wheel-to-deb.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2026 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# +# Usage: wheel-to-deb.sh +# Example: +# wheel-to-deb.sh eclipse_zenoh-1.0.0-cp39-abi3-manylinux_2_17_x86_64.whl \ +# python3-eclipse-zenoh 1.0.0 amd64 + +set -euo pipefail + +if [[ $# -ne 4 ]]; then + echo "Usage: $0 " >&2 + echo "Example: $0 eclipse_zenoh-1.0.0-cp39-abi3-manylinux_2_17_x86_64.whl python3-eclipse-zenoh 1.0.0 amd64" >&2 + exit 1 +fi + +WHEEL=$1 +PKG=$2 +VER=$3 +ARCH=$4 + +WORKDIR=$(mktemp -d) +trap 'rm -rf "$WORKDIR"' EXIT + +unzip -q "$WHEEL" -d "$WORKDIR/contents" + +DIST_PKG="$WORKDIR/deb/usr/lib/python3/dist-packages" +mkdir -p "$DIST_PKG" + +cp -r "$WORKDIR/contents/zenoh" "$DIST_PKG/" + +# Copy dist-info for importlib.metadata and pip compatibility +mapfile -t DIST_INFO_DIRS < <(find "$WORKDIR/contents" -maxdepth 1 -name "*.dist-info" -type d) +if [[ ${#DIST_INFO_DIRS[@]} -gt 1 ]]; then + echo "Expected at most one dist-info directory, found: ${DIST_INFO_DIRS[*]}" >&2 + exit 1 +fi +if [[ ${#DIST_INFO_DIRS[@]} -eq 1 ]]; then + cp -r "${DIST_INFO_DIRS[0]}" "$DIST_PKG/" + # Mark as dpkg-managed so pip does not attempt to uninstall these files + echo "dpkg" > "$DIST_PKG/$(basename "${DIST_INFO_DIRS[0]}")/INSTALLER" +fi + +# Derive minimum glibc version from the manylinux tag in the wheel filename +# e.g. manylinux_2_17 -> libc6 (>= 2.17), manylinux_2_28 -> libc6 (>= 2.28) +LIBC6_DEP="libc6" +if [[ "$WHEEL" =~ manylinux_([0-9]+)_([0-9]+) ]]; then + LIBC6_DEP="libc6 (>= ${BASH_REMATCH[1]}.${BASH_REMATCH[2]})" +fi + +mkdir -p "$WORKDIR/deb/DEBIAN" +cat > "$WORKDIR/deb/DEBIAN/control" < +Depends: python3 (>= 3.9), $LIBC6_DEP +Section: python +Priority: optional +Homepage: https://zenoh.io +Description: Eclipse Zenoh Python bindings + Eclipse Zenoh: Zero Overhead Pub/sub, Store/Query and Compute. + . + This package provides the Python bindings for Eclipse Zenoh, enabling + pub/sub, queryable and geo-distributed storage in Python. + . + Built from manylinux wheels. +CTRL + +dpkg-deb --build --root-owner-group "$WORKDIR/deb" "${PKG}_${VER}_${ARCH}.deb" +echo "Built: ${PKG}_${VER}_${ARCH}.deb" From 8033f03974477d96ec4c82ab8758e08020f3d88b Mon Sep 17 00:00:00 2001 From: eclipse-zenoh-bot <61247838+eclipse-zenoh-bot@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:55:23 +0200 Subject: [PATCH 02/20] build: Sync with eclipse-zenoh/zenoh@513073e from 2026-06-11 (#735) Co-authored-by: eclipse-zenoh-bot --- Cargo.lock | 70 +++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be64bc53..1efb0612 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2572,9 +2572,9 @@ dependencies = [ [[package]] name = "stabby" -version = "72.1.1" +version = "72.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976399a0c48ea769ef7f5dc303bb88240ab8d84008647a6b2303eced3dab3945" +checksum = "ec9e9da673d4db1d470fa36cf4483ad5b1fdea349a392d400fea5d3673a9c5ca" dependencies = [ "rustversion", "stabby-abi", @@ -2582,9 +2582,9 @@ dependencies = [ [[package]] name = "stabby-abi" -version = "72.1.1" +version = "72.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b54832a9a1f92a0e55e74a5c0332744426edc515bb3fbad82f10b874a87f0d" +checksum = "10a281b17b3cf11531b7dc4e5f1c6be27db86a06e19c477e7a88fa4ee1b6daf3" dependencies = [ "rustc_version", "rustversion", @@ -2594,9 +2594,9 @@ dependencies = [ [[package]] name = "stabby-macros" -version = "72.1.1" +version = "72.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a768b1e51e4dbfa4fa52ae5c01241c0a41e2938fdffbb84add0c8238092f9091" +checksum = "605b39114a0c132d77ffdd7d179491323dbaa8369e7dcbcdf3da09d0b43c13cf" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3887,7 +3887,7 @@ dependencies = [ [[package]] name = "zenoh" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "ahash", "arc-swap", @@ -3938,7 +3938,7 @@ dependencies = [ [[package]] name = "zenoh-buffers" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "zenoh-collections", ] @@ -3946,7 +3946,7 @@ dependencies = [ [[package]] name = "zenoh-codec" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "tracing", "uhlc", @@ -3958,7 +3958,7 @@ dependencies = [ [[package]] name = "zenoh-collections" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "ahash", ] @@ -3966,7 +3966,7 @@ dependencies = [ [[package]] name = "zenoh-config" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "json5", "nonempty-collections", @@ -3991,7 +3991,7 @@ dependencies = [ [[package]] name = "zenoh-core" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "lazy_static", "tokio", @@ -4002,7 +4002,7 @@ dependencies = [ [[package]] name = "zenoh-crypto" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "aes", "hmac", @@ -4015,7 +4015,7 @@ dependencies = [ [[package]] name = "zenoh-ext" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "bincode", @@ -4034,7 +4034,7 @@ dependencies = [ [[package]] name = "zenoh-keyexpr" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "getrandom 0.2.17", "hashbrown 0.16.1", @@ -4049,7 +4049,7 @@ dependencies = [ [[package]] name = "zenoh-link" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "zenoh-config", "zenoh-link-commons", @@ -4067,7 +4067,7 @@ dependencies = [ [[package]] name = "zenoh-link-commons" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "base64", @@ -4103,7 +4103,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "rustls-webpki", @@ -4119,7 +4119,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic_datagram" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "rustls-webpki", @@ -4135,7 +4135,7 @@ dependencies = [ [[package]] name = "zenoh-link-tcp" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "socket2 0.5.10", @@ -4152,7 +4152,7 @@ dependencies = [ [[package]] name = "zenoh-link-tls" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "base64", @@ -4181,7 +4181,7 @@ dependencies = [ [[package]] name = "zenoh-link-udp" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "libc", @@ -4203,7 +4203,7 @@ dependencies = [ [[package]] name = "zenoh-link-unixsock_stream" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "nix", @@ -4221,7 +4221,7 @@ dependencies = [ [[package]] name = "zenoh-link-ws" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "futures-util", @@ -4241,7 +4241,7 @@ dependencies = [ [[package]] name = "zenoh-macros" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "proc-macro2", "quote", @@ -4252,7 +4252,7 @@ dependencies = [ [[package]] name = "zenoh-plugin-trait" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "git-version", "libloading", @@ -4269,7 +4269,7 @@ dependencies = [ [[package]] name = "zenoh-protocol" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "const_format", "rand 0.8.5", @@ -4294,7 +4294,7 @@ dependencies = [ [[package]] name = "zenoh-result" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "anyhow", ] @@ -4302,7 +4302,7 @@ dependencies = [ [[package]] name = "zenoh-runtime" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "lazy_static", "ron", @@ -4316,7 +4316,7 @@ dependencies = [ [[package]] name = "zenoh-shm" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "advisory-lock", "async-trait", @@ -4345,7 +4345,7 @@ dependencies = [ [[package]] name = "zenoh-stats" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "ahash", "prometheus-client", @@ -4358,7 +4358,7 @@ dependencies = [ [[package]] name = "zenoh-sync" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "arc-swap", "event-listener", @@ -4372,7 +4372,7 @@ dependencies = [ [[package]] name = "zenoh-task" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "futures", "tokio", @@ -4385,7 +4385,7 @@ dependencies = [ [[package]] name = "zenoh-transport" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "crossbeam-utils", @@ -4421,7 +4421,7 @@ dependencies = [ [[package]] name = "zenoh-util" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#90c06a28c7e1d396ea1a97b3a63a5e6a38afc6ee" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" dependencies = [ "async-trait", "const_format", From 8b85f00ab6725e350b8dd408e07ecf52f80dedd8 Mon Sep 17 00:00:00 2001 From: eclipse-zenoh-bot <61247838+eclipse-zenoh-bot@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:11:02 +0200 Subject: [PATCH 03/20] build: Sync with eclipse-zenoh/zenoh@6685f84 from 2026-06-16 (#737) Co-authored-by: eclipse-zenoh-bot --- Cargo.lock | 73 +++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1efb0612..0d6e0e65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2572,9 +2572,9 @@ dependencies = [ [[package]] name = "stabby" -version = "72.1.2" +version = "72.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9e9da673d4db1d470fa36cf4483ad5b1fdea349a392d400fea5d3673a9c5ca" +checksum = "a7b834ec7ced12095fea1e4b07dcb7e8cf2b59b18afa3eac52494d835965a5ec" dependencies = [ "rustversion", "stabby-abi", @@ -2582,9 +2582,9 @@ dependencies = [ [[package]] name = "stabby-abi" -version = "72.1.2" +version = "72.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a281b17b3cf11531b7dc4e5f1c6be27db86a06e19c477e7a88fa4ee1b6daf3" +checksum = "ff1a4f477858a5bdf927c9fab7f579899de9b13e39f8b3b3b300c89fbab632f4" dependencies = [ "rustc_version", "rustversion", @@ -2594,15 +2594,14 @@ dependencies = [ [[package]] name = "stabby-macros" -version = "72.1.2" +version = "72.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605b39114a0c132d77ffdd7d179491323dbaa8369e7dcbcdf3da09d0b43c13cf" +checksum = "b31c4b2434980b67ad83f300a58088ba14d59454dcd79ba3d87419bbd924d31e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "rand 0.8.5", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -3887,7 +3886,7 @@ dependencies = [ [[package]] name = "zenoh" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "ahash", "arc-swap", @@ -3938,7 +3937,7 @@ dependencies = [ [[package]] name = "zenoh-buffers" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "zenoh-collections", ] @@ -3946,7 +3945,7 @@ dependencies = [ [[package]] name = "zenoh-codec" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "tracing", "uhlc", @@ -3958,7 +3957,7 @@ dependencies = [ [[package]] name = "zenoh-collections" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "ahash", ] @@ -3966,7 +3965,7 @@ dependencies = [ [[package]] name = "zenoh-config" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "json5", "nonempty-collections", @@ -3991,7 +3990,7 @@ dependencies = [ [[package]] name = "zenoh-core" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "lazy_static", "tokio", @@ -4002,7 +4001,7 @@ dependencies = [ [[package]] name = "zenoh-crypto" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "aes", "hmac", @@ -4015,7 +4014,7 @@ dependencies = [ [[package]] name = "zenoh-ext" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "bincode", @@ -4034,7 +4033,7 @@ dependencies = [ [[package]] name = "zenoh-keyexpr" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "getrandom 0.2.17", "hashbrown 0.16.1", @@ -4049,7 +4048,7 @@ dependencies = [ [[package]] name = "zenoh-link" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "zenoh-config", "zenoh-link-commons", @@ -4067,7 +4066,7 @@ dependencies = [ [[package]] name = "zenoh-link-commons" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "base64", @@ -4103,7 +4102,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "rustls-webpki", @@ -4119,7 +4118,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic_datagram" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "rustls-webpki", @@ -4135,7 +4134,7 @@ dependencies = [ [[package]] name = "zenoh-link-tcp" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "socket2 0.5.10", @@ -4152,7 +4151,7 @@ dependencies = [ [[package]] name = "zenoh-link-tls" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "base64", @@ -4181,7 +4180,7 @@ dependencies = [ [[package]] name = "zenoh-link-udp" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "libc", @@ -4203,7 +4202,7 @@ dependencies = [ [[package]] name = "zenoh-link-unixsock_stream" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "nix", @@ -4221,7 +4220,7 @@ dependencies = [ [[package]] name = "zenoh-link-ws" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "futures-util", @@ -4241,7 +4240,7 @@ dependencies = [ [[package]] name = "zenoh-macros" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "proc-macro2", "quote", @@ -4252,7 +4251,7 @@ dependencies = [ [[package]] name = "zenoh-plugin-trait" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "git-version", "libloading", @@ -4269,7 +4268,7 @@ dependencies = [ [[package]] name = "zenoh-protocol" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "const_format", "rand 0.8.5", @@ -4294,7 +4293,7 @@ dependencies = [ [[package]] name = "zenoh-result" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "anyhow", ] @@ -4302,7 +4301,7 @@ dependencies = [ [[package]] name = "zenoh-runtime" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "lazy_static", "ron", @@ -4316,7 +4315,7 @@ dependencies = [ [[package]] name = "zenoh-shm" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "advisory-lock", "async-trait", @@ -4345,7 +4344,7 @@ dependencies = [ [[package]] name = "zenoh-stats" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "ahash", "prometheus-client", @@ -4358,7 +4357,7 @@ dependencies = [ [[package]] name = "zenoh-sync" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "arc-swap", "event-listener", @@ -4372,7 +4371,7 @@ dependencies = [ [[package]] name = "zenoh-task" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "futures", "tokio", @@ -4385,7 +4384,7 @@ dependencies = [ [[package]] name = "zenoh-transport" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "crossbeam-utils", @@ -4421,7 +4420,7 @@ dependencies = [ [[package]] name = "zenoh-util" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#1341765f895eefa643ec01b689eca1b12ddb2d9a" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "const_format", From 28c33abb8d0595d6656369ae32f4a92ca8f1df85 Mon Sep 17 00:00:00 2001 From: Oussama Teffahi <70609372+oteffahi@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:26:37 +0200 Subject: [PATCH 04/20] Expose timestamp instrumentation API (#736) * Add TimestampStack API * Add timestamp stack callback to Session API * Update zenoh git ref * Fix formatting * Fix clippy warning * Apply review comments * Add missing @property to stubs * Apply upstream API changes * Fix file formatting --- Cargo.lock | 58 +++--- Cargo.toml | 4 +- src/ext.rs | 25 ++- src/lib.rs | 5 + src/pubsub.rs | 17 +- src/query.rs | 17 +- src/sample.rs | 6 + src/session.rs | 30 ++- src/timestamp_stack.rs | 174 +++++++++++++++++ tests/test_timestamp_stack.py | 343 ++++++++++++++++++++++++++++++++++ zenoh/__init__.pyi | 163 +++++++++++++++- zenoh/ext.pyi | 3 + 12 files changed, 798 insertions(+), 47 deletions(-) create mode 100644 src/timestamp_stack.rs create mode 100644 tests/test_timestamp_stack.py diff --git a/Cargo.lock b/Cargo.lock index 0d6e0e65..faf74696 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3886,7 +3886,7 @@ dependencies = [ [[package]] name = "zenoh" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "ahash", "arc-swap", @@ -3937,7 +3937,7 @@ dependencies = [ [[package]] name = "zenoh-buffers" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "zenoh-collections", ] @@ -3945,7 +3945,7 @@ dependencies = [ [[package]] name = "zenoh-codec" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "tracing", "uhlc", @@ -3957,7 +3957,7 @@ dependencies = [ [[package]] name = "zenoh-collections" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "ahash", ] @@ -3965,7 +3965,7 @@ dependencies = [ [[package]] name = "zenoh-config" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "json5", "nonempty-collections", @@ -3990,7 +3990,7 @@ dependencies = [ [[package]] name = "zenoh-core" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "lazy_static", "tokio", @@ -4001,7 +4001,7 @@ dependencies = [ [[package]] name = "zenoh-crypto" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "aes", "hmac", @@ -4014,7 +4014,7 @@ dependencies = [ [[package]] name = "zenoh-ext" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "bincode", @@ -4033,7 +4033,7 @@ dependencies = [ [[package]] name = "zenoh-keyexpr" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "getrandom 0.2.17", "hashbrown 0.16.1", @@ -4048,7 +4048,7 @@ dependencies = [ [[package]] name = "zenoh-link" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "zenoh-config", "zenoh-link-commons", @@ -4066,7 +4066,7 @@ dependencies = [ [[package]] name = "zenoh-link-commons" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "base64", @@ -4102,7 +4102,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "rustls-webpki", @@ -4118,7 +4118,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic_datagram" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "rustls-webpki", @@ -4134,7 +4134,7 @@ dependencies = [ [[package]] name = "zenoh-link-tcp" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "socket2 0.5.10", @@ -4151,7 +4151,7 @@ dependencies = [ [[package]] name = "zenoh-link-tls" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "base64", @@ -4180,7 +4180,7 @@ dependencies = [ [[package]] name = "zenoh-link-udp" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "libc", @@ -4202,7 +4202,7 @@ dependencies = [ [[package]] name = "zenoh-link-unixsock_stream" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "nix", @@ -4220,7 +4220,7 @@ dependencies = [ [[package]] name = "zenoh-link-ws" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "futures-util", @@ -4240,7 +4240,7 @@ dependencies = [ [[package]] name = "zenoh-macros" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "proc-macro2", "quote", @@ -4251,7 +4251,7 @@ dependencies = [ [[package]] name = "zenoh-plugin-trait" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "git-version", "libloading", @@ -4268,7 +4268,7 @@ dependencies = [ [[package]] name = "zenoh-protocol" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "const_format", "rand 0.8.5", @@ -4293,7 +4293,7 @@ dependencies = [ [[package]] name = "zenoh-result" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "anyhow", ] @@ -4301,7 +4301,7 @@ dependencies = [ [[package]] name = "zenoh-runtime" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "lazy_static", "ron", @@ -4315,7 +4315,7 @@ dependencies = [ [[package]] name = "zenoh-shm" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "advisory-lock", "async-trait", @@ -4344,7 +4344,7 @@ dependencies = [ [[package]] name = "zenoh-stats" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "ahash", "prometheus-client", @@ -4357,7 +4357,7 @@ dependencies = [ [[package]] name = "zenoh-sync" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "arc-swap", "event-listener", @@ -4371,7 +4371,7 @@ dependencies = [ [[package]] name = "zenoh-task" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "futures", "tokio", @@ -4384,7 +4384,7 @@ dependencies = [ [[package]] name = "zenoh-transport" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "crossbeam-utils", @@ -4420,7 +4420,7 @@ dependencies = [ [[package]] name = "zenoh-util" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" +source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" dependencies = [ "async-trait", "const_format", diff --git a/Cargo.toml b/Cargo.toml index ff161af4..8743279e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,10 +44,10 @@ maintenance = { status = "actively-developed" } [dependencies] paste = "1.0.14" pyo3 = { version = "0.25.1", features = ["abi3-py39", "extension-module"] } -zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = [ +zenoh = { version = "1.9.0", git = "https://github.com/zettascalelabs/zenoh.git", branch = "feat/routing-timestamps", features = [ "internal", "unstable", ], default-features = false } -zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = [ +zenoh-ext = { version = "1.9.0", git = "https://github.com/zettascalelabs/zenoh.git", branch = "feat/routing-timestamps", features = [ "internal", ], optional = true } diff --git a/src/ext.rs b/src/ext.rs index d2de6084..896d5632 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -24,6 +24,7 @@ use crate::{ sample::{Locality, Sample}, session::{EntityGlobalId, Session}, time::Timestamp, + timestamp_stack::TimestampInstrumentation, utils::{duration, generic, wait, MapInto}, ZDeserializeError, }; @@ -492,7 +493,7 @@ impl AdvancedPublisher { Ok(self.get_ref()?.priority().into()) } - #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None))] + #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None, timestamp_instrumentation = None))] fn put( &self, py: Python, @@ -500,22 +501,38 @@ impl AdvancedPublisher { #[pyo3(from_py_with = Encoding::from_py_opt)] encoding: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { let this = self.get_ref()?; wait( py, - build!(this.put(payload), encoding, attachment, timestamp), + build!( + this.put(payload), + encoding, + attachment, + timestamp, + timestamp_instrumentation + ), ) } - #[pyo3(signature = (*, attachment = None, timestamp = None))] + #[pyo3(signature = (*, attachment = None, timestamp = None, timestamp_instrumentation = None))] fn delete( &self, py: Python, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { - wait(py, build!(self.get_ref()?.delete(), attachment, timestamp)) + wait( + py, + build!( + self.get_ref()?.delete(), + attachment, + timestamp, + timestamp_instrumentation + ), + ) } fn undeclare(&mut self, py: Python) -> PyResult<()> { diff --git a/src/lib.rs b/src/lib.rs index 09235cce..7c622880 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ mod session; #[cfg(feature = "shared-memory")] mod shm; mod time; +mod timestamp_stack; mod utils; use pyo3::prelude::*; @@ -77,6 +78,10 @@ pub(crate) mod zenoh { Transport, TransportEvent, TransportEventsListener, }, time::{Timestamp, TimestampId, NTP64}, + timestamp_stack::{ + InterceptionPoint, TimestampContext, TimestampInstrumentation, + TimestampInstrumentationBuilder, TimestampStack, TimestampStackRecord, + }, ZError, }; diff --git a/src/pubsub.rs b/src/pubsub.rs index f5c46ef0..2d036dd1 100644 --- a/src/pubsub.rs +++ b/src/pubsub.rs @@ -27,6 +27,7 @@ use crate::{ sample::{Sample, SourceInfo}, session::EntityGlobalId, time::Timestamp, + timestamp_stack::TimestampInstrumentation, utils::{generic, wait}, }; @@ -84,7 +85,8 @@ impl Publisher { Ok(wait(py, self.get_ref()?.matching_status())?.into()) } - #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None, source_info = None))] + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None, timestamp_instrumentation = None, source_info = None))] fn put( &self, py: Python, @@ -92,6 +94,7 @@ impl Publisher { #[pyo3(from_py_with = Encoding::from_py_opt)] encoding: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, + timestamp_instrumentation: Option, source_info: Option, ) -> PyResult<()> { let this = self.get_ref()?; @@ -100,20 +103,28 @@ impl Publisher { encoding, attachment, timestamp, + timestamp_instrumentation, source_info ); wait(py, builder) } - #[pyo3(signature = (*, attachment = None, timestamp = None, source_info = None))] + #[pyo3(signature = (*, attachment = None, timestamp = None, timestamp_instrumentation = None, source_info = None))] fn delete( &self, py: Python, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, + timestamp_instrumentation: Option, source_info: Option, ) -> PyResult<()> { - let builder = build!(self.get_ref()?.delete(), attachment, timestamp, source_info); + let builder = build!( + self.get_ref()?.delete(), + attachment, + timestamp, + timestamp_instrumentation, + source_info + ); wait(py, builder) } diff --git a/src/query.rs b/src/query.rs index 17d5e674..c59a17d8 100644 --- a/src/query.rs +++ b/src/query.rs @@ -30,6 +30,7 @@ use crate::{ sample::SourceInfo, session::EntityGlobalId, time::Timestamp, + timestamp_stack::{TimestampInstrumentation, TimestampStack}, utils::{generic, wait, IntoPyResult, IntoPython, IntoRust, MapInto}, }; @@ -230,6 +231,11 @@ impl Query { Ok(self.get_ref()?.source_info().cloned().map_into()) } + #[getter] + fn timestamp_stack(&self) -> PyResult> { + Ok(self.get_ref()?.timestamp_stack().cloned().map_into()) + } + fn drop(&mut self) { Python::with_gil(|gil| gil.allow_threads(|| drop(self.0.take()))); } @@ -295,6 +301,11 @@ impl ReplyError { self.0.encoding().clone().into() } + #[getter] + fn timestamp_stack(&self) -> Option { + self.0.timestamp_stack().cloned().map_into() + } + fn __repr__(&self) -> String { format!("{:?}", self.0) } @@ -408,7 +419,7 @@ impl Querier { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (handler = None, *, parameters = None, payload = None, encoding = None, attachment = None, source_info = None, cancellation_token = None))] + #[pyo3(signature = (handler = None, *, parameters = None, payload = None, encoding = None, attachment = None, source_info = None, cancellation_token = None, timestamp_instrumentation = None))] fn get( &self, py: Python, @@ -419,6 +430,7 @@ impl Querier { #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, source_info: Option, cancellation_token: Option, + timestamp_instrumentation: Option, ) -> PyResult> { let this = self.get_ref()?; let (handler, _) = into_handler(py, handler, cancellation_token.as_ref())?; @@ -429,7 +441,8 @@ impl Querier { encoding, attachment, source_info, - cancellation_token + cancellation_token, + timestamp_instrumentation ); wait(py, builder.with(handler)).map_into() } diff --git a/src/sample.rs b/src/sample.rs index dce32cf6..b5b3ede5 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -21,6 +21,7 @@ use crate::{ qos::{CongestionControl, Priority}, session::EntityGlobalId, time::Timestamp, + timestamp_stack::TimestampStack, utils::MapInto, }; @@ -95,6 +96,11 @@ impl Sample { self.0.source_info().cloned().map_into() } + #[getter] + fn timestamp_stack(&self) -> Option { + self.0.timestamp_stack().cloned().map_into() + } + fn __repr__(&self) -> String { format!("{:?}", self.0) } diff --git a/src/session.rs b/src/session.rs index 92ae591e..77cecdc4 100644 --- a/src/session.rs +++ b/src/session.rs @@ -33,6 +33,7 @@ use crate::{ query::{Querier, QueryConsolidation, QueryTarget, Queryable, Reply, ReplyKeyExpr, Selector}, sample::{Locality, SampleKind, SourceInfo}, time::Timestamp, + timestamp_stack::TimestampInstrumentation, utils::{duration, wait, IntoPython, MapInto}, }; @@ -94,7 +95,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, allowed_destination = None, source_info = None))] + #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, timestamp_instrumentation = None, allowed_destination = None, source_info = None))] fn put( &self, py: Python, @@ -106,6 +107,7 @@ impl Session { express: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, + timestamp_instrumentation: Option, allowed_destination: Option, source_info: Option, ) -> PyResult<()> { @@ -117,6 +119,7 @@ impl Session { express, attachment, timestamp, + timestamp_instrumentation, allowed_destination, source_info, ); @@ -124,7 +127,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, *, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, allowed_destination = None, source_info = None))] + #[pyo3(signature = (key_expr, *, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, timestamp_instrumentation = None, allowed_destination = None, source_info = None))] fn delete( &self, py: Python, @@ -134,6 +137,7 @@ impl Session { express: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, + timestamp_instrumentation: Option, allowed_destination: Option, source_info: Option, ) -> PyResult<()> { @@ -144,6 +148,7 @@ impl Session { express, attachment, timestamp, + timestamp_instrumentation, allowed_destination, source_info ); @@ -151,7 +156,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (selector, handler = None, *, target = None, consolidation = None, accept_replies = None, timeout = None, congestion_control = None, priority = None, express = None, payload = None, encoding = None, attachment = None, allowed_destination = None, source_info = None, cancellation_token = None))] + #[pyo3(signature = (selector, handler = None, *, target = None, consolidation = None, accept_replies = None, timeout = None, congestion_control = None, priority = None, express = None, payload = None, encoding = None, attachment = None, allowed_destination = None, source_info = None, cancellation_token = None, timestamp_instrumentation = None))] fn get( &self, py: Python, @@ -172,6 +177,7 @@ impl Session { allowed_destination: Option, source_info: Option, cancellation_token: Option, + timestamp_instrumentation: Option, ) -> PyResult> { let (handler, _) = into_handler(py, handler, cancellation_token.as_ref())?; let builder = build!( @@ -188,7 +194,8 @@ impl Session { attachment, allowed_destination, source_info, - cancellation_token + cancellation_token, + timestamp_instrumentation ); wait(py, builder.with(handler)).map_into() @@ -306,8 +313,19 @@ impl Drop for Session { } #[pyfunction] -pub(crate) fn open(py: Python, config: Config) -> PyResult { - wait(py, zenoh::open(config)).map(Session) +#[pyo3(signature = (config, *, timestamp_callback=None))] +pub(crate) fn open( + py: Python, + config: Config, + timestamp_callback: Option>, +) -> PyResult { + let builder = zenoh::open(config); + let builder = if let Some(callback) = timestamp_callback { + builder.with_timestamp_callback(crate::timestamp_stack::create_timestamp_callback(callback)) + } else { + builder + }; + wait(py, builder).map(Session) } wrapper!(zenoh::session::SessionInfo); diff --git a/src/timestamp_stack.rs b/src/timestamp_stack.rs new file mode 100644 index 00000000..0e3dc418 --- /dev/null +++ b/src/timestamp_stack.rs @@ -0,0 +1,174 @@ +// +// Copyright (c) 2026 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use pyo3::{prelude::*, types::PyBytes}; + +use crate::{ + config::{WhatAmI, ZenohId}, + macros::{enum_mapper, wrapper}, + time::Timestamp, +}; + +enum_mapper!(zenoh::timestamp_stack::InterceptionPoint: u8 { + Send, + Route, + Receive, +}); + +#[pyclass] +pub(crate) struct TimestampContext(pub(crate) zenoh::timestamp_stack::TimestampContext); + +#[pymethods] +impl TimestampContext { + #[getter] + fn zid(&self) -> ZenohId { + ZenohId(self.0.zid) + } + + #[getter] + fn whatami(&self) -> WhatAmI { + self.0.whatami.into() + } + + fn __repr__(&self) -> String { + format!( + "TimestampContext(zid={}, whatami={:?})", + self.0.zid, self.0.whatami + ) + } +} + +fn log_timestamp_callback_error(py: Python, err: PyErr) { + if let Ok(logging) = py.import("logging") { + if let Ok(logger) = logging.call_method1("getLogger", ("zenoh",)) { + let _ = logger.call_method1("error", (format!("timestamp callback error: {err}"),)); + } + } +} + +pub(crate) fn create_timestamp_callback( + callback: Py, +) -> impl Fn(zenoh::timestamp_stack::TimestampContext) -> Vec + Send + Sync + 'static { + move |ctx: zenoh::timestamp_stack::TimestampContext| -> Vec { + Python::with_gil(|py| { + let py_ctx = match Py::new(py, TimestampContext(ctx)) { + Ok(ctx) => ctx, + Err(e) => { + log_timestamp_callback_error(py, e); + return Vec::new(); + } + }; + match callback.call1(py, (py_ctx,)) { + Ok(result) => result.extract::>(py).unwrap_or_else(|e| { + log_timestamp_callback_error(py, e); + Vec::new() + }), + Err(e) => { + log_timestamp_callback_error(py, e); + Vec::new() + } + } + }) + } +} + +wrapper!(zenoh::timestamp_stack::TimestampInstrumentation: Clone, Copy, PartialEq, Eq); + +#[pymethods] +impl TimestampInstrumentation { + fn is_instrumented(&self, point: InterceptionPoint) -> bool { + self.0.is_instrumented(point.into()) + } + + fn __repr__(&self) -> String { + format!("{:?}", self.0) + } +} + +wrapper!(zenoh::timestamp_stack::TimestampInstrumentationBuilder: Clone, Copy); + +#[pymethods] +impl TimestampInstrumentationBuilder { + #[new] + fn new() -> Self { + Self(zenoh::timestamp_stack::TimestampInstrumentationBuilder::new()) + } + + fn set_send(&self, enabled: bool) -> Self { + Self(self.0.set_send(enabled)) + } + + fn set_route(&self, enabled: bool) -> Self { + Self(self.0.set_route(enabled)) + } + + fn set_receive(&self, enabled: bool) -> Self { + Self(self.0.set_receive(enabled)) + } + + fn build(&self) -> PyResult { + self.0 + .build() + .map(TimestampInstrumentation) + .map_err(|e| crate::ZError::new_err(e.to_string())) + } +} + +wrapper!(zenoh::timestamp_stack::TimestampStackRecord: Clone); + +#[pymethods] +impl TimestampStackRecord { + #[getter] + fn point(&self) -> InterceptionPoint { + self.0.point().into() + } + + #[getter] + fn is_custom(&self) -> bool { + self.0.is_custom() + } + + fn timestamp<'py>(&self, py: Python<'py>) -> PyResult> { + match self.0.timestamp() { + zenoh::timestamp_stack::InstrumentationTimestamp::UHLC(ts) => { + Ok(Timestamp::from(*ts).into_pyobject(py)?.into_any()) + } + zenoh::timestamp_stack::InstrumentationTimestamp::Custom(bytes) => { + Ok(PyBytes::new(py, bytes).into_pyobject(py)?.into_any()) + } + } + } + + fn __repr__(&self) -> String { + format!("{:?}", self.0) + } +} + +wrapper!(zenoh::timestamp_stack::TimestampStack: Clone); + +#[pymethods] +impl TimestampStack { + #[getter] + fn instrumentation(&self) -> TimestampInstrumentation { + self.0.instrumentation().into() + } + + #[getter] + fn records(&self) -> Vec { + self.0.records().iter().cloned().map(Into::into).collect() + } + + fn __repr__(&self) -> String { + format!("{:?}", self.0) + } +} diff --git a/tests/test_timestamp_stack.py b/tests/test_timestamp_stack.py new file mode 100644 index 00000000..a547592f --- /dev/null +++ b/tests/test_timestamp_stack.py @@ -0,0 +1,343 @@ +# +# Copyright (c) 2026 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# +import json +import time + +import zenoh +from zenoh import ( + InterceptionPoint, + Sample, + TimestampContext, + TimestampInstrumentation, + TimestampInstrumentationBuilder, + TimestampStack, +) + +SLEEP = 1 + + +def open_session(endpoints: list[str]) -> tuple[zenoh.Session, zenoh.Session]: + conf = zenoh.Config() + conf.insert_json5("listen/endpoints", json.dumps(endpoints)) + conf.insert_json5("scouting/multicast/enabled", "false") + peer01 = zenoh.open(conf) + + conf = zenoh.Config() + conf.insert_json5("connect/endpoints", json.dumps(endpoints)) + conf.insert_json5("scouting/multicast/enabled", "false") + peer02 = zenoh.open(conf) + + return (peer01, peer02) + + +def close_session(peer01: zenoh.Session, peer02: zenoh.Session): + peer01.close() + peer02.close() + + +def test_timestamp_instrumentation_builder(): + """Test TimestampInstrumentationBuilder and TimestampInstrumentation.""" + builder = TimestampInstrumentationBuilder() + assert builder is not None + + # Build with all points enabled + instr = builder.set_send(True).set_route(True).set_receive(True).build() + assert instr is not None + assert isinstance(instr, TimestampInstrumentation) + assert instr.is_instrumented(InterceptionPoint.SEND) + assert instr.is_instrumented(InterceptionPoint.ROUTE) + assert instr.is_instrumented(InterceptionPoint.RECEIVE) + + # Build with only send enabled + instr2 = TimestampInstrumentationBuilder().set_send(True).build() + assert instr2.is_instrumented(InterceptionPoint.SEND) + assert not instr2.is_instrumented(InterceptionPoint.ROUTE) + assert not instr2.is_instrumented(InterceptionPoint.RECEIVE) + + # Build with only route and receive + instr3 = TimestampInstrumentationBuilder().set_route(True).set_receive(True).build() + assert not instr3.is_instrumented(InterceptionPoint.SEND) + assert instr3.is_instrumented(InterceptionPoint.ROUTE) + assert instr3.is_instrumented(InterceptionPoint.RECEIVE) + + +def test_timestamp_instrumentation_builder_empty(): + """Test that building with no points raises an error.""" + try: + TimestampInstrumentationBuilder().build() + assert False, "Expected ZError for empty instrumentation" + except zenoh.ZError: + pass + + +def test_pubsub_timestamp_stack(): + """Test publishing with timestamp_instrumentation and reading from sample.""" + zenoh.try_init_log_from_env() + peer01, peer02 = open_session(["tcp/127.0.0.1:17448"]) + + keyexpr = "test/timestamp_stack" + msg = b"hello with timestamps" + + received_sample = None + + def sub_callback(sample: Sample): + nonlocal received_sample + received_sample = sample + + publisher = peer01.declare_publisher(keyexpr) + subscriber = peer02.declare_subscriber(keyexpr, sub_callback) + time.sleep(SLEEP) + + # Test with timestamp_instrumentation + instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() + publisher.put(msg, timestamp_instrumentation=instr) + + time.sleep(SLEEP) + assert received_sample is not None + assert received_sample.timestamp_stack is not None + assert isinstance(received_sample.timestamp_stack, TimestampStack) + + stack = received_sample.timestamp_stack + assert stack.instrumentation is not None + assert isinstance(stack.instrumentation, TimestampInstrumentation) + assert stack.instrumentation.is_instrumented(InterceptionPoint.SEND) + assert stack.instrumentation.is_instrumented(InterceptionPoint.RECEIVE) + + assert len(stack.records) > 0 + for record in stack.records: + assert record.point in [ + InterceptionPoint.SEND, + InterceptionPoint.ROUTE, + InterceptionPoint.RECEIVE, + ] + # timestamp() returns either Timestamp or bytes + ts = record.timestamp() + assert ts is not None + if record.is_custom: + assert isinstance(ts, bytes) + else: + assert isinstance(ts, zenoh.Timestamp) + + # Test without timestamp_instrumentation - should be None + received_sample = None + publisher.put(msg) + time.sleep(SLEEP) + assert received_sample is not None + assert received_sample.timestamp_stack is None + + publisher.undeclare() + subscriber.undeclare() + close_session(peer01, peer02) + + +def test_session_put_timestamp_stack(): + """Test Session.put() with timestamp_instrumentation.""" + zenoh.try_init_log_from_env() + peer01, peer02 = open_session(["tcp/127.0.0.1:17449"]) + + keyexpr = "test/session_timestamp_stack" + msg = b"session put with timestamps" + + received_sample = None + + def sub_callback(sample: Sample): + nonlocal received_sample + received_sample = sample + + subscriber = peer02.declare_subscriber(keyexpr, sub_callback) + time.sleep(SLEEP) + + instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() + peer01.put(keyexpr, msg, timestamp_instrumentation=instr) + + time.sleep(SLEEP) + assert received_sample is not None + assert received_sample.timestamp_stack is not None + assert isinstance(received_sample.timestamp_stack, TimestampStack) + + subscriber.undeclare() + close_session(peer01, peer02) + + +def test_session_get_timestamp_stack(): + """Test Session.get() with timestamp_instrumentation.""" + zenoh.try_init_log_from_env() + peer01, peer02 = open_session(["tcp/127.0.0.1:17450"]) + + keyexpr = "test/get_timestamp_stack" + + def queryable_callback(query): + # The query should have a timestamp_stack when instrumentation is enabled + query.reply(keyexpr, b"reply") + + queryable = peer01.declare_queryable(keyexpr, queryable_callback) + time.sleep(SLEEP) + + instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() + replies = peer02.get(keyexpr, timestamp_instrumentation=instr) + for reply in replies: + sample = reply.ok + if sample: + assert sample.timestamp_stack is not None + assert isinstance(sample.timestamp_stack, TimestampStack) + stack = sample.timestamp_stack + assert stack.instrumentation is not None + assert isinstance(stack.instrumentation, TimestampInstrumentation) + assert stack.instrumentation.is_instrumented(InterceptionPoint.SEND) + assert stack.instrumentation.is_instrumented(InterceptionPoint.RECEIVE) + assert len(stack.records) == 4 + + queryable.undeclare() + close_session(peer01, peer02) + + +def test_delete_timestamp_stack(): + """Test Publisher.delete() with timestamp_instrumentation.""" + zenoh.try_init_log_from_env() + peer01, peer02 = open_session(["tcp/127.0.0.1:17451"]) + + keyexpr = "test/delete_timestamp_stack" + + received_sample = None + + def sub_callback(sample: Sample): + nonlocal received_sample + received_sample = sample + + publisher = peer01.declare_publisher(keyexpr) + subscriber = peer02.declare_subscriber(keyexpr, sub_callback) + time.sleep(SLEEP) + + instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() + publisher.delete(timestamp_instrumentation=instr) + + time.sleep(SLEEP) + assert received_sample is not None + assert received_sample.kind == zenoh.SampleKind.DELETE + assert received_sample.timestamp_stack is not None + assert isinstance(received_sample.timestamp_stack, TimestampStack) + + publisher.undeclare() + subscriber.undeclare() + close_session(peer01, peer02) + + +def test_querier_get_timestamp_stack(): + """Test Querier.get() with timestamp_instrumentation.""" + zenoh.try_init_log_from_env() + peer01, peer02 = open_session(["tcp/127.0.0.1:17452"]) + + keyexpr = "test/querier_timestamp_stack" + + def queryable_callback(query): + query.reply(keyexpr, b"reply from querier test") + + queryable = peer01.declare_queryable(keyexpr, queryable_callback) + time.sleep(SLEEP) + + querier = peer02.declare_querier(keyexpr) + time.sleep(SLEEP) + + instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() + replies = querier.get(timestamp_instrumentation=instr) + for reply in replies: + sample = reply.ok + if sample: + assert sample.timestamp_stack is not None + assert isinstance(sample.timestamp_stack, TimestampStack) + stack = sample.timestamp_stack + assert stack.instrumentation is not None + assert isinstance(stack.instrumentation, TimestampInstrumentation) + assert stack.instrumentation.is_instrumented(InterceptionPoint.SEND) + assert stack.instrumentation.is_instrumented(InterceptionPoint.RECEIVE) + assert len(stack.records) == 4 + + # Test without timestamp_instrumentation - should be None + replies = querier.get() + for reply in replies: + sample = reply.ok + if sample: + assert sample.timestamp_stack is None + + querier.undeclare() + queryable.undeclare() + close_session(peer01, peer02) + + +def test_timestamp_callback(): + """Test Session open with a timestamp callback.""" + zenoh.try_init_log_from_env() + + contexts = [] + custom_timestamp = b"\xde\xad\xbe\xef" + + def timestamp_callback(ctx: TimestampContext): + contexts.append({"zid": str(ctx.zid), "whatami": ctx.whatami}) + return custom_timestamp + + conf = zenoh.Config() + conf.insert_json5("listen/endpoints", json.dumps(["tcp/127.0.0.1:17453"])) + conf.insert_json5("scouting/multicast/enabled", "false") + peer01 = zenoh.open(conf, timestamp_callback=timestamp_callback) + + conf = zenoh.Config() + conf.insert_json5("connect/endpoints", json.dumps(["tcp/127.0.0.1:17453"])) + conf.insert_json5("scouting/multicast/enabled", "false") + peer02 = zenoh.open(conf) + + keyexpr = "test/timestamp_callback" + msg = b"hello with custom timestamps" + + received_sample = None + + def sub_callback(sample: Sample): + nonlocal received_sample + received_sample = sample + + publisher = peer01.declare_publisher(keyexpr) + subscriber = peer02.declare_subscriber(keyexpr, sub_callback) + time.sleep(SLEEP) + + instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() + publisher.put(msg, timestamp_instrumentation=instr) + + time.sleep(SLEEP) + assert received_sample is not None + assert received_sample.timestamp_stack is not None + assert isinstance(received_sample.timestamp_stack, TimestampStack) + + stack = received_sample.timestamp_stack + assert stack.instrumentation is not None + assert stack.instrumentation.is_instrumented(InterceptionPoint.SEND) + assert stack.instrumentation.is_instrumented(InterceptionPoint.RECEIVE) + + assert len(stack.records) > 0 + + # The callback was set on peer01, so timestamps generated on peer01 + # (Send and possibly Route) must be custom. The Receive timestamp is + # generated on peer02, which has no callback, so it remains UHLC. + custom_records = [r for r in stack.records if r.is_custom] + assert len(custom_records) > 0 + for record in custom_records: + assert record.timestamp() == custom_timestamp + + # The callback should have been invoked once per custom timestamp. + assert len(contexts) == len(custom_records) + for ctx in contexts: + assert ctx["whatami"] == zenoh.WhatAmI.PEER + + publisher.undeclare() + subscriber.undeclare() + peer01.close() + peer02.close() diff --git a/zenoh/__init__.pyi b/zenoh/__init__.pyi index 5fe200e5..5f192c53 100644 --- a/zenoh/__init__.pyi +++ b/zenoh/__init__.pyi @@ -741,6 +741,7 @@ class Publisher: encoding: _IntoEncoding | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, source_info: SourceInfo | None = None, ): """Publish data to :class:`Subscriber` instances matching this publisher's key expression. @@ -754,6 +755,7 @@ class Publisher: *, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, source_info: SourceInfo | None = None, ): """Declare that data associated with this publisher's key expression is deleted. @@ -895,6 +897,15 @@ class Query: def source_info(self) -> SourceInfo | None: """Gets info on the source of this Query.""" + @_unstable + @property + def timestamp_stack(self) -> TimestampStack | None: + """Gets the timestamp stack of this Query. + + The timestamp stack carries interception records (Send, Route, Receive) + collected along the message's path through the network. + """ + def drop(self): """Drop the instance of a query. The query will only be finalized when all query instances (one per queryable @@ -990,6 +1001,7 @@ class Querier: attachment: _IntoZBytes | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> Handler[Reply]: """Sends a query and returns a channel for processing replies. @@ -1006,6 +1018,7 @@ class Querier: attachment: _IntoZBytes | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> _H: """Sends a query and returns a channel for processing replies. @@ -1022,6 +1035,7 @@ class Querier: attachment: _IntoZBytes | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> None: """Sends a query and processes replies using the provided callback. @@ -1205,6 +1219,15 @@ class ReplyError: def encoding(self) -> Encoding: """Gets the encoding of this `ReplyError`.""" + @_unstable + @property + def timestamp_stack(self) -> TimestampStack | None: + """Gets the timestamp stack of this ReplyError. + + The timestamp stack carries interception records (Send, Route, Receive) + collected along the message's path through the network. + """ + @final class SampleKind(Enum): """The kind of a :class:`Sample`, indicating whether it contains data or indicates deletion.""" @@ -1266,6 +1289,15 @@ class Sample: def source_info(self) -> SourceInfo | None: """Gets info on the source of this Sample.""" + @_unstable + @property + def timestamp_stack(self) -> TimestampStack | None: + """Gets the timestamp stack of this Sample. + + The timestamp stack carries interception records (Send, Route, Receive) + collected along the message's path through the network. + """ + @final class Scout(Generic[_H]): """A Scout object that yields :class:`zenoh.Hello` messages for discovered Zenoh nodes on the network. @@ -1438,6 +1470,7 @@ class Session: express: bool | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, ): @@ -1455,6 +1488,7 @@ class Session: express: bool | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, ): @@ -1482,6 +1516,7 @@ class Session: allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> Handler[Reply]: """Query data from the matching queryables in the system. @@ -1507,6 +1542,7 @@ class Session: allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> _H: """Query data from the matching queryables in the system. @@ -1532,6 +1568,7 @@ class Session: allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> None: """Query data from the matching queryables in the system. @@ -2128,6 +2165,120 @@ Used in :meth:`Timestamp.__new__` to accept various byte representations that can be converted to a :class:`TimestampId`. """ +@_unstable +@final +class InterceptionPoint(Enum): + """Identifies which interception point a timestamp record was captured at.""" + + SEND = auto() + ROUTE = auto() + RECEIVE = auto() + +@_unstable +@final +class TimestampContext: + """Context passed to the timestamp callback. + + Provides information about the current Zenoh node. + """ + + @property + def zid(self) -> ZenohId: + """The Zenoh ID of the current node.""" + + @property + def whatami(self) -> WhatAmI: + """The mode of the current node (router, peer, or client).""" + + def __repr__(self) -> str: ... + +@_unstable +@final +class TimestampInstrumentationBuilder: + """Builder for creating :class:`TimestampInstrumentation` instances. + + Used to configure which interception points (Send, Route, Receive) + should record timestamps in the timestamp stack. + """ + + def __new__(cls) -> Self: ... + def set_send(self, enabled: bool) -> Self: + """Enable or disable recording timestamps at the Send point.""" + + def set_route(self, enabled: bool) -> Self: + """Enable or disable recording timestamps at the Route point.""" + + def set_receive(self, enabled: bool) -> Self: + """Enable or disable recording timestamps at the Receive point.""" + + def build(self) -> TimestampInstrumentation: + """Build the :class:`TimestampInstrumentation` configuration. + + Raises: + ZError: If no interception points are enabled. + """ + +@_unstable +@final +class TimestampInstrumentation: + """Configuration for which interception points are active in timestamp stack instrumentation. + + Build via :class:`TimestampInstrumentationBuilder`. + """ + + def is_instrumented(self, point: InterceptionPoint) -> bool: + """Check if the given interception point is instrumented.""" + + def __repr__(self) -> str: ... + +@_unstable +@final +class TimestampStackRecord: + """A single interception record in a timestamp stack. + + Represents one timestamp captured at a specific interception point + along a message's path through the network. + """ + + @property + def point(self) -> InterceptionPoint: + """The interception point where this record was captured.""" + + @property + def is_custom(self) -> bool: + """Whether the timestamp was produced by a user-defined callback. + + Returns ``True`` for custom timestamps, ``False`` for standard UHLC timestamps. + """ + + def timestamp(self) -> Timestamp | bytes: + """The timestamp value. + + Returns a :class:`Timestamp` for UHLC timestamps, or ``bytes`` for custom timestamps. + Use :meth:`is_custom` to determine which type to expect. + """ + + def __repr__(self) -> str: ... + +@_unstable +@final +class TimestampStack: + """The complete timestamp stack carried by a received message. + + Contains the instrumentation configuration and the ordered list of + interception records collected as the message traversed the network. + """ + + @property + def instrumentation(self) -> TimestampInstrumentation: + """The instrumentation configuration for this stack.""" + + @property + def records(self) -> list[TimestampStackRecord]: + """The ordered list of interception records.""" + + def __repr__(self) -> str: ... + @final class WhatAmI(Enum): """The type of the node in the Zenoh network. @@ -2260,10 +2411,20 @@ def init_log_from_env_or(level: str): For example, `RUST_LOG=debug` will set the log level to DEBUG. If `RUST_LOG` is not set, then logging is set to the provided level.""" -def open(config: Config) -> Session: +def open( + config: Config, + *, + timestamp_callback: Callable[[TimestampContext], bytes] | None = None, +) -> Session: """Open a zenoh :class:`zenoh.Session`. For more information about sessions and configuration, see :ref:`session-and-config`. + + Args: + config: The configuration for the session. + timestamp_callback: An optional callback invoked at each interception point + (Send, Route, Receive) when timestamp stack instrumentation is enabled. + The callback receives a :class:`TimestampContext` and must return ``bytes``. """ # Common docstring for all scout function overloads diff --git a/zenoh/ext.pyi b/zenoh/ext.pyi index 46676060..26e360aa 100644 --- a/zenoh/ext.pyi +++ b/zenoh/ext.pyi @@ -27,6 +27,7 @@ from zenoh import ( Session, Subscriber, Timestamp, + TimestampInstrumentation, ZBytes, handlers, ) @@ -164,6 +165,7 @@ class AdvancedPublisher: encoding: _IntoEncoding | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Publish data to the key expression. See :meth:`zenoh.Publisher.put`.""" @@ -172,6 +174,7 @@ class AdvancedPublisher: *, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Delete the value associated with the key expression. See :meth:`zenoh.Publisher.delete`.""" From 27a9dfc0648b24c88293eea85007803f96340def Mon Sep 17 00:00:00 2001 From: Oussama Teffahi <70609372+oteffahi@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:40:48 +0200 Subject: [PATCH 05/20] Revert "Expose timestamp instrumentation API (#736)" (#738) This reverts commit 28c33abb8d0595d6656369ae32f4a92ca8f1df85. --- Cargo.lock | 58 +++--- Cargo.toml | 4 +- src/ext.rs | 25 +-- src/lib.rs | 5 - src/pubsub.rs | 17 +- src/query.rs | 17 +- src/sample.rs | 6 - src/session.rs | 30 +-- src/timestamp_stack.rs | 174 ----------------- tests/test_timestamp_stack.py | 343 ---------------------------------- zenoh/__init__.pyi | 163 +--------------- zenoh/ext.pyi | 3 - 12 files changed, 47 insertions(+), 798 deletions(-) delete mode 100644 src/timestamp_stack.rs delete mode 100644 tests/test_timestamp_stack.py diff --git a/Cargo.lock b/Cargo.lock index faf74696..0d6e0e65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3886,7 +3886,7 @@ dependencies = [ [[package]] name = "zenoh" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "ahash", "arc-swap", @@ -3937,7 +3937,7 @@ dependencies = [ [[package]] name = "zenoh-buffers" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "zenoh-collections", ] @@ -3945,7 +3945,7 @@ dependencies = [ [[package]] name = "zenoh-codec" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "tracing", "uhlc", @@ -3957,7 +3957,7 @@ dependencies = [ [[package]] name = "zenoh-collections" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "ahash", ] @@ -3965,7 +3965,7 @@ dependencies = [ [[package]] name = "zenoh-config" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "json5", "nonempty-collections", @@ -3990,7 +3990,7 @@ dependencies = [ [[package]] name = "zenoh-core" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "lazy_static", "tokio", @@ -4001,7 +4001,7 @@ dependencies = [ [[package]] name = "zenoh-crypto" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "aes", "hmac", @@ -4014,7 +4014,7 @@ dependencies = [ [[package]] name = "zenoh-ext" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "bincode", @@ -4033,7 +4033,7 @@ dependencies = [ [[package]] name = "zenoh-keyexpr" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "getrandom 0.2.17", "hashbrown 0.16.1", @@ -4048,7 +4048,7 @@ dependencies = [ [[package]] name = "zenoh-link" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "zenoh-config", "zenoh-link-commons", @@ -4066,7 +4066,7 @@ dependencies = [ [[package]] name = "zenoh-link-commons" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "base64", @@ -4102,7 +4102,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "rustls-webpki", @@ -4118,7 +4118,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic_datagram" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "rustls-webpki", @@ -4134,7 +4134,7 @@ dependencies = [ [[package]] name = "zenoh-link-tcp" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "socket2 0.5.10", @@ -4151,7 +4151,7 @@ dependencies = [ [[package]] name = "zenoh-link-tls" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "base64", @@ -4180,7 +4180,7 @@ dependencies = [ [[package]] name = "zenoh-link-udp" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "libc", @@ -4202,7 +4202,7 @@ dependencies = [ [[package]] name = "zenoh-link-unixsock_stream" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "nix", @@ -4220,7 +4220,7 @@ dependencies = [ [[package]] name = "zenoh-link-ws" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "futures-util", @@ -4240,7 +4240,7 @@ dependencies = [ [[package]] name = "zenoh-macros" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "proc-macro2", "quote", @@ -4251,7 +4251,7 @@ dependencies = [ [[package]] name = "zenoh-plugin-trait" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "git-version", "libloading", @@ -4268,7 +4268,7 @@ dependencies = [ [[package]] name = "zenoh-protocol" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "const_format", "rand 0.8.5", @@ -4293,7 +4293,7 @@ dependencies = [ [[package]] name = "zenoh-result" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "anyhow", ] @@ -4301,7 +4301,7 @@ dependencies = [ [[package]] name = "zenoh-runtime" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "lazy_static", "ron", @@ -4315,7 +4315,7 @@ dependencies = [ [[package]] name = "zenoh-shm" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "advisory-lock", "async-trait", @@ -4344,7 +4344,7 @@ dependencies = [ [[package]] name = "zenoh-stats" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "ahash", "prometheus-client", @@ -4357,7 +4357,7 @@ dependencies = [ [[package]] name = "zenoh-sync" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "arc-swap", "event-listener", @@ -4371,7 +4371,7 @@ dependencies = [ [[package]] name = "zenoh-task" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "futures", "tokio", @@ -4384,7 +4384,7 @@ dependencies = [ [[package]] name = "zenoh-transport" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "crossbeam-utils", @@ -4420,7 +4420,7 @@ dependencies = [ [[package]] name = "zenoh-util" version = "1.9.0" -source = "git+https://github.com/zettascalelabs/zenoh.git?branch=feat%2Frouting-timestamps#16d4621697b57b56a970fcf7e169d868e68d0cb0" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "const_format", diff --git a/Cargo.toml b/Cargo.toml index 8743279e..ff161af4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,10 +44,10 @@ maintenance = { status = "actively-developed" } [dependencies] paste = "1.0.14" pyo3 = { version = "0.25.1", features = ["abi3-py39", "extension-module"] } -zenoh = { version = "1.9.0", git = "https://github.com/zettascalelabs/zenoh.git", branch = "feat/routing-timestamps", features = [ +zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = [ "internal", "unstable", ], default-features = false } -zenoh-ext = { version = "1.9.0", git = "https://github.com/zettascalelabs/zenoh.git", branch = "feat/routing-timestamps", features = [ +zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = [ "internal", ], optional = true } diff --git a/src/ext.rs b/src/ext.rs index 896d5632..d2de6084 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -24,7 +24,6 @@ use crate::{ sample::{Locality, Sample}, session::{EntityGlobalId, Session}, time::Timestamp, - timestamp_stack::TimestampInstrumentation, utils::{duration, generic, wait, MapInto}, ZDeserializeError, }; @@ -493,7 +492,7 @@ impl AdvancedPublisher { Ok(self.get_ref()?.priority().into()) } - #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None, timestamp_instrumentation = None))] + #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None))] fn put( &self, py: Python, @@ -501,38 +500,22 @@ impl AdvancedPublisher { #[pyo3(from_py_with = Encoding::from_py_opt)] encoding: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, - timestamp_instrumentation: Option, ) -> PyResult<()> { let this = self.get_ref()?; wait( py, - build!( - this.put(payload), - encoding, - attachment, - timestamp, - timestamp_instrumentation - ), + build!(this.put(payload), encoding, attachment, timestamp), ) } - #[pyo3(signature = (*, attachment = None, timestamp = None, timestamp_instrumentation = None))] + #[pyo3(signature = (*, attachment = None, timestamp = None))] fn delete( &self, py: Python, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, - timestamp_instrumentation: Option, ) -> PyResult<()> { - wait( - py, - build!( - self.get_ref()?.delete(), - attachment, - timestamp, - timestamp_instrumentation - ), - ) + wait(py, build!(self.get_ref()?.delete(), attachment, timestamp)) } fn undeclare(&mut self, py: Python) -> PyResult<()> { diff --git a/src/lib.rs b/src/lib.rs index 7c622880..09235cce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,6 @@ mod session; #[cfg(feature = "shared-memory")] mod shm; mod time; -mod timestamp_stack; mod utils; use pyo3::prelude::*; @@ -78,10 +77,6 @@ pub(crate) mod zenoh { Transport, TransportEvent, TransportEventsListener, }, time::{Timestamp, TimestampId, NTP64}, - timestamp_stack::{ - InterceptionPoint, TimestampContext, TimestampInstrumentation, - TimestampInstrumentationBuilder, TimestampStack, TimestampStackRecord, - }, ZError, }; diff --git a/src/pubsub.rs b/src/pubsub.rs index 2d036dd1..f5c46ef0 100644 --- a/src/pubsub.rs +++ b/src/pubsub.rs @@ -27,7 +27,6 @@ use crate::{ sample::{Sample, SourceInfo}, session::EntityGlobalId, time::Timestamp, - timestamp_stack::TimestampInstrumentation, utils::{generic, wait}, }; @@ -85,8 +84,7 @@ impl Publisher { Ok(wait(py, self.get_ref()?.matching_status())?.into()) } - #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None, timestamp_instrumentation = None, source_info = None))] + #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None, source_info = None))] fn put( &self, py: Python, @@ -94,7 +92,6 @@ impl Publisher { #[pyo3(from_py_with = Encoding::from_py_opt)] encoding: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, - timestamp_instrumentation: Option, source_info: Option, ) -> PyResult<()> { let this = self.get_ref()?; @@ -103,28 +100,20 @@ impl Publisher { encoding, attachment, timestamp, - timestamp_instrumentation, source_info ); wait(py, builder) } - #[pyo3(signature = (*, attachment = None, timestamp = None, timestamp_instrumentation = None, source_info = None))] + #[pyo3(signature = (*, attachment = None, timestamp = None, source_info = None))] fn delete( &self, py: Python, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, - timestamp_instrumentation: Option, source_info: Option, ) -> PyResult<()> { - let builder = build!( - self.get_ref()?.delete(), - attachment, - timestamp, - timestamp_instrumentation, - source_info - ); + let builder = build!(self.get_ref()?.delete(), attachment, timestamp, source_info); wait(py, builder) } diff --git a/src/query.rs b/src/query.rs index c59a17d8..17d5e674 100644 --- a/src/query.rs +++ b/src/query.rs @@ -30,7 +30,6 @@ use crate::{ sample::SourceInfo, session::EntityGlobalId, time::Timestamp, - timestamp_stack::{TimestampInstrumentation, TimestampStack}, utils::{generic, wait, IntoPyResult, IntoPython, IntoRust, MapInto}, }; @@ -231,11 +230,6 @@ impl Query { Ok(self.get_ref()?.source_info().cloned().map_into()) } - #[getter] - fn timestamp_stack(&self) -> PyResult> { - Ok(self.get_ref()?.timestamp_stack().cloned().map_into()) - } - fn drop(&mut self) { Python::with_gil(|gil| gil.allow_threads(|| drop(self.0.take()))); } @@ -301,11 +295,6 @@ impl ReplyError { self.0.encoding().clone().into() } - #[getter] - fn timestamp_stack(&self) -> Option { - self.0.timestamp_stack().cloned().map_into() - } - fn __repr__(&self) -> String { format!("{:?}", self.0) } @@ -419,7 +408,7 @@ impl Querier { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (handler = None, *, parameters = None, payload = None, encoding = None, attachment = None, source_info = None, cancellation_token = None, timestamp_instrumentation = None))] + #[pyo3(signature = (handler = None, *, parameters = None, payload = None, encoding = None, attachment = None, source_info = None, cancellation_token = None))] fn get( &self, py: Python, @@ -430,7 +419,6 @@ impl Querier { #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, source_info: Option, cancellation_token: Option, - timestamp_instrumentation: Option, ) -> PyResult> { let this = self.get_ref()?; let (handler, _) = into_handler(py, handler, cancellation_token.as_ref())?; @@ -441,8 +429,7 @@ impl Querier { encoding, attachment, source_info, - cancellation_token, - timestamp_instrumentation + cancellation_token ); wait(py, builder.with(handler)).map_into() } diff --git a/src/sample.rs b/src/sample.rs index b5b3ede5..dce32cf6 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -21,7 +21,6 @@ use crate::{ qos::{CongestionControl, Priority}, session::EntityGlobalId, time::Timestamp, - timestamp_stack::TimestampStack, utils::MapInto, }; @@ -96,11 +95,6 @@ impl Sample { self.0.source_info().cloned().map_into() } - #[getter] - fn timestamp_stack(&self) -> Option { - self.0.timestamp_stack().cloned().map_into() - } - fn __repr__(&self) -> String { format!("{:?}", self.0) } diff --git a/src/session.rs b/src/session.rs index 77cecdc4..92ae591e 100644 --- a/src/session.rs +++ b/src/session.rs @@ -33,7 +33,6 @@ use crate::{ query::{Querier, QueryConsolidation, QueryTarget, Queryable, Reply, ReplyKeyExpr, Selector}, sample::{Locality, SampleKind, SourceInfo}, time::Timestamp, - timestamp_stack::TimestampInstrumentation, utils::{duration, wait, IntoPython, MapInto}, }; @@ -95,7 +94,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, timestamp_instrumentation = None, allowed_destination = None, source_info = None))] + #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, allowed_destination = None, source_info = None))] fn put( &self, py: Python, @@ -107,7 +106,6 @@ impl Session { express: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, - timestamp_instrumentation: Option, allowed_destination: Option, source_info: Option, ) -> PyResult<()> { @@ -119,7 +117,6 @@ impl Session { express, attachment, timestamp, - timestamp_instrumentation, allowed_destination, source_info, ); @@ -127,7 +124,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, *, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, timestamp_instrumentation = None, allowed_destination = None, source_info = None))] + #[pyo3(signature = (key_expr, *, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, allowed_destination = None, source_info = None))] fn delete( &self, py: Python, @@ -137,7 +134,6 @@ impl Session { express: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, - timestamp_instrumentation: Option, allowed_destination: Option, source_info: Option, ) -> PyResult<()> { @@ -148,7 +144,6 @@ impl Session { express, attachment, timestamp, - timestamp_instrumentation, allowed_destination, source_info ); @@ -156,7 +151,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (selector, handler = None, *, target = None, consolidation = None, accept_replies = None, timeout = None, congestion_control = None, priority = None, express = None, payload = None, encoding = None, attachment = None, allowed_destination = None, source_info = None, cancellation_token = None, timestamp_instrumentation = None))] + #[pyo3(signature = (selector, handler = None, *, target = None, consolidation = None, accept_replies = None, timeout = None, congestion_control = None, priority = None, express = None, payload = None, encoding = None, attachment = None, allowed_destination = None, source_info = None, cancellation_token = None))] fn get( &self, py: Python, @@ -177,7 +172,6 @@ impl Session { allowed_destination: Option, source_info: Option, cancellation_token: Option, - timestamp_instrumentation: Option, ) -> PyResult> { let (handler, _) = into_handler(py, handler, cancellation_token.as_ref())?; let builder = build!( @@ -194,8 +188,7 @@ impl Session { attachment, allowed_destination, source_info, - cancellation_token, - timestamp_instrumentation + cancellation_token ); wait(py, builder.with(handler)).map_into() @@ -313,19 +306,8 @@ impl Drop for Session { } #[pyfunction] -#[pyo3(signature = (config, *, timestamp_callback=None))] -pub(crate) fn open( - py: Python, - config: Config, - timestamp_callback: Option>, -) -> PyResult { - let builder = zenoh::open(config); - let builder = if let Some(callback) = timestamp_callback { - builder.with_timestamp_callback(crate::timestamp_stack::create_timestamp_callback(callback)) - } else { - builder - }; - wait(py, builder).map(Session) +pub(crate) fn open(py: Python, config: Config) -> PyResult { + wait(py, zenoh::open(config)).map(Session) } wrapper!(zenoh::session::SessionInfo); diff --git a/src/timestamp_stack.rs b/src/timestamp_stack.rs deleted file mode 100644 index 0e3dc418..00000000 --- a/src/timestamp_stack.rs +++ /dev/null @@ -1,174 +0,0 @@ -// -// Copyright (c) 2026 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// -use pyo3::{prelude::*, types::PyBytes}; - -use crate::{ - config::{WhatAmI, ZenohId}, - macros::{enum_mapper, wrapper}, - time::Timestamp, -}; - -enum_mapper!(zenoh::timestamp_stack::InterceptionPoint: u8 { - Send, - Route, - Receive, -}); - -#[pyclass] -pub(crate) struct TimestampContext(pub(crate) zenoh::timestamp_stack::TimestampContext); - -#[pymethods] -impl TimestampContext { - #[getter] - fn zid(&self) -> ZenohId { - ZenohId(self.0.zid) - } - - #[getter] - fn whatami(&self) -> WhatAmI { - self.0.whatami.into() - } - - fn __repr__(&self) -> String { - format!( - "TimestampContext(zid={}, whatami={:?})", - self.0.zid, self.0.whatami - ) - } -} - -fn log_timestamp_callback_error(py: Python, err: PyErr) { - if let Ok(logging) = py.import("logging") { - if let Ok(logger) = logging.call_method1("getLogger", ("zenoh",)) { - let _ = logger.call_method1("error", (format!("timestamp callback error: {err}"),)); - } - } -} - -pub(crate) fn create_timestamp_callback( - callback: Py, -) -> impl Fn(zenoh::timestamp_stack::TimestampContext) -> Vec + Send + Sync + 'static { - move |ctx: zenoh::timestamp_stack::TimestampContext| -> Vec { - Python::with_gil(|py| { - let py_ctx = match Py::new(py, TimestampContext(ctx)) { - Ok(ctx) => ctx, - Err(e) => { - log_timestamp_callback_error(py, e); - return Vec::new(); - } - }; - match callback.call1(py, (py_ctx,)) { - Ok(result) => result.extract::>(py).unwrap_or_else(|e| { - log_timestamp_callback_error(py, e); - Vec::new() - }), - Err(e) => { - log_timestamp_callback_error(py, e); - Vec::new() - } - } - }) - } -} - -wrapper!(zenoh::timestamp_stack::TimestampInstrumentation: Clone, Copy, PartialEq, Eq); - -#[pymethods] -impl TimestampInstrumentation { - fn is_instrumented(&self, point: InterceptionPoint) -> bool { - self.0.is_instrumented(point.into()) - } - - fn __repr__(&self) -> String { - format!("{:?}", self.0) - } -} - -wrapper!(zenoh::timestamp_stack::TimestampInstrumentationBuilder: Clone, Copy); - -#[pymethods] -impl TimestampInstrumentationBuilder { - #[new] - fn new() -> Self { - Self(zenoh::timestamp_stack::TimestampInstrumentationBuilder::new()) - } - - fn set_send(&self, enabled: bool) -> Self { - Self(self.0.set_send(enabled)) - } - - fn set_route(&self, enabled: bool) -> Self { - Self(self.0.set_route(enabled)) - } - - fn set_receive(&self, enabled: bool) -> Self { - Self(self.0.set_receive(enabled)) - } - - fn build(&self) -> PyResult { - self.0 - .build() - .map(TimestampInstrumentation) - .map_err(|e| crate::ZError::new_err(e.to_string())) - } -} - -wrapper!(zenoh::timestamp_stack::TimestampStackRecord: Clone); - -#[pymethods] -impl TimestampStackRecord { - #[getter] - fn point(&self) -> InterceptionPoint { - self.0.point().into() - } - - #[getter] - fn is_custom(&self) -> bool { - self.0.is_custom() - } - - fn timestamp<'py>(&self, py: Python<'py>) -> PyResult> { - match self.0.timestamp() { - zenoh::timestamp_stack::InstrumentationTimestamp::UHLC(ts) => { - Ok(Timestamp::from(*ts).into_pyobject(py)?.into_any()) - } - zenoh::timestamp_stack::InstrumentationTimestamp::Custom(bytes) => { - Ok(PyBytes::new(py, bytes).into_pyobject(py)?.into_any()) - } - } - } - - fn __repr__(&self) -> String { - format!("{:?}", self.0) - } -} - -wrapper!(zenoh::timestamp_stack::TimestampStack: Clone); - -#[pymethods] -impl TimestampStack { - #[getter] - fn instrumentation(&self) -> TimestampInstrumentation { - self.0.instrumentation().into() - } - - #[getter] - fn records(&self) -> Vec { - self.0.records().iter().cloned().map(Into::into).collect() - } - - fn __repr__(&self) -> String { - format!("{:?}", self.0) - } -} diff --git a/tests/test_timestamp_stack.py b/tests/test_timestamp_stack.py deleted file mode 100644 index a547592f..00000000 --- a/tests/test_timestamp_stack.py +++ /dev/null @@ -1,343 +0,0 @@ -# -# Copyright (c) 2026 ZettaScale Technology -# -# This program and the accompanying materials are made available under the -# terms of the Eclipse Public License 2.0 which is available at -# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -# which is available at https://www.apache.org/licenses/LICENSE-2.0. -# -# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -# -# Contributors: -# ZettaScale Zenoh Team, -# -import json -import time - -import zenoh -from zenoh import ( - InterceptionPoint, - Sample, - TimestampContext, - TimestampInstrumentation, - TimestampInstrumentationBuilder, - TimestampStack, -) - -SLEEP = 1 - - -def open_session(endpoints: list[str]) -> tuple[zenoh.Session, zenoh.Session]: - conf = zenoh.Config() - conf.insert_json5("listen/endpoints", json.dumps(endpoints)) - conf.insert_json5("scouting/multicast/enabled", "false") - peer01 = zenoh.open(conf) - - conf = zenoh.Config() - conf.insert_json5("connect/endpoints", json.dumps(endpoints)) - conf.insert_json5("scouting/multicast/enabled", "false") - peer02 = zenoh.open(conf) - - return (peer01, peer02) - - -def close_session(peer01: zenoh.Session, peer02: zenoh.Session): - peer01.close() - peer02.close() - - -def test_timestamp_instrumentation_builder(): - """Test TimestampInstrumentationBuilder and TimestampInstrumentation.""" - builder = TimestampInstrumentationBuilder() - assert builder is not None - - # Build with all points enabled - instr = builder.set_send(True).set_route(True).set_receive(True).build() - assert instr is not None - assert isinstance(instr, TimestampInstrumentation) - assert instr.is_instrumented(InterceptionPoint.SEND) - assert instr.is_instrumented(InterceptionPoint.ROUTE) - assert instr.is_instrumented(InterceptionPoint.RECEIVE) - - # Build with only send enabled - instr2 = TimestampInstrumentationBuilder().set_send(True).build() - assert instr2.is_instrumented(InterceptionPoint.SEND) - assert not instr2.is_instrumented(InterceptionPoint.ROUTE) - assert not instr2.is_instrumented(InterceptionPoint.RECEIVE) - - # Build with only route and receive - instr3 = TimestampInstrumentationBuilder().set_route(True).set_receive(True).build() - assert not instr3.is_instrumented(InterceptionPoint.SEND) - assert instr3.is_instrumented(InterceptionPoint.ROUTE) - assert instr3.is_instrumented(InterceptionPoint.RECEIVE) - - -def test_timestamp_instrumentation_builder_empty(): - """Test that building with no points raises an error.""" - try: - TimestampInstrumentationBuilder().build() - assert False, "Expected ZError for empty instrumentation" - except zenoh.ZError: - pass - - -def test_pubsub_timestamp_stack(): - """Test publishing with timestamp_instrumentation and reading from sample.""" - zenoh.try_init_log_from_env() - peer01, peer02 = open_session(["tcp/127.0.0.1:17448"]) - - keyexpr = "test/timestamp_stack" - msg = b"hello with timestamps" - - received_sample = None - - def sub_callback(sample: Sample): - nonlocal received_sample - received_sample = sample - - publisher = peer01.declare_publisher(keyexpr) - subscriber = peer02.declare_subscriber(keyexpr, sub_callback) - time.sleep(SLEEP) - - # Test with timestamp_instrumentation - instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() - publisher.put(msg, timestamp_instrumentation=instr) - - time.sleep(SLEEP) - assert received_sample is not None - assert received_sample.timestamp_stack is not None - assert isinstance(received_sample.timestamp_stack, TimestampStack) - - stack = received_sample.timestamp_stack - assert stack.instrumentation is not None - assert isinstance(stack.instrumentation, TimestampInstrumentation) - assert stack.instrumentation.is_instrumented(InterceptionPoint.SEND) - assert stack.instrumentation.is_instrumented(InterceptionPoint.RECEIVE) - - assert len(stack.records) > 0 - for record in stack.records: - assert record.point in [ - InterceptionPoint.SEND, - InterceptionPoint.ROUTE, - InterceptionPoint.RECEIVE, - ] - # timestamp() returns either Timestamp or bytes - ts = record.timestamp() - assert ts is not None - if record.is_custom: - assert isinstance(ts, bytes) - else: - assert isinstance(ts, zenoh.Timestamp) - - # Test without timestamp_instrumentation - should be None - received_sample = None - publisher.put(msg) - time.sleep(SLEEP) - assert received_sample is not None - assert received_sample.timestamp_stack is None - - publisher.undeclare() - subscriber.undeclare() - close_session(peer01, peer02) - - -def test_session_put_timestamp_stack(): - """Test Session.put() with timestamp_instrumentation.""" - zenoh.try_init_log_from_env() - peer01, peer02 = open_session(["tcp/127.0.0.1:17449"]) - - keyexpr = "test/session_timestamp_stack" - msg = b"session put with timestamps" - - received_sample = None - - def sub_callback(sample: Sample): - nonlocal received_sample - received_sample = sample - - subscriber = peer02.declare_subscriber(keyexpr, sub_callback) - time.sleep(SLEEP) - - instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() - peer01.put(keyexpr, msg, timestamp_instrumentation=instr) - - time.sleep(SLEEP) - assert received_sample is not None - assert received_sample.timestamp_stack is not None - assert isinstance(received_sample.timestamp_stack, TimestampStack) - - subscriber.undeclare() - close_session(peer01, peer02) - - -def test_session_get_timestamp_stack(): - """Test Session.get() with timestamp_instrumentation.""" - zenoh.try_init_log_from_env() - peer01, peer02 = open_session(["tcp/127.0.0.1:17450"]) - - keyexpr = "test/get_timestamp_stack" - - def queryable_callback(query): - # The query should have a timestamp_stack when instrumentation is enabled - query.reply(keyexpr, b"reply") - - queryable = peer01.declare_queryable(keyexpr, queryable_callback) - time.sleep(SLEEP) - - instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() - replies = peer02.get(keyexpr, timestamp_instrumentation=instr) - for reply in replies: - sample = reply.ok - if sample: - assert sample.timestamp_stack is not None - assert isinstance(sample.timestamp_stack, TimestampStack) - stack = sample.timestamp_stack - assert stack.instrumentation is not None - assert isinstance(stack.instrumentation, TimestampInstrumentation) - assert stack.instrumentation.is_instrumented(InterceptionPoint.SEND) - assert stack.instrumentation.is_instrumented(InterceptionPoint.RECEIVE) - assert len(stack.records) == 4 - - queryable.undeclare() - close_session(peer01, peer02) - - -def test_delete_timestamp_stack(): - """Test Publisher.delete() with timestamp_instrumentation.""" - zenoh.try_init_log_from_env() - peer01, peer02 = open_session(["tcp/127.0.0.1:17451"]) - - keyexpr = "test/delete_timestamp_stack" - - received_sample = None - - def sub_callback(sample: Sample): - nonlocal received_sample - received_sample = sample - - publisher = peer01.declare_publisher(keyexpr) - subscriber = peer02.declare_subscriber(keyexpr, sub_callback) - time.sleep(SLEEP) - - instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() - publisher.delete(timestamp_instrumentation=instr) - - time.sleep(SLEEP) - assert received_sample is not None - assert received_sample.kind == zenoh.SampleKind.DELETE - assert received_sample.timestamp_stack is not None - assert isinstance(received_sample.timestamp_stack, TimestampStack) - - publisher.undeclare() - subscriber.undeclare() - close_session(peer01, peer02) - - -def test_querier_get_timestamp_stack(): - """Test Querier.get() with timestamp_instrumentation.""" - zenoh.try_init_log_from_env() - peer01, peer02 = open_session(["tcp/127.0.0.1:17452"]) - - keyexpr = "test/querier_timestamp_stack" - - def queryable_callback(query): - query.reply(keyexpr, b"reply from querier test") - - queryable = peer01.declare_queryable(keyexpr, queryable_callback) - time.sleep(SLEEP) - - querier = peer02.declare_querier(keyexpr) - time.sleep(SLEEP) - - instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() - replies = querier.get(timestamp_instrumentation=instr) - for reply in replies: - sample = reply.ok - if sample: - assert sample.timestamp_stack is not None - assert isinstance(sample.timestamp_stack, TimestampStack) - stack = sample.timestamp_stack - assert stack.instrumentation is not None - assert isinstance(stack.instrumentation, TimestampInstrumentation) - assert stack.instrumentation.is_instrumented(InterceptionPoint.SEND) - assert stack.instrumentation.is_instrumented(InterceptionPoint.RECEIVE) - assert len(stack.records) == 4 - - # Test without timestamp_instrumentation - should be None - replies = querier.get() - for reply in replies: - sample = reply.ok - if sample: - assert sample.timestamp_stack is None - - querier.undeclare() - queryable.undeclare() - close_session(peer01, peer02) - - -def test_timestamp_callback(): - """Test Session open with a timestamp callback.""" - zenoh.try_init_log_from_env() - - contexts = [] - custom_timestamp = b"\xde\xad\xbe\xef" - - def timestamp_callback(ctx: TimestampContext): - contexts.append({"zid": str(ctx.zid), "whatami": ctx.whatami}) - return custom_timestamp - - conf = zenoh.Config() - conf.insert_json5("listen/endpoints", json.dumps(["tcp/127.0.0.1:17453"])) - conf.insert_json5("scouting/multicast/enabled", "false") - peer01 = zenoh.open(conf, timestamp_callback=timestamp_callback) - - conf = zenoh.Config() - conf.insert_json5("connect/endpoints", json.dumps(["tcp/127.0.0.1:17453"])) - conf.insert_json5("scouting/multicast/enabled", "false") - peer02 = zenoh.open(conf) - - keyexpr = "test/timestamp_callback" - msg = b"hello with custom timestamps" - - received_sample = None - - def sub_callback(sample: Sample): - nonlocal received_sample - received_sample = sample - - publisher = peer01.declare_publisher(keyexpr) - subscriber = peer02.declare_subscriber(keyexpr, sub_callback) - time.sleep(SLEEP) - - instr = TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() - publisher.put(msg, timestamp_instrumentation=instr) - - time.sleep(SLEEP) - assert received_sample is not None - assert received_sample.timestamp_stack is not None - assert isinstance(received_sample.timestamp_stack, TimestampStack) - - stack = received_sample.timestamp_stack - assert stack.instrumentation is not None - assert stack.instrumentation.is_instrumented(InterceptionPoint.SEND) - assert stack.instrumentation.is_instrumented(InterceptionPoint.RECEIVE) - - assert len(stack.records) > 0 - - # The callback was set on peer01, so timestamps generated on peer01 - # (Send and possibly Route) must be custom. The Receive timestamp is - # generated on peer02, which has no callback, so it remains UHLC. - custom_records = [r for r in stack.records if r.is_custom] - assert len(custom_records) > 0 - for record in custom_records: - assert record.timestamp() == custom_timestamp - - # The callback should have been invoked once per custom timestamp. - assert len(contexts) == len(custom_records) - for ctx in contexts: - assert ctx["whatami"] == zenoh.WhatAmI.PEER - - publisher.undeclare() - subscriber.undeclare() - peer01.close() - peer02.close() diff --git a/zenoh/__init__.pyi b/zenoh/__init__.pyi index 5f192c53..5fe200e5 100644 --- a/zenoh/__init__.pyi +++ b/zenoh/__init__.pyi @@ -741,7 +741,6 @@ class Publisher: encoding: _IntoEncoding | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, source_info: SourceInfo | None = None, ): """Publish data to :class:`Subscriber` instances matching this publisher's key expression. @@ -755,7 +754,6 @@ class Publisher: *, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, source_info: SourceInfo | None = None, ): """Declare that data associated with this publisher's key expression is deleted. @@ -897,15 +895,6 @@ class Query: def source_info(self) -> SourceInfo | None: """Gets info on the source of this Query.""" - @_unstable - @property - def timestamp_stack(self) -> TimestampStack | None: - """Gets the timestamp stack of this Query. - - The timestamp stack carries interception records (Send, Route, Receive) - collected along the message's path through the network. - """ - def drop(self): """Drop the instance of a query. The query will only be finalized when all query instances (one per queryable @@ -1001,7 +990,6 @@ class Querier: attachment: _IntoZBytes | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> Handler[Reply]: """Sends a query and returns a channel for processing replies. @@ -1018,7 +1006,6 @@ class Querier: attachment: _IntoZBytes | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> _H: """Sends a query and returns a channel for processing replies. @@ -1035,7 +1022,6 @@ class Querier: attachment: _IntoZBytes | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> None: """Sends a query and processes replies using the provided callback. @@ -1219,15 +1205,6 @@ class ReplyError: def encoding(self) -> Encoding: """Gets the encoding of this `ReplyError`.""" - @_unstable - @property - def timestamp_stack(self) -> TimestampStack | None: - """Gets the timestamp stack of this ReplyError. - - The timestamp stack carries interception records (Send, Route, Receive) - collected along the message's path through the network. - """ - @final class SampleKind(Enum): """The kind of a :class:`Sample`, indicating whether it contains data or indicates deletion.""" @@ -1289,15 +1266,6 @@ class Sample: def source_info(self) -> SourceInfo | None: """Gets info on the source of this Sample.""" - @_unstable - @property - def timestamp_stack(self) -> TimestampStack | None: - """Gets the timestamp stack of this Sample. - - The timestamp stack carries interception records (Send, Route, Receive) - collected along the message's path through the network. - """ - @final class Scout(Generic[_H]): """A Scout object that yields :class:`zenoh.Hello` messages for discovered Zenoh nodes on the network. @@ -1470,7 +1438,6 @@ class Session: express: bool | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, ): @@ -1488,7 +1455,6 @@ class Session: express: bool | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, ): @@ -1516,7 +1482,6 @@ class Session: allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> Handler[Reply]: """Query data from the matching queryables in the system. @@ -1542,7 +1507,6 @@ class Session: allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> _H: """Query data from the matching queryables in the system. @@ -1568,7 +1532,6 @@ class Session: allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> None: """Query data from the matching queryables in the system. @@ -2165,120 +2128,6 @@ Used in :meth:`Timestamp.__new__` to accept various byte representations that can be converted to a :class:`TimestampId`. """ -@_unstable -@final -class InterceptionPoint(Enum): - """Identifies which interception point a timestamp record was captured at.""" - - SEND = auto() - ROUTE = auto() - RECEIVE = auto() - -@_unstable -@final -class TimestampContext: - """Context passed to the timestamp callback. - - Provides information about the current Zenoh node. - """ - - @property - def zid(self) -> ZenohId: - """The Zenoh ID of the current node.""" - - @property - def whatami(self) -> WhatAmI: - """The mode of the current node (router, peer, or client).""" - - def __repr__(self) -> str: ... - -@_unstable -@final -class TimestampInstrumentationBuilder: - """Builder for creating :class:`TimestampInstrumentation` instances. - - Used to configure which interception points (Send, Route, Receive) - should record timestamps in the timestamp stack. - """ - - def __new__(cls) -> Self: ... - def set_send(self, enabled: bool) -> Self: - """Enable or disable recording timestamps at the Send point.""" - - def set_route(self, enabled: bool) -> Self: - """Enable or disable recording timestamps at the Route point.""" - - def set_receive(self, enabled: bool) -> Self: - """Enable or disable recording timestamps at the Receive point.""" - - def build(self) -> TimestampInstrumentation: - """Build the :class:`TimestampInstrumentation` configuration. - - Raises: - ZError: If no interception points are enabled. - """ - -@_unstable -@final -class TimestampInstrumentation: - """Configuration for which interception points are active in timestamp stack instrumentation. - - Build via :class:`TimestampInstrumentationBuilder`. - """ - - def is_instrumented(self, point: InterceptionPoint) -> bool: - """Check if the given interception point is instrumented.""" - - def __repr__(self) -> str: ... - -@_unstable -@final -class TimestampStackRecord: - """A single interception record in a timestamp stack. - - Represents one timestamp captured at a specific interception point - along a message's path through the network. - """ - - @property - def point(self) -> InterceptionPoint: - """The interception point where this record was captured.""" - - @property - def is_custom(self) -> bool: - """Whether the timestamp was produced by a user-defined callback. - - Returns ``True`` for custom timestamps, ``False`` for standard UHLC timestamps. - """ - - def timestamp(self) -> Timestamp | bytes: - """The timestamp value. - - Returns a :class:`Timestamp` for UHLC timestamps, or ``bytes`` for custom timestamps. - Use :meth:`is_custom` to determine which type to expect. - """ - - def __repr__(self) -> str: ... - -@_unstable -@final -class TimestampStack: - """The complete timestamp stack carried by a received message. - - Contains the instrumentation configuration and the ordered list of - interception records collected as the message traversed the network. - """ - - @property - def instrumentation(self) -> TimestampInstrumentation: - """The instrumentation configuration for this stack.""" - - @property - def records(self) -> list[TimestampStackRecord]: - """The ordered list of interception records.""" - - def __repr__(self) -> str: ... - @final class WhatAmI(Enum): """The type of the node in the Zenoh network. @@ -2411,20 +2260,10 @@ def init_log_from_env_or(level: str): For example, `RUST_LOG=debug` will set the log level to DEBUG. If `RUST_LOG` is not set, then logging is set to the provided level.""" -def open( - config: Config, - *, - timestamp_callback: Callable[[TimestampContext], bytes] | None = None, -) -> Session: +def open(config: Config) -> Session: """Open a zenoh :class:`zenoh.Session`. For more information about sessions and configuration, see :ref:`session-and-config`. - - Args: - config: The configuration for the session. - timestamp_callback: An optional callback invoked at each interception point - (Send, Route, Receive) when timestamp stack instrumentation is enabled. - The callback receives a :class:`TimestampContext` and must return ``bytes``. """ # Common docstring for all scout function overloads diff --git a/zenoh/ext.pyi b/zenoh/ext.pyi index 26e360aa..46676060 100644 --- a/zenoh/ext.pyi +++ b/zenoh/ext.pyi @@ -27,7 +27,6 @@ from zenoh import ( Session, Subscriber, Timestamp, - TimestampInstrumentation, ZBytes, handlers, ) @@ -165,7 +164,6 @@ class AdvancedPublisher: encoding: _IntoEncoding | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Publish data to the key expression. See :meth:`zenoh.Publisher.put`.""" @@ -174,7 +172,6 @@ class AdvancedPublisher: *, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Delete the value associated with the key expression. See :meth:`zenoh.Publisher.delete`.""" From d78fafaa8110a9449ab040c37597d2291a35c149 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 2 Jun 2026 08:31:30 +0000 Subject: [PATCH 06/20] feat(zenoh-python): add timestamp instrumentation bindings - src/timestamp_stack.rs: InterceptionPoint, TsStackContext, TimestampInstrumentation, TimestampStackRecord, TimestampStack, py_to_session_ts_callback - src/lib.rs: module registered, types exported - src/sample.rs, query.rs: timestamp_stack() getters on Sample, Reply, ReplyError - src/session.rs, pubsub.rs: timestamp_instrumentation / timestamp_callback kwargs - zenoh/__init__.pyi: stubs for all new types and kwargs - examples/z_timestamp_instrumentation.py: end-to-end usage example - tests/test_timestamp_stack.py: 10 integration tests --- examples/z_timestamp_instrumentation.py | 114 +++++++++++ src/lib.rs | 5 + src/pubsub.rs | 10 +- src/query.rs | 14 ++ src/sample.rs | 6 + src/session.rs | 39 +++- src/timestamp_stack.rs | 182 ++++++++++++++++++ tests/test_timestamp_stack.py | 239 ++++++++++++++++++++++++ zenoh/__init__.pyi | 10 +- 9 files changed, 607 insertions(+), 12 deletions(-) create mode 100644 examples/z_timestamp_instrumentation.py create mode 100644 src/timestamp_stack.rs create mode 100644 tests/test_timestamp_stack.py diff --git a/examples/z_timestamp_instrumentation.py b/examples/z_timestamp_instrumentation.py new file mode 100644 index 00000000..d4c0ab65 --- /dev/null +++ b/examples/z_timestamp_instrumentation.py @@ -0,0 +1,114 @@ +# +# Copyright (c) 2026 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# +# Demonstrates opt-in end-to-end latency instrumentation. +# +# Run this example to see Send/Route/Receive timestamps on each message. +# The custom_callback variant shows how to inject your own clock bytes. +# +import time + +import zenoh +from zenoh import InterceptionPoint, TimestampInstrumentation + + +def print_stack(stack): + if stack is None: + print(" (no timestamp stack)") + return + for rec in stack.records: + ts = rec.as_timestamp() + if ts is not None: + print(f" {rec.point.name:8s} hlc={ts} custom={rec.is_custom}") + else: + print( + f" {rec.point.name:8s} raw={rec.timestamp().hex()} custom={rec.is_custom}" + ) + + +def example_put_subscribe(session): + print("\n── put/subscribe with send+receive instrumentation ─────────────────") + instr = TimestampInstrumentation(send=True, receive=True) + received = [] + with session.declare_subscriber("demo/ts/**", lambda s: received.append(s)): + time.sleep(0.05) + session.put("demo/ts/hello", b"world", timestamp_instrumentation=instr) + time.sleep(0.2) + if received: + print(f"Received sample on '{received[0].key_expr}':") + print_stack(received[0].timestamp_stack) + + +def example_publisher_default(session): + print("\n── publisher with default instrumentation ───────────────────────────") + instr = TimestampInstrumentation(send=True, receive=True) + received = [] + with session.declare_publisher( + "demo/ts/pub", timestamp_instrumentation=instr + ) as pub: + with session.declare_subscriber("demo/ts/pub", lambda s: received.append(s)): + time.sleep(0.05) + pub.put(b"message-1") + pub.put( + b"message-2", + timestamp_instrumentation=TimestampInstrumentation(send=True), + ) + time.sleep(0.2) + for s in received: + print(f"Received '{s.payload.to_string()}':") + print_stack(s.timestamp_stack) + + +def example_custom_callback(): + print("\n── session with custom timestamp callback ───────────────────────────") + import struct + import time as _t + + def my_clock(ctx): + # Return a simple 8-byte little-endian nanosecond timestamp. + ns = int(_t.time_ns()) + return struct.pack(", timestamp: Option, source_info: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { let this = self.get_ref()?; - let builder = build!( + let mut builder = build!( this.put(payload), encoding, attachment, timestamp, source_info ); + if let Some(instr) = timestamp_instrumentation { + builder = builder.timestamp_instrumentation(Some(instr.0)); + } wait(py, builder) } diff --git a/src/query.rs b/src/query.rs index 17d5e674..ccd785c7 100644 --- a/src/query.rs +++ b/src/query.rs @@ -30,6 +30,7 @@ use crate::{ sample::SourceInfo, session::EntityGlobalId, time::Timestamp, + timestamp_stack::TimestampStack, utils::{generic, wait, IntoPyResult, IntoPython, IntoRust, MapInto}, }; @@ -276,6 +277,14 @@ impl Reply { self.0.replier_id().map_into() } + #[getter] + fn timestamp_stack(&self) -> Option { + match self.0.result() { + Ok(sample) => sample.timestamp_stack().cloned().map(TimestampStack), + Err(err) => err.timestamp_stack().cloned().map(TimestampStack), + } + } + fn __repr__(&self) -> String { format!("{:?}", self.0) } @@ -295,6 +304,11 @@ impl ReplyError { self.0.encoding().clone().into() } + #[getter] + fn timestamp_stack(&self) -> Option { + self.0.timestamp_stack().cloned().map(TimestampStack) + } + fn __repr__(&self) -> String { format!("{:?}", self.0) } diff --git a/src/sample.rs b/src/sample.rs index dce32cf6..07aaea53 100644 --- a/src/sample.rs +++ b/src/sample.rs @@ -21,6 +21,7 @@ use crate::{ qos::{CongestionControl, Priority}, session::EntityGlobalId, time::Timestamp, + timestamp_stack::TimestampStack, utils::MapInto, }; @@ -95,6 +96,11 @@ impl Sample { self.0.source_info().cloned().map_into() } + #[getter] + fn timestamp_stack(&self) -> Option { + self.0.timestamp_stack().cloned().map(TimestampStack) + } + fn __repr__(&self) -> String { format!("{:?}", self.0) } diff --git a/src/session.rs b/src/session.rs index 92ae591e..686fc964 100644 --- a/src/session.rs +++ b/src/session.rs @@ -33,6 +33,7 @@ use crate::{ query::{Querier, QueryConsolidation, QueryTarget, Queryable, Reply, ReplyKeyExpr, Selector}, sample::{Locality, SampleKind, SourceInfo}, time::Timestamp, + timestamp_stack::{py_to_session_ts_callback, TimestampInstrumentation}, utils::{duration, wait, IntoPython, MapInto}, }; @@ -94,7 +95,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, allowed_destination = None, source_info = None))] + #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, allowed_destination = None, source_info = None, timestamp_instrumentation = None))] fn put( &self, py: Python, @@ -108,8 +109,9 @@ impl Session { timestamp: Option, allowed_destination: Option, source_info: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { - let build = build!( + let mut build = build!( self.0.put(key_expr, payload), encoding, congestion_control, @@ -120,6 +122,9 @@ impl Session { allowed_destination, source_info, ); + if let Some(instr) = timestamp_instrumentation { + build = build.timestamp_instrumentation(Some(instr.0)); + } wait(py, build) } @@ -151,7 +156,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (selector, handler = None, *, target = None, consolidation = None, accept_replies = None, timeout = None, congestion_control = None, priority = None, express = None, payload = None, encoding = None, attachment = None, allowed_destination = None, source_info = None, cancellation_token = None))] + #[pyo3(signature = (selector, handler = None, *, target = None, consolidation = None, accept_replies = None, timeout = None, congestion_control = None, priority = None, express = None, payload = None, encoding = None, attachment = None, allowed_destination = None, source_info = None, cancellation_token = None, timestamp_instrumentation = None))] fn get( &self, py: Python, @@ -172,9 +177,10 @@ impl Session { allowed_destination: Option, source_info: Option, cancellation_token: Option, + timestamp_instrumentation: Option, ) -> PyResult> { let (handler, _) = into_handler(py, handler, cancellation_token.as_ref())?; - let builder = build!( + let mut builder = build!( self.0.get(selector), target, consolidation, @@ -190,7 +196,9 @@ impl Session { source_info, cancellation_token ); - + if let Some(instr) = timestamp_instrumentation { + builder = builder.timestamp_instrumentation(Some(instr.0)); + } wait(py, builder.with(handler)).map_into() } @@ -235,7 +243,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, *, encoding = None, congestion_control = None, priority = None, express = None, reliability = None, allowed_destination = None))] + #[pyo3(signature = (key_expr, *, encoding = None, congestion_control = None, priority = None, express = None, reliability = None, allowed_destination = None, timestamp_instrumentation = None))] fn declare_publisher( &self, py: Python, @@ -246,8 +254,9 @@ impl Session { express: Option, reliability: Option, allowed_destination: Option, + timestamp_instrumentation: Option, ) -> PyResult { - let builder = build!( + let mut builder = build!( self.0.declare_publisher(key_expr), encoding, congestion_control, @@ -256,6 +265,9 @@ impl Session { reliability, allowed_destination, ); + if let Some(instr) = timestamp_instrumentation { + builder = builder.timestamp_instrumentation(Some(instr.0)); + } wait(py, builder).map_into() } @@ -306,8 +318,17 @@ impl Drop for Session { } #[pyfunction] -pub(crate) fn open(py: Python, config: Config) -> PyResult { - wait(py, zenoh::open(config)).map(Session) +#[pyo3(signature = (config, *, timestamp_callback = None))] +pub(crate) fn open( + py: Python, + config: Config, + timestamp_callback: Option, +) -> PyResult { + let mut builder = zenoh::open(config); + if let Some(cb) = timestamp_callback { + builder = builder.with_timestamp_callback(py_to_session_ts_callback(cb)); + } + wait(py, builder).map(Session) } wrapper!(zenoh::session::SessionInfo); diff --git a/src/timestamp_stack.rs b/src/timestamp_stack.rs new file mode 100644 index 00000000..e0c13300 --- /dev/null +++ b/src/timestamp_stack.rs @@ -0,0 +1,182 @@ +// +// Copyright (c) 2026 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +use std::sync::Arc; + +use pyo3::{prelude::*, types::PyBytes}; +use zenoh::timestamp_stack::{ + InterceptionPoint as RustInterceptionPoint, SessionTimestampCallback, + TimestampInstrumentation as RustTimestampInstrumentation, TsStackContext as RustTsStackContext, +}; + +use crate::{ + config::{WhatAmI, ZenohId}, + macros::wrapper, + time::Timestamp, + utils::IntoPyResult, +}; + +// InterceptionPoint is #[non_exhaustive] so we can't use enum_mapper! (it generates exhaustive +// From impls). Define it manually with a repr u8 for Python comparison, and a fallback variant. +#[pyo3::pyclass(eq)] +#[repr(u8)] +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum InterceptionPoint { + #[pyo3(name = "SEND")] + Send = 0, + #[pyo3(name = "ROUTE")] + Route = 1, + #[pyo3(name = "RECEIVE")] + Receive = 2, + /// Catch-all for future variants added by the Rust core. + #[pyo3(name = "UNKNOWN")] + Unknown = 255, +} + +impl From for InterceptionPoint { + fn from(v: RustInterceptionPoint) -> Self { + match v { + RustInterceptionPoint::Send => Self::Send, + RustInterceptionPoint::Route => Self::Route, + RustInterceptionPoint::Receive => Self::Receive, + _ => Self::Unknown, + } + } +} + +impl From for RustInterceptionPoint { + fn from(v: InterceptionPoint) -> Self { + match v { + InterceptionPoint::Send => RustInterceptionPoint::Send, + InterceptionPoint::Route => RustInterceptionPoint::Route, + InterceptionPoint::Receive | InterceptionPoint::Unknown => { + RustInterceptionPoint::Receive + } + } + } +} + +wrapper!(zenoh::timestamp_stack::TsStackContext: Clone); + +#[pymethods] +impl TsStackContext { + #[getter] + fn zid(&self) -> ZenohId { + self.0.zid.into() + } + + #[getter] + fn whatami(&self) -> WhatAmI { + self.0.whatami.into() + } + + #[getter] + fn interception_point(&self) -> InterceptionPoint { + self.0.interception_point.into() + } + + fn __repr__(&self) -> String { + format!("{:?}", self.0) + } +} + +wrapper!(zenoh::timestamp_stack::TimestampInstrumentation: Clone, Copy); + +#[pymethods] +impl TimestampInstrumentation { + #[new] + #[pyo3(signature = (*, send = false, route = false, receive = false))] + fn new(send: bool, route: bool, receive: bool) -> PyResult { + RustTimestampInstrumentation::new(send, route, receive) + .map(Self) + .into_pyres() + } + + fn is_instrumented(&self, point: InterceptionPoint) -> bool { + self.0.is_instrumented(point.into()) + } + + fn __repr__(&self) -> String { + format!("{:?}", self.0) + } +} + +wrapper!(zenoh::timestamp_stack::TimestampStackRecord: Clone); + +#[pymethods] +impl TimestampStackRecord { + #[getter] + fn point(&self) -> InterceptionPoint { + self.0.point().into() + } + + #[getter] + fn is_custom(&self) -> bool { + self.0.is_custom() + } + + fn timestamp<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> { + PyBytes::new(py, self.0.timestamp()) + } + + fn as_timestamp(&self) -> Option { + self.0.as_timestamp().map(Timestamp) + } + + fn __repr__(&self) -> String { + format!("{:?}", self.0) + } +} + +wrapper!(zenoh::timestamp_stack::TimestampStack: Clone); + +#[pymethods] +impl TimestampStack { + #[getter] + fn instrumentation(&self) -> TimestampInstrumentation { + TimestampInstrumentation(self.0.instrumentation()) + } + + #[getter] + fn records(&self) -> Vec { + self.0 + .records() + .iter() + .cloned() + .map(TimestampStackRecord) + .collect() + } + + fn __repr__(&self) -> String { + format!("{:?}", self.0) + } +} + +/// Build a `SessionTimestampCallback` Arc from a Python callable. +pub(crate) fn py_to_session_ts_callback(py_cb: PyObject) -> SessionTimestampCallback { + Arc::new(move |ctx: RustTsStackContext| { + Python::with_gil(|py| { + let py_ctx = match Py::new(py, TsStackContext(ctx)) { + Ok(obj) => obj, + Err(_) => return Vec::new(), + }; + match py_cb.call1(py, (py_ctx,)) { + Ok(result) => result.extract::>(py).unwrap_or_default(), + Err(e) => { + e.print(py); + Vec::new() + } + } + }) + }) +} diff --git a/tests/test_timestamp_stack.py b/tests/test_timestamp_stack.py new file mode 100644 index 00000000..d5814f72 --- /dev/null +++ b/tests/test_timestamp_stack.py @@ -0,0 +1,239 @@ +# +# Copyright (c) 2026 ZettaScale Technology +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +# which is available at https://www.apache.org/licenses/LICENSE-2.0. +# +# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +# +# Contributors: +# ZettaScale Zenoh Team, +# +import time +from typing import List, Optional + +import pytest + +import zenoh +from zenoh import InterceptionPoint, TimestampInstrumentation, TimestampStack + +SLEEP = 0.2 + + +def peer_config() -> zenoh.Config: + cfg = zenoh.Config() + cfg.insert_json5("scouting/multicast/enabled", "false") + return cfg + + +# ── helpers ─────────────────────────────────────────────────────────────────── + + +def collect_one(key: str, action, timeout: float = SLEEP) -> Optional[zenoh.Sample]: + received: List[zenoh.Sample] = [] + with zenoh.open(peer_config()) as session: + with session.declare_subscriber(key, lambda s: received.append(s)): + time.sleep(0.05) + action(session) + time.sleep(timeout) + return received[0] if received else None + + +# ── test_no_instrumentation ─────────────────────────────────────────────────── + + +def test_no_instrumentation(): + """Without instrumentation the stack should be None.""" + + def put(session): + session.put("test/ts/none", b"hello") + + sample = collect_one("test/ts/none", put) + assert sample is not None + assert sample.timestamp_stack is None + + +# ── test_put_subscribe_send_receive ────────────────────────────────────────── + + +def test_put_subscribe_send_receive(): + """A put with send+receive instrumentation produces SEND and RECEIVE records.""" + instr = TimestampInstrumentation(send=True, receive=True) + + def put(session): + session.put("test/ts/put", b"hello", timestamp_instrumentation=instr) + + sample = collect_one("test/ts/put", put) + assert sample is not None + stack = sample.timestamp_stack + assert stack is not None + points = [r.point for r in stack.records] + assert InterceptionPoint.SEND in points + assert InterceptionPoint.RECEIVE in points + + +# ── test_send_only ──────────────────────────────────────────────────────────── + + +def test_send_only(): + """send=True, receive=False → only SEND record.""" + instr = TimestampInstrumentation(send=True, receive=False) + + def put(session): + session.put("test/ts/send_only", b"x", timestamp_instrumentation=instr) + + sample = collect_one("test/ts/send_only", put) + assert sample is not None + stack = sample.timestamp_stack + assert stack is not None + points = [r.point for r in stack.records] + assert InterceptionPoint.SEND in points + assert InterceptionPoint.RECEIVE not in points + + +# ── test_receive_only ───────────────────────────────────────────────────────── + + +def test_receive_only(): + """receive=True, send=False → only RECEIVE record.""" + instr = TimestampInstrumentation(send=False, receive=True) + + def put(session): + session.put("test/ts/recv_only", b"x", timestamp_instrumentation=instr) + + sample = collect_one("test/ts/recv_only", put) + assert sample is not None + stack = sample.timestamp_stack + assert stack is not None + points = [r.point for r in stack.records] + assert InterceptionPoint.RECEIVE in points + assert InterceptionPoint.SEND not in points + + +# ── test_publisher_default ──────────────────────────────────────────────────── + + +def test_publisher_default(): + """Publisher-level default instrumentation applies to all puts.""" + instr = TimestampInstrumentation(send=True, receive=True) + received: List[zenoh.Sample] = [] + + with zenoh.open(peer_config()) as session: + with session.declare_subscriber( + "test/ts/pub_default", lambda s: received.append(s) + ): + with session.declare_publisher( + "test/ts/pub_default", timestamp_instrumentation=instr + ) as pub: + time.sleep(0.05) + pub.put(b"data") + time.sleep(SLEEP) + + assert len(received) == 1 + stack = received[0].timestamp_stack + assert stack is not None + points = [r.point for r in stack.records] + assert InterceptionPoint.SEND in points + assert InterceptionPoint.RECEIVE in points + + +# ── test_publisher_per_put_override ────────────────────────────────────────── + + +def test_publisher_per_put_override(): + """Per-put override takes precedence over publisher default.""" + default_instr = TimestampInstrumentation(send=True, receive=True) + override_instr = TimestampInstrumentation(send=True, receive=False) + received: List[zenoh.Sample] = [] + + with zenoh.open(peer_config()) as session: + with session.declare_subscriber( + "test/ts/pub_override", lambda s: received.append(s) + ): + with session.declare_publisher( + "test/ts/pub_override", timestamp_instrumentation=default_instr + ) as pub: + time.sleep(0.05) + pub.put(b"data", timestamp_instrumentation=override_instr) + time.sleep(SLEEP) + + assert len(received) == 1 + points = [r.point for r in received[0].timestamp_stack.records] + assert InterceptionPoint.SEND in points + assert InterceptionPoint.RECEIVE not in points + + +# ── test_as_timestamp ───────────────────────────────────────────────────────── + + +def test_as_timestamp(): + """Standard HLC records decode via as_timestamp(); returns a Timestamp object.""" + instr = TimestampInstrumentation(send=True, receive=True) + + def put(session): + session.put("test/ts/as_ts", b"t", timestamp_instrumentation=instr) + + sample = collect_one("test/ts/as_ts", put) + assert sample is not None + for r in sample.timestamp_stack.records: + if not r.is_custom: + ts = r.as_timestamp() + assert ts is not None + + +# ── test_is_custom_false ────────────────────────────────────────────────────── + + +def test_is_custom_false(): + """Standard (non-callback) records have is_custom == False.""" + instr = TimestampInstrumentation(send=True, receive=True) + + def put(session): + session.put("test/ts/not_custom", b"x", timestamp_instrumentation=instr) + + sample = collect_one("test/ts/not_custom", put) + assert sample is not None + for r in sample.timestamp_stack.records: + assert not r.is_custom + + +# ── test_custom_callback ────────────────────────────────────────────────────── + + +def test_custom_callback(): + """A session-level timestamp callback produces custom records with the returned bytes.""" + MARKER = b"custom-ts-bytes" + + def my_callback(ctx): + return MARKER + + instr = TimestampInstrumentation(send=True, receive=True) + received: List[zenoh.Sample] = [] + + with zenoh.open(peer_config(), timestamp_callback=my_callback) as session: + with session.declare_subscriber( + "test/ts/custom_cb", lambda s: received.append(s) + ): + time.sleep(0.05) + session.put("test/ts/custom_cb", b"x", timestamp_instrumentation=instr) + time.sleep(SLEEP) + + assert len(received) == 1 + stack = received[0].timestamp_stack + assert stack is not None + custom_records = [r for r in stack.records if r.is_custom] + assert len(custom_records) > 0 + for r in custom_records: + assert r.timestamp() == MARKER + assert r.as_timestamp() is None # custom bytes don't decode as UHLC + + +# ── test_invalid_instrumentation ───────────────────────────────────────────── + + +def test_invalid_instrumentation(): + """All-false instrumentation should raise (at least one point required).""" + with pytest.raises(Exception): + TimestampInstrumentation(send=False, route=False, receive=False) diff --git a/zenoh/__init__.pyi b/zenoh/__init__.pyi index 5fe200e5..2f723603 100644 --- a/zenoh/__init__.pyi +++ b/zenoh/__init__.pyi @@ -742,6 +742,7 @@ class Publisher: attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, source_info: SourceInfo | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Publish data to :class:`Subscriber` instances matching this publisher's key expression. @@ -1440,6 +1441,7 @@ class Session: timestamp: Timestamp | None = None, allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Publish data directly from the session. @@ -1482,6 +1484,7 @@ class Session: allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> Handler[Reply]: """Query data from the matching queryables in the system. @@ -1507,6 +1510,7 @@ class Session: allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> _H: """Query data from the matching queryables in the system. @@ -1532,6 +1536,7 @@ class Session: allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> None: """Query data from the matching queryables in the system. @@ -1611,6 +1616,7 @@ class Session: express: bool | None = None, reliability: Reliability | None = None, allowed_destination: Locality | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> Publisher: """Create a :class:`Publisher` for the given key expression.""" @@ -2260,7 +2266,9 @@ def init_log_from_env_or(level: str): For example, `RUST_LOG=debug` will set the log level to DEBUG. If `RUST_LOG` is not set, then logging is set to the provided level.""" -def open(config: Config) -> Session: +def open( + config: Config, *, timestamp_callback: SessionTimestampCallback | None = None +) -> Session: """Open a zenoh :class:`zenoh.Session`. For more information about sessions and configuration, see :ref:`session-and-config`. From ff7a3c717f05b609c99bad109c5749a3b0ae4292 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 2 Jun 2026 08:31:35 +0000 Subject: [PATCH 07/20] build: point zenoh deps at yuan/feat/routing-timestamps for CI Temporary: will be reverted to upstream eclipse-zenoh/zenoh before the final PR once the Rust core PR is merged. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ff161af4..ec312428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,10 +44,10 @@ maintenance = { status = "actively-developed" } [dependencies] paste = "1.0.14" pyo3 = { version = "0.25.1", features = ["abi3-py39", "extension-module"] } -zenoh = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = [ +zenoh = { version = "1.9.0", git = "https://github.com/YuanYuYuan/zenoh.git", branch = "feat/routing-timestamps", features = [ "internal", "unstable", ], default-features = false } -zenoh-ext = { version = "1.9.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = [ +zenoh-ext = { version = "1.9.0", git = "https://github.com/YuanYuYuan/zenoh.git", branch = "feat/routing-timestamps", features = [ "internal", ], optional = true } From 436f7f30cd021bff8d733ee301c3583fd72ba035 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 2 Jun 2026 08:31:35 +0000 Subject: [PATCH 08/20] build: regenerate Cargo.lock with git deps --- Cargo.lock | 44 ++++++++------------------------------------ 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d6e0e65..4500b8ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2572,9 +2572,9 @@ dependencies = [ [[package]] name = "stabby" -version = "72.1.8" +version = "72.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7b834ec7ced12095fea1e4b07dcb7e8cf2b59b18afa3eac52494d835965a5ec" +checksum = "976399a0c48ea769ef7f5dc303bb88240ab8d84008647a6b2303eced3dab3945" dependencies = [ "rustversion", "stabby-abi", @@ -2582,9 +2582,9 @@ dependencies = [ [[package]] name = "stabby-abi" -version = "72.1.8" +version = "72.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1a4f477858a5bdf927c9fab7f579899de9b13e39f8b3b3b300c89fbab632f4" +checksum = "f7b54832a9a1f92a0e55e74a5c0332744426edc515bb3fbad82f10b874a87f0d" dependencies = [ "rustc_version", "rustversion", @@ -2594,14 +2594,15 @@ dependencies = [ [[package]] name = "stabby-macros" -version = "72.1.8" +version = "72.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31c4b2434980b67ad83f300a58088ba14d59454dcd79ba3d87419bbd924d31e" +checksum = "a768b1e51e4dbfa4fa52ae5c01241c0a41e2938fdffbb84add0c8238092f9091" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "rand 0.8.5", + "syn 1.0.109", ] [[package]] @@ -3886,7 +3887,6 @@ dependencies = [ [[package]] name = "zenoh" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "ahash", "arc-swap", @@ -3937,7 +3937,6 @@ dependencies = [ [[package]] name = "zenoh-buffers" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "zenoh-collections", ] @@ -3945,7 +3944,6 @@ dependencies = [ [[package]] name = "zenoh-codec" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "tracing", "uhlc", @@ -3957,7 +3955,6 @@ dependencies = [ [[package]] name = "zenoh-collections" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "ahash", ] @@ -3965,7 +3962,6 @@ dependencies = [ [[package]] name = "zenoh-config" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "json5", "nonempty-collections", @@ -3990,7 +3986,6 @@ dependencies = [ [[package]] name = "zenoh-core" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "lazy_static", "tokio", @@ -4001,7 +3996,6 @@ dependencies = [ [[package]] name = "zenoh-crypto" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "aes", "hmac", @@ -4014,7 +4008,6 @@ dependencies = [ [[package]] name = "zenoh-ext" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "bincode", @@ -4033,7 +4026,6 @@ dependencies = [ [[package]] name = "zenoh-keyexpr" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "getrandom 0.2.17", "hashbrown 0.16.1", @@ -4048,7 +4040,6 @@ dependencies = [ [[package]] name = "zenoh-link" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "zenoh-config", "zenoh-link-commons", @@ -4066,7 +4057,6 @@ dependencies = [ [[package]] name = "zenoh-link-commons" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "base64", @@ -4102,7 +4092,6 @@ dependencies = [ [[package]] name = "zenoh-link-quic" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "rustls-webpki", @@ -4118,7 +4107,6 @@ dependencies = [ [[package]] name = "zenoh-link-quic_datagram" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "rustls-webpki", @@ -4134,7 +4122,6 @@ dependencies = [ [[package]] name = "zenoh-link-tcp" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "socket2 0.5.10", @@ -4151,7 +4138,6 @@ dependencies = [ [[package]] name = "zenoh-link-tls" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "base64", @@ -4180,7 +4166,6 @@ dependencies = [ [[package]] name = "zenoh-link-udp" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "libc", @@ -4202,7 +4187,6 @@ dependencies = [ [[package]] name = "zenoh-link-unixsock_stream" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "nix", @@ -4220,7 +4204,6 @@ dependencies = [ [[package]] name = "zenoh-link-ws" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "futures-util", @@ -4240,7 +4223,6 @@ dependencies = [ [[package]] name = "zenoh-macros" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "proc-macro2", "quote", @@ -4251,7 +4233,6 @@ dependencies = [ [[package]] name = "zenoh-plugin-trait" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "git-version", "libloading", @@ -4268,7 +4249,6 @@ dependencies = [ [[package]] name = "zenoh-protocol" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "const_format", "rand 0.8.5", @@ -4293,7 +4273,6 @@ dependencies = [ [[package]] name = "zenoh-result" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "anyhow", ] @@ -4301,7 +4280,6 @@ dependencies = [ [[package]] name = "zenoh-runtime" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "lazy_static", "ron", @@ -4315,7 +4293,6 @@ dependencies = [ [[package]] name = "zenoh-shm" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "advisory-lock", "async-trait", @@ -4344,7 +4321,6 @@ dependencies = [ [[package]] name = "zenoh-stats" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "ahash", "prometheus-client", @@ -4357,7 +4333,6 @@ dependencies = [ [[package]] name = "zenoh-sync" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "arc-swap", "event-listener", @@ -4371,7 +4346,6 @@ dependencies = [ [[package]] name = "zenoh-task" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "futures", "tokio", @@ -4384,7 +4358,6 @@ dependencies = [ [[package]] name = "zenoh-transport" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "crossbeam-utils", @@ -4420,7 +4393,6 @@ dependencies = [ [[package]] name = "zenoh-util" version = "1.9.0" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#6685f8471ab5a95b67ed7fdab9c9d9e2022b3102" dependencies = [ "async-trait", "const_format", From b0737554181074f2553a8c8f21d161e3a5d52250 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Wed, 3 Jun 2026 04:40:01 +0000 Subject: [PATCH 09/20] fix(timestamp-stack): add timestamp_instrumentation to Query.reply and reply_err --- src/query.rs | 18 +++++++++++++----- zenoh/__init__.pyi | 9 ++++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/query.rs b/src/query.rs index ccd785c7..510c9311 100644 --- a/src/query.rs +++ b/src/query.rs @@ -30,7 +30,7 @@ use crate::{ sample::SourceInfo, session::EntityGlobalId, time::Timestamp, - timestamp_stack::TimestampStack, + timestamp_stack::{TimestampInstrumentation, TimestampStack}, utils::{generic, wait, IntoPyResult, IntoPython, IntoRust, MapInto}, }; @@ -147,7 +147,7 @@ impl Query { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None))] + #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, timestamp_instrumentation = None))] fn reply( &self, py: Python, @@ -159,6 +159,7 @@ impl Query { express: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { if congestion_control.is_some() { import!(py, warnings.warn).call1(( @@ -172,24 +173,31 @@ impl Query { py.get_type::(), ))?; } - let build = build!( + let mut build = build!( self.get_ref()?.reply(key_expr, payload), encoding, express, attachment, timestamp, ); + if let Some(instr) = timestamp_instrumentation { + build = build.timestamp_instrumentation(Some(instr.0)); + } wait(py, build) } - #[pyo3(signature = (payload, *, encoding = None))] + #[pyo3(signature = (payload, *, encoding = None, timestamp_instrumentation = None))] fn reply_err( &self, py: Python, #[pyo3(from_py_with = ZBytes::from_py)] payload: ZBytes, #[pyo3(from_py_with = Encoding::from_py_opt)] encoding: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { - let build = build!(self.get_ref()?.reply_err(payload), encoding); + let mut build = build!(self.get_ref()?.reply_err(payload), encoding); + if let Some(instr) = timestamp_instrumentation { + build = build.timestamp_instrumentation(Some(instr.0)); + } wait(py, build) } diff --git a/zenoh/__init__.pyi b/zenoh/__init__.pyi index 2f723603..752e0023 100644 --- a/zenoh/__init__.pyi +++ b/zenoh/__init__.pyi @@ -855,6 +855,7 @@ class Query: express: bool | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Sends a :class:`Sample` of kind :attr:`SampleKind.PUT` as a reply to this query. @@ -866,7 +867,13 @@ class Query: Response QoS now automatically matches the original query's QoS to avoid priority inversion. """ - def reply_err(self, payload: _IntoZBytes, *, encoding: _IntoEncoding | None = None): + def reply_err( + self, + payload: _IntoZBytes, + *, + encoding: _IntoEncoding | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, + ): """Sends a :class:`ReplyError` as a reply to this query.""" def reply_del( From 4e150e47b0dee31ecefd8a7b848339f087adaed1 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 9 Jun 2026 18:33:05 +0800 Subject: [PATCH 10/20] fix(timestamp-stack): adapt to TimestampInstrumentationBuilder and InstrumentationTimestamp enum --- src/timestamp_stack.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/timestamp_stack.rs b/src/timestamp_stack.rs index e0c13300..37eb1038 100644 --- a/src/timestamp_stack.rs +++ b/src/timestamp_stack.rs @@ -15,8 +15,9 @@ use std::sync::Arc; use pyo3::{prelude::*, types::PyBytes}; use zenoh::timestamp_stack::{ - InterceptionPoint as RustInterceptionPoint, SessionTimestampCallback, - TimestampInstrumentation as RustTimestampInstrumentation, TsStackContext as RustTsStackContext, + InstrumentationTimestamp, InterceptionPoint as RustInterceptionPoint, SessionTimestampCallback, + TimestampInstrumentationBuilder as RustTimestampInstrumentationBuilder, + TsStackContext as RustTsStackContext, }; use crate::{ @@ -97,7 +98,11 @@ impl TimestampInstrumentation { #[new] #[pyo3(signature = (*, send = false, route = false, receive = false))] fn new(send: bool, route: bool, receive: bool) -> PyResult { - RustTimestampInstrumentation::new(send, route, receive) + RustTimestampInstrumentationBuilder::new() + .set_send(send) + .set_route(route) + .set_receive(receive) + .build() .map(Self) .into_pyres() } @@ -125,12 +130,18 @@ impl TimestampStackRecord { self.0.is_custom() } - fn timestamp<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> { - PyBytes::new(py, self.0.timestamp()) + fn timestamp<'py>(&self, py: Python<'py>) -> Option> { + match self.0.timestamp() { + InstrumentationTimestamp::Custom(bytes) => Some(PyBytes::new(py, bytes)), + InstrumentationTimestamp::UHLC(_) => None, + } } fn as_timestamp(&self) -> Option { - self.0.as_timestamp().map(Timestamp) + match self.0.timestamp() { + InstrumentationTimestamp::UHLC(ts) => Some(Timestamp(*ts)), + InstrumentationTimestamp::Custom(_) => None, + } } fn __repr__(&self) -> String { From fc13e5b2c1cb453c245c333e380738342407c741 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 9 Jun 2026 18:51:14 +0800 Subject: [PATCH 11/20] =?UTF-8?q?fix(query):=20remove=20timestamp=5Finstru?= =?UTF-8?q?mentation=20from=20reply/reply=5Ferr=20=E2=80=94=20replies=20in?= =?UTF-8?q?herit=20query=20stack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/query.rs | 18 +++++------------- zenoh/__init__.pyi | 2 -- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/query.rs b/src/query.rs index 510c9311..ccd785c7 100644 --- a/src/query.rs +++ b/src/query.rs @@ -30,7 +30,7 @@ use crate::{ sample::SourceInfo, session::EntityGlobalId, time::Timestamp, - timestamp_stack::{TimestampInstrumentation, TimestampStack}, + timestamp_stack::TimestampStack, utils::{generic, wait, IntoPyResult, IntoPython, IntoRust, MapInto}, }; @@ -147,7 +147,7 @@ impl Query { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, timestamp_instrumentation = None))] + #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None))] fn reply( &self, py: Python, @@ -159,7 +159,6 @@ impl Query { express: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, - timestamp_instrumentation: Option, ) -> PyResult<()> { if congestion_control.is_some() { import!(py, warnings.warn).call1(( @@ -173,31 +172,24 @@ impl Query { py.get_type::(), ))?; } - let mut build = build!( + let build = build!( self.get_ref()?.reply(key_expr, payload), encoding, express, attachment, timestamp, ); - if let Some(instr) = timestamp_instrumentation { - build = build.timestamp_instrumentation(Some(instr.0)); - } wait(py, build) } - #[pyo3(signature = (payload, *, encoding = None, timestamp_instrumentation = None))] + #[pyo3(signature = (payload, *, encoding = None))] fn reply_err( &self, py: Python, #[pyo3(from_py_with = ZBytes::from_py)] payload: ZBytes, #[pyo3(from_py_with = Encoding::from_py_opt)] encoding: Option, - timestamp_instrumentation: Option, ) -> PyResult<()> { - let mut build = build!(self.get_ref()?.reply_err(payload), encoding); - if let Some(instr) = timestamp_instrumentation { - build = build.timestamp_instrumentation(Some(instr.0)); - } + let build = build!(self.get_ref()?.reply_err(payload), encoding); wait(py, build) } diff --git a/zenoh/__init__.pyi b/zenoh/__init__.pyi index 752e0023..4a42f004 100644 --- a/zenoh/__init__.pyi +++ b/zenoh/__init__.pyi @@ -855,7 +855,6 @@ class Query: express: bool | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Sends a :class:`Sample` of kind :attr:`SampleKind.PUT` as a reply to this query. @@ -872,7 +871,6 @@ class Query: payload: _IntoZBytes, *, encoding: _IntoEncoding | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Sends a :class:`ReplyError` as a reply to this query.""" From 70b2f2a8cd5ec6e132e3f599078bb86a654ab2dd Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 9 Jun 2026 15:09:36 +0000 Subject: [PATCH 12/20] build: update Cargo.lock to latest yuan/feat/routing-timestamps --- Cargo.lock | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 4500b8ba..591f9d2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3887,6 +3887,7 @@ dependencies = [ [[package]] name = "zenoh" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "ahash", "arc-swap", @@ -3937,6 +3938,7 @@ dependencies = [ [[package]] name = "zenoh-buffers" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "zenoh-collections", ] @@ -3944,6 +3946,7 @@ dependencies = [ [[package]] name = "zenoh-codec" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "tracing", "uhlc", @@ -3955,6 +3958,7 @@ dependencies = [ [[package]] name = "zenoh-collections" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "ahash", ] @@ -3962,6 +3966,7 @@ dependencies = [ [[package]] name = "zenoh-config" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "json5", "nonempty-collections", @@ -3986,6 +3991,7 @@ dependencies = [ [[package]] name = "zenoh-core" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "lazy_static", "tokio", @@ -3996,6 +4002,7 @@ dependencies = [ [[package]] name = "zenoh-crypto" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "aes", "hmac", @@ -4008,6 +4015,7 @@ dependencies = [ [[package]] name = "zenoh-ext" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "bincode", @@ -4026,6 +4034,7 @@ dependencies = [ [[package]] name = "zenoh-keyexpr" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "getrandom 0.2.17", "hashbrown 0.16.1", @@ -4040,6 +4049,7 @@ dependencies = [ [[package]] name = "zenoh-link" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "zenoh-config", "zenoh-link-commons", @@ -4057,6 +4067,7 @@ dependencies = [ [[package]] name = "zenoh-link-commons" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "base64", @@ -4092,6 +4103,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "rustls-webpki", @@ -4107,6 +4119,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic_datagram" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "rustls-webpki", @@ -4122,6 +4135,7 @@ dependencies = [ [[package]] name = "zenoh-link-tcp" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "socket2 0.5.10", @@ -4138,6 +4152,7 @@ dependencies = [ [[package]] name = "zenoh-link-tls" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "base64", @@ -4166,6 +4181,7 @@ dependencies = [ [[package]] name = "zenoh-link-udp" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "libc", @@ -4187,6 +4203,7 @@ dependencies = [ [[package]] name = "zenoh-link-unixsock_stream" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "nix", @@ -4204,6 +4221,7 @@ dependencies = [ [[package]] name = "zenoh-link-ws" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "futures-util", @@ -4223,6 +4241,7 @@ dependencies = [ [[package]] name = "zenoh-macros" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "proc-macro2", "quote", @@ -4233,6 +4252,7 @@ dependencies = [ [[package]] name = "zenoh-plugin-trait" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "git-version", "libloading", @@ -4249,6 +4269,7 @@ dependencies = [ [[package]] name = "zenoh-protocol" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "const_format", "rand 0.8.5", @@ -4273,6 +4294,7 @@ dependencies = [ [[package]] name = "zenoh-result" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "anyhow", ] @@ -4280,6 +4302,7 @@ dependencies = [ [[package]] name = "zenoh-runtime" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "lazy_static", "ron", @@ -4293,6 +4316,7 @@ dependencies = [ [[package]] name = "zenoh-shm" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "advisory-lock", "async-trait", @@ -4321,6 +4345,7 @@ dependencies = [ [[package]] name = "zenoh-stats" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "ahash", "prometheus-client", @@ -4333,6 +4358,7 @@ dependencies = [ [[package]] name = "zenoh-sync" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "arc-swap", "event-listener", @@ -4346,6 +4372,7 @@ dependencies = [ [[package]] name = "zenoh-task" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "futures", "tokio", @@ -4358,6 +4385,7 @@ dependencies = [ [[package]] name = "zenoh-transport" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "crossbeam-utils", @@ -4393,6 +4421,7 @@ dependencies = [ [[package]] name = "zenoh-util" version = "1.9.0" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" dependencies = [ "async-trait", "const_format", From 9a1146efa1355d2c7bc484855ad232f7cc832a43 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 9 Jun 2026 15:26:20 +0000 Subject: [PATCH 13/20] style(stubs): collapse reply_err signature to single line for black --- zenoh/__init__.pyi | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/zenoh/__init__.pyi b/zenoh/__init__.pyi index 4a42f004..2f723603 100644 --- a/zenoh/__init__.pyi +++ b/zenoh/__init__.pyi @@ -866,12 +866,7 @@ class Query: Response QoS now automatically matches the original query's QoS to avoid priority inversion. """ - def reply_err( - self, - payload: _IntoZBytes, - *, - encoding: _IntoEncoding | None = None, - ): + def reply_err(self, payload: _IntoZBytes, *, encoding: _IntoEncoding | None = None): """Sends a :class:`ReplyError` as a reply to this query.""" def reply_del( From 8f808c52685ce120985bd64b88f189f071c6dd42 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 9 Jun 2026 15:35:44 +0000 Subject: [PATCH 14/20] fix(session): remove timestamp_instrumentation from declare_publisher PublisherBuilder has no timestamp_instrumentation setter. Pass it per-put via publisher.put(timestamp_instrumentation=...) instead. --- src/session.rs | 8 ++------ zenoh/__init__.pyi | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/session.rs b/src/session.rs index 686fc964..35fc9ff3 100644 --- a/src/session.rs +++ b/src/session.rs @@ -243,7 +243,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, *, encoding = None, congestion_control = None, priority = None, express = None, reliability = None, allowed_destination = None, timestamp_instrumentation = None))] + #[pyo3(signature = (key_expr, *, encoding = None, congestion_control = None, priority = None, express = None, reliability = None, allowed_destination = None))] fn declare_publisher( &self, py: Python, @@ -254,9 +254,8 @@ impl Session { express: Option, reliability: Option, allowed_destination: Option, - timestamp_instrumentation: Option, ) -> PyResult { - let mut builder = build!( + let builder = build!( self.0.declare_publisher(key_expr), encoding, congestion_control, @@ -265,9 +264,6 @@ impl Session { reliability, allowed_destination, ); - if let Some(instr) = timestamp_instrumentation { - builder = builder.timestamp_instrumentation(Some(instr.0)); - } wait(py, builder).map_into() } diff --git a/zenoh/__init__.pyi b/zenoh/__init__.pyi index 2f723603..66559742 100644 --- a/zenoh/__init__.pyi +++ b/zenoh/__init__.pyi @@ -1616,7 +1616,6 @@ class Session: express: bool | None = None, reliability: Reliability | None = None, allowed_destination: Locality | None = None, - timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> Publisher: """Create a :class:`Publisher` for the given key expression.""" From ec7baf04fc4fbd24942763f2c335889219467892 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 9 Jun 2026 15:40:14 +0000 Subject: [PATCH 15/20] fix(timestamp_stack): remove unreachable wildcard arm in InterceptionPoint From impl --- src/timestamp_stack.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/timestamp_stack.rs b/src/timestamp_stack.rs index 37eb1038..f5cdff63 100644 --- a/src/timestamp_stack.rs +++ b/src/timestamp_stack.rs @@ -50,7 +50,6 @@ impl From for InterceptionPoint { RustInterceptionPoint::Send => Self::Send, RustInterceptionPoint::Route => Self::Route, RustInterceptionPoint::Receive => Self::Receive, - _ => Self::Unknown, } } } From cef71fedf491b4dd5fb167c208084c1c242e7585 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 9 Jun 2026 15:50:43 +0000 Subject: [PATCH 16/20] fix(tests): adapt publisher tests to per-put instrumentation API --- tests/test_timestamp_stack.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/test_timestamp_stack.py b/tests/test_timestamp_stack.py index d5814f72..879dc05a 100644 --- a/tests/test_timestamp_stack.py +++ b/tests/test_timestamp_stack.py @@ -116,7 +116,7 @@ def put(session): def test_publisher_default(): - """Publisher-level default instrumentation applies to all puts.""" + """Publisher put with instrumentation records SEND and RECEIVE points.""" instr = TimestampInstrumentation(send=True, receive=True) received: List[zenoh.Sample] = [] @@ -124,11 +124,9 @@ def test_publisher_default(): with session.declare_subscriber( "test/ts/pub_default", lambda s: received.append(s) ): - with session.declare_publisher( - "test/ts/pub_default", timestamp_instrumentation=instr - ) as pub: + with session.declare_publisher("test/ts/pub_default") as pub: time.sleep(0.05) - pub.put(b"data") + pub.put(b"data", timestamp_instrumentation=instr) time.sleep(SLEEP) assert len(received) == 1 @@ -143,8 +141,7 @@ def test_publisher_default(): def test_publisher_per_put_override(): - """Per-put override takes precedence over publisher default.""" - default_instr = TimestampInstrumentation(send=True, receive=True) + """Per-put instrumentation controls which points are recorded.""" override_instr = TimestampInstrumentation(send=True, receive=False) received: List[zenoh.Sample] = [] @@ -152,9 +149,7 @@ def test_publisher_per_put_override(): with session.declare_subscriber( "test/ts/pub_override", lambda s: received.append(s) ): - with session.declare_publisher( - "test/ts/pub_override", timestamp_instrumentation=default_instr - ) as pub: + with session.declare_publisher("test/ts/pub_override") as pub: time.sleep(0.05) pub.put(b"data", timestamp_instrumentation=override_instr) time.sleep(SLEEP) From dc37f384abcbf1f39fcf8a9c4d6f6ebebe8c830e Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 16 Jun 2026 08:50:40 +0000 Subject: [PATCH 17/20] chore: update zenoh dependency to rebased feat/routing-timestamps --- Cargo.lock | 58 +++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 591f9d2f..1f56a101 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3887,7 +3887,7 @@ dependencies = [ [[package]] name = "zenoh" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "ahash", "arc-swap", @@ -3938,7 +3938,7 @@ dependencies = [ [[package]] name = "zenoh-buffers" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "zenoh-collections", ] @@ -3946,7 +3946,7 @@ dependencies = [ [[package]] name = "zenoh-codec" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "tracing", "uhlc", @@ -3958,7 +3958,7 @@ dependencies = [ [[package]] name = "zenoh-collections" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "ahash", ] @@ -3966,7 +3966,7 @@ dependencies = [ [[package]] name = "zenoh-config" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "json5", "nonempty-collections", @@ -3991,7 +3991,7 @@ dependencies = [ [[package]] name = "zenoh-core" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "lazy_static", "tokio", @@ -4002,7 +4002,7 @@ dependencies = [ [[package]] name = "zenoh-crypto" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "aes", "hmac", @@ -4015,7 +4015,7 @@ dependencies = [ [[package]] name = "zenoh-ext" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "bincode", @@ -4034,7 +4034,7 @@ dependencies = [ [[package]] name = "zenoh-keyexpr" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "getrandom 0.2.17", "hashbrown 0.16.1", @@ -4049,7 +4049,7 @@ dependencies = [ [[package]] name = "zenoh-link" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "zenoh-config", "zenoh-link-commons", @@ -4067,7 +4067,7 @@ dependencies = [ [[package]] name = "zenoh-link-commons" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "base64", @@ -4103,7 +4103,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "rustls-webpki", @@ -4119,7 +4119,7 @@ dependencies = [ [[package]] name = "zenoh-link-quic_datagram" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "rustls-webpki", @@ -4135,7 +4135,7 @@ dependencies = [ [[package]] name = "zenoh-link-tcp" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "socket2 0.5.10", @@ -4152,7 +4152,7 @@ dependencies = [ [[package]] name = "zenoh-link-tls" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "base64", @@ -4181,7 +4181,7 @@ dependencies = [ [[package]] name = "zenoh-link-udp" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "libc", @@ -4203,7 +4203,7 @@ dependencies = [ [[package]] name = "zenoh-link-unixsock_stream" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "nix", @@ -4221,7 +4221,7 @@ dependencies = [ [[package]] name = "zenoh-link-ws" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "futures-util", @@ -4241,7 +4241,7 @@ dependencies = [ [[package]] name = "zenoh-macros" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "proc-macro2", "quote", @@ -4252,7 +4252,7 @@ dependencies = [ [[package]] name = "zenoh-plugin-trait" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "git-version", "libloading", @@ -4269,7 +4269,7 @@ dependencies = [ [[package]] name = "zenoh-protocol" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "const_format", "rand 0.8.5", @@ -4294,7 +4294,7 @@ dependencies = [ [[package]] name = "zenoh-result" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "anyhow", ] @@ -4302,7 +4302,7 @@ dependencies = [ [[package]] name = "zenoh-runtime" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "lazy_static", "ron", @@ -4316,7 +4316,7 @@ dependencies = [ [[package]] name = "zenoh-shm" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "advisory-lock", "async-trait", @@ -4345,7 +4345,7 @@ dependencies = [ [[package]] name = "zenoh-stats" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "ahash", "prometheus-client", @@ -4358,7 +4358,7 @@ dependencies = [ [[package]] name = "zenoh-sync" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "arc-swap", "event-listener", @@ -4372,7 +4372,7 @@ dependencies = [ [[package]] name = "zenoh-task" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "futures", "tokio", @@ -4385,7 +4385,7 @@ dependencies = [ [[package]] name = "zenoh-transport" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "crossbeam-utils", @@ -4421,7 +4421,7 @@ dependencies = [ [[package]] name = "zenoh-util" version = "1.9.0" -source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#088aa2c6ff98d1e594232b15d29bbb5247a017e7" +source = "git+https://github.com/YuanYuYuan/zenoh.git?branch=feat%2Frouting-timestamps#fff0190b41c414f7b5f11917d9c4ac60731fd43b" dependencies = [ "async-trait", "const_format", From d19455baec9879202f7b667d01e17942648b178d Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 16 Jun 2026 18:50:24 +0800 Subject: [PATCH 18/20] feat(timestamp): complete timestamp instrumentation API coverage - session.delete, publisher.delete: add timestamp_instrumentation kwarg - Querier.get: add timestamp_instrumentation kwarg - Query: expose timestamp_stack getter - AdvancedPublisher.put/delete: wire timestamp_instrumentation - lib.rs: export TimestampInstrumentationBuilder - timestamp_stack.rs: add #[getter] to point and is_custom so Python accesses them as properties (r.point, r.is_custom) not methods --- src/ext.rs | 22 ++++++++----- src/lib.rs | 4 +-- src/pubsub.rs | 8 +++-- src/query.rs | 15 +++++++-- src/session.rs | 8 +++-- src/timestamp_stack.rs | 70 +++++++++++++++++++++++++++++++++++++++--- 6 files changed, 107 insertions(+), 20 deletions(-) diff --git a/src/ext.rs b/src/ext.rs index d2de6084..6ad65e57 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -24,6 +24,7 @@ use crate::{ sample::{Locality, Sample}, session::{EntityGlobalId, Session}, time::Timestamp, + timestamp_stack::TimestampInstrumentation, utils::{duration, generic, wait, MapInto}, ZDeserializeError, }; @@ -492,7 +493,7 @@ impl AdvancedPublisher { Ok(self.get_ref()?.priority().into()) } - #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None))] + #[pyo3(signature = (payload, *, encoding = None, attachment = None, timestamp = None, timestamp_instrumentation = None))] fn put( &self, py: Python, @@ -500,22 +501,29 @@ impl AdvancedPublisher { #[pyo3(from_py_with = Encoding::from_py_opt)] encoding: Option, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { let this = self.get_ref()?; - wait( - py, - build!(this.put(payload), encoding, attachment, timestamp), - ) + let mut builder = build!(this.put(payload), encoding, attachment, timestamp); + if let Some(instr) = timestamp_instrumentation { + builder = builder.timestamp_instrumentation(Some(instr.0)); + } + wait(py, builder) } - #[pyo3(signature = (*, attachment = None, timestamp = None))] + #[pyo3(signature = (*, attachment = None, timestamp = None, timestamp_instrumentation = None))] fn delete( &self, py: Python, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { - wait(py, build!(self.get_ref()?.delete(), attachment, timestamp)) + let mut builder = build!(self.get_ref()?.delete(), attachment, timestamp); + if let Some(instr) = timestamp_instrumentation { + builder = builder.timestamp_instrumentation(Some(instr.0)); + } + wait(py, builder) } fn undeclare(&mut self, py: Python) -> PyResult<()> { diff --git a/src/lib.rs b/src/lib.rs index 72db2f4a..5e6e34dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,8 +79,8 @@ pub(crate) mod zenoh { }, time::{Timestamp, TimestampId, NTP64}, timestamp_stack::{ - InterceptionPoint, TimestampInstrumentation, TimestampStack, TimestampStackRecord, - TsStackContext, + InterceptionPoint, TimestampInstrumentation, TimestampInstrumentationBuilder, + TimestampStack, TimestampStackRecord, TsStackContext, }, ZError, }; diff --git a/src/pubsub.rs b/src/pubsub.rs index 357904ac..b1a5c41c 100644 --- a/src/pubsub.rs +++ b/src/pubsub.rs @@ -111,15 +111,19 @@ impl Publisher { wait(py, builder) } - #[pyo3(signature = (*, attachment = None, timestamp = None, source_info = None))] + #[pyo3(signature = (*, attachment = None, timestamp = None, source_info = None, timestamp_instrumentation = None))] fn delete( &self, py: Python, #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, timestamp: Option, source_info: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { - let builder = build!(self.get_ref()?.delete(), attachment, timestamp, source_info); + let mut builder = build!(self.get_ref()?.delete(), attachment, timestamp, source_info); + if let Some(instr) = timestamp_instrumentation { + builder = builder.timestamp_instrumentation(Some(instr.0)); + } wait(py, builder) } diff --git a/src/query.rs b/src/query.rs index ccd785c7..ada7c859 100644 --- a/src/query.rs +++ b/src/query.rs @@ -30,7 +30,7 @@ use crate::{ sample::SourceInfo, session::EntityGlobalId, time::Timestamp, - timestamp_stack::TimestampStack, + timestamp_stack::{TimestampInstrumentation, TimestampStack}, utils::{generic, wait, IntoPyResult, IntoPython, IntoRust, MapInto}, }; @@ -231,6 +231,11 @@ impl Query { Ok(self.get_ref()?.source_info().cloned().map_into()) } + #[getter] + fn timestamp_stack(&self) -> PyResult> { + Ok(self.get_ref()?.timestamp_stack().cloned().map(TimestampStack)) + } + fn drop(&mut self) { Python::with_gil(|gil| gil.allow_threads(|| drop(self.0.take()))); } @@ -422,7 +427,7 @@ impl Querier { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (handler = None, *, parameters = None, payload = None, encoding = None, attachment = None, source_info = None, cancellation_token = None))] + #[pyo3(signature = (handler = None, *, parameters = None, payload = None, encoding = None, attachment = None, source_info = None, cancellation_token = None, timestamp_instrumentation = None))] fn get( &self, py: Python, @@ -433,10 +438,11 @@ impl Querier { #[pyo3(from_py_with = ZBytes::from_py_opt)] attachment: Option, source_info: Option, cancellation_token: Option, + timestamp_instrumentation: Option, ) -> PyResult> { let this = self.get_ref()?; let (handler, _) = into_handler(py, handler, cancellation_token.as_ref())?; - let builder = build!( + let mut builder = build!( this.get(), parameters, payload, @@ -445,6 +451,9 @@ impl Querier { source_info, cancellation_token ); + if let Some(instr) = timestamp_instrumentation { + builder = builder.timestamp_instrumentation(Some(instr.0)); + } wait(py, builder.with(handler)).map_into() } diff --git a/src/session.rs b/src/session.rs index 35fc9ff3..be1e526c 100644 --- a/src/session.rs +++ b/src/session.rs @@ -129,7 +129,7 @@ impl Session { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (key_expr, *, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, allowed_destination = None, source_info = None))] + #[pyo3(signature = (key_expr, *, congestion_control = None, priority = None, express = None, attachment = None, timestamp = None, allowed_destination = None, source_info = None, timestamp_instrumentation = None))] fn delete( &self, py: Python, @@ -141,8 +141,9 @@ impl Session { timestamp: Option, allowed_destination: Option, source_info: Option, + timestamp_instrumentation: Option, ) -> PyResult<()> { - let build = build!( + let mut build = build!( self.0.delete(key_expr), congestion_control, priority, @@ -152,6 +153,9 @@ impl Session { allowed_destination, source_info ); + if let Some(instr) = timestamp_instrumentation { + build = build.timestamp_instrumentation(Some(instr.0)); + } wait(py, build) } diff --git a/src/timestamp_stack.rs b/src/timestamp_stack.rs index f5cdff63..69a7ba0d 100644 --- a/src/timestamp_stack.rs +++ b/src/timestamp_stack.rs @@ -27,6 +27,59 @@ use crate::{ utils::IntoPyResult, }; +// ── TimestampInstrumentationBuilder ────────────────────────────────────────── + +#[pyclass] +pub(crate) struct TimestampInstrumentationBuilder { + send: bool, + route: bool, + receive: bool, +} + +#[pymethods] +impl TimestampInstrumentationBuilder { + #[new] + fn new() -> Self { + Self { + send: false, + route: false, + receive: false, + } + } + + fn set_send(mut self_: PyRefMut, send: bool) -> PyRefMut { + self_.send = send; + self_ + } + + fn set_route(mut self_: PyRefMut, route: bool) -> PyRefMut { + self_.route = route; + self_ + } + + fn set_receive(mut self_: PyRefMut, receive: bool) -> PyRefMut { + self_.receive = receive; + self_ + } + + fn build(&self) -> PyResult { + RustTimestampInstrumentationBuilder::new() + .set_send(self.send) + .set_route(self.route) + .set_receive(self.receive) + .build() + .map(TimestampInstrumentation) + .into_pyres() + } + + fn __repr__(&self) -> String { + format!( + "TimestampInstrumentationBuilder(send={}, route={}, receive={})", + self.send, self.route, self.receive + ) + } +} + // InterceptionPoint is #[non_exhaustive] so we can't use enum_mapper! (it generates exhaustive // From impls). Define it manually with a repr u8 for Python comparison, and a fallback variant. #[pyo3::pyclass(eq)] @@ -129,10 +182,14 @@ impl TimestampStackRecord { self.0.is_custom() } - fn timestamp<'py>(&self, py: Python<'py>) -> Option> { + fn timestamp(&self, py: Python) -> PyResult { match self.0.timestamp() { - InstrumentationTimestamp::Custom(bytes) => Some(PyBytes::new(py, bytes)), - InstrumentationTimestamp::UHLC(_) => None, + InstrumentationTimestamp::UHLC(ts) => { + Ok(Timestamp(*ts).into_pyobject(py)?.into_any().unbind()) + } + InstrumentationTimestamp::Custom(bytes) => { + Ok(PyBytes::new(py, bytes).into_any().unbind()) + } } } @@ -183,7 +240,12 @@ pub(crate) fn py_to_session_ts_callback(py_cb: PyObject) -> SessionTimestampCall match py_cb.call1(py, (py_ctx,)) { Ok(result) => result.extract::>(py).unwrap_or_default(), Err(e) => { - e.print(py); + if let Ok(logging) = py.import("logging") { + if let Ok(logger) = logging.call_method1("getLogger", ("zenoh",)) { + let _ = logger + .call_method1("error", (format!("Timestamp callback error: {e}"),)); + } + } Vec::new() } } From 479f1be8e64e4384e0c6ea1585b1fc0e51869e69 Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 16 Jun 2026 18:50:33 +0800 Subject: [PATCH 19/20] feat(stubs): add full type stubs for all 6 timestamp types; extend tests - __init__.pyi: add class bodies for InterceptionPoint, TsStackContext, TimestampInstrumentationBuilder, TimestampInstrumentation, TimestampStackRecord, TimestampStack, SessionTimestampCallback alias - __init__.pyi: add timestamp_stack property to Sample, ReplyError, Query, Reply - __init__.pyi: add timestamp_instrumentation to Publisher.delete, Session.delete, and all 3 Querier.get overloads - ext.pyi: add timestamp_instrumentation to AdvancedPublisher.put/delete; import TimestampInstrumentation - example: fix declare_publisher incorrectly passing timestamp_instrumentation (no setter on PublisherBuilder); move instrumentation to pub.put calls - tests: add 4 new tests covering delete instrumentation, Query.timestamp_stack, and Querier.get instrumentation --- examples/z_timestamp_instrumentation.py | 8 +- tests/test_timestamp_stack.py | 113 ++++++++++++++++- zenoh/__init__.pyi | 161 ++++++++++++++++++++++++ zenoh/ext.pyi | 3 + 4 files changed, 279 insertions(+), 6 deletions(-) diff --git a/examples/z_timestamp_instrumentation.py b/examples/z_timestamp_instrumentation.py index d4c0ab65..ea6efc87 100644 --- a/examples/z_timestamp_instrumentation.py +++ b/examples/z_timestamp_instrumentation.py @@ -50,15 +50,13 @@ def example_put_subscribe(session): def example_publisher_default(session): - print("\n── publisher with default instrumentation ───────────────────────────") + print("\n── publisher with per-put instrumentation ───────────────────────────") instr = TimestampInstrumentation(send=True, receive=True) received = [] - with session.declare_publisher( - "demo/ts/pub", timestamp_instrumentation=instr - ) as pub: + with session.declare_publisher("demo/ts/pub") as pub: with session.declare_subscriber("demo/ts/pub", lambda s: received.append(s)): time.sleep(0.05) - pub.put(b"message-1") + pub.put(b"message-1", timestamp_instrumentation=instr) pub.put( b"message-2", timestamp_instrumentation=TimestampInstrumentation(send=True), diff --git a/tests/test_timestamp_stack.py b/tests/test_timestamp_stack.py index 879dc05a..f447bd87 100644 --- a/tests/test_timestamp_stack.py +++ b/tests/test_timestamp_stack.py @@ -17,7 +17,7 @@ import pytest import zenoh -from zenoh import InterceptionPoint, TimestampInstrumentation, TimestampStack +from zenoh import InterceptionPoint, TimestampInstrumentation, TimestampStack, SampleKind SLEEP = 0.2 @@ -232,3 +232,114 @@ def test_invalid_instrumentation(): """All-false instrumentation should raise (at least one point required).""" with pytest.raises(Exception): TimestampInstrumentation(send=False, route=False, receive=False) + + +# ── test_session_delete_instrumentation ────────────────────────────────────── + + +def test_session_delete_instrumentation(): + """session.delete with instrumentation produces a stack on the DELETE sample.""" + instr = TimestampInstrumentation(send=True, receive=True) + received: List[zenoh.Sample] = [] + + with zenoh.open(peer_config()) as session: + with session.declare_subscriber("test/ts/del", lambda s: received.append(s)): + time.sleep(0.05) + session.delete("test/ts/del", timestamp_instrumentation=instr) + time.sleep(SLEEP) + + assert len(received) == 1 + assert received[0].kind == SampleKind.DELETE + stack = received[0].timestamp_stack + assert stack is not None + points = [r.point for r in stack.records] + assert InterceptionPoint.SEND in points + assert InterceptionPoint.RECEIVE in points + + +# ── test_publisher_delete_instrumentation ──────────────────────────────────── + + +def test_publisher_delete_instrumentation(): + """publisher.delete with instrumentation produces a stack on the DELETE sample.""" + instr = TimestampInstrumentation(send=True, receive=True) + received: List[zenoh.Sample] = [] + + with zenoh.open(peer_config()) as session: + with session.declare_subscriber( + "test/ts/pub_del", lambda s: received.append(s) + ): + with session.declare_publisher("test/ts/pub_del") as pub: + time.sleep(0.05) + pub.delete(timestamp_instrumentation=instr) + time.sleep(SLEEP) + + assert len(received) == 1 + assert received[0].kind == SampleKind.DELETE + stack = received[0].timestamp_stack + assert stack is not None + points = [r.point for r in stack.records] + assert InterceptionPoint.SEND in points + assert InterceptionPoint.RECEIVE in points + + +# ── test_query_timestamp_stack ──────────────────────────────────────────────── + + +def test_query_timestamp_stack(): + """Query.timestamp_stack carries the instrumentation from the get caller.""" + instr = TimestampInstrumentation(send=True, receive=True) + query_stacks: List[Optional[TimestampStack]] = [] + + with zenoh.open(peer_config()) as session: + + def on_query(q): + query_stacks.append(q.timestamp_stack) + q.reply(q.key_expr, b"answer") + + with session.declare_queryable("test/ts/q/**", on_query): + time.sleep(0.05) + replies = list( + session.get( + "test/ts/q/key", + timestamp_instrumentation=instr, + ) + ) + time.sleep(SLEEP) + + assert len(query_stacks) == 1 + stack = query_stacks[0] + assert stack is not None + points = [r.point for r in stack.records] + assert InterceptionPoint.SEND in points + + assert len(replies) >= 1 + + +# ── test_querier_get_instrumentation ───────────────────────────────────────── + + +def test_querier_get_instrumentation(): + """Querier.get with instrumentation produces a stack visible at the queryable.""" + instr = TimestampInstrumentation(send=True, receive=True) + query_stacks: List[Optional[TimestampStack]] = [] + + with zenoh.open(peer_config()) as session: + + def on_query(q): + query_stacks.append(q.timestamp_stack) + q.reply(q.key_expr, b"querier-answer") + + with session.declare_queryable("test/ts/qr/**", on_query): + with session.declare_querier("test/ts/qr/key") as querier: + time.sleep(0.05) + replies = list(querier.get(timestamp_instrumentation=instr)) + time.sleep(SLEEP) + + assert len(query_stacks) == 1 + stack = query_stacks[0] + assert stack is not None + points = [r.point for r in stack.records] + assert InterceptionPoint.SEND in points + + assert len(replies) >= 1 diff --git a/zenoh/__init__.pyi b/zenoh/__init__.pyi index 66559742..0c2ade5d 100644 --- a/zenoh/__init__.pyi +++ b/zenoh/__init__.pyi @@ -756,6 +756,7 @@ class Publisher: attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, source_info: SourceInfo | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Declare that data associated with this publisher's key expression is deleted. @@ -896,6 +897,11 @@ class Query: def source_info(self) -> SourceInfo | None: """Gets info on the source of this Query.""" + @_unstable + @property + def timestamp_stack(self) -> TimestampStack | None: + """Gets the timestamp stack of this Query, if timestamp instrumentation was active.""" + def drop(self): """Drop the instance of a query. The query will only be finalized when all query instances (one per queryable @@ -991,6 +997,7 @@ class Querier: attachment: _IntoZBytes | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> Handler[Reply]: """Sends a query and returns a channel for processing replies. @@ -1007,6 +1014,7 @@ class Querier: attachment: _IntoZBytes | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> _H: """Sends a query and returns a channel for processing replies. @@ -1023,6 +1031,7 @@ class Querier: attachment: _IntoZBytes | None = None, source_info: SourceInfo | None = None, cancellation_token: CancellationToken | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ) -> None: """Sends a query and processes replies using the provided callback. @@ -1194,6 +1203,11 @@ class Reply: def replier_id(self) -> EntityGlobalId | None: """Returns the ID of the zenoh instance that answered this reply.""" + @_unstable + @property + def timestamp_stack(self) -> TimestampStack | None: + """Gets the timestamp stack of the inner Sample or ReplyError, if timestamp instrumentation was active.""" + @final class ReplyError: """An error reply received from a :class:`Queryable` and available in the :class:`Reply` structure.""" @@ -1206,6 +1220,11 @@ class ReplyError: def encoding(self) -> Encoding: """Gets the encoding of this `ReplyError`.""" + @_unstable + @property + def timestamp_stack(self) -> TimestampStack | None: + """Gets the timestamp stack of this ReplyError, if timestamp instrumentation was active.""" + @final class SampleKind(Enum): """The kind of a :class:`Sample`, indicating whether it contains data or indicates deletion.""" @@ -1267,6 +1286,11 @@ class Sample: def source_info(self) -> SourceInfo | None: """Gets info on the source of this Sample.""" + @_unstable + @property + def timestamp_stack(self) -> TimestampStack | None: + """Gets the timestamp stack of this Sample, if timestamp instrumentation was active.""" + @final class Scout(Generic[_H]): """A Scout object that yields :class:`zenoh.Hello` messages for discovered Zenoh nodes on the network. @@ -1459,6 +1483,7 @@ class Session: timestamp: Timestamp | None = None, allowed_destination: Locality | None = None, source_info: SourceInfo | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Publish a delete sample directly from the session. @@ -2133,6 +2158,142 @@ Used in :meth:`Timestamp.__new__` to accept various byte representations that can be converted to a :class:`TimestampId`. """ +@_unstable +@final +class InterceptionPoint(Enum): + """A point along a message's routing path where a timestamp is recorded.""" + + SEND = 0 + ROUTE = 1 + RECEIVE = 2 + UNKNOWN = 255 + +InterceptionPoint.SEND.__doc__ = """Timestamp recorded at the sending side (before transmission).""" +InterceptionPoint.ROUTE.__doc__ = """Timestamp recorded at the routing layer.""" +InterceptionPoint.RECEIVE.__doc__ = """Timestamp recorded at the receiving side (on delivery).""" +InterceptionPoint.UNKNOWN.__doc__ = """Catch-all for future variants added by the Rust core.""" + +@_unstable +@final +class TsStackContext: + """Context passed to a :class:`SessionTimestampCallback` when a timestamp is requested. + + Provides information about the session and the interception point where the timestamp is being collected. + """ + + @property + def zid(self) -> ZenohId: + """The ZenohId of the session that is generating the timestamp.""" + + @property + def whatami(self) -> WhatAmI: + """The mode (router/peer/client) of the session generating the timestamp.""" + + @property + def interception_point(self) -> InterceptionPoint: + """The routing stage at which this timestamp is being collected.""" + + def __repr__(self) -> str: ... + +SessionTimestampCallback = Callable[[TsStackContext], bytes] +"""A callable that receives a :class:`TsStackContext` and returns raw timestamp bytes. + +Used with :func:`open` to provide custom per-session timestamps at each interception point. +The returned bytes are stored verbatim in the :class:`TimestampStackRecord` and exposed via +:meth:`TimestampStackRecord.timestamp`. Use :meth:`TimestampStackRecord.as_timestamp` to +check whether the bytes decode as a UHLC :class:`Timestamp`. +""" + +@_unstable +@final +class TimestampInstrumentationBuilder: + """Builder for :class:`TimestampInstrumentation`. + + Construct one via :class:`TimestampInstrumentationBuilder()`, configure which interception + points to record, then call :meth:`build` to produce the final :class:`TimestampInstrumentation`. + """ + + def __new__(cls) -> Self: ... + def set_send(self, send: bool) -> Self: + """Enable or disable recording a timestamp at the SEND interception point.""" + + def set_route(self, route: bool) -> Self: + """Enable or disable recording a timestamp at the ROUTE interception point.""" + + def set_receive(self, receive: bool) -> Self: + """Enable or disable recording a timestamp at the RECEIVE interception point.""" + + def build(self) -> TimestampInstrumentation: + """Build the :class:`TimestampInstrumentation`. Raises :class:`ZError` if all flags are False.""" + + def __repr__(self) -> str: ... + +@_unstable +@final +class TimestampInstrumentation: + """Specifies which interception points should record timestamps for a message. + + Create one directly (keyword-only constructor) or via :class:`TimestampInstrumentationBuilder`: + + .. code-block:: python + + instr = zenoh.TimestampInstrumentation(send=True, receive=True) + instr = zenoh.TimestampInstrumentationBuilder().set_send(True).set_receive(True).build() + + Pass it to :meth:`Session.put`, :meth:`Session.delete`, :meth:`Publisher.put`, + :meth:`Publisher.delete`, :meth:`Session.get`, or :meth:`Querier.get`. + """ + + def __new__(cls, *, send: bool = False, route: bool = False, receive: bool = False) -> Self: ... + def is_instrumented(self, point: InterceptionPoint) -> bool: + """Returns True if the given interception point is enabled.""" + + def __repr__(self) -> str: ... + +@_unstable +@final +class TimestampStackRecord: + """A single timestamp entry in a :class:`TimestampStack`. + + Each record carries the interception point, whether the timestamp was generated by a + custom :data:`SessionTimestampCallback`, and the raw or UHLC timestamp value. + """ + + @property + def point(self) -> InterceptionPoint: + """The interception point at which this record was captured.""" + + @property + def is_custom(self) -> bool: + """True if the timestamp was generated by a custom :data:`SessionTimestampCallback`.""" + + def timestamp(self) -> Timestamp | bytes: + """Returns the timestamp as a :class:`Timestamp` (UHLC) or raw :class:`bytes` (custom).""" + + def as_timestamp(self) -> Timestamp | None: + """Returns the timestamp as a :class:`Timestamp`, or None if it is a custom bytes timestamp.""" + + def __repr__(self) -> str: ... + +@_unstable +@final +class TimestampStack: + """A stack of :class:`TimestampStackRecord` entries accumulated along a message's routing path. + + Accessible via :attr:`Sample.timestamp_stack`, :attr:`ReplyError.timestamp_stack`, + :attr:`Query.timestamp_stack`, and :attr:`Reply.timestamp_stack`. + """ + + @property + def instrumentation(self) -> TimestampInstrumentation: + """The instrumentation configuration that was active when this stack was created.""" + + @property + def records(self) -> list[TimestampStackRecord]: + """The list of timestamp records collected along the message's path.""" + + def __repr__(self) -> str: ... + @final class WhatAmI(Enum): """The type of the node in the Zenoh network. diff --git a/zenoh/ext.pyi b/zenoh/ext.pyi index 46676060..26e360aa 100644 --- a/zenoh/ext.pyi +++ b/zenoh/ext.pyi @@ -27,6 +27,7 @@ from zenoh import ( Session, Subscriber, Timestamp, + TimestampInstrumentation, ZBytes, handlers, ) @@ -164,6 +165,7 @@ class AdvancedPublisher: encoding: _IntoEncoding | None = None, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Publish data to the key expression. See :meth:`zenoh.Publisher.put`.""" @@ -172,6 +174,7 @@ class AdvancedPublisher: *, attachment: _IntoZBytes | None = None, timestamp: Timestamp | None = None, + timestamp_instrumentation: TimestampInstrumentation | None = None, ): """Delete the value associated with the key expression. See :meth:`zenoh.Publisher.delete`.""" From 8414c4243b721eeb0f2511603537ffb9209de5cc Mon Sep 17 00:00:00 2001 From: yuanyuyuan Date: Tue, 23 Jun 2026 07:56:37 +0000 Subject: [PATCH 20/20] refactor(timestamp-stack): rename TsStackContext to TimestampContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses OlivierHecart review comment on eclipse-zenoh/zenoh#2620: the name TsStackContext ties the callback context to the timestamp-stack implementation. TimestampContext is more generic and less coupled to the wire extension name. The interception_point field is retained — it is necessary for callbacks that want to stamp different values at Send vs Route vs Receive. --- src/lib.rs | 2 +- src/timestamp_stack.rs | 16 +++++++++++++--- zenoh/__init__.pyi | 6 +++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5e6e34dc..2e61ef7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,7 +80,7 @@ pub(crate) mod zenoh { time::{Timestamp, TimestampId, NTP64}, timestamp_stack::{ InterceptionPoint, TimestampInstrumentation, TimestampInstrumentationBuilder, - TimestampStack, TimestampStackRecord, TsStackContext, + TimestampContext, TimestampStack, TimestampStackRecord, }, ZError, }; diff --git a/src/timestamp_stack.rs b/src/timestamp_stack.rs index 69a7ba0d..238b2471 100644 --- a/src/timestamp_stack.rs +++ b/src/timestamp_stack.rs @@ -119,10 +119,20 @@ impl From for RustInterceptionPoint { } } -wrapper!(zenoh::timestamp_stack::TsStackContext: Clone); +// Renamed from TsStackContext (OlivierHecart review: make the callback context name +// less implementation-specific, drop the "Stack" coupling). +#[pyclass] +#[derive(Clone)] +pub(crate) struct TimestampContext(pub(crate) RustTsStackContext); + +impl From for TimestampContext { + fn from(value: RustTsStackContext) -> Self { + Self(value) + } +} #[pymethods] -impl TsStackContext { +impl TimestampContext { #[getter] fn zid(&self) -> ZenohId { self.0.zid.into() @@ -233,7 +243,7 @@ impl TimestampStack { pub(crate) fn py_to_session_ts_callback(py_cb: PyObject) -> SessionTimestampCallback { Arc::new(move |ctx: RustTsStackContext| { Python::with_gil(|py| { - let py_ctx = match Py::new(py, TsStackContext(ctx)) { + let py_ctx = match Py::new(py, TimestampContext(ctx)) { Ok(obj) => obj, Err(_) => return Vec::new(), }; diff --git a/zenoh/__init__.pyi b/zenoh/__init__.pyi index 0c2ade5d..032b5490 100644 --- a/zenoh/__init__.pyi +++ b/zenoh/__init__.pyi @@ -2175,7 +2175,7 @@ InterceptionPoint.UNKNOWN.__doc__ = """Catch-all for future variants added by th @_unstable @final -class TsStackContext: +class TimestampContext: """Context passed to a :class:`SessionTimestampCallback` when a timestamp is requested. Provides information about the session and the interception point where the timestamp is being collected. @@ -2195,8 +2195,8 @@ class TsStackContext: def __repr__(self) -> str: ... -SessionTimestampCallback = Callable[[TsStackContext], bytes] -"""A callable that receives a :class:`TsStackContext` and returns raw timestamp bytes. +SessionTimestampCallback = Callable[[TimestampContext], bytes] +"""A callable that receives a :class:`TimestampContext` and returns raw timestamp bytes. Used with :func:`open` to provide custom per-session timestamps at each interception point. The returned bytes are stored verbatim in the :class:`TimestampStackRecord` and exposed via