Skip to content

Commit 04b3bcd

Browse files
ma2bdMathieuDutSik
andauthored
Fix chunks_exact when the chunk size is zero (#4731)
## Motivation See #4727 ## Proposal `chunks_exact` may face a "0 divided by 0" situation, but in this case we can return an infinite stream of empty chunks. ## Test Plan CI ## Release Plan - These changes should be backported to the latest `testnet` branch, then - be released in a new SDK, --------- Co-authored-by: Mathieu Dutour Sikiric <[email protected]>
1 parent 8ca10f5 commit 04b3bcd

File tree

8 files changed

+90
-12
lines changed

8 files changed

+90
-12
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

linera-sdk/tests/fixtures/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

linera-views/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ derive_more = { workspace = true, features = ["from"] }
4444
futures.workspace = true
4545
generic-array.workspace = true
4646
hex = { workspace = true, optional = true }
47+
itertools.workspace = true
4748
linera-base.workspace = true
4849
linera-views-derive.workspace = true
4950
linera-witty.workspace = true

linera-views/src/common.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ use std::{
99
Bound,
1010
Bound::{Excluded, Included, Unbounded},
1111
},
12+
slice::ChunksExact,
1213
};
1314

15+
use itertools::Either;
1416
use serde::de::DeserializeOwned;
1517

1618
use crate::ViewError;
@@ -313,6 +315,30 @@ pub(crate) const fn get_uleb128_size(len: usize) -> usize {
313315
expo
314316
}
315317

