Metadata-Version: 2.4
Name: globallock
Version: 0.1.5
Summary: Distributed lock manager, support many types of backend, e.g. redis, django-redis, etcd, zookeeper...
Author-email: rRR0VrFP <rrr0vrfp@qq.com>
Maintainer-email: rRR0VrFP <rrr0vrfp@qq.com>
License-Expression: MIT
Project-URL: Homepage, https://gitee.com/rRR0VrFP/globallock
Keywords: global lock,distributed lock,redis lock,django redis lock,zookeeper lock,etcd lock
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.6
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: zenutils
Dynamic: license-file

# globallock

统一接口的分布式锁管理器，支持 **Redis**、**Django-Redis**、**etcd**、**ZooKeeper** 四种后端。

> 名称 `globallock` 中有两个 `l`。

---

## 安装

```bash
pip install globallock
```

---

## 快速开始

### Redis 后端（默认）

```python
from globallock import GlobalLockManager

config = {
    "global_lock_engine_options": {
        "host": "localhost",
        "port": 6379,
        "db": 0,
    },
}
lockman = GlobalLockManager(config)

with lockman.lock("my_lock", timeout=60, blocking=True, blocking_timeout=5) as lock:
    if lock.is_locked:
        # 获得锁，执行临界区代码
        ...
    else:
        # 未获得锁，多数情况下什么都不做
        ...
```

### etcd 后端

```python
from globallock import GlobalLockManager
from globallock import ETCD_GLOBAL_LOCK_CLASS

config = {
    "global_lock_engine_class": ETCD_GLOBAL_LOCK_CLASS,
    "global_lock_engine_options": {
        "host": "localhost",
        "port": 2379,
    },
}
lockman = GlobalLockManager(config)
with lockman.lock("my_lock", timeout=60, blocking=True, blocking_timeout=5) as lock:
    if lock.is_locked:
        ...
```

### ZooKeeper 后端

```python
from globallock import GlobalLockManager
from globallock import ZOOKEEPER_GLOBAL_LOCK_CLASS

config = {
    "global_lock_engine_class": ZOOKEEPER_GLOBAL_LOCK_CLASS,
    "global_lock_engine_options": {
        "hosts": "localhost:2181",
    },
}
lockman = GlobalLockManager(config)
with lockman.lock("my_lock", blocking=True, blocking_timeout=5) as lock:
    if lock.is_locked:
        ...
```

> ZooKeeper 后端不支持 `timeout` 参数（锁不会自动超时释放）。

### Django 集成

```python
# settings.py
GLOBAL_LOCK_CONFIG = {
    "global_lock_engine_options": {
        "redis-cache-name": "default",
    },
}
```

```python
from globallock.django import get_default_global_lock_manager

lockman = get_default_global_lock_manager()
with lockman.lock("my_lock", timeout=60) as lock:
    if lock.is_locked:
        ...
```

---

## 配置项说明

### `GlobalLockManager(config)`

`config` 是一个字典，支持以下键：

| 键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `global_lock_engine_class` | `str` | `globallock.redis_global_lock.RedisGlobalLock` | 锁定引擎类的完整路径，可使用 `REDIS_GLOBAL_LOCK_CLASS`、`DJANGO_REDIS_GLOBAL_LOCK_CLASS`、`ETCD_GLOBAL_LOCK_CLASS`、`ZOOKEEPER_GLOBAL_LOCK_CLASS` 常量 |
| `global_lock_engine_options` | `dict` | `{}` | 传递给后端驱动的连接参数 |
| `global_lock_key_prefix` | `str` | `_glocks:` | 锁 key 的前缀，不同后端会自动转换为合适的格式（见"key 前缀策略"） |
| `timeout` | `float` / `None` | `None` | 锁超时时间（秒），超时后自动释放 |
| `sleep` | `float` | `0.1` | 轮询等待锁的间隔（秒） |
| `blocking` | `bool` | `True` | 是否阻塞等待锁 |
| `blocking_timeout` | `float` / `None` | `None` | 阻塞等待超时（秒），超时未获得锁则返回 `False` |

> `timeout`、`sleep`、`blocking`、`blocking_timeout` 也可以在 `lockman.lock()` 调用时传入，会覆盖 config 中的默认值。

### `lockman.lock(name, ...)`

| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `name` | `str` | 必填 | 锁的名称，同一时间同一名称的锁只能被一个进程持有 |
| `timeout` | `float` / `None` | config 中的值 | 锁超时（秒），ZooKeeper 后端不支持 |
| `sleep` | `float` | config 中的值 / `0.1` | 轮询间隔（秒） |
| `blocking` | `bool` | config 中的值 / `True` | 是否阻塞 |
| `blocking_timeout` | `float` / `None` | config 中的值 | 阻塞超时（秒） |

### 锁实例方法 / 属性

| 方法/属性 | 类型 | 说明 |
|---|---|---|
| `lock.acquire()` | `bool` | 主动请求锁，成功返回 `True` |
| `lock.release()` | `None` | 释放锁 |
| `lock.is_locked` | `bool` | 当前是否持有锁 |
| `lock.lock_key` | `str` | 锁在存储引擎中的实际 key |

---

## 后端引擎说明

### 依赖安装

各后端的 Python 驱动需要额外安装：

