zoocache 2026.1.20__tar.gz → 2026.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zoocache-2026.1.20 → zoocache-2026.2.5}/.gitignore +4 -0
- zoocache-2026.2.5/.readthedocs.yaml +14 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/Cargo.lock +1 -1
- {zoocache-2026.1.20 → zoocache-2026.2.5}/Cargo.toml +8 -1
- {zoocache-2026.1.20 → zoocache-2026.2.5}/PKG-INFO +13 -6
- {zoocache-2026.1.20 → zoocache-2026.2.5}/README.md +12 -5
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/architecture.md +2 -2
- zoocache-2026.2.5/docs/index.md +9 -0
- zoocache-2026.2.5/lint.sh +8 -0
- zoocache-2026.2.5/mkdocs.yml +64 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/pyproject.toml +7 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache/core.py +1 -8
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/bus/redis_pubsub.rs +7 -5
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/flight.rs +20 -2
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/lib.rs +67 -37
- zoocache-2026.2.5/src/zoocache_core/storage/lmdb.rs +238 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/storage/memory.rs +13 -21
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/storage/mod.rs +7 -14
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/storage/redis.rs +10 -19
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/trie.rs +60 -60
- zoocache-2026.2.5/src/zoocache_core/utils.rs +15 -0
- zoocache-2026.2.5/uv.lock +998 -0
- zoocache-2026.1.20/lint.sh +0 -3
- zoocache-2026.1.20/src/zoocache_core/storage/lmdb.rs +0 -182
- zoocache-2026.1.20/uv.lock +0 -356
- {zoocache-2026.1.20 → zoocache-2026.2.5}/.cargo/config.toml +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/.github/workflows/ci.yml +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/.github/workflows/release.yml +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/.python-version +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_benchmarks.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_heavy_load.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_lazy_update_bench.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_lmdb_bench.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_tti_bench.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0001-prefix-trie-invalidation.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0002-rust-core-python-wrapper.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0003-hlc-distributed-consistency.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0004-serialization-strategy.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0005-singleflight-pattern.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0006-trie-performance-optimizations.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0007-zero-bridge-serialization.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0008-redis-bus-connection-pooling.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/architecture.svg +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/entities.svg +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/favicon.svg +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/hlc_consistency.svg +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/invalidation.svg +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/logo-dark.svg +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/logo-light.svg +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/serialization.svg +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/concurrency.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/consistency.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/invalidation.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/reliability.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/serialization.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/user_guide.md +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/examples/usage_demo.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache/__init__.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache/context.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/bus/local.rs +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/bus/mod.rs +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/conftest.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_async.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_complex_sqlite.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_concurrency.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_fixed_ttl.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_invalidation.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_lru_eviction.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_multiprocess_lmdb.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_passive_resync.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_storage.py +0 -0
- {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_ttl.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "zoocache"
|
|
3
|
-
version = "2026.
|
|
3
|
+
version = "2026.2.5"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
|
|
@@ -23,3 +23,10 @@ lz4_flex = "0.11"
|
|
|
23
23
|
pythonize = "0.27"
|
|
24
24
|
serde_bytes = "0.11"
|
|
25
25
|
serde-transcode = "1.1"
|
|
26
|
+
|
|
27
|
+
[profile.release]
|
|
28
|
+
codegen-units = 1
|
|
29
|
+
lto = "fat"
|
|
30
|
+
opt-level = 3
|
|
31
|
+
strip = true
|
|
32
|
+
panic = "abort"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zoocache
|
|
3
|
-
Version: 2026.
|
|
3
|
+
Version: 2026.2.5
|
|
4
4
|
Summary: Cache that invalidates when your data changes, not when a timer expires. Rust-powered semantic invalidation for Python.
|
|
5
5
|
Author-email: Alberto Daniel Badia <alberto_badia@enlacepatagonia.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,11 +18,18 @@ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
Zoocache is a high-performance caching library with a Rust core, designed for applications where data consistency and read performance are critical.
|
|
20
20
|
</p>
|
|
21
|
+
<div align="center" markdown="1">
|
|
22
|
+
|
|
23
|
+
[**📖 Read the User Guide**](docs/user_guide.md)
|
|
24
|
+
|
|
25
|
+
</div>
|
|
21
26
|
<p align="center">
|
|
22
|
-
<a href="https://www.python.org/downloads/"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10+-blue.svg"></a>
|
|
23
|
-
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg"></a>
|
|
24
|
-
<img alt="PyPI" src="https://img.shields.io/pypi/v/zoocache">
|
|
25
|
-
<img alt="Downloads" src="https://img.shields.io/
|
|
27
|
+
<a href="https://www.python.org/downloads/"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10+-blue.svg?style=flat-square&logo=python"></a>
|
|
28
|
+
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg?style=flat-square"></a>
|
|
29
|
+
<a href="https://pypi.org/project/zoocache/"><img alt="PyPI" src="https://img.shields.io/pypi/v/zoocache?style=flat-square&logo=pypi&logoColor=white"></a>
|
|
30
|
+
<a href="https://pypi.org/project/zoocache/"><img alt="Downloads" src="https://img.shields.io/pepy/dt/zoocache?style=flat-square&color=blue"></a>
|
|
31
|
+
<a href="https://github.com/albertobadia/zoocache/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/albertobadia/zoocache/ci.yml?branch=main&style=flat-square&logo=github"></a>
|
|
32
|
+
<a href="https://zoocache.readthedocs.io/"><img alt="ReadTheDocs" src="https://img.shields.io/readthedocs/zoocache?style=flat-square&logo=readthedocs"></a>
|
|
26
33
|
</p>
|
|
27
34
|
|
|
28
35
|
---
|
|
@@ -131,5 +138,5 @@ Explore the deep dives into Zoocache's architecture and features:
|
|
|
131
138
|
|
|
132
139
|
## 📄 License
|
|
133
140
|
|
|
134
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
141
|
+
This project is licensed under the MIT License - see the [LICENSE](https://github.com/albertobadia/zoocache/blob/main/LICENSE) file for details.
|
|
135
142
|
|
|
@@ -9,11 +9,18 @@
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
Zoocache is a high-performance caching library with a Rust core, designed for applications where data consistency and read performance are critical.
|
|
11
11
|
</p>
|
|
12
|
+
<div align="center" markdown="1">
|
|
13
|
+
|
|
14
|
+
[**📖 Read the User Guide**](docs/user_guide.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
12
17
|
<p align="center">
|
|
13
|
-
<a href="https://www.python.org/downloads/"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10+-blue.svg"></a>
|
|
14
|
-
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg"></a>
|
|
15
|
-
<img alt="PyPI" src="https://img.shields.io/pypi/v/zoocache">
|
|
16
|
-
<img alt="Downloads" src="https://img.shields.io/
|
|
18
|
+
<a href="https://www.python.org/downloads/"><img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10+-blue.svg?style=flat-square&logo=python"></a>
|
|
19
|
+
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg?style=flat-square"></a>
|
|
20
|
+
<a href="https://pypi.org/project/zoocache/"><img alt="PyPI" src="https://img.shields.io/pypi/v/zoocache?style=flat-square&logo=pypi&logoColor=white"></a>
|
|
21
|
+
<a href="https://pypi.org/project/zoocache/"><img alt="Downloads" src="https://img.shields.io/pepy/dt/zoocache?style=flat-square&color=blue"></a>
|
|
22
|
+
<a href="https://github.com/albertobadia/zoocache/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/albertobadia/zoocache/ci.yml?branch=main&style=flat-square&logo=github"></a>
|
|
23
|
+
<a href="https://zoocache.readthedocs.io/"><img alt="ReadTheDocs" src="https://img.shields.io/readthedocs/zoocache?style=flat-square&logo=readthedocs"></a>
|
|
17
24
|
</p>
|
|
18
25
|
|
|
19
26
|
---
|
|
@@ -122,4 +129,4 @@ Explore the deep dives into Zoocache's architecture and features:
|
|
|
122
129
|
|
|
123
130
|
## 📄 License
|
|
124
131
|
|
|
125
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
132
|
+
This project is licensed under the MIT License - see the [LICENSE](https://github.com/albertobadia/zoocache/blob/main/LICENSE) file for details.
|
|
@@ -6,13 +6,13 @@ Zoocache is designed as a high-performance caching layer that bridge the gap bet
|
|
|
6
6
|
|
|
7
7
|
The system is split into two main layers:
|
|
8
8
|
|
|
9
|
-
### 1. The
|
|
9
|
+
### 1. The Control Plane (Rust Core)
|
|
10
10
|
The Rust engine manages the complex logic of the cache:
|
|
11
11
|
- **PrefixTrie**: A thread-safe, hierarchical structure that tracks versioning for dependency tags. Includes a **Global Version Counter** for $O(1)$ validation short-circuiting.
|
|
12
12
|
- **Flight Manager**: Handles synchronization to prevent "thundering herd" scenarios for both Sync and Async functions.
|
|
13
13
|
- **[Hybrid Logical Clocks (HLC)](consistency.md#hybrid-logical-clocks-hlc)**: Ensures causal consistency across distributed nodes by ratcheting timestamps based on wall clocks and logical counters.
|
|
14
14
|
|
|
15
|
-
### 2. The
|
|
15
|
+
### 2. The Data Plane (Python Wrapper)
|
|
16
16
|
The Python layer provides the user-facing API:
|
|
17
17
|
- **Decorators**: `@cacheable` intercepts function calls.
|
|
18
18
|
- **Context Tracking**: `DepsTracker` uses `contextvars` to register dynamic dependencies during function execution.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="assets/logo-light.svg">
|
|
5
|
+
<img alt="ZooCache Logo" src="assets/logo-light.svg" width="600">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
{% include-markdown "../README.md" start="</p>" %}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
site_name: ZooCache
|
|
2
|
+
site_description: ZooCache is a high-performance caching library with a Rust core, designed for consistency and read performance.
|
|
3
|
+
site_url: https://zoocache.readthedocs.io/
|
|
4
|
+
|
|
5
|
+
theme:
|
|
6
|
+
name: material
|
|
7
|
+
logo: assets/logo-light.svg
|
|
8
|
+
favicon: assets/favicon.svg
|
|
9
|
+
features:
|
|
10
|
+
- navigation.indexes
|
|
11
|
+
- navigation.expand
|
|
12
|
+
- content.code.copy
|
|
13
|
+
palette:
|
|
14
|
+
# Light mode
|
|
15
|
+
- scheme: default
|
|
16
|
+
primary: orange
|
|
17
|
+
accent: amber
|
|
18
|
+
toggle:
|
|
19
|
+
icon: material/weather-night
|
|
20
|
+
name: Switch to dark mode
|
|
21
|
+
# Dark mode
|
|
22
|
+
- scheme: slate
|
|
23
|
+
primary: orange
|
|
24
|
+
accent: amber
|
|
25
|
+
toggle:
|
|
26
|
+
icon: material/weather-sunny
|
|
27
|
+
name: Switch to light mode
|
|
28
|
+
|
|
29
|
+
plugins:
|
|
30
|
+
- search
|
|
31
|
+
- mkdocstrings
|
|
32
|
+
- include-markdown
|
|
33
|
+
|
|
34
|
+
markdown_extensions:
|
|
35
|
+
- pymdownx.highlight:
|
|
36
|
+
anchor_linenums: true
|
|
37
|
+
line_spans: __span
|
|
38
|
+
pygments_lang_class: true
|
|
39
|
+
- pymdownx.inlinehilite
|
|
40
|
+
- pymdownx.snippets:
|
|
41
|
+
base_path: ["."]
|
|
42
|
+
- pymdownx.superfences
|
|
43
|
+
- admonition
|
|
44
|
+
- pymdownx.details
|
|
45
|
+
- md_in_html
|
|
46
|
+
|
|
47
|
+
nav:
|
|
48
|
+
- Home: index.md
|
|
49
|
+
- User Guide: user_guide.md
|
|
50
|
+
- Architecture: architecture.md
|
|
51
|
+
- Serialization: serialization.md
|
|
52
|
+
- Reliability: reliability.md
|
|
53
|
+
- Invalidation: invalidation.md
|
|
54
|
+
- Consistency: consistency.md
|
|
55
|
+
- Concurrency: concurrency.md
|
|
56
|
+
- Architectural Decisions:
|
|
57
|
+
- "ADR 0001: Prefix-Trie Invalidation": adr/0001-prefix-trie-invalidation.md
|
|
58
|
+
- "ADR 0002: Rust Core Python Wrapper": adr/0002-rust-core-python-wrapper.md
|
|
59
|
+
- "ADR 0003: HLC Distributed Consistency": adr/0003-hlc-distributed-consistency.md
|
|
60
|
+
- "ADR 0004: Serialization Strategy": adr/0004-serialization-strategy.md
|
|
61
|
+
- "ADR 0005: Singleflight Pattern": adr/0005-singleflight-pattern.md
|
|
62
|
+
- "ADR 0006: Trie Performance Optimizations": adr/0006-trie-performance-optimizations.md
|
|
63
|
+
- "ADR 0007: Zero-Bridge Serialization": adr/0007-zero-bridge-serialization.md
|
|
64
|
+
- "ADR 0008: Redis Bus Connection Pooling": adr/0008-redis-bus-connection-pooling.md
|
|
@@ -12,7 +12,6 @@ _op_counter: int = 0
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def _reset() -> None:
|
|
15
|
-
"""Internal use only: reset the global state for testing."""
|
|
16
15
|
global _core, _config, _op_counter
|
|
17
16
|
_core = None
|
|
18
17
|
_config = {}
|
|
@@ -30,9 +29,7 @@ def configure(
|
|
|
30
29
|
) -> None:
|
|
31
30
|
global _core, _config
|
|
32
31
|
if _core is not None:
|
|
33
|
-
raise RuntimeError(
|
|
34
|
-
"zoocache already initialized, call configure() before any cache operation"
|
|
35
|
-
)
|
|
32
|
+
raise RuntimeError("zoocache already initialized")
|
|
36
33
|
_config = {
|
|
37
34
|
"storage_url": storage_url,
|
|
38
35
|
"bus_url": bus_url,
|
|
@@ -47,7 +44,6 @@ def configure(
|
|
|
47
44
|
def _get_core() -> Core:
|
|
48
45
|
global _core
|
|
49
46
|
if _core is None:
|
|
50
|
-
# Filter config for Rust Core.__init__
|
|
51
47
|
core_args = {k: v for k, v in _config.items() if k != "prune_after"}
|
|
52
48
|
_core = Core(**core_args)
|
|
53
49
|
return _core
|
|
@@ -63,7 +59,6 @@ def _maybe_prune() -> None:
|
|
|
63
59
|
|
|
64
60
|
|
|
65
61
|
def prune(max_age_secs: int = 3600) -> None:
|
|
66
|
-
"""Manually trigger pruning of the PrefixTrie."""
|
|
67
62
|
_get_core().prune(max_age_secs)
|
|
68
63
|
|
|
69
64
|
|
|
@@ -120,7 +115,6 @@ def cacheable(
|
|
|
120
115
|
if fut is not None:
|
|
121
116
|
return await fut
|
|
122
117
|
|
|
123
|
-
# Fallback if flight was already finished before we could wait
|
|
124
118
|
return await execute(key, args, kwargs)
|
|
125
119
|
|
|
126
120
|
async def execute(key, args, kwargs):
|
|
@@ -154,5 +148,4 @@ def cacheable(
|
|
|
154
148
|
|
|
155
149
|
|
|
156
150
|
def version() -> str:
|
|
157
|
-
"""Return the version of the Rust core."""
|
|
158
151
|
return _get_core().version()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
use r2d2::Pool;
|
|
1
2
|
use redis::{Client, Commands};
|
|
2
3
|
use std::sync::Arc;
|
|
3
4
|
use std::thread;
|
|
4
|
-
use r2d2::Pool;
|
|
5
5
|
|
|
6
6
|
use super::InvalidateBus;
|
|
7
7
|
|
|
@@ -34,11 +34,14 @@ impl RedisPubSubBus {
|
|
|
34
34
|
let mut backoff_ms = 100;
|
|
35
35
|
loop {
|
|
36
36
|
let conn_res = pool.get();
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
let mut conn = match conn_res {
|
|
39
39
|
Ok(c) => c,
|
|
40
40
|
Err(e) => {
|
|
41
|
-
eprintln!(
|
|
41
|
+
eprintln!(
|
|
42
|
+
"[zoocache] Bus listener connection failed: {}. Retrying in {}ms...",
|
|
43
|
+
e, backoff_ms
|
|
44
|
+
);
|
|
42
45
|
thread::sleep(std::time::Duration::from_millis(backoff_ms));
|
|
43
46
|
backoff_ms = (backoff_ms * 2).min(5000);
|
|
44
47
|
continue;
|
|
@@ -64,8 +67,7 @@ impl RedisPubSubBus {
|
|
|
64
67
|
callback(tag, ver);
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
|
-
|
|
68
|
-
eprintln!("[zoocache] Bus connection lost. Reconnecting...");
|
|
70
|
+
|
|
69
71
|
thread::sleep(std::time::Duration::from_millis(100));
|
|
70
72
|
}
|
|
71
73
|
});
|
|
@@ -46,7 +46,11 @@ pub(crate) fn complete_flight(
|
|
|
46
46
|
) -> Option<Py<PyAny>> {
|
|
47
47
|
if let Some((_, flight)) = flights.remove(key) {
|
|
48
48
|
let mut state = flight.state.lock().unwrap();
|
|
49
|
-
state.0 = if is_error {
|
|
49
|
+
state.0 = if is_error {
|
|
50
|
+
FlightStatus::Error
|
|
51
|
+
} else {
|
|
52
|
+
FlightStatus::Done
|
|
53
|
+
};
|
|
50
54
|
state.1 = value;
|
|
51
55
|
flight.condvar.notify_all();
|
|
52
56
|
return flight.py_future.lock().unwrap().take();
|
|
@@ -56,8 +60,22 @@ pub(crate) fn complete_flight(
|
|
|
56
60
|
|
|
57
61
|
pub(crate) fn wait_for_flight(flight: &Flight) -> FlightStatus {
|
|
58
62
|
let mut state = flight.state.lock().unwrap();
|
|
63
|
+
let timeout = std::time::Duration::from_secs(60);
|
|
64
|
+
let start = std::time::Instant::now();
|
|
65
|
+
|
|
59
66
|
while state.0 == FlightStatus::Pending {
|
|
60
|
-
|
|
67
|
+
let elapsed = start.elapsed();
|
|
68
|
+
if elapsed >= timeout {
|
|
69
|
+
return FlightStatus::Error;
|
|
70
|
+
}
|
|
71
|
+
let (new_state, result) = flight
|
|
72
|
+
.condvar
|
|
73
|
+
.wait_timeout(state, timeout - elapsed)
|
|
74
|
+
.unwrap();
|
|
75
|
+
state = new_state;
|
|
76
|
+
if result.timed_out() {
|
|
77
|
+
return FlightStatus::Error;
|
|
78
|
+
}
|
|
61
79
|
}
|
|
62
80
|
state.0
|
|
63
81
|
}
|
|
@@ -2,6 +2,7 @@ mod bus;
|
|
|
2
2
|
mod flight;
|
|
3
3
|
mod storage;
|
|
4
4
|
mod trie;
|
|
5
|
+
mod utils;
|
|
5
6
|
|
|
6
7
|
use dashmap::DashMap;
|
|
7
8
|
use pyo3::prelude::*;
|
|
@@ -11,12 +12,12 @@ use std::collections::HashMap;
|
|
|
11
12
|
use std::sync::Arc;
|
|
12
13
|
|
|
13
14
|
use bus::{InvalidateBus, LocalBus, RedisPubSubBus};
|
|
14
|
-
use flight::{
|
|
15
|
-
use storage::{CacheEntry, InMemoryStorage, LmdbStorage, RedisStorage, Storage};
|
|
16
|
-
use trie::{build_dependency_snapshots, validate_dependencies, PrefixTrie};
|
|
15
|
+
use flight::{Flight, FlightStatus, complete_flight, try_enter_flight, wait_for_flight};
|
|
17
16
|
use std::sync::mpsc::{self, Sender};
|
|
18
17
|
use std::thread;
|
|
19
18
|
use std::time::{Duration, Instant};
|
|
19
|
+
use storage::{CacheEntry, InMemoryStorage, LmdbStorage, RedisStorage, Storage};
|
|
20
|
+
use trie::{PrefixTrie, build_dependency_snapshots, validate_dependencies};
|
|
20
21
|
|
|
21
22
|
#[pyclass]
|
|
22
23
|
struct Core {
|
|
@@ -35,23 +36,31 @@ struct Core {
|
|
|
35
36
|
impl Core {
|
|
36
37
|
#[new]
|
|
37
38
|
#[pyo3(signature = (storage_url=None, bus_url=None, prefix=None, default_ttl=None, read_extend_ttl=true, max_entries=None))]
|
|
38
|
-
fn new(
|
|
39
|
+
fn new(
|
|
40
|
+
storage_url: Option<&str>,
|
|
41
|
+
bus_url: Option<&str>,
|
|
42
|
+
prefix: Option<&str>,
|
|
43
|
+
default_ttl: Option<u64>,
|
|
44
|
+
read_extend_ttl: bool,
|
|
45
|
+
max_entries: Option<usize>,
|
|
46
|
+
) -> PyResult<Self> {
|
|
39
47
|
let storage: Arc<dyn Storage> = match storage_url {
|
|
40
|
-
Some(url) if url.starts_with("redis://") =>
|
|
41
|
-
RedisStorage::new(url, prefix)
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
Some(url) if url.starts_with("redis://") => {
|
|
49
|
+
Arc::new(RedisStorage::new(url, prefix).map_err(|e| {
|
|
50
|
+
PyErr::new::<pyo3::exceptions::PyConnectionError, _>(e.to_string())
|
|
51
|
+
})?)
|
|
52
|
+
}
|
|
44
53
|
Some(url) if url.starts_with("lmdb://") => {
|
|
45
54
|
let path = &url[7..];
|
|
46
|
-
Arc::new(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
55
|
+
Arc::new(LmdbStorage::new(path).map_err(|e| {
|
|
56
|
+
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string())
|
|
57
|
+
})?)
|
|
50
58
|
}
|
|
51
59
|
Some(url) => {
|
|
52
|
-
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
|
|
61
|
+
"Unsupported storage scheme: {}",
|
|
62
|
+
url
|
|
63
|
+
)));
|
|
55
64
|
}
|
|
56
65
|
None => Arc::new(InMemoryStorage::new()),
|
|
57
66
|
};
|
|
@@ -61,10 +70,10 @@ impl Core {
|
|
|
61
70
|
let bus: Arc<dyn InvalidateBus> = match bus_url {
|
|
62
71
|
Some(url) => {
|
|
63
72
|
let channel = prefix.map(|p| format!("{}:invalidate", p));
|
|
64
|
-
let r_bus =
|
|
65
|
-
RedisPubSubBus::new(url, channel.as_deref())
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
let r_bus =
|
|
74
|
+
Arc::new(RedisPubSubBus::new(url, channel.as_deref()).map_err(|e| {
|
|
75
|
+
PyErr::new::<pyo3::exceptions::PyConnectionError, _>(e.to_string())
|
|
76
|
+
})?);
|
|
68
77
|
|
|
69
78
|
let t_clone = trie.clone();
|
|
70
79
|
r_bus.start_listener(move |tag, ver| {
|
|
@@ -79,21 +88,25 @@ impl Core {
|
|
|
79
88
|
if default_ttl.is_some() && read_extend_ttl {
|
|
80
89
|
let (tx, rx) = mpsc::channel::<(String, u64)>();
|
|
81
90
|
let storage_worker = Arc::clone(&storage);
|
|
82
|
-
|
|
91
|
+
|
|
83
92
|
thread::spawn(move || {
|
|
84
93
|
let mut last_touches: HashMap<String, Instant> = HashMap::new();
|
|
85
94
|
while let Ok((key, ttl)) = rx.recv() {
|
|
86
95
|
let now = Instant::now();
|
|
87
|
-
if last_touches
|
|
96
|
+
if last_touches
|
|
97
|
+
.get(&key)
|
|
98
|
+
.is_some_and(|&last| now.duration_since(last) < Duration::from_secs(60))
|
|
99
|
+
{
|
|
88
100
|
continue;
|
|
89
101
|
}
|
|
90
|
-
|
|
102
|
+
|
|
91
103
|
storage_worker.touch(&key, ttl);
|
|
92
104
|
last_touches.insert(key, now);
|
|
93
|
-
|
|
94
|
-
// Periodic cleanup of last_touches to avoid memory leak
|
|
105
|
+
|
|
95
106
|
if last_touches.len() > 10000 {
|
|
96
|
-
|
|
107
|
+
last_touches.retain(|_, &mut instant| {
|
|
108
|
+
now.duration_since(instant) < Duration::from_secs(300)
|
|
109
|
+
});
|
|
97
110
|
}
|
|
98
111
|
}
|
|
99
112
|
});
|
|
@@ -138,7 +151,11 @@ impl Core {
|
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
#[allow(clippy::type_complexity)]
|
|
141
|
-
fn get_or_entry_async(
|
|
154
|
+
fn get_or_entry_async(
|
|
155
|
+
&self,
|
|
156
|
+
py: Python,
|
|
157
|
+
key: &str,
|
|
158
|
+
) -> PyResult<(Option<Py<PyAny>>, bool, Option<Py<PyAny>>)> {
|
|
142
159
|
if let Some(res) = self.get(py, key)? {
|
|
143
160
|
return Ok((Some(res), false, None));
|
|
144
161
|
}
|
|
@@ -149,8 +166,12 @@ impl Core {
|
|
|
149
166
|
return Ok((None, true, None));
|
|
150
167
|
}
|
|
151
168
|
|
|
152
|
-
|
|
153
|
-
|
|
169
|
+
let fut = flight
|
|
170
|
+
.py_future
|
|
171
|
+
.lock()
|
|
172
|
+
.unwrap()
|
|
173
|
+
.as_ref()
|
|
174
|
+
.map(|f| f.clone_ref(py));
|
|
154
175
|
Ok((None, false, fut))
|
|
155
176
|
}
|
|
156
177
|
|
|
@@ -162,7 +183,13 @@ impl Core {
|
|
|
162
183
|
}
|
|
163
184
|
|
|
164
185
|
#[pyo3(signature = (key, is_error, value=None))]
|
|
165
|
-
fn finish_flight(
|
|
186
|
+
fn finish_flight(
|
|
187
|
+
&self,
|
|
188
|
+
py: Python,
|
|
189
|
+
key: &str,
|
|
190
|
+
is_error: bool,
|
|
191
|
+
value: Option<Py<PyAny>>,
|
|
192
|
+
) -> Option<Py<PyAny>> {
|
|
166
193
|
py.detach(|| complete_flight(&self.flights, key, is_error, value))
|
|
167
194
|
}
|
|
168
195
|
|
|
@@ -177,7 +204,6 @@ impl Core {
|
|
|
177
204
|
|
|
178
205
|
let global_version = self.trie.get_global_version();
|
|
179
206
|
|
|
180
|
-
// Short-circuit: O(1) validation if no invalidations occurred globally
|
|
181
207
|
if entry.trie_version == global_version {
|
|
182
208
|
return Ok(Some(entry.value.clone_ref(py)));
|
|
183
209
|
}
|
|
@@ -189,8 +215,6 @@ impl Core {
|
|
|
189
215
|
return Ok(None);
|
|
190
216
|
}
|
|
191
217
|
|
|
192
|
-
// Lazy Update: Re-stamp the entry with the current global version
|
|
193
|
-
// so that the next hit can use the O(1) short-circuit.
|
|
194
218
|
let current_global_version = self.trie.get_global_version();
|
|
195
219
|
if entry.trie_version < current_global_version {
|
|
196
220
|
let storage = Arc::clone(&self.storage);
|
|
@@ -203,7 +227,6 @@ impl Core {
|
|
|
203
227
|
py.detach(move || storage.set(key_str, updated_entry, None));
|
|
204
228
|
}
|
|
205
229
|
|
|
206
|
-
// TTI: Deferred refresh
|
|
207
230
|
if let (Some(tx), Some(ttl)) = (&self.tti_tx, self.default_ttl) {
|
|
208
231
|
let _ = tx.send((key.to_string(), ttl));
|
|
209
232
|
}
|
|
@@ -212,7 +235,14 @@ impl Core {
|
|
|
212
235
|
}
|
|
213
236
|
|
|
214
237
|
#[pyo3(signature = (key, value, dependencies, ttl=None))]
|
|
215
|
-
fn set(
|
|
238
|
+
fn set(
|
|
239
|
+
&self,
|
|
240
|
+
py: Python,
|
|
241
|
+
key: String,
|
|
242
|
+
value: Py<PyAny>,
|
|
243
|
+
dependencies: Vec<String>,
|
|
244
|
+
ttl: Option<u64>,
|
|
245
|
+
) {
|
|
216
246
|
let trie_version = self.trie.get_global_version();
|
|
217
247
|
let snapshots = py.detach(|| build_dependency_snapshots(&self.trie, dependencies));
|
|
218
248
|
let entry = Arc::new(CacheEntry {
|
|
@@ -222,10 +252,10 @@ impl Core {
|
|
|
222
252
|
});
|
|
223
253
|
let storage = Arc::clone(&self.storage);
|
|
224
254
|
let final_ttl = ttl.or(self.default_ttl);
|
|
225
|
-
|
|
255
|
+
|
|
226
256
|
py.detach(|| {
|
|
227
257
|
storage.set(key, entry, final_ttl);
|
|
228
|
-
|
|
258
|
+
|
|
229
259
|
if let Some(max) = self.max_entries {
|
|
230
260
|
let current = storage.len();
|
|
231
261
|
if current > max {
|
|
@@ -271,7 +301,7 @@ fn hash_key(_py: Python<'_>, obj: Bound<'_, PyAny>, prefix: Option<&str>) -> PyR
|
|
|
271
301
|
let mut data = Vec::new();
|
|
272
302
|
let mut serializer = rmp_serde::Serializer::new(&mut data);
|
|
273
303
|
let mut depythonizer = pythonize::Depythonizer::from_object(&obj);
|
|
274
|
-
|
|
304
|
+
|
|
275
305
|
serde_transcode::transcode(&mut depythonizer, &mut serializer)
|
|
276
306
|
.map_err(|e| PyErr::new::<pyo3::exceptions::PyTypeError, _>(e.to_string()))?;
|
|
277
307
|
|