1use std::sync::Arc;
33
34use async_trait::async_trait;
35use bytes::Bytes;
36
37use crate::{
38 gate, BootstrapReport, ComposeSpec, DeleteManyReport, EpochLease, EpochState,
39 GenerationStamps, LifecycleView,
40 ListedObject, ListedVersion, ObjectMeta, ObjectStore, PendingUpload, PresignedPut, PutCondition,
41 RequestCounts, RetentionOutcome, StoreError, StoreResult,
42};
43
44pub struct ReadOnly<S: ?Sized> {
47 inner: Arc<S>,
48}
49
50impl<S: ObjectStore + ?Sized> ReadOnly<S> {
51 pub fn new(inner: Arc<S>) -> Self {
54 ReadOnly { inner }
55 }
56}
57
58fn refuse<T>(verb: &str, key: &str) -> StoreResult<T> {
59 Err(StoreError::Auth(format!("read-only store: {verb} {key}")))
60}
61
62#[async_trait]
63impl<S: ObjectStore + ?Sized> ObjectStore for ReadOnly<S> {
64 async fn put_whole(
67 &self,
68 key: &str,
69 _body: Bytes,
70 _condition: &PutCondition,
71 _stamps: &GenerationStamps,
72 _crc64: u64,
73 ) -> StoreResult<ObjectMeta> {
74 refuse("put_whole", key)
75 }
76
77 async fn copy_object(
78 &self,
79 _src_key: &str,
80 _src_if_match: Option<&str>,
81 dst_key: &str,
82 _condition: &PutCondition,
83 _stamps: &GenerationStamps,
84 ) -> StoreResult<ObjectMeta> {
85 refuse("copy_object", dst_key)
86 }
87
88 async fn compose_generation(&self, spec: &ComposeSpec<'_>) -> StoreResult<ObjectMeta> {
89 refuse("compose_generation", spec.key)
90 }
91
92 async fn delete(&self, key: &str) -> StoreResult<()> {
93 refuse("delete", key)
94 }
95
96 async fn delete_if_match(&self, key: &str, _etag: &str) -> StoreResult<()> {
97 refuse("delete_if_match", key)
98 }
99
100 async fn delete_many(&self, keys: &[String]) -> StoreResult<DeleteManyReport> {
101 refuse("delete_many", keys.first().map(String::as_str).unwrap_or("(no keys)"))
104 }
105
106 async fn delete_version(&self, key: &str, _version_id: &str) -> StoreResult<()> {
107 refuse("delete_version", key)
108 }
109
110 async fn presign_put(
111 &self,
112 key: &str,
113 _ttl_secs: u64,
114 _sha256: &[u8; 32],
115 ) -> StoreResult<PresignedPut> {
116 refuse("presign_put", key)
117 }
118
119 async fn ensure_noncurrent_retention(
120 &self,
121 prefix: &str,
122 _days: u64,
123 ) -> StoreResult<RetentionOutcome> {
124 refuse("ensure_noncurrent_retention", prefix)
125 }
126
127 async fn abort_upload(&self, key: &str, _upload_id: &str) -> StoreResult<()> {
128 refuse("abort_upload", key)
129 }
130
131 async fn bootstrap(&self, prefix: &str) -> StoreResult<BootstrapReport> {
134 refuse("bootstrap", prefix)
135 }
136
137 async fn epoch_acquire(
138 &self,
139 key: &str,
140 _holder_id: &str,
141 _supersede: Option<&EpochState>,
142 ) -> StoreResult<EpochLease> {
143 refuse("epoch_acquire", key)
144 }
145
146 async fn epoch_renew(
147 &self,
148 key: &str,
149 _lease: &EpochLease,
150 _echo: Option<&str>,
151 ) -> StoreResult<EpochLease> {
152 refuse("epoch_renew", key)
153 }
154
155 async fn epoch_release(&self, key: &str, _lease: &EpochLease) -> StoreResult<()> {
156 refuse("epoch_release", key)
157 }
158
159 async fn epoch_enqueue(
160 &self,
161 key: &str,
162 _observed: &EpochState,
163 _holder_id: &str,
164 ) -> StoreResult<EpochState> {
165 refuse("epoch_enqueue", key)
166 }
167
168 async fn epoch_handoff(
169 &self,
170 key: &str,
171 _lease: &EpochLease,
172 _echo: Option<&str>,
173 ) -> StoreResult<()> {
174 refuse("epoch_handoff", key)
175 }
176
177 async fn head(&self, key: &str) -> StoreResult<ObjectMeta> {
180 self.inner.head(key).await
181 }
182
183 async fn get_whole(&self, key: &str, if_match: Option<&str>) -> StoreResult<(ObjectMeta, Bytes)> {
184 self.inner.get_whole(key, if_match).await
185 }
186
187 async fn get_range(
188 &self,
189 key: &str,
190 offset: u64,
191 len: u64,
192 if_match: &str,
193 ) -> StoreResult<Bytes> {
194 self.inner.get_range(key, offset, len, if_match).await
195 }
196
197 async fn get_range_segments(
198 &self,
199 key: &str,
200 offset: u64,
201 len: u64,
202 if_match: &str,
203 ) -> StoreResult<Vec<Bytes>> {
204 self.inner.get_range_segments(key, offset, len, if_match).await
205 }
206
207 async fn list(&self, prefix: &str) -> StoreResult<Vec<ListedObject>> {
208 self.inner.list(prefix).await
209 }
210
211 async fn head_version(&self, key: &str, version_id: &str) -> StoreResult<ObjectMeta> {
212 self.inner.head_version(key, version_id).await
213 }
214
215 async fn get_version(&self, key: &str, version_id: &str) -> StoreResult<(ObjectMeta, Bytes)> {
216 self.inner.get_version(key, version_id).await
217 }
218
219 async fn list_versions(&self, prefix: &str) -> StoreResult<Vec<ListedVersion>> {
220 self.inner.list_versions(prefix).await
221 }
222
223 async fn presign_get(&self, key: &str, ttl_secs: u64) -> StoreResult<String> {
225 self.inner.presign_get(key, ttl_secs).await
226 }
227
228 async fn lifecycle_rules(&self) -> StoreResult<Vec<LifecycleView>> {
229 self.inner.lifecycle_rules().await
230 }
231
232 async fn list_uploads(&self, prefix: &str) -> StoreResult<Vec<PendingUpload>> {
233 self.inner.list_uploads(prefix).await
234 }
235
236 async fn epoch_read(&self, key: &str) -> StoreResult<Option<EpochState>> {
237 self.inner.epoch_read(key).await
238 }
239
240 fn min_part_size(&self) -> u64 {
241 self.inner.min_part_size()
242 }
243
244 fn max_parts(&self) -> usize {
245 self.inner.max_parts()
246 }
247
248 fn upload_gate(&self) -> Option<Arc<gate::ByteGate>> {
249 self.inner.upload_gate()
250 }
251
252 fn request_counts(&self) -> Option<RequestCounts> {
253 self.inner.request_counts()
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::memory::MemoryStore;
261 use crate::PartSource;
262 use std::sync::Mutex;
263
264 const WRITES: &[&str] = &[
266 "put_whole", "copy_object", "compose_generation", "delete", "delete_if_match",
267 "delete_many", "delete_version", "presign_put", "ensure_noncurrent_retention",
268 "abort_upload",
269 "bootstrap", "epoch_acquire", "epoch_renew", "epoch_release", "epoch_enqueue",
270 "epoch_handoff",
271 ];
272
273 const READS: &[&str] = &[
276 "head", "get_whole", "get_range", "get_range_segments", "list", "head_version",
277 "get_version", "list_versions", "presign_get", "lifecycle_rules", "list_uploads",
278 "epoch_read", "min_part_size", "max_parts", "upload_gate", "request_counts",
279 ];
280
281 fn declared(src: &str, opener: &str) -> Vec<String> {
285 let lines: Vec<&str> = src.lines().collect();
286 let start = lines
287 .iter()
288 .position(|l| l.trim() == opener)
289 .unwrap_or_else(|| panic!("no line `{opener}` in the source"));
290 let indent = &lines[start][..lines[start].len() - lines[start].trim_start().len()];
291 let close = format!("{indent}}}");
292 let end = start + lines[start..].iter().position(|l| *l == close).expect("the block closes");
293 let mut names: Vec<String> = lines[start + 1..end]
294 .iter()
295 .filter_map(|l| {
296 let l = l.trim_start();
297 let rest = l.strip_prefix("async fn ").or_else(|| l.strip_prefix("fn "))?;
298 Some(rest[..rest.find(|c: char| !(c.is_alphanumeric() || c == '_'))?].to_string())
299 })
300 .collect();
301 names.sort();
302 names
303 }
304
305 #[test]
311 fn every_trait_method_is_filed_and_every_one_is_implemented_by_name() {
312 let mut filed: Vec<String> = WRITES.iter().chain(READS).map(|s| s.to_string()).collect();
313 filed.sort();
314 let mut dedup = filed.clone();
315 dedup.dedup();
316 assert_eq!(filed, dedup, "a method is filed twice");
317
318 let trait_fns = declared(include_str!("lib.rs"), "pub trait ObjectStore: Send + Sync {");
319 assert!(trait_fns.contains(&"put_whole".to_string()) && trait_fns.contains(&"request_counts".to_string()));
322 assert_eq!(trait_fns, filed, "the trait's methods and the read/write filing disagree");
323
324 let src = include_str!("readonly.rs");
325 assert_eq!(
326 declared(src, "impl<S: ObjectStore + ?Sized> ObjectStore for ReadOnly<S> {"),
327 filed,
328 "ReadOnly must name every trait method, defaulted ones included"
329 );
330 assert_eq!(
331 declared(src, "impl ObjectStore for Recorder {"),
332 filed,
333 "the recording double must name every trait method, or it cannot see the wrapper call one"
334 );
335 }
336
337 struct Recorder {
342 inner: MemoryStore,
343 calls: Mutex<Vec<&'static str>>,
344 }
345
346 impl Recorder {
347 fn saw(&self, m: &'static str) {
348 self.calls.lock().unwrap().push(m);
349 }
350 fn take(&self) -> Vec<&'static str> {
351 std::mem::take(&mut *self.calls.lock().unwrap())
352 }
353 }
354
355 #[async_trait]
356 impl ObjectStore for Recorder {
357 async fn put_whole(
358 &self,
359 key: &str,
360 body: Bytes,
361 condition: &PutCondition,
362 stamps: &GenerationStamps,
363 crc64: u64,
364 ) -> StoreResult<ObjectMeta> {
365 self.saw("put_whole");
366 self.inner.put_whole(key, body, condition, stamps, crc64).await
367 }
368 async fn copy_object(
369 &self,
370 src_key: &str,
371 src_if_match: Option<&str>,
372 dst_key: &str,
373 condition: &PutCondition,
374 stamps: &GenerationStamps,
375 ) -> StoreResult<ObjectMeta> {
376 self.saw("copy_object");
377 self.inner.copy_object(src_key, src_if_match, dst_key, condition, stamps).await
378 }
379 async fn compose_generation(&self, spec: &ComposeSpec<'_>) -> StoreResult<ObjectMeta> {
380 self.saw("compose_generation");
381 self.inner.compose_generation(spec).await
382 }
383 async fn head(&self, key: &str) -> StoreResult<ObjectMeta> {
384 self.saw("head");
385 self.inner.head(key).await
386 }
387 async fn get_whole(&self, key: &str, if_match: Option<&str>) -> StoreResult<(ObjectMeta, Bytes)> {
388 self.saw("get_whole");
389 self.inner.get_whole(key, if_match).await
390 }
391 async fn get_range(&self, key: &str, offset: u64, len: u64, if_match: &str) -> StoreResult<Bytes> {
392 self.saw("get_range");
393 self.inner.get_range(key, offset, len, if_match).await
394 }
395 async fn get_range_segments(
396 &self,
397 key: &str,
398 offset: u64,
399 len: u64,
400 if_match: &str,
401 ) -> StoreResult<Vec<Bytes>> {
402 self.saw("get_range_segments");
403 self.inner.get_range_segments(key, offset, len, if_match).await
404 }
405 async fn list(&self, prefix: &str) -> StoreResult<Vec<ListedObject>> {
406 self.saw("list");
407 self.inner.list(prefix).await
408 }
409 async fn delete(&self, key: &str) -> StoreResult<()> {
410 self.saw("delete");
411 self.inner.delete(key).await
412 }
413 async fn delete_if_match(&self, key: &str, etag: &str) -> StoreResult<()> {
414 self.saw("delete_if_match");
415 self.inner.delete_if_match(key, etag).await
416 }
417 async fn delete_many(&self, keys: &[String]) -> StoreResult<DeleteManyReport> {
418 self.saw("delete_many");
419 self.inner.delete_many(keys).await
420 }
421 async fn head_version(&self, key: &str, version_id: &str) -> StoreResult<ObjectMeta> {
422 self.saw("head_version");
423 self.inner.head_version(key, version_id).await
424 }
425 async fn get_version(&self, key: &str, version_id: &str) -> StoreResult<(ObjectMeta, Bytes)> {
426 self.saw("get_version");
427 self.inner.get_version(key, version_id).await
428 }
429 async fn delete_version(&self, key: &str, version_id: &str) -> StoreResult<()> {
430 self.saw("delete_version");
431 self.inner.delete_version(key, version_id).await
432 }
433 async fn list_versions(&self, prefix: &str) -> StoreResult<Vec<ListedVersion>> {
434 self.saw("list_versions");
435 self.inner.list_versions(prefix).await
436 }
437 async fn presign_get(&self, key: &str, ttl_secs: u64) -> StoreResult<String> {
438 self.saw("presign_get");
439 self.inner.presign_get(key, ttl_secs).await
440 }
441 async fn presign_put(
442 &self,
443 key: &str,
444 ttl_secs: u64,
445 sha256: &[u8; 32],
446 ) -> StoreResult<PresignedPut> {
447 self.saw("presign_put");
448 self.inner.presign_put(key, ttl_secs, sha256).await
449 }
450 async fn lifecycle_rules(&self) -> StoreResult<Vec<LifecycleView>> {
451 self.saw("lifecycle_rules");
452 self.inner.lifecycle_rules().await
453 }
454 async fn ensure_noncurrent_retention(&self, prefix: &str, days: u64) -> StoreResult<RetentionOutcome> {
455 self.saw("ensure_noncurrent_retention");
456 self.inner.ensure_noncurrent_retention(prefix, days).await
457 }
458 async fn list_uploads(&self, prefix: &str) -> StoreResult<Vec<PendingUpload>> {
459 self.saw("list_uploads");
460 self.inner.list_uploads(prefix).await
461 }
462 async fn abort_upload(&self, key: &str, upload_id: &str) -> StoreResult<()> {
463 self.saw("abort_upload");
464 self.inner.abort_upload(key, upload_id).await
465 }
466 async fn bootstrap(&self, prefix: &str) -> StoreResult<BootstrapReport> {
467 self.saw("bootstrap");
468 self.inner.bootstrap(prefix).await
469 }
470 async fn epoch_read(&self, key: &str) -> StoreResult<Option<EpochState>> {
471 self.saw("epoch_read");
472 self.inner.epoch_read(key).await
473 }
474 async fn epoch_acquire(
475 &self,
476 key: &str,
477 holder_id: &str,
478 supersede: Option<&EpochState>,
479 ) -> StoreResult<EpochLease> {
480 self.saw("epoch_acquire");
481 self.inner.epoch_acquire(key, holder_id, supersede).await
482 }
483 async fn epoch_renew(&self, key: &str, lease: &EpochLease, echo: Option<&str>) -> StoreResult<EpochLease> {
484 self.saw("epoch_renew");
485 self.inner.epoch_renew(key, lease, echo).await
486 }
487 async fn epoch_release(&self, key: &str, lease: &EpochLease) -> StoreResult<()> {
488 self.saw("epoch_release");
489 self.inner.epoch_release(key, lease).await
490 }
491 async fn epoch_enqueue(&self, key: &str, observed: &EpochState, holder_id: &str) -> StoreResult<EpochState> {
492 self.saw("epoch_enqueue");
493 self.inner.epoch_enqueue(key, observed, holder_id).await
494 }
495 async fn epoch_handoff(&self, key: &str, lease: &EpochLease, echo: Option<&str>) -> StoreResult<()> {
496 self.saw("epoch_handoff");
497 self.inner.epoch_handoff(key, lease, echo).await
498 }
499 fn min_part_size(&self) -> u64 {
500 self.saw("min_part_size");
501 self.inner.min_part_size()
502 }
503 fn max_parts(&self) -> usize {
504 self.saw("max_parts");
505 self.inner.max_parts()
506 }
507 fn upload_gate(&self) -> Option<Arc<gate::ByteGate>> {
508 self.saw("upload_gate");
509 self.inner.upload_gate()
510 }
511 fn request_counts(&self) -> Option<RequestCounts> {
512 self.saw("request_counts");
513 self.inner.request_counts()
514 }
515 }
516
517 fn stamps() -> GenerationStamps {
518 GenerationStamps { generation: 1, epoch: 0, flush_uuid: "ro-test".into(), boundary_source: None, posix: None }
519 }
520
521 #[tokio::test]
527 async fn a_read_only_store_refuses_every_write_unsent_and_forwards_every_read() {
528 let mut mem = MemoryStore::new().with_upload_inflight_max_bytes(1 << 20);
531 mem.min_part = 7;
532 mem.max_parts = 9;
533 mem.plant_lifecycle_rule(LifecycleView {
534 id: "customer".into(),
535 enabled: true,
536 prefix: "p/".into(),
537 noncurrent_days: Some(3),
538 expired_delete_marker: false,
539 });
540 let rec = Arc::new(Recorder { inner: mem, calls: Mutex::new(vec![]) });
541 let body = Bytes::from_static(b"hello, read-only world");
542 let crc = crate::crc64_nvme(&body);
543 let v1 = rec.inner.put_whole("p/k", body.clone(), &PutCondition::IfNoneMatchAny, &stamps(), crc).await.unwrap();
544 let v2 = rec
545 .inner
546 .put_whole("p/k", body.clone(), &PutCondition::IfMatch(v1.etag.clone()), &stamps(), crc)
547 .await
548 .unwrap();
549 let head_before = rec.inner.head("p/k").await.unwrap();
550 let lease = rec.inner.epoch_acquire("p/epoch", "holder", None).await.unwrap();
551 let cell = rec.inner.epoch_read("p/epoch").await.unwrap().unwrap();
552 let upload_id = rec.inner.raw_begin_upload("p/mpu");
553 rec.inner.reset_op_counts();
554 rec.take();
555
556 let ro = ReadOnly::new(rec.clone());
557
558 let mut refused: Vec<&str> = vec![];
560 let mut expect_refused = |verb: &'static str, r: StoreResult<()>| {
561 match r {
562 Err(StoreError::Auth(m)) if m.starts_with(&format!("read-only store: {verb} ")) => {}
563 other => panic!("{verb} on a read-only store answered {other:?}, not Auth"),
564 }
565 assert_eq!(rec.take(), Vec::<&str>::new(), "{verb} reached the inner store");
566 assert_eq!(rec.inner.total_ops(), 0, "{verb} sent a request: {:?}", rec.inner.op_counts());
567 refused.push(verb);
568 };
569 let spec = ComposeSpec {
570 key: "p/composed",
571 local: None,
573 parts: vec![PartSource::Local { offset: 0, len: 1 }],
574 base_key: None,
575 base_etag: None,
576 condition: PutCondition::Unconditional,
577 stamps: stamps(),
578 crc64: Some(0),
579 progress: None,
580 };
581 let cond = PutCondition::Unconditional;
582 expect_refused("put_whole", ro.put_whole("p/new", body.clone(), &cond, &stamps(), crc).await.map(drop));
583 expect_refused("copy_object", ro.copy_object("p/k", None, "p/copy", &cond, &stamps()).await.map(drop));
584 expect_refused("compose_generation", ro.compose_generation(&spec).await.map(drop));
585 expect_refused("delete", ro.delete("p/k").await);
586 expect_refused("delete_if_match", ro.delete_if_match("p/k", &v2.etag).await);
587 expect_refused("delete_many", ro.delete_many(&["p/k".to_string()]).await.map(drop));
588 expect_refused("delete_version", ro.delete_version("p/k", v1.version_id.as_deref().unwrap()).await);
589 expect_refused("presign_put", ro.presign_put("p/k", 60, &[0; 32]).await.map(drop));
590 expect_refused("ensure_noncurrent_retention", ro.ensure_noncurrent_retention("p/", 30).await.map(drop));
591 expect_refused("abort_upload", ro.abort_upload("p/mpu", &upload_id).await);
592 expect_refused("bootstrap", ro.bootstrap("p/").await.map(drop));
593 expect_refused("epoch_acquire", ro.epoch_acquire("p/epoch2", "h2", None).await.map(drop));
594 expect_refused("epoch_renew", ro.epoch_renew("p/epoch", &lease, None).await.map(drop));
595 expect_refused("epoch_release", ro.epoch_release("p/epoch", &lease).await);
596 expect_refused("epoch_enqueue", ro.epoch_enqueue("p/epoch", &cell, "h3").await.map(drop));
597 expect_refused("epoch_handoff", ro.epoch_handoff("p/epoch", &lease, None).await);
598 assert_eq!(refused, WRITES, "every write, and only the writes, were exercised");
599 assert_eq!(rec.inner.head("p/k").await.unwrap(), head_before);
602 assert_eq!(rec.inner.version_count("p/k"), 2);
603 assert_eq!(rec.inner.epoch_read("p/epoch").await.unwrap().unwrap(), cell);
604 assert_eq!(rec.inner.list_uploads("p/").await.unwrap().len(), 1);
605 rec.take();
606
607 let mut forwarded: Vec<&str> = vec![];
609 let mut reached = |verb: &'static str| {
610 assert_eq!(rec.take(), vec![verb], "{verb} did not reach the inner store as itself");
611 forwarded.push(verb);
612 };
613 let vid1 = v1.version_id.clone().unwrap();
614 assert_eq!(ro.head("p/k").await.unwrap(), head_before);
615 reached("head");
616 let whole = ro.get_whole("p/k", Some(&v2.etag)).await.unwrap();
617 assert_eq!(whole, rec.inner.get_whole("p/k", Some(&v2.etag)).await.unwrap());
618 assert_eq!(whole.1, body);
619 reached("get_whole");
620 assert_eq!(ro.get_range("p/k", 7, 9, &v2.etag).await.unwrap(), body.slice(7..16));
621 reached("get_range");
622 assert_eq!(ro.get_range_segments("p/k", 7, 9, &v2.etag).await.unwrap().concat(), body.slice(7..16));
623 reached("get_range_segments");
624 assert_eq!(ro.list("p/").await.unwrap(), rec.inner.list("p/").await.unwrap());
625 reached("list");
626 assert_eq!(ro.head_version("p/k", &vid1).await.unwrap(), rec.inner.head_version("p/k", &vid1).await.unwrap());
627 reached("head_version");
628 assert_eq!(ro.get_version("p/k", &vid1).await.unwrap(), rec.inner.get_version("p/k", &vid1).await.unwrap());
629 reached("get_version");
630 let versions = ro.list_versions("p/").await.unwrap();
631 assert_eq!(versions, rec.inner.list_versions("p/").await.unwrap());
632 reached("list_versions");
633 assert!(versions.iter().filter(|v| v.key == "p/k").count() == 2, "{versions:?}");
634 assert_eq!(ro.presign_get("p/k", 60).await.unwrap(), rec.inner.presign_get("p/k", 60).await.unwrap());
635 reached("presign_get");
636 let rules = ro.lifecycle_rules().await.unwrap();
637 assert_eq!(rules, rec.inner.lifecycle_rules().await.unwrap());
638 assert_eq!(rules.len(), 1);
639 reached("lifecycle_rules");
640 assert_eq!(ro.list_uploads("p/").await.unwrap(), rec.inner.list_uploads("p/").await.unwrap());
641 reached("list_uploads");
642 assert_eq!(ro.epoch_read("p/epoch").await.unwrap(), Some(cell.clone()));
643 reached("epoch_read");
644 assert_eq!(ro.min_part_size(), 7);
645 reached("min_part_size");
646 assert_eq!(ro.max_parts(), 9);
647 reached("max_parts");
648 let gate = ro.upload_gate().expect("the inner store has a byte gate");
649 assert!(Arc::ptr_eq(&gate, &rec.inner.upload_gate().unwrap()));
650 reached("upload_gate");
651 let counts = ro.request_counts().expect("the inner store counts");
652 assert_eq!(counts, rec.inner.request_counts().unwrap());
653 assert!(counts.get > 0 && counts.put == 0, "{counts:?}");
654 reached("request_counts");
655 assert_eq!(forwarded, READS, "every read, and only the reads, were exercised");
656
657 let writes: Vec<_> = rec
659 .inner
660 .op_counts()
661 .into_iter()
662 .filter(|(op, _)| !READS.contains(op))
663 .collect();
664 assert_eq!(writes, vec![], "a read sent a write");
665 }
666}