Skip to main content

risingwave_backup/
meta_snapshot_v2.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashSet;
16use std::fmt::{Display, Formatter};
17use std::mem::size_of;
18
19use anyhow::anyhow;
20use bytes::{BufMut, BytesMut};
21use itertools::Itertools;
22use risingwave_hummock_sdk::HummockRawObjectId;
23use risingwave_hummock_sdk::change_log::EpochNewChangeLog;
24use risingwave_hummock_sdk::version::HummockVersion;
25use risingwave_object_store::object::{MonitoredStreamingReader, ObjectDataStreamReader};
26use serde::Serialize;
27use serde::de::DeserializeOwned;
28use tracing::info;
29
30use crate::meta_snapshot::{MetaSnapshot, Metadata, SnapshotPayloadReader, SnapshotPayloadWriter};
31use crate::{BackupError, BackupResult};
32pub type MetaSnapshotV2 = MetaSnapshot<MetadataV2>;
33
34impl From<serde_json::Error> for BackupError {
35    fn from(value: serde_json::Error) -> Self {
36        BackupError::Other(value.into())
37    }
38}
39
40/// Add new item in the end. Do not change the order.
41#[macro_export]
42macro_rules! for_all_metadata_models_v2 {
43    ($macro:ident) => {
44        $macro! {
45            {seaql_migrations, risingwave_meta_model::serde_seaql_migration},
46            {version_stats, risingwave_meta_model::hummock_version_stats},
47            {compaction_configs, risingwave_meta_model::compaction_config},
48            {clusters, risingwave_meta_model::cluster},
49            {fragment_relation, risingwave_meta_model::fragment_relation},
50            {catalog_versions, risingwave_meta_model::catalog_version},
51            {connections, risingwave_meta_model::connection},
52            {databases, risingwave_meta_model::database},
53            {fragments, risingwave_meta_model::fragment},
54            {functions, risingwave_meta_model::function},
55            {indexes, risingwave_meta_model::index},
56            {objects, risingwave_meta_model::object},
57            {object_dependencies, risingwave_meta_model::object_dependency},
58            {schemas, risingwave_meta_model::schema},
59            {sinks, risingwave_meta_model::sink},
60            {sources, risingwave_meta_model::source},
61            {streaming_jobs, risingwave_meta_model::streaming_job},
62            {subscriptions, risingwave_meta_model::subscription},
63            {system_parameters, risingwave_meta_model::system_parameter},
64            {tables, risingwave_meta_model::table},
65            {users, risingwave_meta_model::user},
66            {user_privileges, risingwave_meta_model::user_privilege},
67            {views, risingwave_meta_model::view},
68            {workers, risingwave_meta_model::worker},
69            {worker_properties, risingwave_meta_model::worker_property},
70            {hummock_sequences, risingwave_meta_model::hummock_sequence},
71            {session_parameters, risingwave_meta_model::session_parameter},
72            {secrets, risingwave_meta_model::secret},
73            {exactly_once_iceberg_sinks, risingwave_meta_model::exactly_once_iceberg_sink},
74            {iceberg_tables, risingwave_meta_model::iceberg_tables},
75            {iceberg_namespace_properties, risingwave_meta_model::iceberg_namespace_properties},
76            {user_default_privilege, risingwave_meta_model::user_default_privilege},
77            {fragment_splits, risingwave_meta_model::fragment_splits},
78            {pending_sink_state, risingwave_meta_model::pending_sink_state},
79            {refresh_jobs, risingwave_meta_model::refresh_job},
80            {cdc_table_snapshot_splits, risingwave_meta_model::cdc_table_snapshot_split},
81            {hummock_table_change_logs, risingwave_meta_model::hummock_table_change_log}
82        }
83    };
84}
85
86macro_rules! define_metadata_v2 {
87    ($({ $name:ident, $mod_path:ident::$mod_name:ident }),*) => {
88        #[derive(Default)]
89        pub struct MetadataV2 {
90            pub hummock_version: HummockVersion,
91            $(
92                pub $name: Vec<$mod_path::$mod_name::Model>,
93            )*
94        }
95    };
96}
97
98for_all_metadata_models_v2!(define_metadata_v2);
99
100macro_rules! define_encode_metadata_to_writer {
101    ($( {$name:ident, $mod_path:ident::$mod_name:ident} ),*) => {
102        async fn encode_metadata_to_writer(
103            metadata: &MetadataV2,
104            writer: &mut SnapshotPayloadWriter,
105        ) -> BackupResult<()> {
106            // Keep each section's encoding future off the stack. The generated encoder
107            // otherwise needs a large poll frame in unoptimized simulation builds.
108            let mut _idx = 0;
109            $(
110                if _idx == 1 {
111                    Box::pin(write_1(writer, &metadata.hummock_version.to_protobuf())).await?;
112                }
113                Box::pin(write_n(writer, &metadata.$name)).await?;
114                _idx += 1;
115            )*
116            Ok(())
117        }
118    };
119}
120
121for_all_metadata_models_v2!(define_encode_metadata_to_writer);
122
123macro_rules! define_decode_metadata {
124    ($( {$name:ident, $mod_path:ident::$mod_name:ident} ),*) => {
125        async fn decode_metadata_from_reader(
126            metadata: &mut MetadataV2,
127            reader: &mut SnapshotPayloadReader,
128        ) -> BackupResult<()> {
129            let mut _idx = 0;
130            $(
131                if _idx == 1 {
132                    metadata.hummock_version = HummockVersion::from_persisted_protobuf_owned(read_1(reader).await?);
133                }
134                metadata.$name = read_n(reader).await?;
135                _idx += 1;
136            )*
137            Ok(())
138        }
139    };
140}
141
142for_all_metadata_models_v2!(define_decode_metadata);
143
144// Metadata V2 is encoded as a positional list of sections. The actual encoded order inserts
145// `hummock_version` before `version_stats`, so `hummock_sequences` is section 26:
146// 0 seaql_migrations, 1 hummock_version, 2 version_stats, ..., 26 hummock_sequences.
147//
148// Keep this as a fixed index so partial decoding can skip directly to `hummock_sequences`.
149const HUMMOCK_SEQUENCES_METADATA_SECTION_INDEX: usize = 26;
150
151pub async fn decode_hummock_sequences_from_stream(
152    stream: MonitoredStreamingReader,
153) -> BackupResult<Vec<risingwave_meta_model::hummock_sequence::Model>> {
154    let mut reader = SnapshotPayloadReader::new(ObjectDataStreamReader::new(stream.into_stream()));
155    let (format_version, _snapshot_id) = reader.read_snapshot_header().await?;
156    if format_version < 2 {
157        return Err(BackupError::Other(anyhow!(
158            "unsupported metadata snapshot format version for hummock sequence partial decoding: {}",
159            format_version
160        )));
161    }
162    for _ in 0..HUMMOCK_SEQUENCES_METADATA_SECTION_INDEX {
163        skip_metadata_list(&mut reader).await?;
164    }
165    let hummock_sequences = read_n(&mut reader).await?;
166    reader.finish_after_skipping_to_end().await?;
167    Ok(hummock_sequences)
168}
169
170impl Display for MetadataV2 {
171    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
172        writeln!(f, "clusters: {:#?}", self.clusters)?;
173        writeln!(
174            f,
175            "Hummock version: id {}, committed_epoch: {:?}",
176            self.hummock_version.id,
177            self.hummock_version.state_table_info.info(),
178        )?;
179        // optionally dump other metadata
180        Ok(())
181    }
182}
183
184impl Metadata for MetadataV2 {
185    async fn encode_to_writer(&self, mut writer: SnapshotPayloadWriter) -> BackupResult<()> {
186        encode_metadata_to_writer(self, &mut writer).await?;
187        let snapshot_size_bytes = writer.finish().await?;
188        info!(snapshot_size_bytes, "encoded v2 meta snapshot backup file");
189        Ok(())
190    }
191
192    async fn decode_from_reader(mut reader: SnapshotPayloadReader) -> BackupResult<Self> {
193        let mut metadata = Self::default();
194        decode_metadata_from_reader(&mut metadata, &mut reader).await?;
195        reader.finish_after_skipping_to_end().await?;
196        Ok(metadata)
197    }
198
199    fn hummock_version_ref(&self) -> &HummockVersion {
200        &self.hummock_version
201    }
202
203    fn hummock_version(self) -> HummockVersion {
204        self.hummock_version
205    }
206
207    fn storage_url(&self) -> BackupResult<String> {
208        let storage_url_from_snapshot =
209            Itertools::exactly_one(self.system_parameters.iter().filter_map(|m| {
210                if m.name == "state_store" {
211                    return Some(m.value.clone());
212                }
213                None
214            }))
215            .map_err(|_| BackupError::Other(anyhow!("expect state_store")))?;
216        storage_url_from_snapshot
217            .strip_prefix("hummock+")
218            .map(|s| s.to_owned())
219            .ok_or_else(|| {
220                BackupError::Other(anyhow!(
221                    "invalid state_store from metadata snapshot: {}",
222                    storage_url_from_snapshot
223                ))
224            })
225    }
226
227    fn storage_directory(&self) -> BackupResult<String> {
228        Itertools::exactly_one(self.system_parameters.iter().filter_map(|m| {
229            if m.name == "data_directory" {
230                return Some(m.value.clone());
231            }
232            None
233        }))
234        .map_err(|_| BackupError::Other(anyhow!("expect data_directory")))
235    }
236
237    fn table_change_log_object_ids(&self) -> HashSet<HummockRawObjectId> {
238        self.hummock_table_change_logs
239            .iter()
240            .flat_map(|m| {
241                // We cannot use `change_log_ssts` here because `to_table_change_log` returns an owned value, not a reference.
242                let EpochNewChangeLog {
243                    new_value,
244                    old_value,
245                    ..
246                } = to_table_change_log(m);
247                new_value
248                    .into_iter()
249                    .chain(old_value)
250                    .map(|t| t.object_id.as_raw())
251            })
252            .collect()
253    }
254}
255
256fn to_table_change_log(
257    change_log: &risingwave_meta_model::hummock_table_change_log::Model,
258) -> EpochNewChangeLog {
259    EpochNewChangeLog {
260        new_value: change_log
261            .new_value_sst
262            .to_protobuf()
263            .into_iter()
264            .map(Into::into)
265            .collect(),
266        old_value: change_log
267            .old_value_sst
268            .to_protobuf()
269            .into_iter()
270            .map(Into::into)
271            .collect(),
272        non_checkpoint_epochs: change_log
273            .non_checkpoint_epochs
274            .0
275            .iter()
276            .map(|e| *e as _)
277            .collect(),
278        checkpoint_epoch: change_log.checkpoint_epoch as _,
279    }
280}
281
282async fn write_len(writer: &mut SnapshotPayloadWriter, len: usize) -> BackupResult<()> {
283    let mut buf = BytesMut::with_capacity(size_of::<u32>());
284    buf.put_u32_le(
285        len.try_into()
286            .unwrap_or_else(|_| panic!("cannot convert {} into u32", len)),
287    );
288    writer.write_snapshot_bytes(buf.freeze()).await
289}
290
291async fn write_n<T: Serialize>(writer: &mut SnapshotPayloadWriter, data: &[T]) -> BackupResult<()> {
292    write_len(writer, data.len()).await?;
293    for d in data {
294        write_with_len_prefix(writer, d).await?;
295    }
296    Ok(())
297}
298
299async fn write_1<T: Serialize>(writer: &mut SnapshotPayloadWriter, data: &T) -> BackupResult<()> {
300    write_n(writer, &[data]).await
301}
302
303async fn read_n<T>(reader: &mut SnapshotPayloadReader) -> BackupResult<Vec<T>>
304where
305    T: DeserializeOwned,
306{
307    let n = reader.read_u32_le().await? as usize;
308    let mut elements = Vec::with_capacity(n);
309    for _ in 0..n {
310        elements.push(read_with_len_prefix(reader).await?);
311    }
312    Ok(elements)
313}
314
315async fn read_1<T>(reader: &mut SnapshotPayloadReader) -> BackupResult<T>
316where
317    T: DeserializeOwned,
318{
319    let elements = read_n(reader).await?;
320    assert_eq!(elements.len(), 1);
321    Ok(elements.into_iter().next().unwrap())
322}
323
324async fn skip_metadata_list(reader: &mut SnapshotPayloadReader) -> BackupResult<()> {
325    let n = reader.read_u32_le().await? as usize;
326    for _ in 0..n {
327        skip_with_len_prefix(reader).await?;
328    }
329    Ok(())
330}
331
332async fn skip_with_len_prefix(reader: &mut SnapshotPayloadReader) -> BackupResult<()> {
333    let len = read_len_prefix(reader).await?;
334    reader.skip_exact(len).await
335}
336
337async fn read_len_prefix(reader: &mut SnapshotPayloadReader) -> BackupResult<usize> {
338    match reader.read_u32_le().await? {
339        0 => {
340            reader.read_u64_le().await?.try_into().map_err(|_| {
341                BackupError::Other(anyhow!("metadata JSON payload length exceeds usize"))
342            })
343        }
344        len => Ok(len as usize),
345    }
346}
347
348async fn write_with_len_prefix<T: Serialize>(
349    writer: &mut SnapshotPayloadWriter,
350    data: &T,
351) -> BackupResult<()> {
352    let b = serde_json::to_vec(data)?;
353    // Any valid JSON value serialized by serde_json is non-empty, such as `null`, `{}`, or `[]`.
354    assert!(!b.is_empty());
355    let len_prefix_len = if u32::try_from(b.len()).is_ok() {
356        size_of::<u32>()
357    } else {
358        size_of::<u32>() + size_of::<u64>()
359    };
360    let mut buf = BytesMut::with_capacity(len_prefix_len + b.len());
361    if let Ok(len) = u32::try_from(b.len()) {
362        buf.put_u32_le(len);
363    } else {
364        buf.put_u32_le(0);
365        buf.put_u64_le(
366            b.len()
367                .try_into()
368                .unwrap_or_else(|_| panic!("cannot convert {} into u64", b.len())),
369        );
370    }
371    buf.put_slice(&b);
372    writer.write_snapshot_bytes(buf.freeze()).await
373}
374
375async fn read_with_len_prefix<T>(reader: &mut SnapshotPayloadReader) -> BackupResult<T>
376where
377    T: DeserializeOwned,
378{
379    let len = read_len_prefix(reader).await?;
380    let bytes = reader.read_exact(len).await?;
381    Ok(serde_json::from_slice(&bytes)?)
382}
383
384#[cfg(test)]
385mod tests {
386    use std::sync::Arc;
387
388    use bytes::{Buf, BufMut, Bytes};
389    use risingwave_common::config::ObjectStoreConfig;
390    use risingwave_hummock_sdk::HummockVersionId;
391    use risingwave_meta_model::hummock_sequence;
392    use risingwave_object_store::object::object_metrics::ObjectStoreMetrics;
393    use risingwave_object_store::object::{
394        InMemObjectStore, MonitoredObjectStore, ObjectStoreImpl,
395    };
396
397    use super::*;
398    use crate::meta_snapshot::read_u32_le;
399
400    fn test_store() -> ObjectStoreImpl {
401        ObjectStoreImpl::InMem(MonitoredObjectStore::new(
402            InMemObjectStore::for_test(),
403            Arc::new(ObjectStoreMetrics::unused()),
404            Arc::new(ObjectStoreConfig::default()),
405        ))
406    }
407
408    async fn upload_bytes(store: &ObjectStoreImpl, path: &str, bytes: Vec<u8>) {
409        let mut uploader = store.streaming_upload(path).await.unwrap();
410        uploader.write_bytes(Bytes::from(bytes)).await.unwrap();
411        uploader.finish().await.unwrap();
412    }
413
414    async fn read_bytes(store: &ObjectStoreImpl, path: &str) -> Vec<u8> {
415        store.read(path, ..).await.unwrap().to_vec()
416    }
417
418    async fn upload_snapshot(store: &ObjectStoreImpl, path: &str, snapshot: &MetaSnapshotV2) {
419        let uploader = store.streaming_upload(path).await.unwrap();
420        snapshot.encode_to_uploader(uploader).await.unwrap();
421    }
422
423    async fn snapshot_reader(store: &ObjectStoreImpl, path: &str) -> SnapshotPayloadReader {
424        SnapshotPayloadReader::new(ObjectDataStreamReader::new(
425            store.streaming_read(path, ..).await.unwrap().into_stream(),
426        ))
427    }
428
429    fn append_checksum(mut payload: Vec<u8>) -> Vec<u8> {
430        let checksum = crate::xxhash64_checksum(&payload);
431        payload.put_u64_le(checksum);
432        payload
433    }
434
435    fn read_u64_le(buf: &mut &[u8]) -> BackupResult<u64> {
436        if buf.remaining() < size_of::<u64>() {
437            return Err(BackupError::Other(anyhow!(
438                "metadata snapshot is truncated while reading u64"
439            )));
440        }
441        Ok(buf.get_u64_le())
442    }
443
444    fn read_len_prefix_from_slice(buf: &mut &[u8]) -> BackupResult<usize> {
445        match read_u32_le(buf)? {
446            0 => read_u64_le(buf)?.try_into().map_err(|_| {
447                BackupError::Other(anyhow!("metadata JSON payload length exceeds usize"))
448            }),
449            len => Ok(len as usize),
450        }
451    }
452
453    fn skip_with_len_prefix_from_slice(buf: &mut &[u8]) -> BackupResult<()> {
454        let len = read_len_prefix_from_slice(buf)?;
455        if buf.remaining() < len {
456            return Err(BackupError::Other(anyhow!(
457                "metadata JSON payload length {} exceeds remaining buffer {}",
458                len,
459                buf.remaining()
460            )));
461        }
462        buf.advance(len);
463        Ok(())
464    }
465
466    fn skip_metadata_list_from_slice(buf: &mut &[u8]) -> BackupResult<()> {
467        let n = read_u32_le(buf)? as usize;
468        for _ in 0..n {
469            skip_with_len_prefix_from_slice(buf)?;
470        }
471        Ok(())
472    }
473
474    fn put_with_len_prefix(buf: &mut Vec<u8>, data: &impl Serialize) {
475        let payload = serde_json::to_vec(data).unwrap();
476        if let Ok(len) = u32::try_from(payload.len()) {
477            buf.put_u32_le(len);
478        } else {
479            buf.put_u32_le(0);
480            buf.put_u64_le(payload.len() as u64);
481        }
482        buf.put_slice(&payload);
483    }
484
485    fn put_n(buf: &mut Vec<u8>, data: &[impl Serialize]) {
486        buf.put_u32_le(data.len() as u32);
487        for item in data {
488            put_with_len_prefix(buf, item);
489        }
490    }
491
492    async fn encoded_snapshot_with_invalid_trailing_metadata_after_hummock_sequences(
493        metadata: &MetadataV2,
494    ) -> Vec<u8> {
495        let store = test_store();
496        let snapshot = MetaSnapshotV2 {
497            format_version: 2,
498            id: 123,
499            metadata: MetadataV2 {
500                hummock_sequences: metadata.hummock_sequences.clone(),
501                ..Default::default()
502            },
503        };
504        upload_snapshot(&store, "snapshot", &snapshot).await;
505        let encoded = read_bytes(&store, "snapshot").await;
506        let payload = &encoded[..encoded.len() - size_of::<u64>()];
507        let mut metadata_buf = &payload[size_of::<u32>() + size_of::<u64>()..];
508
509        for _ in 0..=HUMMOCK_SEQUENCES_METADATA_SECTION_INDEX {
510            skip_metadata_list_from_slice(&mut metadata_buf).unwrap();
511        }
512
513        let mut partial_payload = payload[..payload.len() - metadata_buf.len()].to_vec();
514        partial_payload.put_u32_le(1);
515        partial_payload.put_u32_le(100);
516        partial_payload.put_slice(br#""short""#);
517        append_checksum(partial_payload)
518    }
519
520    async fn append_future_metadata_section(store: &ObjectStoreImpl, path: &str) -> Vec<u8> {
521        let mut encoded = read_bytes(store, path).await;
522        encoded.truncate(encoded.len() - size_of::<u64>());
523        put_n(&mut encoded, &["future section"]);
524        append_checksum(encoded)
525    }
526
527    #[tokio::test]
528    async fn test_len_prefix_legacy_u32_round_trip() {
529        let store = test_store();
530        let uploader = store.streaming_upload("len-prefix").await.unwrap();
531        let mut writer = SnapshotPayloadWriter::new(uploader);
532        write_with_len_prefix(&mut writer, &"hello").await.unwrap();
533        writer.finish().await.unwrap();
534
535        let encoded = read_bytes(&store, "len-prefix").await;
536        assert_ne!(u32::from_le_bytes(encoded[..4].try_into().unwrap()), 0);
537
538        let mut reader = snapshot_reader(&store, "len-prefix").await;
539        let decoded: String = read_with_len_prefix(&mut reader).await.unwrap();
540        reader.finish().await.unwrap();
541        assert_eq!(decoded, "hello");
542    }
543
544    #[tokio::test]
545    async fn test_len_prefix_extended_u64_decoding() {
546        let payload = serde_json::to_vec("hello").unwrap();
547        let mut bytes = vec![];
548        bytes.put_u32_le(0);
549        bytes.put_u64_le(payload.len() as u64);
550        bytes.put_slice(&payload);
551
552        let store = test_store();
553        upload_bytes(&store, "extended-len-prefix", append_checksum(bytes)).await;
554
555        let mut reader = snapshot_reader(&store, "extended-len-prefix").await;
556        let decoded: String = read_with_len_prefix(&mut reader).await.unwrap();
557        reader.finish().await.unwrap();
558        assert_eq!(decoded, "hello");
559    }
560
561    #[tokio::test]
562    async fn test_len_prefix_errors_on_missing_extended_u64() {
563        let store = test_store();
564        let mut bytes = vec![];
565        bytes.put_u32_le(0);
566        upload_bytes(&store, "missing-extended-len", bytes).await;
567
568        let mut reader = snapshot_reader(&store, "missing-extended-len").await;
569        assert!(read_with_len_prefix::<String>(&mut reader).await.is_err());
570    }
571
572    #[tokio::test]
573    async fn test_len_prefix_errors_on_payload_shorter_than_declared_len() {
574        let store = test_store();
575        let mut bytes = vec![];
576        bytes.put_u32_le(100);
577        bytes.put_slice(br#""hello""#);
578        upload_bytes(&store, "short-payload", bytes).await;
579
580        let mut reader = snapshot_reader(&store, "short-payload").await;
581        assert!(read_with_len_prefix::<String>(&mut reader).await.is_err());
582    }
583
584    #[tokio::test]
585    async fn test_snapshot_v2_encoding_decoding() {
586        let store = test_store();
587        let mut metadata = MetadataV2::default();
588        metadata.hummock_version.id = HummockVersionId::new(123);
589        let raw = MetaSnapshotV2 {
590            format_version: 2,
591            id: 123,
592            metadata,
593        };
594
595        upload_snapshot(&store, "snapshot-v2", &raw).await;
596        let decoded = MetaSnapshotV2::decode_from_stream(
597            store.streaming_read("snapshot-v2", ..).await.unwrap(),
598        )
599        .await
600        .unwrap();
601
602        assert_eq!(decoded.format_version, raw.format_version);
603        assert_eq!(decoded.id, raw.id);
604        assert_eq!(
605            decoded.metadata.hummock_version.id,
606            raw.metadata.hummock_version.id
607        );
608    }
609
610    #[tokio::test]
611    async fn test_partial_decode_hummock_sequences() {
612        let store = test_store();
613        let raw = MetaSnapshotV2 {
614            format_version: 2,
615            id: 123,
616            metadata: MetadataV2 {
617                hummock_sequences: vec![
618                    hummock_sequence::Model {
619                        name: "meta_backup".to_owned(),
620                        seq: 42,
621                    },
622                    hummock_sequence::Model {
623                        name: "sstable_object".to_owned(),
624                        seq: 100,
625                    },
626                ],
627                ..Default::default()
628            },
629        };
630
631        upload_snapshot(&store, "partial-decode", &raw).await;
632        let decoded = decode_hummock_sequences_from_stream(
633            store.streaming_read("partial-decode", ..).await.unwrap(),
634        )
635        .await
636        .unwrap();
637        assert_eq!(decoded, raw.metadata.hummock_sequences);
638    }
639
640    #[tokio::test]
641    async fn test_partial_decode_hummock_sequences_ignores_trailing_metadata() {
642        let metadata = MetadataV2 {
643            hummock_sequences: vec![hummock_sequence::Model {
644                name: "meta_backup".to_owned(),
645                seq: 42,
646            }],
647            ..Default::default()
648        };
649        let encoded =
650            encoded_snapshot_with_invalid_trailing_metadata_after_hummock_sequences(&metadata)
651                .await;
652        let store = test_store();
653        upload_bytes(&store, "invalid-trailing", encoded).await;
654
655        let decoded = decode_hummock_sequences_from_stream(
656            store.streaming_read("invalid-trailing", ..).await.unwrap(),
657        )
658        .await
659        .unwrap();
660        assert_eq!(decoded, metadata.hummock_sequences);
661
662        let full_decode_result = MetaSnapshotV2::decode_from_stream(
663            store.streaming_read("invalid-trailing", ..).await.unwrap(),
664        )
665        .await;
666        assert!(
667            full_decode_result.is_err(),
668            "full metadata decoding should not accept the intentionally invalid trailing section"
669        );
670    }
671
672    #[tokio::test]
673    async fn test_partial_decode_hummock_sequences_ignores_appended_future_sections() {
674        let store = test_store();
675        let raw = MetaSnapshotV2 {
676            format_version: 2,
677            id: 123,
678            metadata: MetadataV2 {
679                hummock_sequences: vec![hummock_sequence::Model {
680                    name: "meta_backup".to_owned(),
681                    seq: 42,
682                }],
683                ..Default::default()
684            },
685        };
686
687        upload_snapshot(&store, "future-section-source", &raw).await;
688        let encoded = append_future_metadata_section(&store, "future-section-source").await;
689        upload_bytes(&store, "future-section", encoded).await;
690
691        let decoded = decode_hummock_sequences_from_stream(
692            store.streaming_read("future-section", ..).await.unwrap(),
693        )
694        .await
695        .unwrap();
696        assert_eq!(decoded, raw.metadata.hummock_sequences);
697
698        let decoded = MetaSnapshotV2::decode_from_stream(
699            store.streaming_read("future-section", ..).await.unwrap(),
700        )
701        .await
702        .unwrap();
703        assert_eq!(decoded.format_version, raw.format_version);
704        assert_eq!(decoded.id, raw.id);
705        assert_eq!(
706            decoded.metadata.hummock_sequences,
707            raw.metadata.hummock_sequences
708        );
709    }
710}