318+
/// Extention trait for slices.
319+
pub trait SliceExt<T> {
320+
/// Same as `chunks_exact` but we allow the `chunk_size` to be zero when the slice is empty.
321+
fn chunks_exact_or_repeat(
322+
&self,
323+
chunk_size: usize,
324+
) -> Either<ChunksExact<'_, T>, std::iter::Repeat<&[T]>>;
325+
}
326+
327+
impl<T> SliceExt<T> for [T] {
328+
fn chunks_exact_or_repeat(
329+
&self,
330+
chunk_size: usize,
331+
) -> Either<ChunksExact<'_, T>, std::iter::Repeat<&[T]>> {
332+
if chunk_size > 0 {
333+
Either::Left(self.chunks_exact(chunk_size))
334+
} else if self.is_empty() {
335+
Either::Right(std::iter::repeat(&[]))
336+
} else {
337+
panic!("chunk_size must be nonzero unless the slice is empty")
338+
}
339+
}
340+
}
341+
316342
#[cfg(test)]
317343
mod tests {
318344
use std::collections::BTreeSet;

linera-views/src/views/collection_view.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use serde::{de::DeserializeOwned, Serialize};
1616

1717
use crate::{
1818
batch::Batch,
19-
common::{CustomSerialize, HasherOutput, Update},
19+
common::{CustomSerialize, HasherOutput, SliceExt as _, Update},
2020
context::{BaseKey, Context},
2121
hashable_wrapper::WrappedHashableContainerView,
2222
store::ReadableKeyValueStore as _,
@@ -396,8 +396,9 @@ impl<W: View> ByteCollectionView<W::Context, W> {
396396
.read_multi_values_bytes(keys_to_load)
397397
.await?;
398398

399-
for (loaded_values, (position, context)) in
400-
values.chunks_exact(W::NUM_INIT_KEYS).zip(entries_to_load)
399+
for (loaded_values, (position, context)) in values
400+
.chunks_exact_or_repeat(W::NUM_INIT_KEYS)
401+
.zip(entries_to_load)
401402
{
402403
let view = W::post_load(context, loaded_values)?;
403404
let updates = self.updates.read().await;

linera-views/src/views/reentrant_collection_view.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use serde::{de::DeserializeOwned, Serialize};
1717

1818
use crate::{
1919
batch::Batch,
20-
common::{CustomSerialize, HasherOutput, Update},
20+
common::{CustomSerialize, HasherOutput, SliceExt as _, Update},
2121
context::{BaseKey, Context},
2222
hashable_wrapper::WrappedHashableContainerView,
2323
store::ReadableKeyValueStore as _,
@@ -540,7 +540,7 @@ impl<W: View> ReentrantByteCollectionView<W::Context, W> {
540540
}
541541
let values = self.context.store().read_multi_values_bytes(keys).await?;
542542
for (loaded_values, short_key) in values
543-
.chunks_exact(W::NUM_INIT_KEYS)
543+
.chunks_exact_or_repeat(W::NUM_INIT_KEYS)
544544
.zip(short_keys_to_load)
545545
{
546546
let key = self
@@ -637,8 +637,9 @@ impl<W: View> ReentrantByteCollectionView<W::Context, W> {
637637
.store()
638638
.read_multi_values_bytes(keys_to_load)
639639
.await?;
640-
for (loaded_values, (position, short_key, context)) in
641-
values.chunks_exact(W::NUM_INIT_KEYS).zip(entries_to_load)
640+
for (loaded_values, (position, short_key, context)) in values
641+
.chunks_exact_or_repeat(W::NUM_INIT_KEYS)
642+
.zip(entries_to_load)
642643
{
643644
let view = W::post_load(context, loaded_values)?;
644645
let wrapped_view = Arc::new(RwLock::new(view));
@@ -698,7 +699,7 @@ impl<W: View> ReentrantByteCollectionView<W::Context, W> {
698699
}
699700
let values = self.context.store().read_multi_values_bytes(keys).await?;
700701
for (loaded_values, (short_key, index)) in values
701-
.chunks_exact(W::NUM_INIT_KEYS)
702+
.chunks_exact_or_repeat(W::NUM_INIT_KEYS)
702703
.zip(short_keys_and_indexes)
703704
{
704705
let key = self
@@ -772,7 +773,7 @@ impl<W: View> ReentrantByteCollectionView<W::Context, W> {
772773

773774
let values = self.context.store().read_multi_values_bytes(keys).await?;
774775
for (loaded_values, short_key) in values
775-
.chunks_exact(W::NUM_INIT_KEYS)
776+
.chunks_exact_or_repeat(W::NUM_INIT_KEYS)
776777
.zip(short_keys_to_load)
777778
{
778779
let key = self

linera-views/tests/views_tests.rs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ use linera_views::{
1515
Batch, WriteOperation,
1616
WriteOperation::{Delete, DeletePrefix, Put},
1717
},
18-
collection_view::HashedCollectionView,
18+
collection_view::{CollectionView, HashedCollectionView},
1919
context::{Context, MemoryContext, ViewContext},
2020
key_value_store_view::{KeyValueStoreView, ViewContainer},
2121
log_view::HashedLogView,
2222
lru_caching::LruCachingMemoryDatabase,
23-
map_view::{ByteMapView, HashedMapView},
23+
map_view::{ByteMapView, HashedMapView, MapView},
2424
memory::MemoryDatabase,
2525
queue_view::HashedQueueView,
2626
random::make_deterministic_rng,
27-
reentrant_collection_view::HashedReentrantCollectionView,
27+
reentrant_collection_view::{HashedReentrantCollectionView, ReentrantCollectionView},
2828
register_view::HashedRegisterView,
2929
set_view::HashedSetView,
3030
store::{KeyValueDatabase, TestKeyValueDatabase as _, WritableKeyValueStore as _},
@@ -614,6 +614,52 @@ where
614614
.await
615615
}
616616

617+
#[derive(RootView)]
618+
pub struct NestedCollectionMapView<C> {
619+
pub map: CollectionView<C, String, MapView<C, String, u64>>,
620+
}
621+
622+
// This test exercise the case W::NUM_INIT_KEYS == 0
623+
// in CollectionView.
624+
#[tokio::test]
625+
async fn test_nested_collection_map_view() -> anyhow::Result<()> {
626+
let context = MemoryContext::new_for_testing(());
627+
{
628+
let mut view = NestedCollectionMapView::load(context.clone()).await?;
629+
let subview = view.map.load_entry_mut("Bonjour").await?;
630+
subview.insert("A bientot", 49)?;
631+
view.save().await?;
632+
}
633+
let view = NestedCollectionMapView::load(context).await?;
634+
let keys = vec!["Bonjour".to_string()];
635+
let subviews = view.map.try_load_entries(&keys).await?;
636+
assert!(subviews[0].is_some());
637+
Ok(())
638+
}
639+
640+
#[derive(RootView)]
641+
pub struct NestedReentrantCollectionMapView<C> {
642+
pub map: ReentrantCollectionView<C, String, MapView<C, String, u64>>,
643+
}
644+
645+
#[tokio::test]
646+
async fn test_nested_reentrant_collection_map_view() -> anyhow::Result<()> {
647+
let context = MemoryContext::new_for_testing(());
648+
{
649+
let mut view = NestedReentrantCollectionMapView::load(context.clone()).await?;
650+
{
651+
let mut subview = view.map.try_load_entry_mut("Bonjour").await?;
652+
subview.insert("A bientot", 49)?;
653+
}
654+
view.save().await?;
655+
}
656+
let view = NestedReentrantCollectionMapView::load(context).await?;
657+
let keys = vec!["Bonjour".to_string()];
658+
let subviews = view.map.try_load_entries(&keys).await?;
659+
assert!(subviews[0].is_some());
660+
Ok(())
661+
}
662+
617663
#[derive(CryptoHashRootView)]
618664
pub struct ByteMapStateView<C> {
619665
pub map: ByteMapView<C, u8>,

0 commit comments

Comments
 (0)