Metadata-Version: 2.4
Name: lockingcenter
Version: 1.0.0
Summary: Python client for Locking-Center, a TCP mutex server that synchronizes access to shared resources between services
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/freakmaxi/locking-center-client-python
Keywords: mutex,lock,distributed,locking-center
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Classifier: Topic :: System :: Distributed Computing
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# Locking-Center Python Client

The Python connector for [Locking-Center](https://github.com/freakmaxi/locking-center), a mutex point that synchronizes
access to shared resources between different services. Lock a key before you touch the resource, do the work, unlock the
key. Only one caller holds a given key at a time, the rest queue up and are served in order.

- [Locking-Center Server](https://github.com/freakmaxi/locking-center)

Python 3.10 or newer, standard library only (no dependencies).

## Installation

```shell
pip install ./clients/python          # from a checkout of the repository
```

## Quick start

```python
from lockingcenter import LockingCenter

m = LockingCenter("localhost:22119")

m.lock("locking-key")
try:
    print("Hello from the locked area!")
finally:
    m.unlock("locking-key")
```

## Connecting

```python
# simplest form
m = LockingCenter("localhost:22119")

# with a source address, which identifies this owner for crash recovery, see below
m = LockingCenter("localhost:22119", source="10.0.0.4")
```

The constructor dials the server once to make sure it is reachable and raises `ConnectionError` if it is not. A
malformed address or a source longer than 127 bytes raises `ValueError`. The returned object is safe to keep and share
across threads; every call opens its own short-lived connection.

## API

| Method | Blocks | Description |
| --- | --- | --- |
| `lock(key)` | yes | Acquires the key, waiting in the queue until it is free |
| `try_lock(key) -> bool` | no | Acquires the key only if it is free right now, returns whether it did |
| `unlock(key)` | no | Releases the key |
| `wait(key)` | yes | Waits for the key to be free, then releases it again without holding it |
| `reset_by_key(key)` | no | Force releases a key, whoever holds it (crash recovery) |
| `reset_by_source(source)` | no | Force releases everything a given owner held (crash recovery) |

### Locking

`lock` blocks until the key is free, then takes it. It keeps trying through connection failures, so it returns only
once the key is held.

```python
m.lock("orders/batch-7")
try:
    ...  # exclusive work
finally:
    m.unlock("orders/batch-7")
```

### Try locking

`try_lock` is the non-blocking form. It takes the key only if it is free at that moment and returns immediately, so you
decide what to do when somebody else holds it.

```python
if m.try_lock("orders/batch-7"):
    try:
        ...  # exclusive work
    finally:
        m.unlock("orders/batch-7")
else:
    ...  # someone else holds it, skip, retry later, or do something else
```

`try_lock` returns `False` when the key is held by another owner **and** when the server cannot be reached, so a
`False` means only "you did not get the lock". If you need to tell the two apart, check reachability separately.

### Waiting

`wait` blocks until the key is free and then releases it immediately, without holding it. Use it to pause until whoever
holds the key is done.

```python
m.wait("migration-done")  # returns once the key is free
```

## Crash recovery: reset

A lock is not tied to its TCP connection, so a client that crashes while holding a key leaves that key locked. Nothing
releases it automatically. Reset is how an operator or a supervisor clears such a stuck lock.

```python
m.reset_by_key("orders/batch-7")  # release this key, whoever holds it

m.reset_by_source("10.0.0.9")     # release everything 10.0.0.9 held
```

`reset_by_source` matches on the **source address**. Pass the source when you construct the client
(`LockingCenter(address, source=...)`) so that each owner is identifiable; on Kubernetes, pass the pod IP. A `None`
source lets the server fall back to the connection's peer address.

## Keys

A key must be **between 1 and 127 bytes**. Keys are sent UTF-8 encoded and the limit is on the encoded size, so a
non-ASCII key such as `"café-ключ"` (9 characters, 14 bytes) counts as 14. An empty or over-long key is a programming
error, so the client raises `ValueError` right away, before touching the network, instead of hanging in the retry loop.
Keep keys within that range, they are arbitrary text otherwise.

## Behaviour to know

- **`lock`, `unlock` and the resets keep retrying until they succeed.** They do not raise; a server that is down just
means the call keeps trying (with a 500 ms delay between attempts, logged as a warning through the `logging` module
under the `lockingcenter.mutex` logger). Run a call in your own thread with a timeout if you need to give up.
- **There is no read timeout on the connection for `lock`.** The server holds the connection open for as long as the
key is held by its current owner, which is unbounded. The client already accounts for this and ignores any global
`socket.setdefaulttimeout()`.
- **Every call is one short-lived TCP connection.** There is no pool to manage and nothing to close.
- **The client is safe for concurrent use** from many threads.

## Development

```shell
cd clients/python
python3 -m unittest discover -s tests -v
```

`tests/test_encoding.py` checks the exact bytes of every request and needs no server. `tests/test_integration.py`
starts a real server binary for every test; point `LOCKD_SERVER` at the binary and, if needed, `LOCKD_PORT` at a free
port (the server also takes the two ports after it). The integration tests are skipped when the binary is not found.

### Getting a server for the tests

The integration tests start a real server themselves. Build it from the
[server repository](https://github.com/freakmaxi/locking-center) and point `LOCKD_SERVER` at it;

```shell
go build -o lockd-server ./mutex
LOCKD_SERVER=/path/to/lockd-server python3 -m unittest discover -s tests -v
```

Without `LOCKD_SERVER` the integration tests are skipped with a message; the encoding tests always run. `LOCKD_PORT`
(default `29300`) picks the port the test server binds; it also takes the two ports above it.

## License

[Apache License 2.0](LICENSE). The Locking-Center server itself is licensed separately under the GPL-3.0; the
clients are permissive so they can be embedded in any service.