| 后端 | 安装命令 |
|---|---|
| RedisGlobalLock | `pip install redis` |
| DjangoRedisGlobalLock | `pip install django-redis` |
| EtcdGlobalLock | `pip install etcd3` |
| ZookeeperGlobalLock | `pip install kazoo` |

> 这些驱动未声明在 `globallock` 的运行时依赖中，需使用者自行安装。

### tenacity 兼容性

`etcd3 0.12.0` 需要 `tenacity<8`。如使用 etcd 后端，需安装兼容版本：

```bash
pip install 'tenacity<8' etcd3
```

### Key 前缀策略

统一配置 `global_lock_key_prefix`（默认 `_glocks:`），各后端自动转换：

| 后端 | 实际 key 示例 | 说明 |
|---|---|---|
| Redis | `_glocks:my_lock` | 直接使用原始前缀，`:` 是 Redis 的 namespace 分隔符 |
| etcd | `_glocks/my_lock` | `:` 替换为 `/`，去掉前导 `/`（etcd3 Lock 内部添加 `/locks/` 前缀） |
| ZooKeeper | `/_glocks/my_lock` | `:` 替换为 `/`，确保以 `/` 开头（ZooKeeper 路径要求） |

如果锁名称中包含 `:`，该字符会保留不变（例如 `lock_name = "event:test"`，Redis 中为 `_glocks:event:test`）。

### ZooKeeper 后端注意事项

- `timeout` 参数无效（锁不会自动释放）。
- 进程被 `kill -9` 杀死后，其他进程约 10 秒后可重新获得锁。
- 进程正常退出时，ZooKeeper 客户端会自动关闭（通过 `atexit` 注册清理）。

---

## 设计原理

### 架构

```
GlobalLockManager (工厂)
    │
    ├─ RedisGlobalLock ────── redis.Redis + ConnectionPool (缓存)
    ├─ DjangoRedisGlobalLock ─ django_redis.get_redis_connection
    ├─ EtcdGlobalLock ─────── etcd3.client (每个实例独立)
    └─ ZookeeperGlobalLock ── KazooClient (缓存) + atexit 清理
```

### 连接池缓存

- Redis 的 `ConnectionPool` 通过 `@cacheutils.simple_cache` 缓存，同一进程中所有 `RedisGlobalLock` 实例共享一个连接池。
- ZooKeeper 的 `KazooClient` 同样缓存，所有 `ZookeeperGlobalLock` 实例共享一个客户端连接。

---

## 开发指南

### 启动测试服务

```bash
# 启动所有缺失的服务
bash scripts/start-test-services.sh

# 查看状态
bash scripts/start-test-services.sh status

# 停止所有服务
bash scripts/start-test-services.sh stop

# 使用 docker 而非 podman
CONTAINER_ENGINE=docker bash scripts/start-test-services.sh start
```

### 运行测试

```bash
pip3 install -r pytest.requirements.txt

# 单元测试（无需外部服务）
python3 -m pytest tests/test_base.py tests/test_config.py tests/test_implementations.py tests/test_security.py

# 集成测试（需要对应服务）
python3 -m pytest tests/test_integration_redis.py
python3 -m pytest tests/test_integration_etcd.py
python3 -m pytest tests/test_integration_zookeeper.py

# 全部测试
python3 -m pytest tests/
```

### 构建发布

```bash
pip3 install build twine
python3 -m build
python3 -m twine upload dist/*
```

---

## 测试通过版本

- Python 3.6 ～ 3.12

---

## 发布历史

### v0.1.5

- **Breaking**: 改用 `pyproject.toml` 打包，移除 `setup.py`。
- **Bug fix**: `acquire()` 现在会正确设置 `is_locked` 属性。
- **Bug fix**: ZooKeeper `_do_acquire` 捕获 `LockTimeout` 返回 `False`，而非抛异常。
- **Bug fix**: 修复 Django demo settings.py 中示例 app 被错误放在 `MIDDLEWARE` 的问题。
- **Bug fix**: 修复 ZK 测试类名、docstring 复制粘贴错误。
- **Feature**: 添加 KazooClient 进程退出自动关闭机制（`atexit`）。
- **Feature**: 添加 `__all__`、类型注解。
- **Feature**: 添加 key 前缀在各后端的一致性策略 (`normalize_key_prefix_for_path`)。
- **Feature**: 添加 `scripts/start-test-services.sh` 一键启动测试服务。
- **Test**: 新增 74 个单元测试（mock 方式，无需外部服务）。
- **Test**: 新增 29 个集成测试（Redis/etcd/ZK 真实后端）。
- **Test**: pytest markers 自动按服务可用性跳过 (`redis_required`, `etcd_required`, `zookeeper_required`)。
- **Security**: 移除本地配置文件中硬编码的凭据。
- **Chore**: 清理开发临时文件（`t1.py`, `t1zk.py`, `t2etcd.py`, `t3redis.py`）。

### v0.1.4

- 文档更新。

### v0.1.3

- 修正 `globallock.django` 默认设置。

### v0.1.2

- `GlobalLockManager.lock()` 方法参数可以在初始化时设置默认值。
- 添加 `globallock.django.get_default_global_lock_manager()` 方法，允许在 Django 中便捷使用。

### v0.1.1

- 文档更新。

### v0.1.0

- 首次发布。
- 支持 RedisGlobalLock、DjangoRedisGlobalLock、EtcdGlobalLock、ZookeeperGlobalLock。
