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.
- client_query_cache-0.9.1/PKG-INFO +106 -0
- client_query_cache-0.9.1/README.md +80 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/Cargo.lock +12 -12
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/Cargo.toml +1 -1
- client_query_cache-0.9.1/native/shared-index/deny.toml +16 -0
- client_query_cache-0.9.1/native/shared-index/src/echo/tests.rs +303 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/echo.rs +1 -194
- client_query_cache-0.9.1/native/shared-index/src/frames/tests.rs +77 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/frames.rs +1 -72
- client_query_cache-0.9.1/native/shared-index/src/hash/tests.rs +11 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/hash.rs +1 -13
- client_query_cache-0.9.1/native/shared-index/src/index/support_tests.rs +10 -0
- client_query_cache-0.9.1/native/shared-index/src/index/tests.rs +693 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/index.rs +323 -538
- client_query_cache-0.9.1/native/shared-index/src/lib.rs +25 -0
- client_query_cache-0.9.1/native/shared-index/src/memory/support_tests.rs +11 -0
- client_query_cache-0.9.1/native/shared-index/src/memory/tests.rs +22 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/memory.rs +4 -33
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/python.rs +19 -23
- client_query_cache-0.9.1/native/shared-index/src/relay/tests.rs +130 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/relay.rs +1 -91
- client_query_cache-0.9.1/native/shared-index/src/ring/tests.rs +60 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/ring.rs +6 -63
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/pyproject.toml +34 -23
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/codec.py +9 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/collection_metadata.py +8 -20
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/cursor_capture.py +4 -6
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/find_reads.py +24 -4
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/manager.py +425 -68
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/namespace.py +2 -0
- client_query_cache-0.9.1/src/client_query_cache/_core/namespace_metadata.py +84 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/residency.py +5 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/snapshots.py +1 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_events.py +25 -4
- client_query_cache-0.9.1/src/client_query_cache/_core/stream_startup.py +29 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/unique_keys.py +7 -13
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/adapters.py +773 -144
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/attachment.py +120 -24
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/coordinator.py +102 -28
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/shared_index.py +80 -26
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/wire.py +51 -3
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/collection.py +15 -15
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/cursors.py +8 -9
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/manager.py +41 -13
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/shared.py +0 -7
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/streams.py +18 -4
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/shared.py +2 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/collection.py +15 -15
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/cursors.py +8 -9
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/manager.py +41 -13
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/shared.py +0 -7
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/streams.py +18 -4
- client_query_cache-0.8.1/PKG-INFO +0 -94
- client_query_cache-0.8.1/README.md +0 -68
- client_query_cache-0.8.1/native/shared-index/src/lib.rs +0 -16
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/LICENSE +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/clock.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/native/shared-index/src/sync.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/__init__.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/__init__.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/canonical.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/collation.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/count_reads.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/entries.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/errors.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/find_one_reads.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/identity_reads.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/keys.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/lifecycle.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/locking.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/lru.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/order_sensitive_keys.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/projection.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/query_filters.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/read_classification.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/read_validation.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_activation.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_cost.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_health.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/stream_options.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_core/traversal.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared/__init__.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_shared_index.pyi +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/_types.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/__init__.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/asynchronous/database.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/otel.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/py.typed +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.1}/src/client_query_cache/synchronous/__init__.py +0 -0
- {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
|
+
[](https://pypi.org/project/client-query-cache/)
|
|
29
|
+
[](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
|
|
30
|
+
[](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
|
|
31
|
+
[](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
|
+

|
|
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
|
+
[](https://pypi.org/project/client-query-cache/)
|
|
4
|
+
[](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
|
|
5
|
+
[](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
|
|
6
|
+
[](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
|
+

|
|
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.
|
|
150
|
+
version = "0.30.1"
|
|
151
151
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
152
|
-
checksum = "
|
|
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.
|
|
164
|
+
version = "0.30.1"
|
|
165
165
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
166
|
-
checksum = "
|
|
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.
|
|
173
|
+
version = "0.30.1"
|
|
174
174
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
175
|
-
checksum = "
|
|
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.
|
|
183
|
+
version = "0.30.1"
|
|
184
184
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
185
|
-
checksum = "
|
|
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.
|
|
195
|
+
version = "0.30.1"
|
|
196
196
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
197
|
-
checksum = "
|
|
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 = "
|
|
266
|
+
version = "3.0.9"
|
|
267
267
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
268
|
-
checksum = "
|
|
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.
|
|
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
|
+
}
|