repolock: add py bindings for RepoLocker

Summary:
Expose the RepoLocker via the pyrepo bindings. Python will make use of this to allow sharing of locks between Python and Rust (i.e. allow locks to be re-entrant across Python -> Rust boundary).

I added a new RepoLockHandle.count() method to enable some sanity checks in the bindings. In particular, we make sure that when the Python code acquires a lock, it isn't already acquired. Python has its own logic for storing the lock on the repo, so it should only try to acquire once. In general Python assumes it is the top level locker, so I want to catch any craziness where Rust acquires a lock and then calls into Python and the lock is re-acquired.

Similarly, we check that when Python releases a lock, the lock is _actually_ released (i.e. there isn't some other Rust code still holding a reference to the lock handle).

Reviewed By: quark-zju

Differential Revision: D45510004

fbshipit-source-id: 8625da19b82b4cd92a052bca56e6e46affe1bc08
This commit is contained in:
Muir Manders 2023-05-04 09:05:16 -07:00 committed by Facebook GitHub Bot
parent 9f78d6b183
commit 48fe991547
3 changed files with 56 additions and 1 deletions

View File

@ -8,6 +8,7 @@ edition = "2021"
cpython_ext = { path = "../../../../lib/cpython-ext" }
cpython = { version = "0.7", default-features = false }
repo = { path = "../../../../lib/repo" }
repolock = { path = "../../../../lib/repolock" }
util = { path = "../../../../lib/util" }
parking_lot = "0.11.2"
pyconfigloader = { path = "../pyconfigloader" }
@ -16,4 +17,3 @@ pyedenapi = { path = "../pyedenapi" }
pymetalog = { path = "../pymetalog" }
pyworkingcopy = { path = "../pyworkingcopy" }
workingcopy = { path = "../../../../lib/workingcopy" }

View File

@ -8,8 +8,10 @@
#![allow(non_camel_case_types)]
extern crate repo as rsrepo;
extern crate repolock as rsrepolock;
extern crate workingcopy as rsworkingcopy;
use std::cell::Cell;
use std::cell::RefCell;
use std::sync::Arc;
@ -30,6 +32,7 @@ pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
let name = [package, "repo"].join(".");
let m = PyModule::new(py, &name)?;
m.add_class::<repo>(py)?;
m.add_class::<repolock>(py)?;
Ok(m)
}
@ -127,4 +130,40 @@ py_class!(pub class repo |py| {
def dotpath(&self) -> PyResult<PyPathBuf> {
self.inner(py).read().dot_hg_path().try_into().map_pyerr(py)
}
def trywlock(&self, wc_dot_hg: PyPathBuf) -> PyResult<repolock> {
let lock = self.inner(py).read().locker().try_lock_working_copy(wc_dot_hg.to_path_buf()).map_pyerr(py)?;
if lock.count() > 1 {
Err(PyErr::new::<exc::ValueError, _>(py, "lock is already locked"))
} else {
repolock::create_instance(py, Cell::new(Some(lock)))
}
}
def trylock(&self) -> PyResult<repolock> {
let lock = self.inner(py).read().locker().try_lock_store().map_pyerr(py)?;
if lock.count() > 1 {
Err(PyErr::new::<exc::ValueError, _>(py, "lock is already locked"))
} else {
repolock::create_instance(py, Cell::new(Some(lock)))
}
}
});
py_class!(pub class repolock |py| {
data lock: Cell<Option<rsrepolock::RepoLockHandle>>;
def unlock(&self) -> PyResult<PyNone> {
if let Some(f) = self.lock(py).replace(None) {
let count = f.count();
drop(f);
if count == 1 {
Ok(PyNone)
} else {
Err(PyErr::new::<exc::ValueError, _>(py, "lock is still locked"))
}
} else {
Err(PyErr::new::<exc::ValueError, _>(py, "lock is already unlocked"))
}
}
});

View File

@ -233,6 +233,22 @@ impl RepoLockHandle {
wc_path: Some(wc_path),
}
}
/// Return ref count for this lock handle. Ref count of 1 means lock will be
/// relased when this handle is dropped.
pub fn count(&self) -> u64 {
let locker = self.locker.lock();
if self.store {
locker.store_lock.as_ref().unwrap().1.get()
} else {
locker
.wc_locks
.get(self.wc_path.as_ref().unwrap())
.unwrap()
.1
.get()
}
}
}
impl fmt::Debug for RepoLockHandle {