client-query-cache 0.9.0__tar.gz → 0.10.0__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 (92) hide show
  1. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/PKG-INFO +4 -2
  2. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/README.md +3 -1
  3. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/Cargo.lock +12 -12
  4. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/Cargo.toml +1 -1
  5. client_query_cache-0.10.0/native/shared-index/deny.toml +16 -0
  6. client_query_cache-0.10.0/native/shared-index/src/echo/tests.rs +303 -0
  7. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/echo.rs +1 -194
  8. client_query_cache-0.10.0/native/shared-index/src/frames/tests.rs +77 -0
  9. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/frames.rs +1 -72
  10. client_query_cache-0.10.0/native/shared-index/src/hash/tests.rs +11 -0
  11. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/hash.rs +1 -13
  12. client_query_cache-0.10.0/native/shared-index/src/index/support_tests.rs +10 -0
  13. client_query_cache-0.10.0/native/shared-index/src/index/tests.rs +693 -0
  14. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/index.rs +323 -538
  15. client_query_cache-0.10.0/native/shared-index/src/key_encoder.rs +224 -0
  16. client_query_cache-0.10.0/native/shared-index/src/lib.rs +29 -0
  17. client_query_cache-0.10.0/native/shared-index/src/local_cache.rs +2872 -0
  18. client_query_cache-0.10.0/native/shared-index/src/memory/support_tests.rs +11 -0
  19. client_query_cache-0.10.0/native/shared-index/src/memory/tests.rs +22 -0
  20. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/memory.rs +4 -33
  21. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/python.rs +23 -23
  22. client_query_cache-0.10.0/native/shared-index/src/relay/tests.rs +130 -0
  23. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/relay.rs +1 -91
  24. client_query_cache-0.10.0/native/shared-index/src/ring/tests.rs +60 -0
  25. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/ring.rs +6 -63
  26. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/pyproject.toml +36 -22
  27. client_query_cache-0.10.0/src/client_query_cache/_core/contracts.pyi +209 -0
  28. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/cursor_capture.py +3 -3
  29. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/entries.py +4 -0
  30. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/manager.py +28 -12
  31. client_query_cache-0.10.0/src/client_query_cache/_core/native.py +121 -0
  32. client_query_cache-0.9.0/src/client_query_cache/_core/residency.py → client_query_cache-0.10.0/src/client_query_cache/_core/residency.pyi +8 -20
  33. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/stream_events.py +1 -1
  34. client_query_cache-0.10.0/src/client_query_cache/_core/stream_startup.py +29 -0
  35. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_shared/adapters.py +20 -24
  36. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_shared/attachment.py +52 -15
  37. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_shared/shared_index.py +5 -1
  38. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_shared/wire.py +49 -2
  39. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_shared_index.pyi +21 -0
  40. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/asynchronous/__init__.py +2 -1
  41. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/asynchronous/manager.py +1 -1
  42. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/asynchronous/streams.py +3 -4
  43. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/shared.py +5 -2
  44. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/synchronous/__init__.py +2 -1
  45. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/synchronous/manager.py +1 -1
  46. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/synchronous/streams.py +3 -4
  47. client_query_cache-0.9.0/native/shared-index/src/lib.rs +0 -16
  48. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/LICENSE +0 -0
  49. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/clock.rs +0 -0
  50. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/native/shared-index/src/sync.rs +0 -0
  51. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/__init__.py +0 -0
  52. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/__init__.py +0 -0
  53. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/canonical.py +0 -0
  54. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/codec.py +0 -0
  55. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/collation.py +0 -0
  56. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/collection_metadata.py +0 -0
  57. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/count_reads.py +0 -0
  58. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/errors.py +0 -0
  59. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/find_one_reads.py +0 -0
  60. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/find_reads.py +0 -0
  61. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/identity_reads.py +0 -0
  62. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/keys.py +0 -0
  63. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/lifecycle.py +0 -0
  64. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/locking.py +0 -0
  65. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/lru.py +0 -0
  66. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/namespace.py +0 -0
  67. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/namespace_metadata.py +0 -0
  68. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/order_sensitive_keys.py +0 -0
  69. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/projection.py +0 -0
  70. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/query_filters.py +0 -0
  71. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/read_classification.py +0 -0
  72. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/read_validation.py +0 -0
  73. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/snapshots.py +0 -0
  74. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/stream_activation.py +0 -0
  75. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/stream_cost.py +0 -0
  76. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/stream_health.py +0 -0
  77. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/stream_options.py +0 -0
  78. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/traversal.py +0 -0
  79. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_core/unique_keys.py +0 -0
  80. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_shared/__init__.py +0 -0
  81. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_shared/coordinator.py +0 -0
  82. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/_types.py +0 -0
  83. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/asynchronous/collection.py +0 -0
  84. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/asynchronous/cursors.py +0 -0
  85. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/asynchronous/database.py +0 -0
  86. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/asynchronous/shared.py +0 -0
  87. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/otel.py +0 -0
  88. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/py.typed +0 -0
  89. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/synchronous/collection.py +0 -0
  90. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/synchronous/cursors.py +0 -0
  91. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/synchronous/database.py +0 -0
  92. {client_query_cache-0.9.0 → client_query_cache-0.10.0}/src/client_query_cache/synchronous/shared.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: client-query-cache
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Classifier: Framework :: AsyncIO
5
5
  Classifier: Operating System :: POSIX :: Linux
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -26,7 +26,7 @@ Project-URL: Repository, https://github.com/alessio-locatelli/client-query-cache
26
26
  # Client-side query caching for PyMongo
