diff --git a/examples/serial/cpp/CMakeLists.txt b/examples/serial/cpp/CMakeLists.txt index e15ef965..dd5070e3 100644 --- a/examples/serial/cpp/CMakeLists.txt +++ b/examples/serial/cpp/CMakeLists.txt @@ -75,6 +75,7 @@ list(APPEND EXECUTABLES smartredis_dataset smartredis_model smartredis_mnist + smartredis_put_get_bytes ) # Build the examples diff --git a/examples/serial/cpp/smartredis_put_get_bytes.cpp b/examples/serial/cpp/smartredis_put_get_bytes.cpp new file mode 100644 index 00000000..fbe25133 --- /dev/null +++ b/examples/serial/cpp/smartredis_put_get_bytes.cpp @@ -0,0 +1,62 @@ +/* + * BSD 2-Clause License + * + * Copyright (c) 2021-2024, Hewlett Packard Enterprise + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "client.h" +#include +#include + +int main(int argc, char* argv[]) { + + // Initialize some byte data + size_t n_bytes = 255; + std::vector input_bytes(n_bytes); + for(size_t i = 0; i < n_bytes; i++) { + input_bytes[i] = i; + } + + SmartRedis::Client client("client_test_put_get_bytes"); + + std::string key = "put_get_bytes_test"; + + client.put_bytes(key, input_bytes.data(), n_bytes); + + std::vector output_bytes(n_bytes, 0); + + client.unpack_bytes(key, output_bytes.data(), n_bytes); + + for(int i = 0; i < n_bytes; i++) { + if (output_bytes[i] != input_bytes[i]) { + std::cout<<"Byte "<put_bytes(key, bytes, n_bytes); + + if (reply.has_error()) + throw SRRuntimeException("put_bytes failed"); +} + +// Get byte data and fill an already allocated array memory space that +// has the specified size. This method is the most memory +// efficient way to retrieve tensor data. +void Client::unpack_bytes(const std::string& name, + void* data, + const size_t n_bytes) +{ + // Track calls to this API function + LOG_API_FUNCTION(); + + std::string get_key = _build_tensor_key(name, true); + CommandReply reply = _redis_server->get_bytes(get_key); + + std::memcpy(data, reply.str(), reply.str_len()); +} + // Get the tensor data, dimensions, and type for the provided tensor name. // This function will allocate and retain management of the memory for the // tensor data. @@ -2226,6 +2259,25 @@ TensorBase* Client::_get_tensorbase_obj(const std::string& name) return ptr; } +// Get the bytes stored in the database and allocate memory via malloc +// to hold it (deep copy) +void Client::_get_bytes_no_mem_handling(const std::string& name, + void*& data, + size_t& n_bytes) +{ + std::string get_key = _build_tensor_key(name, true); + CommandReply reply = _redis_server->get_bytes(get_key); + + // TODO We don't have a way with CommandReply to transfer ownership of a str() reply + // to an outside pointer. For now we do a naive memcopy, + // but we really shouldn't have to do that + // We could set the internal str reply value to NULL and the redisreply destructor + // which calls free on it will likely do a no-op, but this isn't proven. + n_bytes = reply.str_len(); + data = malloc(n_bytes); + std::memcpy(data, (void*)(reply.str()), n_bytes); +} + // Determine datset name from aggregation list entry std::string Client::_get_dataset_name_from_list_entry( const std::string& dataset_key) diff --git a/src/cpp/redis.cpp b/src/cpp/redis.cpp index f730ed50..0c514067 100644 --- a/src/cpp/redis.cpp +++ b/src/cpp/redis.cpp @@ -185,6 +185,31 @@ CommandReply Redis::put_tensor(TensorBase& tensor) return run(cmd); } +// Put bytes on the server +CommandReply Redis::put_bytes(const std::string& key, + const void* bytes, + const size_t n_bytes) +{ + // Build the command + SingleKeyCommand cmd; + cmd << "SET" << Keyfield(key) + << std::string_view((char*)bytes, n_bytes); + + // Run it + return run(cmd); +} + +// Get bytes from the server +CommandReply Redis::get_bytes(const std::string& key) +{ + // Build the command + SingleKeyCommand cmd; + cmd << "GET" << Keyfield(key); + + // Run it + return run(cmd); +} + // Get a Tensor from the server CommandReply Redis::get_tensor(const std::string& key) { diff --git a/src/cpp/rediscluster.cpp b/src/cpp/rediscluster.cpp index 2813a67b..dc1abddf 100644 --- a/src/cpp/rediscluster.cpp +++ b/src/cpp/rediscluster.cpp @@ -377,6 +377,31 @@ CommandReply RedisCluster::put_tensor(TensorBase& tensor) return run(cmd); } +// Put bytes on the server +CommandReply RedisCluster::put_bytes(const std::string& key, + const void* bytes, + const size_t n_bytes) +{ + // Build the command + SingleKeyCommand cmd; + cmd << "SET" << Keyfield(key) + << std::string_view((char*)bytes, n_bytes); + + // Run it + return run(cmd); +} + +// Get bytes from the server +CommandReply RedisCluster::get_bytes(const std::string& key) +{ + // Build the command + SingleKeyCommand cmd; + cmd << "GET" << Keyfield(key); + + // Run it + return run(cmd); +} + // Get a Tensor from the server CommandReply RedisCluster::get_tensor(const std::string& key) { diff --git a/src/python/bindings/bind.cpp b/src/python/bindings/bind.cpp index 11ff4e9a..7c72bd7a 100644 --- a/src/python/bindings/bind.cpp +++ b/src/python/bindings/bind.cpp @@ -62,6 +62,8 @@ PYBIND11_MODULE(smartredisPy, m) { .def(py::init()) .def(py::init()) .CLIENT_METHOD(put_tensor) + .CLIENT_METHOD(put_bytes) + .CLIENT_METHOD(get_bytes) .CLIENT_METHOD(get_tensor) .CLIENT_METHOD(delete_tensor) .CLIENT_METHOD(copy_tensor) diff --git a/src/python/module/smartredis/client.py b/src/python/module/smartredis/client.py index d271b15e..9d058bff 100644 --- a/src/python/module/smartredis/client.py +++ b/src/python/module/smartredis/client.py @@ -30,6 +30,7 @@ import os.path as osp import typing as t import numpy as np +import io from .dataset import Dataset from .configoptions import ConfigOptions @@ -188,6 +189,24 @@ def put_tensor(self, name: str, data: np.ndarray) -> None: else: self._client.put_tensor(name, dtype, data.copy()) + @exception_handler + def put_bytes(self, name: str, data: io.BytesIO) -> None: + """Put bytes to a Redis database + + The final key under which the bytes are stored + may be formed by applying a prefix to the supplied + name. See use_tensor_ensemble_prefix() for more details. + + :param name: name for bytes to be stored at + :type name: str + :param data: bytes to be stored + :type data: io.BytesIO + :raises RedisReplyError: if put fails + """ + typecheck(name, "name", str) + typecheck(data, "data", io.BytesIO) + self._client.put_bytes(name, data) + @exception_handler def get_tensor(self, name: str) -> np.ndarray: """Get a tensor from the database @@ -206,6 +225,26 @@ def get_tensor(self, name: str) -> np.ndarray: typecheck(name, "name", str) return self._client.get_tensor(name) + @exception_handler + def get_bytes(self, name: str) -> str: + """Get bytes from the database + + The key used to locate the bytes + may be formed by applying a prefix to the supplied + name. See set_data_source() + and use_tensor_ensemble_prefix() for more details. + + :param name: name associated with the bytes + :type name: str + :raises RedisReplyError: if get fails + :return: data bytes + :rtype: io.BytesIO + """ + + typecheck(name, "name", str) + # TODO this is a deep copy of the returned bytes object + return io.BytesIO(self._client.get_bytes(name)) + @exception_handler def delete_tensor(self, name: str) -> None: """Delete a tensor from the database diff --git a/src/python/module/smartredis/error.py b/src/python/module/smartredis/error.py index e00fe392..f93e6a5e 100644 --- a/src/python/module/smartredis/error.py +++ b/src/python/module/smartredis/error.py @@ -73,7 +73,7 @@ def _check_error(cpp_error: str, method: str = "", key: str = "") -> str: if method: msg = f"{method} execution failed\n" if "REDIS_REPLY_NIL" in cpp_error: - msg += f"No Dataset stored at key: {key}" + msg += f"No data stored at key: {key}" return msg msg += cpp_error return msg diff --git a/src/python/src/pyclient.cpp b/src/python/src/pyclient.cpp index f521418b..3266e163 100644 --- a/src/python/src/pyclient.cpp +++ b/src/python/src/pyclient.cpp @@ -125,6 +125,17 @@ void PyClient::put_tensor( }); } +void PyClient::put_bytes(std::string& name, py::object data) +{ + MAKE_CLIENT_API({ + + // This does an implicit copy but it's not clear + // how we can get access to only the data + std::string bytes_data = data.attr("getvalue")().cast(); + _client->put_bytes(name, bytes_data.data(), bytes_data.size()); + }); +} + py::array PyClient::get_tensor(const std::string& name) { return MAKE_CLIENT_API({ @@ -184,6 +195,25 @@ py::array PyClient::get_tensor(const std::string& name) }); } +// Get a py::bytes object pointing to the underlying bytes data +py::bytes PyClient::get_bytes(const std::string& name) +{ + return MAKE_CLIENT_API({ + + void* data = NULL; + size_t n_bytes = 0; + + // Get the bytes and store in data pointer and update n_bytes + _client->_get_bytes_no_mem_handling(name, data, n_bytes); + + // TODO by using py::bytes we are doing another explicit deep copy + // and _get_bytes_no_mem_handling has already done a deep copy + py::bytes py_bytes = py::bytes((char*)data, n_bytes); + free(data); + return py_bytes; + }); +} + void PyClient::delete_tensor(const std::string& name) { MAKE_CLIENT_API({ diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index e649dd8a..f0a925b0 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -93,6 +93,7 @@ list(APPEND EXECUTABLES client_test_put_get_transpose_3D client_test_put_get_2D client_test_put_get_1D + client_test_put_get_bytes client_test_mnist client_test_mnist_dataset client_test_ensemble diff --git a/tests/cpp/client_test_put_get_bytes.cpp b/tests/cpp/client_test_put_get_bytes.cpp new file mode 100644 index 00000000..cf8b8a73 --- /dev/null +++ b/tests/cpp/client_test_put_get_bytes.cpp @@ -0,0 +1,66 @@ +/* + * BSD 2-Clause License + * + * Copyright (c) 2021-2024, Hewlett Packard Enterprise + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "client.h" +#include "client_test_utils.h" +#include +#include + +int main(int argc, char* argv[]) { + + // Initialize some byte data + size_t n_bytes = 255; + std::vector input_bytes(n_bytes); + for(size_t i = 0; i < n_bytes; i++) { + input_bytes[i] = i; + } + + SmartRedis::Client client("client_test_put_get_bytes"); + + std::string key = "put_get_bytes_test"; + + client.put_bytes(key, input_bytes.data(), n_bytes); + + if(!client.key_exists(get_prefix() + key)) + throw std::runtime_error("The key does not exist in the database."); + + std::vector output_bytes(n_bytes, 0); + + client.unpack_bytes(key, output_bytes.data(), n_bytes); + + for(int i = 0; i < n_bytes; i++) { + if (output_bytes[i] != input_bytes[i]) { + std::cout<<"Byte "<