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            let mut _idx = 0;
107            $(
108                if _idx == 1 {
109                    write_1(writer, &metadata.hummock_version.to_protobuf()).await?;
110                }
111                write_n(writer, &metadata.$name).await?;
112                _idx += 1;
113            )*
114            Ok(())
115        }
116    };
117}
118
119for_all_metadata_models_v2!(define_encode_metadata_to_writer);
120
121macro_rules! define_decode_metadata {
122    ($( {$name:ident, $mod_path:ident::$mod_name:ident} ),*) => {
123        async fn decode_metadata_from_reader(
124            metadata: &mut MetadataV2,
125            reader: &mut SnapshotPayloadReader,
126        ) -> BackupResult<()> {
127            let mut _idx = 0;
128            $(
129                if _idx == 1 {
130                    metadata.hummock_version = HummockVersion::from_persisted_protobuf_owned(read_1(reader).await?);
131                }
132                metadata.$name = read_n(reader).await?;
133                _idx += 1;
134            )*
135            Ok(())
136        }
137    };
138}
139
140for_all_metadata_models_v2!(define_decode_metadata);
141
142// Metadata V2 is encoded as a positional list of sections. The actual encoded order inserts
143// `hummock_version` before `version_stats`, so `hummock_sequences` is section 26:
144// 0 seaql_migrations, 1 hummock_version, 2 version_stats, ..., 26 hummock_sequences.
145//
146// Keep this as a fixed index so partial decoding can skip directly to `hummock_sequences`.
147const HUMMOCK_SEQUENCES_METADATA_SECTION_INDEX: usize = 26;
148
149pub async fn decode_hummock_sequences_from_stream(
150    stream: MonitoredStreamingReader,
151) -> BackupResult<Vec<risingwave_meta_model::hummock_sequence::Model>> {
152    let mut reader = SnapshotPayloadReader::new(ObjectDataStreamReader::new(stream.into_stream()));
153    let (format_version, _snapshot_id) = reader.read_snapshot_header().await?;
154    if format_version < 2 {
155        return Err(BackupError::Other(anyhow!(
156            "unsupported metadata snapshot format version for hummock sequence partial decoding: {}",
157            format_version
158        )));
159    }
160    for _ in 0..HUMMOCK_SEQUENCES_METADATA_SECTION_INDEX {
161        skip_metadata_list(&mut reader).await?;
162    }
163    let hummock_sequences = read_n(&mut reader).await?;
164    reader.finish_after_skipping_to_end().await?;
165    Ok(hummock_sequences)
166}
167
168impl Display for MetadataV2 {
169    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
170        writeln!(f, "clusters: {:#?}", self.clusters)?;
171        writeln!(
172            f,
173            "Hummock version: id {}, committed_epoch: {:?}",
174            self.hummock_version.id,
175            self.hummock_version.state_table_info.info(),
176        )?;
177        // optionally dump other metadata
178        Ok(())
179    }
180}
181
182impl Metadata for MetadataV2 {
183    async fn encode_to_writer(&self, mut writer: SnapshotPayloadWriter) -> BackupResult<()> {
184        encode_metadata_to_writer(self, &mut writer).await?;
185        let snapshot_size_bytes = writer.finish().await?;
186        info!(snapshot_size_bytes, "encoded v2 meta snapshot backup file");
187        Ok(())
188    }
189
190    async fn decode_from_reader(mut reader: SnapshotPayloadReader) -> BackupResult<Self> {
191        let mut metadata = Self::default();
192        decode_metadata_from_reader(&mut metadata, &mut reader).await?;
193        reader.finish_after_skipping_to_end().await?;
194        Ok(metadata)
195    }
196
197    fn hummock_version_ref(&self) -> &HummockVersion {
198        &self.hummock_version
199    }
200
201    fn hummock_version(self) -> HummockVersion {
202        self.hummock_version
203    }
204
205    fn storage_url(&self) -> BackupResult<String> {
206        let storage_url_from_snapshot =
207            Itertools::exactly_one(self.system_parameters.iter().filter_map(|m| {
208                if m.name == "state_store" {
209                    return Some(m.value.clone());
210                }
211                None
212            }))
213            .map_err(|_| BackupError::Other(anyhow!("expect state_store")))?;
214        storage_url_from_snapshot
215            .strip_prefix("hummock+")
216            .map(|s| s.to_owned())
217            .ok_or_else(|| {
218                BackupError::Other(anyhow!(
219                    "invalid state_store from metadata snapshot: {}",
220                    storage_url_from_snapshot
221                ))
222            })
223    }
224
225    fn storage_directory(&self) -> BackupResult<String> {
226        Itertools::exactly_one(self.system_parameters.iter().filter_map(|m| {
227            if m.name == "data_directory" {
228                return Some(m.value.clone());
229            }
230            None
231        }))
232        .map_err(|_| BackupError::Other(anyhow!("expect data_directory")))
233    }
234
235    fn table_change_log_object_ids(&self) -> HashSet<HummockRawObjectId> {
236        self.hummock_table_change_logs
237            .iter()
238            .flat_map(|m| {
239                // We cannot use `change_log_ssts` here because `to_table_change_log` returns an owned value, not a reference.
240                let EpochNewChangeLog {
241                    new_value,
242                    old_value,
243                    ..
244                } = to_table_change_log(m);
245                new_value
246                    .into_iter()
247                    .chain(old_value)
248                    .map(|t| t.object_id.as_raw())
249            })
250            .collect()
251    }
252}
253
254fn to_table_change_log(
255    change_log: &risingwave_meta_model::hummock_table_change_log::Model,
256) -> EpochNewChangeLog {
257    EpochNewChangeLog {
258        new_value: change_log
259            .new_value_sst
260            .to_protobuf()
261            .into_iter()
262            .map(Into::into)
263            .collect(),
264        old_value: change_log
265            .old_value_sst
266            .to_protobuf()
267            .into_iter()
268            .map(Into::into)
269            .collect(),
270        non_checkpoint_epochs: change_log
271            .non_checkpoint_epochs
272            .0
273            .iter()
274            .map(|e| *e as _)
275            .collect(),
276        checkpoint_epoch: change_log.checkpoint_epoch as _,
277    }
278}
279
280async fn write_len(writer: &mut SnapshotPayloadWriter, len: usize) -> BackupResult<()> {
281    let mut buf = BytesMut::with_capacity(size_of::<u32>());
282    buf.put_u32_le(
283        len.try_into()
284            .unwrap_or_else(|_| panic!("cannot convert {} into u32", len)),
285    );
286    writer.write_snapshot_bytes(buf.freeze()).await
287}
288
289async fn write_n<T: Serialize>(writer: &mut SnapshotPayloadWriter, data: &[T]) -> BackupResult<()> {
290    write_len(writer, data.len()).await?;
291    for d in data {
292        write_with_len_prefix(writer, d).await?;
293    }
294    Ok(())
295}
296
297async fn write_1<T: Serialize>(writer: &mut SnapshotPayloadWriter, data: &T) -> BackupResult<()> {
298    write_n(writer, &[data]).await
299}
300
301async fn read_n<T>(reader: &mut SnapshotPayloadReader) -> BackupResult<Vec<T>>
302where
303    T: DeserializeOwned,
304{
305    let n = reader.read_u32_le().await? as usize;
306    let mut elements = Vec::with_capacity(n);
307    for _ in 0..n {
308        elements.push(read_with_len_prefix(reader).await?);
309    }
310    Ok(elements)
311}
312
313async fn read_1<T>(reader: &mut SnapshotPayloadReader) -> BackupResult<T>
314where
315    T: DeserializeOwned,
316{
317    let elements = read_n(reader).await?;
318    assert_eq!(elements.len(), 1);
319    Ok(elements.into_iter().next().unwrap())
320}
321
322async fn skip_metadata_list(reader: &mut SnapshotPayloadReader) -> BackupResult<()> {
323    let n = reader.read_u32_le().await? as usize;
324    for _ in 0..n {
325        skip_with_len_prefix(reader).await?;
326    }
327    Ok(())
328}
329
330async fn skip_with_len_prefix(reader: &mut SnapshotPayloadReader) -> BackupResult<()> {
331    let len = read_len_prefix(reader).await?;
332    reader.skip_exact(len).await
333}
334
335async fn read_len_prefix(reader: &mut SnapshotPayloadReader) -> BackupResult<usize> {
336    match reader.read_u32_le().await? {
337        0 => {
338            reader.read_u64_le().await?.try_into().map_err(|_| {
339                BackupError::Other(anyhow!("metadata JSON payload length exceeds usize"))
340            })
341        }
342        len => Ok(len as usize),
343    }
344}
345
346async fn write_with_len_prefix<T: Serialize>(
347    writer: &mut SnapshotPayloadWriter,
348    data: &T,
349) -> BackupResult<()> {
350    let b = serde_json::to_vec(data)?;
351    // Any valid JSON value serialized by serde_json is non-empty, such as `null`, `{}`, or `[]`.
352    assert!(!b.is_empty());
353    let len_prefix_len = if u32::try_from(b.len()).is_ok() {
354        size_of::<u32>()
355    } else {
356        size_of::<u32>() + size_of::<u64>()
357    };
358    let mut buf = BytesMut::with_capacity(len_prefix_len + b.len());
359    if let Ok(len) = u32::try_from(b.len()) {
360        buf.put_u32_le(len);
361    } else {
362        buf.put_u32_le(0);
363        buf.put_u64_le(
364            b.len()
365                .try_into()
366                .unwrap_or_else(|_| panic!("cannot convert {} into u64", b.len())),
367        );
368    }
369    buf.put_slice(&b);
370    writer.write_snapshot_bytes(buf.freeze()).await
371}
372
373async fn read_with_len_prefix<T>(reader: &mut SnapshotPayloadReader) -> BackupResult<T>
374where
375    T: DeserializeOwned,
376{
377    let len = read_len_prefix(reader).await?;
378    let bytes = reader.read_exact(len).await?;
379    Ok(serde_json::from_slice(&bytes)?)
380}
381
382#[cfg(test)]
383mod tests {
384    use std::sync::Arc;
385
386    use bytes::{Buf, BufMut, Bytes};
387    use risingwave_common::config::ObjectStoreConfig;
388    use risingwave_hummock_sdk::HummockVersionId;
389    use risingwave_meta_model::hummock_sequence;
390    use risingwave_object_store::object::object_metrics::ObjectStoreMetrics;
391    use risingwave_object_store::object::{
392        InMemObjectStore, MonitoredObjectStore, ObjectStoreImpl,
393    };
394
395    use super::*;
396    use crate::meta_snapshot::read_u32_le;
397
398    fn test_store() -> ObjectStoreImpl {
399        ObjectStoreImpl::InMem(MonitoredObjectStore::new(
400            InMemObjectStore::for_test(),
401            Arc::new(ObjectStoreMetrics::unused()),
402            Arc::new(ObjectStoreConfig::default()),
403        ))
404    }
405
406    async fn upload_bytes(store: &ObjectStoreImpl, path: &str, bytes: Vec<u8>) {
407        let mut uploader = store.streaming_upload(path).await.unwrap();
408        uploader.write_bytes(Bytes::from(bytes)).await.unwrap();
409        uploader.finish().await.unwrap();
410    }
411
412    async fn read_bytes(store: &ObjectStoreImpl, path: &str) -> Vec<u8> {
413        store.read(path, ..).await.unwrap().to_vec()
414    }
415
416    async fn upload_snapshot(store: &ObjectStoreImpl, path: &str, snapshot: &MetaSnapshotV2) {
417        let uploader = store.streaming_upload(path).await.unwrap();
418        snapshot.encode_to_uploader(uploader).await.unwrap();
419    }
420
421    async fn snapshot_reader(store: &ObjectStoreImpl, path: &str) -> SnapshotPayloadReader {
422        SnapshotPayloadReader::new(ObjectDataStreamReader::new(
423            store.streaming_read(path, ..).await.unwrap().into_stream(),
424        ))
425    }
426
427    fn append_checksum(mut payload: Vec<u8>) -> Vec<u8> {
428        let checksum = crate::xxhash64_checksum(&payload);
429        payload.put_u64_le(checksum);
430        payload
431    }
432
433    fn read_u64_le(buf: &mut &[u8]) -> BackupResult<u64> {
434        if buf.remaining() < size_of::<u64>() {
435            return Err(BackupError::Other(anyhow!(
436                "metadata snapshot is truncated while reading u64"
437            )));
438        }
439        Ok(buf.get_u64_le())
440    }
441
442    fn read_len_prefix_from_slice(buf: &mut &[u8]) -> BackupResult<usize> {
443        match read_u32_le(buf)? {
444            0 => read_u64_le(buf)?.try_into().map_err(|_| {
445                BackupError::Other(anyhow!("metadata JSON payload length exceeds usize"))
446            }),
447            len => Ok(len as usize),
448        }
449    }
450
451    fn skip_with_len_prefix_from_slice(buf: &mut &[u8]) -> BackupResult<()> {
452        let len = read_len_prefix_from_slice(buf)?;
453        if buf.remaining() < len {
454            return Err(BackupError::Other(anyhow!(
455                "metadata JSON payload length {} exceeds remaining buffer {}",
456                len,
457                buf.remaining()
458            )));
459        }
460        buf.advance(len);
461        Ok(())
462    }
463
464    fn skip_metadata_list_from_slice(buf: &mut &[u8]) -> BackupResult<()> {
465        let n = read_u32_le(buf)? as usize;
466        for _ in 0..n {
467            skip_with_len_prefix_from_slice(buf)?;
468        }
469        Ok(())
470    }
471
472    fn put_with_len_prefix(buf: &mut Vec<u8>, data: &impl Serialize) {
473        let payload = serde_json::to_vec(data).unwrap();
474        if let Ok(len) = u32::try_from(payload.len()) {
475            buf.put_u32_le(len);
476        } else {
477            buf.put_u32_le(0);
478            buf.put_u64_le(payload.len() as u64);
479        }
480        buf.put_slice(&payload);
481    }
482
483    fn put_n(buf: &mut Vec<u8>, data: &[impl Serialize]) {
484        buf.put_u32_le(data.len() as u32);
485        for item in data {
486            put_with_len_prefix(buf, item);
487        }
488    }
489
490    async fn encoded_snapshot_with_invalid_trailing_metadata_after_hummock_sequences(
491        metadata: &MetadataV2,
492    ) -> Vec<u8> {
493        let store = test_store();
494        let snapshot = MetaSnapshotV2 {
495            format_version: 2,
496            id: 123,
497            metadata: MetadataV2 {
498                hummock_sequences: metadata.hummock_sequences.clone(),
499                ..Default::default()
500            },
501        };
502        upload_snapshot(&store, "snapshot", &snapshot).await;
503        let encoded = read_bytes(&store, "snapshot").await;
504        let payload = &encoded[..encoded.len() - size_of::<u64>()];
505        let mut metadata_buf = &payload[size_of::<u32>() + size_of::<u64>()..];
506
507        for _ in 0..=HUMMOCK_SEQUENCES_METADATA_SECTION_INDEX {
508            skip_metadata_list_from_slice(&mut metadata_buf).unwrap();
509        }
510
511        let mut partial_payload = payload[..payload.len() - metadata_buf.len()].to_vec();
512        partial_payload.put_u32_le(1);
513        partial_payload.put_u32_le(100);
514        partial_payload.put_slice(br#""short""#);
515        append_checksum(partial_payload)
516    }
517
518    async fn append_future_metadata_section(store: &ObjectStoreImpl, path: &str) -> Vec<u8> {
519        let mut encoded = read_bytes(store, path).await;
520        encoded.truncate(encoded.len() - size_of::<u64>());
521        put_n(&mut encoded, &["future section"]);
522        append_checksum(encoded)
523    }
524
525    #[tokio::test]
526    async fn test_len_prefix_legacy_u32_round_trip() {
527        let store = test_store();
528        let uploader = store.streaming_upload("len-prefix").await.unwrap();
529        let mut writer = SnapshotPayloadWriter::new(uploader);
530        write_with_len_prefix(&mut writer, &"hello").await.unwrap();
531        writer.finish().await.unwrap();
532
533        let encoded = read_bytes(&store, "len-prefix").await;
534        assert_ne!(u32::from_le_bytes(encoded[..4].try_into().unwrap()), 0);
535
536        let mut reader = snapshot_reader(&store, "len-prefix").await;
537        let decoded: String = read_with_len_prefix(&mut reader).await.unwrap();
538        reader.finish().await.unwrap();
539        assert_eq!(decoded, "hello");
540    }
541
542    #[tokio::test]
543    async fn test_len_prefix_extended_u64_decoding() {
544        let payload = serde_json::to_vec("hello").unwrap();
545        let mut bytes = vec![];
546        bytes.put_u32_le(0);
547        bytes.put_u64_le(payload.len() as u64);
548        bytes.put_slice(&payload);
549
550        let store = test_store();
551        upload_bytes(&store, "extended-len-prefix", append_checksum(bytes)).await;
552
553        let mut reader = snapshot_reader(&store, "extended-len-prefix").await;
554        let decoded: String = read_with_len_prefix(&mut reader).await.unwrap();
555        reader.finish().await.unwrap();
556        assert_eq!(decoded, "hello");
557    }
558
559    #[tokio::test]
560    async fn test_len_prefix_errors_on_missing_extended_u64() {
561        let store = test_store();
562        let mut bytes = vec![];
563        bytes.put_u32_le(0);
564        upload_bytes(&store, "missing-extended-len", bytes).await;
565
566        let mut reader = snapshot_reader(&store, "missing-extended-len").await;
567        assert!(read_with_len_prefix::<String>(&mut reader).await.is_err());
568    }
569
570    #[tokio::test]
571    async fn test_len_prefix_errors_on_payload_shorter_than_declared_len() {
572        let store = test_store();
573        let mut bytes = vec![];
574        bytes.put_u32_le(100);
575        bytes.put_slice(br#""hello""#);
576        upload_bytes(&store, "short-payload", bytes).await;
577
578        let mut reader = snapshot_reader(&store, "short-payload").await;
579        assert!(read_with_len_prefix::<String>(&mut reader).await.is_err());
580    }
581
582    #[tokio::test]
583    async fn test_snapshot_v2_encoding_decoding() {
584        let store = test_store();
585        let mut metadata = MetadataV2::default();
586        metadata.hummock_version.id = HummockVersionId::new(123);
587        let raw = MetaSnapshotV2 {
588            format_version: 2,
589            id: 123,
590            metadata,
591        };
592
593        upload_snapshot(&store, "snapshot-v2", &raw).await;
594        let decoded = MetaSnapshotV2::decode_from_stream(
595            store.streaming_read("snapshot-v2", ..).await.unwrap(),
596        )
597        .await
598        .unwrap();
599
600        assert_eq!(decoded.format_version, raw.format_version);
601        assert_eq!(decoded.id, raw.id);
602        assert_eq!(
603            decoded.metadata.hummock_version.id,
604            raw.metadata.hummock_version.id
605        );
606    }
607
608    #[tokio::test]
609    async fn test_partial_decode_hummock_sequences() {
610        let store = test_store();
611        let raw = MetaSnapshotV2 {
612            format_version: 2,
613            id: 123,
614            metadata: MetadataV2 {
615                hummock_sequences: vec![
616                    hummock_sequence::Model {
617                        name: "meta_backup".to_owned(),
618                        seq: 42,
619                    },
620                    hummock_sequence::Model {
621                        name: "sstable_object".to_owned(),
622                        seq: 100,
623                    },
624                ],
625                ..Default::default()
626            },
627        };
628
629        upload_snapshot(&store, "partial-decode", &raw).await;
630        let decoded = decode_hummock_sequences_from_stream(
631            store.streaming_read("partial-decode", ..).await.unwrap(),
632        )
633        .await
634        .unwrap();
635        assert_eq!(decoded, raw.metadata.hummock_sequences);
636    }
637
638    #[tokio::test]
639    async fn test_partial_decode_hummock_sequences_ignores_trailing_metadata() {
640        let metadata = MetadataV2 {
641            hummock_sequences: vec![hummock_sequence::Model {
642                name: "meta_backup".to_owned(),
643                seq: 42,
644            }],
645            ..Default::default()
646        };
647        let encoded =
648            encoded_snapshot_with_invalid_trailing_metadata_after_hummock_sequences(&metadata)
649                .await;
650        let store = test_store();
651        upload_bytes(&store, "invalid-trailing", encoded).await;
652
653        let decoded = decode_hummock_sequences_from_stream(
654            store.streaming_read("invalid-trailing", ..).await.unwrap(),
655        )
656        .await
657        .unwrap();
658        assert_eq!(decoded, metadata.hummock_sequences);
659
660        let full_decode_result = MetaSnapshotV2::decode_from_stream(
661            store.streaming_read("invalid-trailing", ..).await.unwrap(),
662        )
663        .await;
664        assert!(
665            full_decode_result.is_err(),
666            "full metadata decoding should not accept the intentionally invalid trailing section"
667        );
668    }
669
670    #[tokio::test]
671    async fn test_partial_decode_hummock_sequences_ignores_appended_future_sections() {
672        let store = test_store();
673        let raw = MetaSnapshotV2 {
674            format_version: 2,
675            id: 123,
676            metadata: MetadataV2 {
677                hummock_sequences: vec![hummock_sequence::Model {
678                    name: "meta_backup".to_owned(),
679                    seq: 42,
680                }],
681                ..Default::default()
682            },
683        };
684
685        upload_snapshot(&store, "future-section-source", &raw).await;
686        let encoded = append_future_metadata_section(&store, "future-section-source").await;
687        upload_bytes(&store, "future-section", encoded).await;
688
689        let decoded = decode_hummock_sequences_from_stream(
690            store.streaming_read("future-section", ..).await.unwrap(),
691        )
692        .await
693        .unwrap();
694        assert_eq!(decoded, raw.metadata.hummock_sequences);
695
696        let decoded = MetaSnapshotV2::decode_from_stream(
697            store.streaming_read("future-section", ..).await.unwrap(),
698        )
699        .await
700        .unwrap();
701        assert_eq!(decoded.format_version, raw.format_version);
702        assert_eq!(decoded.id, raw.id);
703        assert_eq!(
704            decoded.metadata.hummock_sequences,
705            raw.metadata.hummock_sequences
706        );
707    }
708}