27
27
 
28
28
  [![PyPI](https://img.shields.io/pypi/v/client-query-cache?color=lightgrey)](https://pypi.org/project/client-query-cache/)
29
- [![Python](https://img.shields.io/pypi/pyversions/client-query-cache)](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
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
30
  [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
31
31
  [![Typing](https://img.shields.io/pypi/types/client-query-cache)](https://pypi.org/project/client-query-cache/)
32
32
 
@@ -68,6 +68,8 @@ uv add client-query-cache
68
68
 
69
69
  Or use `pip install client-query-cache`.
70
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
+
71
73
  ```python
72
74
  from pymongo import MongoClient
73
75
 
@@ -1,7 +1,7 @@
1
1
  # Client-side query caching for PyMongo
2
2
 
3
3
  [![PyPI](https://img.shields.io/pypi/v/client-query-cache?color=lightgrey)](https://pypi.org/project/client-query-cache/)
4
- [![Python](https://img.shields.io/pypi/pyversions/client-query-cache)](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
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
5
  [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
6
6
  [![Typing](https://img.shields.io/pypi/types/client-query-cache)](https://pypi.org/project/client-query-cache/)
7
7
 
@@ -43,6 +43,8 @@ uv add client-query-cache
43
43
 
44
44
  Or use `pip install client-query-cache`.
45
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
+
46
48
  ```python
47
49
  from pymongo import MongoClient
48
50
 
@@ -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
+ }
@@ -340,197 +340,4 @@ impl Drop for Echo {
340
340
  }
341
341
 
342
342
  #[cfg(test)]
343
- mod tests {
344
- use std::sync::Arc;
345
-
346
- use super::*;
347
- use crate::index::{Layout, Writer};
348
- use crate::memory::zeroed;
349
- use crate::sync::AtomicU64;
350
-
351
- const EPOCH: u64 = 3;
352
-
353
- fn published() -> Index<Arc<[AtomicU64]>> {
354
- let layout = Layout::new(8, 64, 1, 1).unwrap();
355
- let index = Index::create(zeroed(layout.words()), layout, 5, u64::MAX).unwrap();
356
- assert!(
357
- index.set_available(0, true) && index.set_progress(0, 1) && index.set_epoch(0, EPOCH)
358
- );
359
- let query = Query {
360
- database: 0,
361
- namespace: 0,
362
- epoch: EPOCH,
363
- identity: b"present",
364
- shape: b"shape",
365
- };
366
- let mut writer = Writer::new(index.clone());
367
- let payload = writer.store(b"payload").unwrap();
368
- writer.publish(&query, payload).unwrap();
369
- index
370
- }
371
-
372
- fn selection(identity: &[u8]) -> Vec<u8> {
373
- let mut bytes = Vec::new();
374
- bytes.extend_from_slice(&0_u32.to_le_bytes());
375
- bytes.extend_from_slice(&0_u32.to_le_bytes());
376
- bytes.extend_from_slice(&EPOCH.to_le_bytes());
377
- bytes.extend_from_slice(&(identity.len() as u32).to_le_bytes());
378
- bytes.extend_from_slice(&5_u32.to_le_bytes());
379
- bytes.extend_from_slice(identity);
380
- bytes.extend_from_slice(b"shape");
381
- bytes
382
- }
383
-
384
- fn request(request_id: u32, identities: &[&[u8]]) -> Vec<u8> {
385
- let mut body = Vec::new();
386
- body.extend_from_slice(&request_id.to_le_bytes());
387
- body.extend_from_slice(&(identities.len() as u32).to_le_bytes());
388
- for identity in identities {
389
- body.extend_from_slice(&selection(identity));
390
- }
391
- let mut frame = (body.len() as u32).to_le_bytes().to_vec();
392
- frame.extend_from_slice(&body);
393
- frame
394
- }
395
-
396
- fn expected_reply(request_id: u32) -> Vec<u8> {
397
- let mut body = Vec::new();
398
- body.extend_from_slice(&request_id.to_le_bytes());
399
- body.extend_from_slice(&2_u32.to_le_bytes());
400
- body.extend_from_slice(&7_u32.to_le_bytes());
401
- body.extend_from_slice(b"payload");
402
- body.extend_from_slice(&MISS.to_le_bytes());
403
- let mut frame = (body.len() as u32).to_le_bytes().to_vec();
404
- frame.extend_from_slice(&body);
405
- frame
406
- }
407
-
408
- #[test]
409
- fn answers_each_selection_in_order() {
410
- let index = published();
411
- let mut out = Vec::new();
412
- let frame = request(9, &[b"present", b"absent"]);
413
- answer_frame(&index, 5, &frame[LENGTH_BYTES..], 1, &mut out).unwrap();
414
- assert_eq!(out, expected_reply(9));
415
- }
416
-
417
- #[test]
418
- fn rejects_malformed_frames() {
419
- let index = published();
420
- let frame = request(9, &[b"present"]);
421
- let body = &frame[LENGTH_BYTES..];
422
- let mut overstated = body.to_vec();
423
- overstated[4] = 2;
424
- let mut trailing = body.to_vec();
425
- trailing.push(0);
426
- for malformed in [
427
- &body[..body.len() - 1],
428
- &body[..6],
429
- &overstated[..],
430
- &trailing[..],
431
- ] {
432
- assert!(answer_frame(&index, 5, malformed, 1, &mut Vec::new()).is_err());
433
- }
434
- }
435
-
436
- fn connection() -> (Connection, UnixStream) {
437
- let (stream, peer) = UnixStream::pair().unwrap();
438
- stream.set_nonblocking(true).unwrap();
439
- let connection = Connection {
440
- stream,
441
- inbox: Vec::new(),
442
- outbox: Vec::new(),
443
- sent: 0,
444
- writable: false,
445
- };
446
- (connection, peer)
447
- }
448
-
449
- #[test]
450
- fn rejects_more_selections_than_one_frame_may_carry() {
451
- let index = published();
452
- let identities = vec![&b"present"[..]; SELECTION_LIMIT as usize + 1];
453
- let frame = request(9, &identities);
454
- assert!(answer_frame(&index, 5, &frame[LENGTH_BYTES..], 1, &mut Vec::new()).is_err());
455
- let frame = request(9, &identities[1..]);
456
- assert!(answer_frame(&index, 5, &frame[LENGTH_BYTES..], 1, &mut Vec::new()).is_ok());
457
- }
458
-
459
- #[test]
460
- fn rejects_replies_larger_than_a_frame() {
461
- let payload = vec![7_u8; REPLY_LIMIT / 9];
462
- let layout = Layout::new(8, payload.len(), 1, 1).unwrap();
463
- let index = Index::create(zeroed(layout.words()), layout, 5, u64::MAX).unwrap();
464
- assert!(
465
- index.set_available(0, true) && index.set_progress(0, 1) && index.set_epoch(0, EPOCH)
466
- );
467
- let mut writer = Writer::new(index.clone());
468
- let stored = writer.store(&payload).unwrap();
469
- let query = Query {
470
- database: 0,
471
- namespace: 0,
472
- epoch: EPOCH,
473
- identity: b"present",
474
- shape: b"shape",
475
- };
476
- writer.publish(&query, stored).unwrap();
477
- let fits = request(9, &[&b"present"[..]; 8]);
478
- assert!(answer_frame(&index, 5, &fits[LENGTH_BYTES..], 1, &mut Vec::new()).is_ok());
479
- let oversized = request(9, &[&b"present"[..]; 9]);
480
- assert!(answer_frame(&index, 5, &oversized[LENGTH_BYTES..], 1, &mut Vec::new()).is_err());
481
- }
482
-
483
- #[test]
484
- fn closes_a_connection_whose_replies_are_not_read() {
485
- let index = published();
486
- let (mut connection, _peer) = connection();
487
- connection.outbox = vec![0; QUEUE_LIMIT];
488
- connection.inbox = request(9, &[b"present"]);
489
- assert!(connection.answer(&index, 5).is_err());
490
- connection
491
- .outbox
492
- .truncate(QUEUE_LIMIT - expected_reply(9).len());
493
- connection.inbox = request(9, &[b"present", b"absent"]);
494
- assert!(connection.answer(&index, 5).is_ok());
495
- }
496
-
497
- #[test]
498
- fn buffers_at_most_one_frame_of_unanswered_input() {
499
- let (mut connection, mut peer) = connection();
500
- let writer = std::thread::spawn(move || {
501
- let _ = peer.write_all(&vec![0xff; 2 * INBOX_LIMIT]);
502
- });
503
- let mut scratch = vec![0_u8; RECEIVE_BYTES];
504
- while connection.inbox.len() < INBOX_LIMIT {
505
- assert!(connection.receive(&mut scratch));
506
- }
507
- assert!(connection.receive(&mut scratch));
508
- assert_eq!(connection.inbox.len(), INBOX_LIMIT);
509
- drop(connection);
510
- writer.join().unwrap();
511
- }
512
-
513
- #[test]
514
- fn serves_frames_split_across_reads_over_a_socket() {
515
- let directory =
516
- std::env::temp_dir().join(format!("shared-index-echo-{}", std::process::id()));
517
- std::fs::create_dir_all(&directory).unwrap();
518
- let path = directory.join("echo.sock");
519
- let _ = std::fs::remove_file(&path);
520
- let mut echo = Echo::start(published(), UnixListener::bind(&path).unwrap()).unwrap();
521
- let mut client = UnixStream::connect(&path).unwrap();
522
- let mut frames = request(1, &[b"present", b"absent"]);
523
- frames.extend_from_slice(&request(2, &[b"present", b"absent"]));
524
- let (first, second) = frames.split_at(10);
525
- client.write_all(first).unwrap();
526
- std::thread::sleep(std::time::Duration::from_millis(20));
527
- client.write_all(second).unwrap();
528
- let mut expected = expected_reply(1);
529
- expected.extend_from_slice(&expected_reply(2));
530
- let mut reply = vec![0; expected.len()];
531
- client.read_exact(&mut reply).unwrap();
532
- assert_eq!(reply, expected);
533
- echo.stop();
534
- std::fs::remove_dir_all(&directory).unwrap();
535
- }
536
- }
343
+ mod tests;