client-query-cache 0.8.1__tar.gz → 0.9.1__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 (90) hide show
  1. client_query_cache-0.9.1/PKG-INFO +106 -0
  2. client_query_cache-0.9.1/README.md +80 -0
  3. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/Cargo.lock +12 -12
  4. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/Cargo.toml +1 -1
  5. client_query_cache-0.9.1/native/shared-index/deny.toml +16 -0
  6. client_query_cache-0.9.1/native/shared-index/src/echo/tests.rs +303 -0
  7. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/echo.rs +1 -194
  8. client_query_cache-0.9.1/native/shared-index/src/frames/tests.rs +77 -0
  9. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/frames.rs +1 -72
  10. client_query_cache-0.9.1/native/shared-index/src/hash/tests.rs +11 -0
  11. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/hash.rs +1 -13
  12. client_query_cache-0.9.1/native/shared-index/src/index/support_tests.rs +10 -0
  13. client_query_cache-0.9.1/native/shared-index/src/index/tests.rs +693 -0
  14. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/index.rs +323 -538
  15. client_query_cache-0.9.1/native/shared-index/src/lib.rs +25 -0
  16. client_query_cache-0.9.1/native/shared-index/src/memory/support_tests.rs +11 -0
  17. client_query_cache-0.9.1/native/shared-index/src/memory/tests.rs +22 -0
  18. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/memory.rs +4 -33
  19. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/python.rs +19 -23
  20. client_query_cache-0.9.1/native/shared-index/src/relay/tests.rs +130 -0
  21. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/relay.rs +1 -91
  22. client_query_cache-0.9.1/native/shared-index/src/ring/tests.rs +60 -0
  23. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/ring.rs +6 -63
  24. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/pyproject.toml +34 -23
  25. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/codec.py +9 -0
  26. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/collection_metadata.py +8 -20
  27. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/cursor_capture.py +4 -6
  28. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/find_reads.py +24 -4
  29. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/manager.py +425 -68
  30. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/namespace.py +2 -0
  31. client_query_cache-0.9.1/src/client_query_cache/_core/namespace_metadata.py +84 -0
  32. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/residency.py +5 -0
  33. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/snapshots.py +1 -0
  34. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_events.py +25 -4
  35. client_query_cache-0.9.1/src/client_query_cache/_core/stream_startup.py +29 -0
  36. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/unique_keys.py +7 -13
  37. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/adapters.py +773 -144
  38. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/attachment.py +120 -24
  39. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/coordinator.py +102 -28
  40. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/shared_index.py +80 -26
  41. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/wire.py +51 -3
  42. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/collection.py +15 -15
  43. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/cursors.py +8 -9
  44. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/manager.py +41 -13
  45. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/shared.py +0 -7
  46. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/streams.py +18 -4
  47. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/shared.py +2 -0
  48. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/collection.py +15 -15
  49. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/cursors.py +8 -9
  50. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/manager.py +41 -13
  51. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/shared.py +0 -7
  52. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/streams.py +18 -4
  53. client_query_cache-0.8.1/PKG-INFO +0 -94
  54. client_query_cache-0.8.1/README.md +0 -68
  55. client_query_cache-0.8.1/native/shared-index/src/lib.rs +0 -16
  56. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/LICENSE +0 -0
  57. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/clock.rs +0 -0
  58. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/sync.rs +0 -0
  59. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/__init__.py +0 -0
  60. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/__init__.py +0 -0
  61. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/canonical.py +0 -0
  62. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/collation.py +0 -0
  63. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/count_reads.py +0 -0
  64. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/entries.py +0 -0
  65. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/errors.py +0 -0
  66. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/find_one_reads.py +0 -0
  67. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/identity_reads.py +0 -0
  68. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/keys.py +0 -0
  69. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/lifecycle.py +0 -0
  70. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/locking.py +0 -0
  71. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/lru.py +0 -0
  72. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/order_sensitive_keys.py +0 -0
  73. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/projection.py +0 -0
  74. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/query_filters.py +0 -0
  75. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/read_classification.py +0 -0
  76. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/read_validation.py +0 -0
  77. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_activation.py +0 -0
  78. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_cost.py +0 -0
  79. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_health.py +0 -0
  80. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_options.py +0 -0
  81. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/traversal.py +0 -0
  82. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/__init__.py +0 -0
  83. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared_index.pyi +0 -0
  84. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_types.py +0 -0
  85. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/__init__.py +0 -0
  86. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/database.py +0 -0
  87. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/otel.py +0 -0
  88. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/py.typed +0 -0
  89. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/__init__.py +0 -0
  90. {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/database.py +0 -0
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: client-query-cache
3
+ Version: 0.9.1
4
+ Classifier: Framework :: AsyncIO
5
+ Classifier: Operating System :: POSIX :: Linux
6
+ Classifier: Programming Language :: Python :: Implementation :: CPython
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python :: 3.14
9
+ Classifier: Programming Language :: Python :: 3.15
10
+ Classifier: Topic :: Database
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ Classifier: Typing :: Typed
13
+ Requires-Dist: annotated-types>=0.8.0
14
+ Requires-Dist: pymongo>=4.18.1
15
+ Requires-Dist: opentelemetry-api>=1.45.0 ; extra == 'otel'
16
+ Provides-Extra: otel
17
+ License-File: LICENSE
18
+ Summary: Sync and asyncio PyMongo query caching with change-stream invalidation and Rust-backed sharing across workers.
19
+ Author-email: Alessio Locatelli <<software.development@secure.mailbox.org>>
20
+ License-Expression: Apache-2.0
21
+ Requires-Python: >=3.14
22
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
23
+ Project-URL: Documentation, https://alessio-locatelli.github.io/client-query-cache/
24
+ Project-URL: Repository, https://github.com/alessio-locatelli/client-query-cache
25
+
26
+ # Client-side query caching for PyMongo
27
+
28
+ [![PyPI](https://img.shields.io/pypi/v/client-query-cache?color=lightgrey)](https://pypi.org/project/client-query-cache/)
29
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/client-query-cache)](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
30
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
31
+ [![Typing](https://img.shields.io/pypi/types/client-query-cache)](https://pypi.org/project/client-query-cache/)
32
+
33
+ **Speed up read-heavy, high-throughput Python applications.** `client-query-cache` serves eligible repeated PyMongo reads from memory, reducing read latency and avoiding MongoDB round trips on cache hits.
34
+
35
+ Add caching through an explicit [cached collection view](https://alessio-locatelli.github.io/client-query-cache/usage/cached-reads/). Keep your existing MongoDB queries and use PyMongo for writes.
36
+
37
+ - **[Synchronous](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) and [asyncio](https://alessio-locatelli.github.io/client-query-cache/getting-started/asyncio/) APIs** for long-lived Python applications.
38
+ - **[MongoDB change streams](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/)** invalidate affected results, including writes from other applications.
39
+ - **[Shared caching across application workers](https://alessio-locatelli.github.io/client-query-cache/operations/shared-cache/)** on one [supported Linux host](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements) reduces duplicate cached documents and uses one change stream per active database.
40
+ - **[A Rust-backed shared cache](https://alessio-locatelli.github.io/client-query-cache/benchmarks/shared-cache/)** serves eligible warm reads from native shared memory without a MongoDB round trip.
41
+
42
+ Choose `CacheManager` for caching within one process, or `SharedCacheManager` to reuse eligible collection reads across trusted same-host workers. Both support sync and asyncio. See [which manager to choose](https://alessio-locatelli.github.io/client-query-cache/getting-started/choose-a-manager/).
43
+
44
+ With shared caching, workers look up the same cache and use their own PyMongo clients for misses and bypasses:
45
+
46
+ ```mermaid
47
+ flowchart TB
48
+ subgraph Application["Python application"]
49
+ Worker1["Python worker 1"] -->|"Cache lookup"| Cache["Rust-backed<br/>shared cache"]
50
+ Worker2["Python worker 2"] -->|"Cache lookup"| Cache
51
+ end
52
+ Worker1 -->|"Miss / bypass"| DB[("MongoDB")]
53
+ Worker2 -->|"Miss / bypass"| DB
54
+ DB -.->|"Change streams<br/>Async invalidation"| Cache
55
+ ```
56
+
57
+ [Benchmarks](https://alessio-locatelli.github.io/client-query-cache/benchmarks/shared-cache/) compare direct reads, independent caches and shared caching. Sharing avoids duplicated values and streams, but its owner process and cache misses have costs. Rust does not guarantee a faster application or lower total memory for every workload.
58
+
59
+ ![Illustrative process-local read latency on a logarithmic scale: direct local MongoDB read, about 120 microseconds; direct Atlas M0 read, 79.4 milliseconds; cached reads in either deployment, about 61 microseconds.](docs/user/assets/benchmark-latency-light.svg)
60
+
61
+ The graphic shows [process-local measurements from local MongoDB and Atlas deployments](https://alessio-locatelli.github.io/client-query-cache/benchmarks/#illustrative-read-latency); it is separate from the shared-cache comparison above.
62
+
63
+ ## Quick start
64
+
65
+ ```bash
66
+ uv add client-query-cache
67
+ ```
68
+
69
+ Or use `pip install client-query-cache`.
70
+
71
+ Start a MongoDB server before running this example. For local development, follow the [single-member replica-set setup](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#local-mongodb).
72
+
73
+ ```python
74
+ from pymongo import MongoClient
75
+
76
+ from client_query_cache import CacheManager
77
+
78
+ with (
79
+ MongoClient("mongodb://localhost:27017") as client,
80
+ CacheManager(client) as cache_manager,
81
+ ):
82
+ collection = client["client_query_cache_tutorial"]["items"]
83
+ cached_collection = cache_manager.get_cached_collection(collection)
84
+ collection.replace_one(
85
+ {"_id": "example"}, {"_id": "example", "value": 42}, upsert=True
86
+ )
87
+ cached_collection.find_one({"_id": "example"})
88
+ cached_collection.find_one({"_id": "example"}) # Repeated reads can hit the cache.
89
+ ```
90
+
91
+ Check the [platform and MongoDB requirements](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements) before installing. [Read eligibility](https://alessio-locatelli.github.io/client-query-cache/reference/api/#bypass-conditions) determines which requests can use the cache.
92
+
93
+ Invalidation is asynchronous: cached reads can briefly return a preceding value after a write. Use direct PyMongo reads with suitable consistency controls for read-after-write and current authorization; see [consistency](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/).
94
+
95
+ ## Run an application
96
+
97
+ Start with the [synchronous](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) or [asyncio](https://alessio-locatelli.github.io/client-query-cache/getting-started/asyncio/) tutorial. The [shared FastAPI example](https://alessio-locatelli.github.io/client-query-cache/examples/fastapi-shared/) runs real Granian workers and verifies cross-process hits, invalidation and graceful shutdown. The [compact FastAPI example](https://alessio-locatelli.github.io/client-query-cache/examples/fastapi-minimal/) demonstrates process-local caching.
98
+
99
+ [Deployment](https://alessio-locatelli.github.io/client-query-cache/operations/) · [API reference](https://alessio-locatelli.github.io/client-query-cache/reference/api/) · [Examples](https://alessio-locatelli.github.io/client-query-cache/examples/)
100
+
101
+ The library is tested against real MongoDB replica sets, including [concurrency stress tests](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/concurrency-stress-tests.md). Separate checks guard against [performance](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/performance-regression-guard.md) and [memory](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/memory-regression-tests.md) regressions.
102
+
103
+ ---
104
+
105
+ > MongoDB is a registered trademark of MongoDB, Inc. This project is independent and is not affiliated with or endorsed by MongoDB, Inc.
106
+
@@ -0,0 +1,80 @@
1
+ # Client-side query caching for PyMongo
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/client-query-cache?color=lightgrey)](https://pypi.org/project/client-query-cache/)
4
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/client-query-cache)](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
5
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
6
+ [![Typing](https://img.shields.io/pypi/types/client-query-cache)](https://pypi.org/project/client-query-cache/)
7
+
8
+ **Speed up read-heavy, high-throughput Python applications.** `client-query-cache` serves eligible repeated PyMongo reads from memory, reducing read latency and avoiding MongoDB round trips on cache hits.
9
+
10
+ Add caching through an explicit [cached collection view](https://alessio-locatelli.github.io/client-query-cache/usage/cached-reads/). Keep your existing MongoDB queries and use PyMongo for writes.
11
+
12
+ - **[Synchronous](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) and [asyncio](https://alessio-locatelli.github.io/client-query-cache/getting-started/asyncio/) APIs** for long-lived Python applications.
13
+ - **[MongoDB change streams](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/)** invalidate affected results, including writes from other applications.
14
+ - **[Shared caching across application workers](https://alessio-locatelli.github.io/client-query-cache/operations/shared-cache/)** on one [supported Linux host](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements) reduces duplicate cached documents and uses one change stream per active database.
15
+ - **[A Rust-backed shared cache](https://alessio-locatelli.github.io/client-query-cache/benchmarks/shared-cache/)** serves eligible warm reads from native shared memory without a MongoDB round trip.
16
+
17
+ Choose `CacheManager` for caching within one process, or `SharedCacheManager` to reuse eligible collection reads across trusted same-host workers. Both support sync and asyncio. See [which manager to choose](https://alessio-locatelli.github.io/client-query-cache/getting-started/choose-a-manager/).
18
+
19
+ With shared caching, workers look up the same cache and use their own PyMongo clients for misses and bypasses:
20
+
21
+ ```mermaid
22
+ flowchart TB
23
+ subgraph Application["Python application"]
24
+ Worker1["Python worker 1"] -->|"Cache lookup"| Cache["Rust-backed<br/>shared cache"]
25
+ Worker2["Python worker 2"] -->|"Cache lookup"| Cache
26
+ end
27
+ Worker1 -->|"Miss / bypass"| DB[("MongoDB")]
28
+ Worker2 -->|"Miss / bypass"| DB
29
+ DB -.->|"Change streams<br/>Async invalidation"| Cache
30
+ ```
31
+
32
+ [Benchmarks](https://alessio-locatelli.github.io/client-query-cache/benchmarks/shared-cache/) compare direct reads, independent caches and shared caching. Sharing avoids duplicated values and streams, but its owner process and cache misses have costs. Rust does not guarantee a faster application or lower total memory for every workload.
33
+
34
+ ![Illustrative process-local read latency on a logarithmic scale: direct local MongoDB read, about 120 microseconds; direct Atlas M0 read, 79.4 milliseconds; cached reads in either deployment, about 61 microseconds.](docs/user/assets/benchmark-latency-light.svg)
35
+
36
+ The graphic shows [process-local measurements from local MongoDB and Atlas deployments](https://alessio-locatelli.github.io/client-query-cache/benchmarks/#illustrative-read-latency); it is separate from the shared-cache comparison above.
37
+
38
+ ## Quick start
39
+
40
+ ```bash
41
+ uv add client-query-cache
42
+ ```
43
+
44
+ Or use `pip install client-query-cache`.
45
+
46
+ Start a MongoDB server before running this example. For local development, follow the [single-member replica-set setup](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#local-mongodb).
47
+
48
+ ```python
49
+ from pymongo import MongoClient
50
+
51
+ from client_query_cache import CacheManager
52
+
53
+ with (
54
+ MongoClient("mongodb://localhost:27017") as client,
55
+ CacheManager(client) as cache_manager,
56
+ ):
57
+ collection = client["client_query_cache_tutorial"]["items"]
58
+ cached_collection = cache_manager.get_cached_collection(collection)
59
+ collection.replace_one(
60
+ {"_id": "example"}, {"_id": "example", "value": 42}, upsert=True
61
+ )
62
+ cached_collection.find_one({"_id": "example"})
63
+ cached_collection.find_one({"_id": "example"}) # Repeated reads can hit the cache.
64
+ ```
65
+
66
+ Check the [platform and MongoDB requirements](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements) before installing. [Read eligibility](https://alessio-locatelli.github.io/client-query-cache/reference/api/#bypass-conditions) determines which requests can use the cache.
67
+
68
+ Invalidation is asynchronous: cached reads can briefly return a preceding value after a write. Use direct PyMongo reads with suitable consistency controls for read-after-write and current authorization; see [consistency](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/).
69
+
70
+ ## Run an application
71
+
72
+ Start with the [synchronous](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) or [asyncio](https://alessio-locatelli.github.io/client-query-cache/getting-started/asyncio/) tutorial. The [shared FastAPI example](https://alessio-locatelli.github.io/client-query-cache/examples/fastapi-shared/) runs real Granian workers and verifies cross-process hits, invalidation and graceful shutdown. The [compact FastAPI example](https://alessio-locatelli.github.io/client-query-cache/examples/fastapi-minimal/) demonstrates process-local caching.
73
+
74
+ [Deployment](https://alessio-locatelli.github.io/client-query-cache/operations/) · [API reference](https://alessio-locatelli.github.io/client-query-cache/reference/api/) · [Examples](https://alessio-locatelli.github.io/client-query-cache/examples/)
75
+
76
+ The library is tested against real MongoDB replica sets, including [concurrency stress tests](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/concurrency-stress-tests.md). Separate checks guard against [performance](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/performance-regression-guard.md) and [memory](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/memory-regression-tests.md) regressions.
77
+
78
+ ---
79
+
80
+ > MongoDB is a registered trademark of MongoDB, Inc. This project is independent and is not affiliated with or endorsed by MongoDB, Inc.
@@ -147,9 +147,9 @@ dependencies = [
147
147
 
148
148
  [[package]]
149
149
  name = "pyo3"
150
- version = "0.29.3"
150
+ version = "0.30.1"
151
151
  source = "registry+https://github.com/rust-lang/crates.io-index"
152
- checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
152
+ checksum = "229ca13b8a15bb0d1487b81ab975bfcd97513416499a8312011b6036e42e0de4"
153
153
  dependencies = [
154
154
  "libc",
155
155
  "once_cell",
@@ -161,18 +161,18 @@ dependencies = [
161
161
 
162
162
  [[package]]
163
163
  name = "pyo3-build-config"
164
- version = "0.29.3"
164
+ version = "0.30.1"
165
165
  source = "registry+https://github.com/rust-lang/crates.io-index"
166
- checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
166
+ checksum = "70130e2b6dd1ee589a8788026a429561ca5c40fa7a2fa7914c2282eddae24700"
167
167
  dependencies = [
168
168
  "target-lexicon",
169
169
  ]
170
170
 
171
171
  [[package]]
172
172
  name = "pyo3-ffi"
173
- version = "0.29.3"
173
+ version = "0.30.1"
174
174
  source = "registry+https://github.com/rust-lang/crates.io-index"
175
- checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
175
+ checksum = "5e7ff73c555c286d212d4ce7e5fbda845a973c4f61bd5f160f5f2c3c2a04d1c4"
176
176
  dependencies = [
177
177
  "libc",
178
178
  "pyo3-build-config",
@@ -180,9 +180,9 @@ dependencies = [
180
180
 
181
181
  [[package]]
182
182
  name = "pyo3-macros"
183
- version = "0.29.3"
183
+ version = "0.30.1"
184
184
  source = "registry+https://github.com/rust-lang/crates.io-index"
185
- checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
185
+ checksum = "c46d2e5862c7bb8207e3b639f713dda3c499933444784154c11d013c5a691b60"
186
186
  dependencies = [
187
187
  "proc-macro2",
188
188
  "pyo3-macros-backend",
@@ -192,9 +192,9 @@ dependencies = [
192
192
 
193
193
  [[package]]
194
194
  name = "pyo3-macros-backend"
195
- version = "0.29.3"
195
+ version = "0.30.1"
196
196
  source = "registry+https://github.com/rust-lang/crates.io-index"
197
- checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
197
+ checksum = "38b89f066f2ae72a2aef52d1537ca2fcd104440ebfadee06e97aa90738a25b9b"
198
198
  dependencies = [
199
199
  "heck",
200
200
  "proc-macro2",
@@ -263,9 +263,9 @@ checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
263
263
 
264
264
  [[package]]
265
265
  name = "syn"
266
- version = "2.0.119"
266
+ version = "3.0.9"
267
267
  source = "registry+https://github.com/rust-lang/crates.io-index"
268
- checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
268
+ checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
269
269
  dependencies = [
270
270
  "proc-macro2",
271
271
  "quote",
@@ -20,7 +20,7 @@ python = ["dep:pyo3"]
20
20
 
21
21
  [dependencies]
22
22
  libc = "0.2.190"
23
- pyo3 = { version = "0.29.3", features = ["abi3-py314"], optional = true }
23
+ pyo3 = { version = "0.30.1", features = ["abi3-py314"], optional = true }
24
24
 
25
25
  [target.'cfg(loom)'.dependencies]
26
26
  loom = "0.7.2"
@@ -0,0 +1,16 @@
1
+ # cargo-deny 0.19.9: https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
2
+ # The default license allowlist is empty. We override it because these permissive
3
+ # licenses cover the resolved native dependency graph and permit distribution.
4
+ [licenses]
5
+ allow = ["Apache-2.0", "Apache-2.0 WITH LLVM-exception", "MIT", "Unicode-3.0"]
6
+
7
+ [bans]
8
+ # The default is warn. We override it because duplicate versions must be resolved.
9
+ multiple-versions = "deny"
10
+ # The default is allow. We override it because unbounded dependencies hinder review.
11
+ wildcards = "deny"
12
+
13
+ [sources]
14
+ # Both defaults are warn. We override them because only crates.io is approved.
15
+ unknown-registry = "deny"
16
+ unknown-git = "deny"
@@ -0,0 +1,303 @@
1
+ use std::sync::Arc;
2
+
3
+ use super::*;
4
+ use crate::index::{Layout, Writer};
5
+ use crate::memory::zeroed;
6
+ use crate::sync::AtomicU64;
7
+
8
+ const EPOCH: u64 = 3;
9
+
10
+ fn published() -> Index<Arc<[AtomicU64]>> {
11
+ let layout = Layout::new(8, 64, 1, 1).unwrap();
12
+ let index = Index::create(zeroed(layout.words()), layout, 5, u64::MAX).unwrap();
13
+ assert!(index.set_available(0, true) && index.set_progress(0, 1) && index.set_epoch(0, EPOCH));
14
+ let query = Query {
15
+ database: 0,
16
+ namespace: 0,
17
+ epoch: EPOCH,
18
+ identity: b"present",
19
+ shape: b"shape",
20
+ };
21
+ let mut writer = Writer::new(index.clone());
22
+ let payload = writer.store(b"payload").unwrap();
23
+ writer.publish(&query, payload).unwrap();
24
+ index
25
+ }
26
+
27
+ fn selection(identity: &[u8]) -> Vec<u8> {
28
+ let mut bytes = Vec::new();
29
+ bytes.extend_from_slice(&0_u32.to_le_bytes());
30
+ bytes.extend_from_slice(&0_u32.to_le_bytes());
31
+ bytes.extend_from_slice(&EPOCH.to_le_bytes());
32
+ bytes.extend_from_slice(&(identity.len() as u32).to_le_bytes());
33
+ bytes.extend_from_slice(&5_u32.to_le_bytes());
34
+ bytes.extend_from_slice(identity);
35
+ bytes.extend_from_slice(b"shape");
36
+ bytes
37
+ }
38
+
39
+ fn request(request_id: u32, identities: &[&[u8]]) -> Vec<u8> {
40
+ let mut body = Vec::new();
41
+ body.extend_from_slice(&request_id.to_le_bytes());
42
+ body.extend_from_slice(&(identities.len() as u32).to_le_bytes());
43
+ for identity in identities {
44
+ body.extend_from_slice(&selection(identity));
45
+ }
46
+ let mut frame = (body.len() as u32).to_le_bytes().to_vec();
47
+ frame.extend_from_slice(&body);
48
+ frame
49
+ }
50
+
51
+ fn expected_reply(request_id: u32) -> Vec<u8> {
52
+ let mut body = Vec::new();
53
+ body.extend_from_slice(&request_id.to_le_bytes());
54
+ body.extend_from_slice(&2_u32.to_le_bytes());
55
+ body.extend_from_slice(&7_u32.to_le_bytes());
56
+ body.extend_from_slice(b"payload");
57
+ body.extend_from_slice(&MISS.to_le_bytes());
58
+ let mut frame = (body.len() as u32).to_le_bytes().to_vec();
59
+ frame.extend_from_slice(&body);
60
+ frame
61
+ }
62
+
63
+ #[test]
64
+ fn answers_each_selection_in_order() {
65
+ let index = published();
66
+ let mut out = Vec::new();
67
+ let frame = request(9, &[b"present", b"absent"]);
68
+ answer_frame(&index, 5, &frame[LENGTH_BYTES..], 1, &mut out).unwrap();
69
+ assert_eq!(out, expected_reply(9));
70
+ }
71
+
72
+ #[test]
73
+ fn rejects_malformed_frames() {
74
+ let index = published();
75
+ let frame = request(9, &[b"present"]);
76
+ let body = &frame[LENGTH_BYTES..];
77
+ let mut overstated = body.to_vec();
78
+ overstated[4] = 2;
79
+ let mut trailing = body.to_vec();
80
+ trailing.push(0);
81
+ for malformed in [
82
+ &body[..body.len() - 1],
83
+ &body[..6],
84
+ &overstated[..],
85
+ &trailing[..],
86
+ ] {
87
+ assert!(answer_frame(&index, 5, malformed, 1, &mut Vec::new()).is_err());
88
+ }
89
+ }
90
+
91
+ fn connection() -> (Connection, UnixStream) {
92
+ let (stream, peer) = UnixStream::pair().unwrap();
93
+ stream.set_nonblocking(true).unwrap();
94
+ let connection = Connection {
95
+ stream,
96
+ inbox: Vec::new(),
97
+ outbox: Vec::new(),
98
+ sent: 0,
99
+ writable: false,
100
+ };
101
+ (connection, peer)
102
+ }
103
+
104
+ #[test]
105
+ fn rejects_more_selections_than_one_frame_may_carry() {
106
+ let index = published();
107
+ let identities = vec![&b"present"[..]; SELECTION_LIMIT as usize + 1];
108
+ let frame = request(9, &identities);
109
+ assert!(answer_frame(&index, 5, &frame[LENGTH_BYTES..], 1, &mut Vec::new()).is_err());
110
+ let frame = request(9, &identities[1..]);
111
+ assert!(answer_frame(&index, 5, &frame[LENGTH_BYTES..], 1, &mut Vec::new()).is_ok());
112
+ }
113
+
114
+ #[test]
115
+ fn rejects_replies_larger_than_a_frame() {
116
+ let payload = vec![7_u8; REPLY_LIMIT / 9];
117
+ let layout = Layout::new(8, payload.len(), 1, 1).unwrap();
118
+ let index = Index::create(zeroed(layout.words()), layout, 5, u64::MAX).unwrap();
119
+ assert!(index.set_available(0, true) && index.set_progress(0, 1) && index.set_epoch(0, EPOCH));
120
+ let mut writer = Writer::new(index.clone());
121
+ let stored = writer.store(&payload).unwrap();
122
+ let query = Query {
123
+ database: 0,
124
+ namespace: 0,
125
+ epoch: EPOCH,
126
+ identity: b"present",
127
+ shape: b"shape",
128
+ };
129
+ writer.publish(&query, stored).unwrap();
130
+ let fits = request(9, &[&b"present"[..]; 8]);
131
+ assert!(answer_frame(&index, 5, &fits[LENGTH_BYTES..], 1, &mut Vec::new()).is_ok());
132
+ let oversized = request(9, &[&b"present"[..]; 9]);
133
+ assert!(answer_frame(&index, 5, &oversized[LENGTH_BYTES..], 1, &mut Vec::new()).is_err());
134
+ }
135
+
136
+ #[test]
137
+ fn closes_a_connection_whose_replies_are_not_read() {
138
+ let index = published();
139
+ let (mut connection, _peer) = connection();
140
+ connection.outbox = vec![0; QUEUE_LIMIT];
141
+ connection.inbox = request(9, &[b"present"]);
142
+ assert!(connection.answer(&index, 5).is_err());
143
+ connection
144
+ .outbox
145
+ .truncate(QUEUE_LIMIT - expected_reply(9).len());
146
+ connection.inbox = request(9, &[b"present", b"absent"]);
147
+ assert!(connection.answer(&index, 5).is_ok());
148
+ }
149
+
150
+ #[test]
151
+ fn buffers_at_most_one_frame_of_unanswered_input() {
152
+ let (mut connection, mut peer) = connection();
153
+ let writer = std::thread::spawn(move || {
154
+ let _ = peer.write_all(&vec![0xff; 2 * INBOX_LIMIT]);
155
+ });
156
+ let mut scratch = vec![0_u8; RECEIVE_BYTES];
157
+ while connection.inbox.len() < INBOX_LIMIT {
158
+ assert!(connection.receive(&mut scratch));
159
+ }
160
+ assert!(connection.receive(&mut scratch));
161
+ assert_eq!(connection.inbox.len(), INBOX_LIMIT);
162
+ drop(connection);
163
+ writer.join().unwrap();
164
+ }
165
+
166
+ struct EchoSocket {
167
+ echo: Echo,
168
+ directory: std::path::PathBuf,
169
+ }
170
+
171
+ impl Drop for EchoSocket {
172
+ fn drop(&mut self) {
173
+ self.echo.stop();
174
+ std::fs::remove_dir_all(&self.directory).unwrap();
175
+ }
176
+ }
177
+
178
+ fn echo_socket() -> (EchoSocket, UnixStream) {
179
+ let directory = std::env::temp_dir().join("agents").join(format!(
180
+ "shared-index-echo-{}-{}",
181
+ std::process::id(),
182
+ monotonic_ns()
183
+ ));
184
+ std::fs::create_dir_all(&directory).unwrap();
185
+ let path = directory.join("echo.sock");
186
+ let echo = Echo::start(published(), UnixListener::bind(&path).unwrap()).unwrap();
187
+ let client = UnixStream::connect(&path).unwrap();
188
+ (EchoSocket { echo, directory }, client)
189
+ }
190
+
191
+ #[test]
192
+ fn serves_frames_split_across_reads_over_a_socket() {
193
+ let (_server, mut client) = echo_socket();
194
+ let mut frames = request(1, &[b"present", b"absent"]);
195
+ frames.extend_from_slice(&request(2, &[b"present", b"absent"]));
196
+ let (first, second) = frames.split_at(10);
197
+ client.write_all(first).unwrap();
198
+ std::thread::sleep(std::time::Duration::from_millis(20));
199
+ client.write_all(second).unwrap();
200
+ let mut expected = expected_reply(1);
201
+ expected.extend_from_slice(&expected_reply(2));
202
+ let mut reply = vec![0; expected.len()];
203
+ client.read_exact(&mut reply).unwrap();
204
+ assert_eq!(reply, expected);
205
+ }
206
+
207
+ #[test]
208
+ fn backpressure_preserves_unsent_reply_bytes() {
209
+ let (mut connection, mut peer) = connection();
210
+ let poller = Poller::new().unwrap();
211
+ poller
212
+ .control(
213
+ libc::EPOLL_CTL_ADD,
214
+ connection.stream.as_raw_fd(),
215
+ FIRST_CONNECTION,
216
+ libc::EPOLLIN,
217
+ )
218
+ .unwrap();
219
+ let expected = vec![b'r'; 2 * FRAME_LIMIT];
220
+ connection.outbox.clone_from(&expected);
221
+ let mut received = Vec::new();
222
+ assert!(connection.flush(&poller, FIRST_CONNECTION));
223
+ assert!(connection.writable);
224
+ assert!(connection.sent > 0 && connection.sent < expected.len());
225
+ while !connection.outbox.is_empty() {
226
+ let sent = connection.sent;
227
+ let start = received.len();
228
+ received.resize(sent, 0);
229
+ peer.read_exact(&mut received[start..]).unwrap();
230
+ assert!(connection.flush(&poller, FIRST_CONNECTION));
231
+ }
232
+ let start = received.len();
233
+ received.resize(expected.len(), 0);
234
+ peer.read_exact(&mut received[start..]).unwrap();
235
+ assert_eq!(received, expected);
236
+ assert!(!connection.writable);
237
+ assert_eq!(connection.sent, 0);
238
+ }
239
+
240
+ #[test]
241
+ fn closed_peer_rejects_pending_replies() {
242
+ let (mut connection, peer) = connection();
243
+ drop(peer);
244
+ connection.outbox.extend_from_slice(b"reply");
245
+ assert!(!connection.flush(&Poller::new().unwrap(), FIRST_CONNECTION));
246
+ }
247
+
248
+ #[test]
249
+ fn peer_reset_closes_the_connection() {
250
+ let (mut connection, peer) = connection();
251
+ connection.stream.write_all(b"unread reply").unwrap();
252
+ drop(peer);
253
+ assert!(!connection.receive(&mut [0; 64]));
254
+ }
255
+
256
+ struct IgnoredUserSignal(libc::sigaction);
257
+
258
+ extern "C" fn ignore_user_signal(_signal: libc::c_int) {}
259
+
260
+ impl IgnoredUserSignal {
261
+ fn install() -> Self {
262
+ let mut action = unsafe { std::mem::zeroed::<libc::sigaction>() };
263
+ action.sa_sigaction = ignore_user_signal as *const () as usize;
264
+ assert_eq!(unsafe { libc::sigemptyset(&raw mut action.sa_mask) }, 0);
265
+ let mut previous = unsafe { std::mem::zeroed::<libc::sigaction>() };
266
+ assert_eq!(
267
+ unsafe { libc::sigaction(libc::SIGUSR1, &raw const action, &raw mut previous) },
268
+ 0
269
+ );
270
+ Self(previous)
271
+ }
272
+ }
273
+
274
+ impl Drop for IgnoredUserSignal {
275
+ fn drop(&mut self) {
276
+ assert_eq!(
277
+ unsafe { libc::sigaction(libc::SIGUSR1, &raw const self.0, std::ptr::null_mut()) },
278
+ 0
279
+ );
280
+ }
281
+ }
282
+
283
+ #[test]
284
+ fn signals_do_not_stop_the_echo_server() {
285
+ use std::os::unix::thread::JoinHandleExt;
286
+ use std::time::Duration;
287
+
288
+ let _handler = IgnoredUserSignal::install();
289
+ let (server, mut client) = echo_socket();
290
+ client
291
+ .set_read_timeout(Some(Duration::from_secs(3)))
292
+ .unwrap();
293
+ let thread = server.echo.thread.as_ref().unwrap().as_pthread_t();
294
+ for request_id in 0..8 {
295
+ std::thread::sleep(Duration::from_millis(1));
296
+ assert_eq!(unsafe { libc::pthread_kill(thread, libc::SIGUSR1) }, 0);
297
+ let frame = request(request_id, &[]);
298
+ client.write_all(&frame).unwrap();
299
+ let mut reply = vec![0; frame.len()];
300
+ client.read_exact(&mut reply).unwrap();
301
+ assert_eq!(reply, frame);
302
+ }
303
+ }