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.
Files changed (72) hide show
  1. {zoocache-2026.1.20 → zoocache-2026.2.5}/.gitignore +4 -0
  2. zoocache-2026.2.5/.readthedocs.yaml +14 -0
  3. {zoocache-2026.1.20 → zoocache-2026.2.5}/Cargo.lock +1 -1
  4. {zoocache-2026.1.20 → zoocache-2026.2.5}/Cargo.toml +8 -1
  5. {zoocache-2026.1.20 → zoocache-2026.2.5}/PKG-INFO +13 -6
  6. {zoocache-2026.1.20 → zoocache-2026.2.5}/README.md +12 -5
  7. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/architecture.md +2 -2
  8. zoocache-2026.2.5/docs/index.md +9 -0
  9. zoocache-2026.2.5/lint.sh +8 -0
  10. zoocache-2026.2.5/mkdocs.yml +64 -0
  11. {zoocache-2026.1.20 → zoocache-2026.2.5}/pyproject.toml +7 -0
  12. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache/core.py +1 -8
  13. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/bus/redis_pubsub.rs +7 -5
  14. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/flight.rs +20 -2
  15. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/lib.rs +67 -37
  16. zoocache-2026.2.5/src/zoocache_core/storage/lmdb.rs +238 -0
  17. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/storage/memory.rs +13 -21
  18. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/storage/mod.rs +7 -14
  19. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/storage/redis.rs +10 -19
  20. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/trie.rs +60 -60
  21. zoocache-2026.2.5/src/zoocache_core/utils.rs +15 -0
  22. zoocache-2026.2.5/uv.lock +998 -0
  23. zoocache-2026.1.20/lint.sh +0 -3
  24. zoocache-2026.1.20/src/zoocache_core/storage/lmdb.rs +0 -182
  25. zoocache-2026.1.20/uv.lock +0 -356
  26. {zoocache-2026.1.20 → zoocache-2026.2.5}/.cargo/config.toml +0 -0
  27. {zoocache-2026.1.20 → zoocache-2026.2.5}/.github/workflows/ci.yml +0 -0
  28. {zoocache-2026.1.20 → zoocache-2026.2.5}/.github/workflows/release.yml +0 -0
  29. {zoocache-2026.1.20 → zoocache-2026.2.5}/.python-version +0 -0
  30. {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_benchmarks.py +0 -0
  31. {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_heavy_load.py +0 -0
  32. {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_lazy_update_bench.py +0 -0
  33. {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_lmdb_bench.py +0 -0
  34. {zoocache-2026.1.20 → zoocache-2026.2.5}/benchmarks/run_tti_bench.py +0 -0
  35. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0001-prefix-trie-invalidation.md +0 -0
  36. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0002-rust-core-python-wrapper.md +0 -0
  37. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0003-hlc-distributed-consistency.md +0 -0
  38. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0004-serialization-strategy.md +0 -0
  39. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0005-singleflight-pattern.md +0 -0
  40. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0006-trie-performance-optimizations.md +0 -0
  41. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0007-zero-bridge-serialization.md +0 -0
  42. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/adr/0008-redis-bus-connection-pooling.md +0 -0
  43. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/architecture.svg +0 -0
  44. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/entities.svg +0 -0
  45. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/favicon.svg +0 -0
  46. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/hlc_consistency.svg +0 -0
  47. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/invalidation.svg +0 -0
  48. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/logo-dark.svg +0 -0
  49. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/logo-light.svg +0 -0
  50. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/assets/serialization.svg +0 -0
  51. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/concurrency.md +0 -0
  52. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/consistency.md +0 -0
  53. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/invalidation.md +0 -0
  54. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/reliability.md +0 -0
  55. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/serialization.md +0 -0
  56. {zoocache-2026.1.20 → zoocache-2026.2.5}/docs/user_guide.md +0 -0
  57. {zoocache-2026.1.20 → zoocache-2026.2.5}/examples/usage_demo.py +0 -0
  58. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache/__init__.py +0 -0
  59. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache/context.py +0 -0
  60. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/bus/local.rs +0 -0
  61. {zoocache-2026.1.20 → zoocache-2026.2.5}/src/zoocache_core/bus/mod.rs +0 -0
  62. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/conftest.py +0 -0
  63. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_async.py +0 -0
  64. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_complex_sqlite.py +0 -0
  65. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_concurrency.py +0 -0
  66. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_fixed_ttl.py +0 -0
  67. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_invalidation.py +0 -0
  68. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_lru_eviction.py +0 -0
  69. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_multiprocess_lmdb.py +0 -0
  70. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_passive_resync.py +0 -0
  71. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_storage.py +0 -0
  72. {zoocache-2026.1.20 → zoocache-2026.2.5}/tests/test_ttl.py +0 -0
@@ -21,3 +21,7 @@ target/
21
21
 
22
22
  # LMDB files
23
23
  *.mdb
24
+
25
+
26
+ # Docs files
27
+ site/
@@ -0,0 +1,14 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-22.04
5
+ tools:
6
+ python: "3.10"
7
+ rust: "latest"
8
+ jobs:
9
+ install:
10
+ - pip install uv
11
+ - uv sync --group docs
12
+ build:
13
+ html:
14
+ - uv run mkdocs build -d $READTHEDOCS_OUTPUT/html
@@ -963,7 +963,7 @@ dependencies = [
963
963
 
964
964
  [[package]]
965
965
  name = "zoocache"
966
- version = "2026.1.20"
966
+ version = "2026.2.5"
967
967
  dependencies = [
968
968
  "dashmap",
969
969
  "lmdb",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "zoocache"
3
- version = "2026.1.20"
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.1.20
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/pypi/dm/zoocache">
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/pypi/dm/zoocache">
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 Plano de Control (Rust Core)
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 Plano de Datos (Python Wrapper)
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,8 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+
5
+ cargo fmt
6
+ cargo clippy --all-targets --all-features -- -D warnings
7
+ uv run ruff check . --fix
8
+ uv run ruff format .
@@ -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
@@ -29,4 +29,11 @@ dev = [
29
29
  "pytest-cov>=7.0.0",
30
30
  "ruff>=0.14.13",
31
31
  "maturin>=1.7",
32
+ "mkdocstrings>=1.0.1",
33
+ ]
34
+ docs = [
35
+ "mkdocs>=1.6.1",
36
+ "mkdocs-material>=9.7.1",
37
+ "mkdocstrings[python]>=0.24.0",
38
+ "mkdocs-include-markdown-plugin>=7.2.0",
32
39
  ]
@@ -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!("[zoocache] Bus listener connection failed: {}. Retrying in {}ms...", e, backoff_ms);
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 { FlightStatus::Error } else { FlightStatus::Done };
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
- state = flight.condvar.wait(state).unwrap();
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::{complete_flight, try_enter_flight, wait_for_flight, Flight, FlightStatus};
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(storage_url: Option<&str>, bus_url: Option<&str>, prefix: Option<&str>, default_ttl: Option<u64>, read_extend_ttl: bool, max_entries: Option<usize>) -> PyResult<Self> {
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://") => Arc::new(
41
- RedisStorage::new(url, prefix)
42
- .map_err(|e| PyErr::new::<pyo3::exceptions::PyConnectionError, _>(e.to_string()))?,
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
- LmdbStorage::new(path)
48
- .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?,
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
- format!("Unsupported storage scheme: {}", url),
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 = Arc::new(
65
- RedisPubSubBus::new(url, channel.as_deref())
66
- .map_err(|e| PyErr::new::<pyo3::exceptions::PyConnectionError, _>(e.to_string()))?,
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.get(&key).is_some_and(|&last| now.duration_since(last) < Duration::from_secs(60)) {
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
- last_touches.retain(|_, &mut instant| now.duration_since(instant) < Duration::from_secs(300));
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(&self, py: Python, key: &str) -> PyResult<(Option<Py<PyAny>>, bool, Option<Py<PyAny>>)> {
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
- // Return the existing future if it exists
153
- let fut = flight.py_future.lock().unwrap().as_ref().map(|f| f.clone_ref(py));
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(&self, py: Python, key: &str, is_error: bool, value: Option<Py<PyAny>>) -> Option<Py<PyAny>> {
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(&self, py: Python, key: String, value: Py<PyAny>, dependencies: Vec<String>, ttl: Option<u64>) {
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