From 456b4d900caf18d574decfb72868f847c0262570 Mon Sep 17 00:00:00 2001 From: Gil Bregman Date: Thu, 14 Aug 2025 08:55:49 +0300 Subject: [PATCH] pybind/rados: Add list_lockers() and break_lock() to Rados Python interface. Fixes: https://tracker.ceph.com/issues/72544 Signed-off-by: Gil Bregman --- src/pybind/rados/c_rados.pxd | 7 +++ src/pybind/rados/mock_rados.pxi | 8 +++ src/pybind/rados/rados.pyx | 108 ++++++++++++++++++++++++++++++++ src/test/pybind/test_rados.py | 54 +++++++++++++++- 4 files changed, 176 insertions(+), 1 deletion(-) diff --git a/src/pybind/rados/c_rados.pxd b/src/pybind/rados/c_rados.pxd index 563f3ec6afbb6..d4e245142065f 100644 --- a/src/pybind/rados/c_rados.pxd +++ b/src/pybind/rados/c_rados.pxd @@ -239,6 +239,13 @@ cdef extern from "rados/librados.h" nogil: const char * cookie, const char * tag, const char * desc, timeval * duration, uint8_t flags) int rados_unlock(rados_ioctx_t io, const char * o, const char * name, const char * cookie) + int rados_break_lock(rados_ioctx_t io, const char *o, const char *name, + const char *client, const char *cookie) + ssize_t rados_list_lockers(rados_ioctx_t io, const char *o, const char *name, + int *exclusive, char *tag, size_t *tag_len, + char *clients, size_t *clients_len, + char *cookies, size_t *cookies_len, + char *addrs, size_t *addrs_len) rados_write_op_t rados_create_write_op() void rados_release_write_op(rados_write_op_t write_op) diff --git a/src/pybind/rados/mock_rados.pxi b/src/pybind/rados/mock_rados.pxi index 36f5aad7d0a6f..ad4a161c2a461 100644 --- a/src/pybind/rados/mock_rados.pxi +++ b/src/pybind/rados/mock_rados.pxi @@ -331,6 +331,14 @@ cdef nogil: pass int rados_unlock(rados_ioctx_t io, const char * o, const char * name, const char * cookie): pass + int rados_break_lock(rados_ioctx_t io, const char *o, const char *name, + const char *client, const char *cookie): + pass + ssize_t rados_list_lockers(rados_ioctx_t io, const char *o, const char *name, + int *exclusive, char *tag, size_t *tag_len, + char *clients, size_t *clients_len, char *cookies, + size_t *cookies_len, char *addrs, size_t *addrs_len): + pass rados_write_op_t rados_create_write_op(): pass diff --git a/src/pybind/rados/rados.pyx b/src/pybind/rados/rados.pyx index bcfa6777f3de1..27fa56e5f2879 100644 --- a/src/pybind/rados/rados.pyx +++ b/src/pybind/rados/rados.pyx @@ -4009,6 +4009,114 @@ returned %d, but should return zero on success." % (self.name, ret)) if ret < 0: raise make_ex(ret, "Ioctx.rados_lock_exclusive(%s): failed to set lock %s on %s" % (self.name, name, key)) + def break_lock(self, key: str, name: str, client: str, cookie: str): + + """ + Release a shared or exclusive lock on an object, which was taken by the + specified client. + + :param key: name of the object + :param name: name of the lock + :param client: the client currently holding the lock + :param cookie: cookie of the lock + + :raises: :class:`TypeError` + :raises: :class:`Error` + """ + self.require_ioctx_open() + + key_raw = cstr(key, 'key') + name_raw = cstr(name, 'name') + client_raw = cstr(client, 'client') + cookie_raw = cstr(cookie, 'cookie') + + cdef: + char* _key = key_raw + char* _name = name_raw + char* _client = client_raw + char* _cookie = cookie_raw + + with nogil: + ret = rados_break_lock(self.io, _key, _name, _client, _cookie) + if ret < 0: + raise make_ex(ret, + "Ioctx.rados_break_lock(%s): failed to break lock %s " + "on %s for client %s " + "cookie %s" % (self.name, name, key, client, cookie)) + + def list_lockers(self, key: str, name: str): + + """ + List clients that have locked the named object lock and + information about the lock. + + :param key: name of the object + :param name: name of the lock + :returns: dict - contains the following keys: + * ``tag`` - the tag associated with the lock (every + additional locker must use the same tag) + * ``exclusive`` - boolean indicating whether the + lock is exclusive or shared + * ``lockers`` - a list of (client, cookie, address) + tuples + + :raises: :class:`TypeError` + :raises: :class:`Error` + """ + self.require_ioctx_open() + + key_raw = cstr(key, 'key') + name_raw = cstr(name, 'name') + + cdef: + char* _key = key_raw + char* _name = name_raw + int exclusive = 0 + char* c_tag = NULL + size_t tag_size = 512 + char* c_clients = NULL + size_t clients_size = 512 + char* c_cookies = NULL + size_t cookies_size = 512 + char* c_addrs = NULL + size_t addrs_size = 512 + + try: + while True: + c_tag = realloc_chk(c_tag, tag_size) + c_cookies = realloc_chk(c_cookies, cookies_size) + c_clients = realloc_chk(c_clients, clients_size) + c_addrs = realloc_chk(c_addrs, addrs_size) + with nogil: + ret = rados_list_lockers(self.io, _key, _name, &exclusive, + c_tag, &tag_size, c_clients, &clients_size, + c_cookies, &cookies_size, c_addrs, &addrs_size) + if ret >= 0: + break + elif ret != -errno.ERANGE: + raise make_ex(ret, + "Ioctx.rados_list_lockers(%s): failed to " + "list lockers of lock %s on %s" % (self.name, name, key)) + clients = [] + cookies = [] + addrs = [] + + if ret > 0: + clients = map(decode_cstr, c_clients[:clients_size - 1].split(b'\0')) + cookies = map(decode_cstr, c_cookies[:cookies_size - 1].split(b'\0')) + addrs = map(decode_cstr, c_addrs[:addrs_size - 1].split(b'\0')) + + return { + 'tag' : decode_cstr(c_tag), + 'exclusive' : exclusive == 1, + 'lockers' : list(zip(clients, cookies, addrs)), + } + finally: + free(c_tag) + free(c_cookies) + free(c_clients) + free(c_addrs) + def set_osdmap_full_try(self): """ Set global osdmap_full_try label to true diff --git a/src/test/pybind/test_rados.py b/src/test/pybind/test_rados.py index 881b29c915221..cd8a955dfd4a1 100644 --- a/src/test/pybind/test_rados.py +++ b/src/test/pybind/test_rados.py @@ -1,7 +1,7 @@ from __future__ import print_function from assertions import assert_equal as eq, assert_raises from rados import (Rados, Error, RadosStateError, Object, ObjectExists, - ObjectNotFound, ObjectBusy, NotConnected, + ObjectNotFound, ObjectBusy, NotConnected, InvalidArgumentError, LIBRADOS_ALL_NSPACES, WriteOpCtx, ReadOpCtx, LIBRADOS_CREATE_EXCLUSIVE, LIBRADOS_CMPXATTR_OP_EQ, LIBRADOS_CMPXATTR_OP_GT, LIBRADOS_CMPXATTR_OP_LT, OSError, LIBRADOS_SNAP_HEAD, LIBRADOS_OPERATION_BALANCE_READS, LIBRADOS_OPERATION_SKIPRWLOCKS, MonitorLog, MAX_ERRNO, NoData, ExtendMismatch) @@ -1057,23 +1057,75 @@ class TestIoctx(object): def test_lock(self): self.ioctx.lock_exclusive("foo", "lock", "locker", "desc_lock", 10000, 0) + lockers_info = self.ioctx.list_lockers("foo", "lock") + eq(lockers_info["tag"], "") + eq(lockers_info["exclusive"], True) + eq(len(lockers_info["lockers"]), 1) assert_raises(ObjectExists, self.ioctx.lock_exclusive, "foo", "lock", "locker", "desc_lock", 10000, 0) self.ioctx.unlock("foo", "lock", "locker") assert_raises(ObjectNotFound, self.ioctx.unlock, "foo", "lock", "locker") + lockers_info = self.ioctx.list_lockers("foo", "lock") + eq(lockers_info["tag"], "") + eq(lockers_info["exclusive"], True) + eq(len(lockers_info["lockers"]), 0) self.ioctx.lock_shared("foo", "lock", "locker1", "tag", "desc_lock", 10000, 0) self.ioctx.lock_shared("foo", "lock", "locker2", "tag", "desc_lock", 10000, 0) + lockers_info = self.ioctx.list_lockers("foo", "lock") + eq(lockers_info["tag"], "tag") + eq(lockers_info["exclusive"], False) + eq(len(lockers_info["lockers"]), 2) assert_raises(ObjectBusy, self.ioctx.lock_exclusive, "foo", "lock", "locker3", "desc_lock", 10000, 0) self.ioctx.unlock("foo", "lock", "locker1") self.ioctx.unlock("foo", "lock", "locker2") + lockers_info = self.ioctx.list_lockers("foo", "lock") + eq(lockers_info["tag"], "tag") + eq(lockers_info["exclusive"], False) + eq(len(lockers_info["lockers"]), 0) assert_raises(ObjectNotFound, self.ioctx.unlock, "foo", "lock", "locker1") assert_raises(ObjectNotFound, self.ioctx.unlock, "foo", "lock", "locker2") + self.ioctx.lock_shared("foo", "lock", "locker3", "tag", "desc_lock", + 10000, 0) + lockers_info = self.ioctx.list_lockers("foo", "lock") + eq(lockers_info["tag"], "tag") + eq(lockers_info["exclusive"], False) + eq(len(lockers_info["lockers"]), 1) + lock_client = lockers_info["lockers"][0][0] + assert_raises(ObjectNotFound, + self.ioctx.break_lock, "bar", "lock", + lock_client, "locker3") + assert_raises(ObjectNotFound, + self.ioctx.break_lock, "foo", "wrong", + lock_client, "locker3") + assert_raises(InvalidArgumentError, + self.ioctx.break_lock, "foo", "lock", + "wrong_client", "locker3") + assert_raises(ObjectNotFound, + self.ioctx.break_lock, "foo", "lock", + lock_client, "wrong cookie") + self.ioctx.lock_shared("foo", "lock", "locker4", "tag", "desc_lock", + 10000, 0) + lockers_info = self.ioctx.list_lockers("foo", "lock") + eq(lockers_info["tag"], "tag") + eq(lockers_info["exclusive"], False) + eq(len(lockers_info["lockers"]), 2) + self.ioctx.break_lock("foo", "lock", lock_client, "locker3") + lockers_info = self.ioctx.list_lockers("foo", "lock") + eq(lockers_info["tag"], "tag") + eq(lockers_info["exclusive"], False) + eq(len(lockers_info["lockers"]), 1) + assert_raises(ObjectNotFound, self.ioctx.unlock, "foo", "lock", "locker3") + self.ioctx.unlock("foo", "lock", "locker4") + lockers_info = self.ioctx.list_lockers("foo", "lock") + eq(lockers_info["tag"], "tag") + eq(lockers_info["exclusive"], False) + eq(len(lockers_info["lockers"]), 0) def test_execute(self): self.ioctx.write("foo", b"") # ensure object exists -- 2.39.5