diff --git a/.github/workflows/ort.yml b/.github/workflows/ort.yml index 486229e733..a01659e4f5 100644 --- a/.github/workflows/ort.yml +++ b/.github/workflows/ort.yml @@ -43,7 +43,11 @@ jobs: - name: Checkout target branch uses: actions/checkout@v4 with: - ref: ${{ env.TARGET_BRANCH }} + ref: ${{ env.TARGET_BRANCH }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 # Fetch all history for all branches and tags + - name: Setup target commit run: | @@ -251,10 +255,32 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} INPUT_VERSION: ${{ github.event.inputs.version }} - ### Warn of outdated attributions for PR ### + ### Warn of outdated attributions for PR ### - name: Warn of outdated attributions due to the PR - if: ${{ env.FOUND_DIFF == 'true' && github.event_name == 'pull_request' }} - uses: actions/github-script@v6 - with: - script: | - core.warning('WARNING! Note the attribution files differ with this PR, make sure an updating PR is issued using scheduled or manual run of this workflow!'); + if: ${{ env.FOUND_DIFF == 'true' && github.event_name == 'pull_request' }} + run: | + ATTRIBUTION_FILES=( + "${{ env.PYTHON_ATTRIBUTIONS }}" + "${{ env.NODE_ATTRIBUTIONS }}" + "${{ env.RUST_ATTRIBUTIONS }}" + "${{ env.JAVA_ATTRIBUTIONS }}" + ) + + MESSAGE="WARNING! The attribution files differ in this PR. Please ensure an updating PR is issued using a scheduled or manual run of this workflow!" + + # Echo the message to the console + echo "$MESSAGE" + + # Emit a general warning in the action log + echo "::warning::$MESSAGE" + + # Loop through the attribution files + for FILE in "${ATTRIBUTION_FILES[@]}"; do + if git diff --quiet "$FILE"; then + continue + else + # Emit a warning associated with the changed file + echo "::warning file=$FILE::WARNING! The attribution file '$FILE' differs in this PR." + fi + done + diff --git a/CHANGELOG.md b/CHANGELOG.md index 23e0dc32f4..0bdef111eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Python: Client API for retrieving internal statistics ([#2707](https://github.com/valkey-io/valkey-glide/pull/2707)) * Node, Python: Adding support for replacing connection configured password ([#2651](https://github.com/valkey-io/valkey-glide/pull/2651)) * Node: Add FT._ALIASLIST command([#2652](https://github.com/valkey-io/valkey-glide/pull/2652)) * Python: Python: `FT._ALIASLIST` command added([#2638](https://github.com/valkey-io/valkey-glide/pull/2638)) diff --git a/python/python/glide/glide.pyi b/python/python/glide/glide.pyi index b544a3948e..bbd5274770 100644 --- a/python/python/glide/glide.pyi +++ b/python/python/glide/glide.pyi @@ -31,5 +31,6 @@ def start_socket_listener_external(init_callback: Callable) -> None: ... def value_from_pointer(pointer: int) -> TResult: ... def create_leaked_value(message: str) -> int: ... def create_leaked_bytes_vec(args_vec: List[bytes]) -> int: ... +def get_statistics() -> dict: ... def py_init(level: Optional[Level], file_name: Optional[str]) -> Level: ... def py_log(log_level: Level, log_identifier: str, message: str) -> None: ... diff --git a/python/python/glide/glide_client.py b/python/python/glide/glide_client.py index 2838ae288e..1c1dc07ee7 100644 --- a/python/python/glide/glide_client.py +++ b/python/python/glide/glide_client.py @@ -33,6 +33,7 @@ MAX_REQUEST_ARGS_LEN, ClusterScanCursor, create_leaked_bytes_vec, + get_statistics, start_socket_listener_external, value_from_pointer, ) @@ -533,6 +534,9 @@ async def _reader_loop(self) -> None: else: await self._process_response(response=response) + async def get_statistics(self) -> dict: + return get_statistics() + async def _update_connection_password( self, password: Optional[str], re_auth: bool ) -> TResult: diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 7560cc7b23..da812c07a3 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -286,6 +286,15 @@ async def test_closed_client_raises_error(self, glide_client: TGlideClient): await glide_client.set("foo", "bar") assert "the client is closed" in str(e) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_statistics(self, glide_client: TGlideClient): + stats = await glide_client.get_statistics() + assert isinstance(stats, dict) + assert "total_connections" in stats + assert "total_clients" in stats + assert len(stats) == 2 + @pytest.mark.asyncio class TestCommands: diff --git a/python/src/lib.rs b/python/src/lib.rs index 8a1a0d3444..6b41123dd3 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -4,12 +4,14 @@ use glide_core::client::FINISHED_SCAN_CURSOR; * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ use glide_core::start_socket_listener; +use glide_core::Telemetry; use glide_core::MAX_REQUEST_ARGS_LENGTH; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; -use pyo3::types::{PyAny, PyBool, PyBytes, PyDict, PyFloat, PyList, PySet}; +use pyo3::types::{PyAny, PyBool, PyBytes, PyDict, PyFloat, PyList, PySet, PyString}; use pyo3::Python; use redis::Value; +use std::collections::HashMap; use std::sync::Arc; pub const DEFAULT_TIMEOUT_IN_MILLISECONDS: u32 = @@ -120,12 +122,39 @@ fn glide(_py: Python, m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(value_from_pointer, m)?)?; m.add_function(wrap_pyfunction!(create_leaked_value, m)?)?; m.add_function(wrap_pyfunction!(create_leaked_bytes_vec, m)?)?; + m.add_function(wrap_pyfunction!(get_statistics, m)?)?; #[pyfunction] fn py_log(log_level: Level, log_identifier: String, message: String) { log(log_level, log_identifier, message); } + #[pyfunction] + fn get_statistics(_py: Python) -> PyResult { + let mut stats_map = HashMap::::new(); + stats_map.insert( + "total_connections".to_string(), + Telemetry::total_connections().to_string(), + ); + stats_map.insert( + "total_clients".to_string(), + Telemetry::total_clients().to_string(), + ); + + Python::with_gil(|py| { + let py_dict = PyDict::new_bound(py); + + for (key, value) in stats_map { + py_dict.set_item( + PyString::new_bound(py, &key), + PyString::new_bound(py, &value), + )?; + } + + Ok(py_dict.into_py(py)) + }) + } + #[pyfunction] #[pyo3(signature = (level=None, file_name=None))] fn py_init(level: Option, file_name: Option<&str>) -> Level {