mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* fix(seaweed-volume): fall back to idx dir when reading .vif
EcVolume::new and read_ec_shard_config only looked for .vif at the
data dir. With the cross-disk reconcile path (where shards live on
one disk and .ecx / .ecj / .vif live on a sibling disk —
seaweedfs/seaweedfs#9212 / #9244), this would either write a stub
.vif on the shard disk and lose the real EC config + dat_file_size
or fall back to default ratios despite a perfectly good .vif being
present elsewhere on the same volume server.
Add a small `locate_vif_path` helper that prefers the data dir and
falls back to the idx dir when it differs, and thread the data dir
+ idx dir pair through `read_ec_shard_config`. Three call sites in
grpc_server.rs (VolumeEcShardsGenerate, VolumeEcShardsRebuild, scrub)
updated; the scrub path passes the same dir for both args because
`find_ec_dir` is the only locator there.
* feat(seaweed-volume): primitives for cross-disk EC shard reconcile
Adds the three small helpers the reconcile pass needs:
- DiskLocation::mount_ec_shards_with_idx_dir — mounts shards on this
disk while pointing the EcVolume at a sibling disk's idx dir for
.ecx / .ecj / .vif. Mirrors loadEcShardsWithIdxDir in
weed/storage/disk_location_ec.go. The existing mount_ec_shards is
kept as a thin wrapper over it.
- EcVolume::has_shard — `pub` accessor over the internal Vec<Option>
shard slot so the reconcile pass can skip shards that are already
registered.
- pub(crate) re-exports of parse_collection_volume_id and
parse_ec_shard_extension under names parse_collection_volume_id_pub
and is_ec_shard_extension so the reconcile module can call them
without re-implementing the parsers.
No behaviour change. Reconciliation logic in the next commit.
* feat(seaweed-volume): cross-disk EC shard reconciliation (#9212)
Closes the loader half of seaweedfs/seaweedfs#9212 on the Rust side,
mirroring the Go fix in seaweedfs/seaweedfs#9244. With the auto-load
in feat/rust-load-all-ec-shards-9212 in place, the only remaining gap
is shards that landed on a disk without their `.ecx` — for example
when ec.balance / ec.rebuild moved them onto a destination node's
second disk while leaving the index files on the disk that already
held the volume. Without this, those orphan shards stay invisible to
the master and ec.rebuild reports the volume as unrepairable.
After every DiskLocation has finished its per-disk EC scan, sweep the
store for shards that live on a disk without local index files and
load them by reaching across to a sibling disk's `.ecx` / `.ecj` /
`.vif`:
- Store::reconcile_ec_shards_across_disks walks each disk for
orphan `.ec??` files (present on disk, not yet registered to an
EcVolume) and matches them against an `(collection, vid) ->
EcxOwnerInfo` map of which disk owns each `.ecx`.
- Each matched group is mounted on its physical disk's ec_volumes
map (so heartbeat reporting carries the right disk_id per shard)
via `mount_ec_shards_with_idx_dir`, pointing the EcVolume at the
sibling's idx dir.
- `index_ecx_owners` records the directory each `.ecx` was found in
(IdxDirectory or Directory) so the loader doesn't ENOENT when the
legacy "written before -dir.idx was set" layout puts `.ecx` in
the data dir. This mirrors the PR #9244 review fix from
@gemini-code-assist / @coderabbitai (see Go commit af57cc652).
- True orphans (no `.ecx` anywhere on this server) log a warning
and stay on disk untouched — operator can restore the index later.
Wired into Store::add_location and Store::load_new_volumes so a fresh
restart and any later disk additions both pick up cross-disk shards.
Tests cover all four behaviour shapes:
- shards on dir0 + .ecx on dir1 → reconciled to dir0's ec_volumes
- .ecx in owner's data dir (legacy layout) → reconciled correctly
- self-contained disks → reconcile is a no-op
- truly-orphan shards (no .ecx anywhere) → left on disk, logged
* fix(seaweed-volume): propagate EcVolume::new errors instead of unwrap
mount_ec_shards_with_idx_dir built the missing EcVolume inside an
entry().or_insert_with() closure, which can't return a Result — so
any EcVolume::new failure (e.g. .ecx open error, .ecj create error,
malformed .vif) panicked the volume server via unwrap(). The
constructor already returns Result<>, so propagate it as
VolumeError::Io instead.
Reported in PR #9252 review by @gemini-code-assist (high) and
@coderabbitai (critical).
* perf(seaweed-volume): use DirEntry::metadata in collect_orphan_ec_shards
Replaced the extra fs::metadata(&path) lookup with ent.metadata() so
we don't pay an additional stat syscall per directory entry beyond
what read_dir already returned. Drops the now-unused std::path::Path
import alongside.
Reported in PR #9252 review by @gemini-code-assist.
* fix(seaweed-volume): scrub uses EcVolume's real dir_idx for split-disk volumes
After cross-disk reconciliation an EcVolume can legitimately have
ecv.dir != ecv.dir_idx (shards on one disk, .ecx / .ecj / .vif on a
sibling). The scrub path collapsed both args to find_ec_dir's single
answer, so read_ec_shard_config fell back to the wrong .vif location
for exactly the split-disk layout this PR loads — skewing
shard-count detection and verification results.
Use ecv.dir / ecv.dir_idx directly so scrub reads the metadata from
where the volume's index files actually live.
Reported in PR #9252 review by @coderabbitai.
* feat(seaweed-volume): primitives for split-disk EC volume operations
Reconciliation can mount the same `vid` on multiple DiskLocations
with disjoint shard subsets. The existing first-match `find_ec_volume`
isn't enough for read/unmount/delete/decode paths that need to act on
a specific shard or aggregate across the whole volume — they have to
walk every location and find the right home for each shard.
Add the small Store-level lookup primitives Go's findEcShard /
CollectEcShards already provide:
- `Store::find_ec_shard_location(vid, shard_id)` — returns the index
of the location that has `(vid, shard_id)` mounted, if any.
- `Store::find_ec_volume_with_shard(vid, shard_id)` — same idea but
returns the EcVolume directly.
- `Store::collect_ec_shard_dirs(vid, max_shard_count)` — returns
the EcVolume to use for metadata plus per-shard data dirs (None
when the shard isn't mounted on any disk). Mirrors
`Store.CollectEcShards` in `weed/storage/store_ec.go`.
And the EcVolume accessors callers need:
- `EcVolume::has_shard(shard_id)` — was already added for the cross-
disk reconcile but is now a load-bearing primitive for placement
decisions on a per-shard basis. Pulled into the dedicated commit.
- `EcVolume::ecx_actual_dir()` — exposes the directory the `.ecx`
was actually opened from. The decoder needs it for the .ecx
lookup when shards are split across data dirs and `.ecx` lives on
a sibling idx dir.
Plus a small defensive change to `DiskLocation::unmount_ec_shards`:
only decrement the per-shard gauge for shards that were actually
mounted. Without this, the upcoming `Store::unmount_ec_shards`
fan-out to every location would underflow the metric whenever a
shard is requested for unmount on a sibling disk that doesn't have
it.
No behaviour change at the call sites yet — wiring follows in the
next commits.
* fix(seaweed-volume): unmount_ec_shards visits every location with the vid
Store::unmount_ec_shards and Store::unmount_ec_shard returned after
the first DiskLocation with the volume id, even if that location did
not contain the requested shard. With reconciled split-disk volumes
(shards 0/12 on disk 0, shard 1 on disk 1 — the issue #9212 layout
this PR loads), VolumeEcShardsUnmount for a later-disk shard became a
silent no-op and Store::delete_ec_shards could remove the shard file
while leaving an in-memory shard + open file handle stale on the
later location.
Walk all locations that have the EcVolume and ask each to unmount
whatever subset of `shard_ids` it actually has — the
`DiskLocation::unmount_ec_shards` defensive guard from the previous
commit makes the fan-out safe (no metric underflow when a sibling
disk is asked to unmount a shard it doesn't hold).
* fix(seaweed-volume): VolumeEcShardRead reads from the shard's home disk
VolumeEcShardRead resolved the EcVolume via first-match
`find_ec_volume(vid)` and then looked up the requested shard on that
single EcVolume. With reconciled split-disk volumes (the layout
seaweedfs/seaweedfs#9212 produces — shards 0/12 on disk 0, shard 1
on disk 1), a request for shard 1 hit disk 0 first and returned
"shard 1 not mounted" even though it was happily mounted on disk 1.
Switch to `find_ec_volume_with_shard(vid, shard_id)` so the lookup
walks every location and returns the EcVolume whose disk actually
holds the shard. The deleted-needle check still works because every
per-disk EcVolume for the same vid points at the same `.ecx` file
(post-reconcile, both disks open the same sealed index).
* fix(seaweed-volume): VolumeEcShardsToVolume aggregates shards across disks
VolumeEcShardsToVolume resolved a single EcVolume via
`find_ec_volume(vid)` and then checked `ec_vol.shards[i]` for each
data shard. With reconciled split-disk volumes that's the wrong
view: the first-match EcVolume only carries the shards on its disk,
so the presence check would either reject the request as
"missing shard" or — if shards happened to be on the first disk —
fall through to `write_dat_file_from_shards(&dir, ...)` which only
reads from the EcVolume's single dir.
Mirror Go's CollectEcShards by aggregating per-shard data dirs
across every location with the volume:
- Add `Store::collect_ec_shard_dirs` (in the previous primitives
commit) returning the EcVolume to use for metadata + per-shard
dir slots.
- Extend `find_dat_file_size` and `write_dat_file_from_shards` with
`_with_dirs` variants that take the `.ec00` dir and per-shard
dirs separately, so a decoded volume whose shards live on
several disks can still be reconstructed. The original signatures
delegate to the new ones with the same dir for all shards, so
every existing caller keeps working unchanged.
- Rewire VolumeEcShardsToVolume through the helpers — presence
check sees the union, dat_file_size reads `.ec00` from the right
disk and `.ecx` from the EcVolume's actual idx dir, the decoder
reads each shard from its own home dir.
* test(seaweed-volume): split-disk read / unmount / delete / collect
Five tests exercising the four behaviour shapes the PR #9252 review
flagged on multi-location EC volumes. Each builds the cross-disk
split layout from issue #9212 (shards 0 and 12 on disk 0, shard 1 +
.ecx on disk 1) via the new `build_split_disk_store` helper and
asserts:
- `find_ec_shard_location` / `find_ec_volume_with_shard` route to
the disk that actually holds each shard (not first-match).
- `Store::unmount_ec_shards([1])` reaches disk 1 and removes shard 1
while leaving disk 0's unrelated shards mounted (used to be a
silent no-op).
- `Store::unmount_ec_shard(vid, 1)` ditto for the single-shard
variant.
- `Store::delete_ec_shards` removes both the on-disk file and the
in-memory mount on the right disk; previously deletion could
remove the file while the in-memory shard with its open file
handle survived on a different location.
- `collect_ec_shard_dirs` reports the right per-shard data dir for
each location and `None` for unmounted shards.
* fix(seaweed-volume): retry same-disk legacy .ecx layout in reconcile
The unconditional `owner.location == loc_idx` skip missed the layout
where `idx_directory` is configured but the owner's `.ecx` / `.ecj` /
`.vif` still live in `loc.directory` (the legacy "written before
-dir.idx was set" shape). In that case the per-disk loader's
mount_ec_shards used `loc.idx_directory` and ENOENT'd, then this
branch suppressed the only recovery path — the owner disk's own
shards stayed unloaded after startup.
Tighten the skip so it only fires when the discovered owner dir is
already `loc.idx_directory` (the loader-already-tried-and-failed
case). When `owner.idx_dir` differs (legacy data-dir layout), queue
a same-disk retry through `mount_ec_shards_with_idx_dir(...,
&owner.idx_dir)` so reconcile becomes the recovery path.
Reported in PR #9252 review by @coderabbitai.
* fix(seaweed-volume): roll back partial mounts on cross-disk reconcile failure
mount_ec_shards_with_idx_dir adds shards one at a time and
increments the `ec_shards` gauge per shard that successfully attaches.
A mid-loop failure (e.g. an EcVolumeShard::open error after the
first few shards already attached) used to leave the EcVolume
half-mounted with stale metric increments — the warn!() branch only
logged the error.
Mirror DiskLocation::handle_found_ecx_file's recovery path: drive
the cleanup through `loc.unmount_ec_shards(vid, &shard_ids)` after
a failed mount. The defensive change in #9251 makes
unmount_ec_shards only decrement the gauge for shards that were
actually mounted and drops the EcVolume when it reaches zero
shards, so the rollback is safe even though some of `shard_ids`
never attached.
Reported in PR #9252 review by @coderabbitai.
* test(seaweed-volume): cover the two reconcile fixes from PR #9252 review
Two new tests in store_ec_reconcile:
- test_reconcile_recovers_same_disk_legacy_ecx_layout — sets up the
layout where idx_directory is configured but the owner's .ecx
lives in loc.directory. The per-disk loader's mount_ec_shards
uses loc.idx_directory and fails; reconcile should retry on the
same disk with the owner's actual idx_dir and the owner's own
shards must come back online.
- test_reconcile_rolls_back_partial_mounts_on_failure — sabotages
one of the orphan shard files (replaces it with a directory of
the same name) so EcVolumeShard::open errors out partway through
mount_ec_shards_with_idx_dir. Asserts the post-condition that no
EcVolume entry retains a "shard mounted" claim that doesn't
correspond to a real shard file.
323 lines
10 KiB
Rust
323 lines
10 KiB
Rust
//! EC decoding: reconstruct a .dat file from EC shards.
|
|
//!
|
|
//! Rebuilds the original .dat + .idx files from data shards (.ec00-.ec09)
|
|
//! and the sorted index (.ecx) + deletion journal (.ecj).
|
|
|
|
use std::fs::File;
|
|
use std::io::{self, Read, Write};
|
|
|
|
use crate::storage::erasure_coding::ec_shard::*;
|
|
use crate::storage::idx;
|
|
use crate::storage::needle::needle::get_actual_size;
|
|
use crate::storage::super_block::SUPER_BLOCK_SIZE;
|
|
use crate::storage::types::*;
|
|
use crate::storage::volume::volume_file_name;
|
|
|
|
/// Calculate .dat file size from the max offset entry in .ecx.
|
|
/// Reads the volume version from the first EC shard (.ec00) superblock,
|
|
/// then scans .ecx entries to find the largest (offset + needle_actual_size).
|
|
///
|
|
/// `dir` is used both for reading `.ec00` and `.ecx`. For split-disk
|
|
/// reconciled volumes call [`find_dat_file_size_with_dirs`] instead.
|
|
pub fn find_dat_file_size(dir: &str, collection: &str, volume_id: VolumeId) -> io::Result<i64> {
|
|
find_dat_file_size_with_dirs(dir, dir, collection, volume_id)
|
|
}
|
|
|
|
/// Like [`find_dat_file_size`] but lets the caller pass separate dirs
|
|
/// for `.ec00` (the data shard) and `.ecx` (the sealed index). This
|
|
/// is the form needed when shards are split across data dirs and the
|
|
/// `.ecx` lives on a sibling disk's idx dir (#9252).
|
|
pub fn find_dat_file_size_with_dirs(
|
|
ec00_dir: &str,
|
|
ecx_dir: &str,
|
|
collection: &str,
|
|
volume_id: VolumeId,
|
|
) -> io::Result<i64> {
|
|
let ec00_base = volume_file_name(ec00_dir, collection, volume_id);
|
|
let ecx_base = volume_file_name(ecx_dir, collection, volume_id);
|
|
|
|
// Read volume version from .ec00 superblock
|
|
let ec00_path = format!("{}.ec00", ec00_base);
|
|
let mut ec00 = File::open(&ec00_path)?;
|
|
let mut sb_buf = [0u8; SUPER_BLOCK_SIZE];
|
|
ec00.read_exact(&mut sb_buf)?;
|
|
let version = Version(sb_buf[0]);
|
|
|
|
// Start with at least the superblock size
|
|
let mut dat_size: i64 = SUPER_BLOCK_SIZE as i64;
|
|
|
|
// Scan .ecx entries
|
|
let ecx_path = format!("{}.ecx", ecx_base);
|
|
let ecx_data = std::fs::read(&ecx_path)?;
|
|
let entry_count = ecx_data.len() / NEEDLE_MAP_ENTRY_SIZE;
|
|
|
|
for i in 0..entry_count {
|
|
let start = i * NEEDLE_MAP_ENTRY_SIZE;
|
|
let (_, offset, size) =
|
|
idx_entry_from_bytes(&ecx_data[start..start + NEEDLE_MAP_ENTRY_SIZE]);
|
|
if size.is_deleted() {
|
|
continue;
|
|
}
|
|
let entry_stop = offset.to_actual_offset() + get_actual_size(size, version);
|
|
if entry_stop > dat_size {
|
|
dat_size = entry_stop;
|
|
}
|
|
}
|
|
|
|
Ok(dat_size)
|
|
}
|
|
|
|
/// Reconstruct a .dat file from EC data shards.
|
|
///
|
|
/// Reads from .ec00-.ec09 and writes a new .dat file. All data shards
|
|
/// must live in `dir`. For the cross-disk reconciled layout where
|
|
/// shards are split across multiple data dirs of the same node, use
|
|
/// [`write_dat_file_from_shards_with_dirs`] instead.
|
|
pub fn write_dat_file_from_shards(
|
|
dir: &str,
|
|
collection: &str,
|
|
volume_id: VolumeId,
|
|
dat_file_size: i64,
|
|
data_shards: usize,
|
|
) -> io::Result<()> {
|
|
let dirs: Vec<String> = (0..data_shards).map(|_| dir.to_string()).collect();
|
|
write_dat_file_from_shards_with_dirs(
|
|
dir,
|
|
collection,
|
|
volume_id,
|
|
dat_file_size,
|
|
data_shards,
|
|
&dirs,
|
|
)
|
|
}
|
|
|
|
/// Reconstruct a .dat file from EC data shards, taking the source
|
|
/// directory for each shard separately.
|
|
///
|
|
/// `dat_dir` is where the produced `.dat` is written. `shard_dirs[i]`
|
|
/// is the directory holding shard `i`. For the simple "all shards in
|
|
/// one dir" case both can be the same value.
|
|
///
|
|
/// Mirrors Go's `WriteDatFile(baseFileName, datFileSize,
|
|
/// shardFileNames)` shape — Go passes per-shard paths so a
|
|
/// reconciled volume with shards split across disks of the same
|
|
/// volume server can still be decoded back to a regular .dat
|
|
/// (seaweedfs/seaweedfs#9252).
|
|
pub fn write_dat_file_from_shards_with_dirs(
|
|
dat_dir: &str,
|
|
collection: &str,
|
|
volume_id: VolumeId,
|
|
dat_file_size: i64,
|
|
data_shards: usize,
|
|
shard_dirs: &[String],
|
|
) -> io::Result<()> {
|
|
if shard_dirs.len() < data_shards {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!(
|
|
"shard_dirs len {} < data_shards {}",
|
|
shard_dirs.len(),
|
|
data_shards
|
|
),
|
|
));
|
|
}
|
|
let base = volume_file_name(dat_dir, collection, volume_id);
|
|
let dat_path = format!("{}.dat", base);
|
|
|
|
// Open data shards from their individual home dirs.
|
|
let mut shards: Vec<EcVolumeShard> = (0..data_shards as u8)
|
|
.map(|i| EcVolumeShard::new(&shard_dirs[i as usize], collection, volume_id, i))
|
|
.collect();
|
|
|
|
for shard in &mut shards {
|
|
shard.open()?;
|
|
}
|
|
|
|
let mut dat_file = File::create(&dat_path)?;
|
|
let mut remaining = dat_file_size;
|
|
let large_block_size = ERASURE_CODING_LARGE_BLOCK_SIZE;
|
|
let small_block_size = ERASURE_CODING_SMALL_BLOCK_SIZE;
|
|
let large_row_size = (large_block_size * data_shards) as i64;
|
|
|
|
let mut shard_offset: u64 = 0;
|
|
|
|
// Read large blocks
|
|
while remaining >= large_row_size {
|
|
for i in 0..data_shards {
|
|
let mut buf = vec![0u8; large_block_size];
|
|
shards[i].read_at(&mut buf, shard_offset)?;
|
|
let to_write = large_block_size.min(remaining as usize);
|
|
dat_file.write_all(&buf[..to_write])?;
|
|
remaining -= to_write as i64;
|
|
if remaining <= 0 {
|
|
break;
|
|
}
|
|
}
|
|
shard_offset += large_block_size as u64;
|
|
}
|
|
|
|
// Read small blocks
|
|
while remaining > 0 {
|
|
for i in 0..data_shards {
|
|
let mut buf = vec![0u8; small_block_size];
|
|
shards[i].read_at(&mut buf, shard_offset)?;
|
|
let to_write = small_block_size.min(remaining as usize);
|
|
dat_file.write_all(&buf[..to_write])?;
|
|
remaining -= to_write as i64;
|
|
if remaining <= 0 {
|
|
break;
|
|
}
|
|
}
|
|
shard_offset += small_block_size as u64;
|
|
}
|
|
|
|
for shard in &mut shards {
|
|
shard.close();
|
|
}
|
|
|
|
dat_file.sync_all()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Write .idx file from .ecx index + .ecj deletion journal.
|
|
///
|
|
/// Copies sorted .ecx entries to .idx, then appends tombstones for
|
|
/// deleted needles from .ecj.
|
|
pub fn write_idx_file_from_ec_index(
|
|
dir: &str,
|
|
collection: &str,
|
|
volume_id: VolumeId,
|
|
) -> io::Result<()> {
|
|
let base = volume_file_name(dir, collection, volume_id);
|
|
let ecx_path = format!("{}.ecx", base);
|
|
let ecj_path = format!("{}.ecj", base);
|
|
let idx_path = format!("{}.idx", base);
|
|
|
|
// Copy .ecx to .idx
|
|
std::fs::copy(&ecx_path, &idx_path)?;
|
|
|
|
// Append deletions from .ecj as tombstones
|
|
if std::path::Path::new(&ecj_path).exists() {
|
|
let ecj_data = std::fs::read(&ecj_path)?;
|
|
if !ecj_data.is_empty() {
|
|
let mut idx_file = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.append(true)
|
|
.open(&idx_path)?;
|
|
|
|
let count = ecj_data.len() / NEEDLE_ID_SIZE;
|
|
for i in 0..count {
|
|
let start = i * NEEDLE_ID_SIZE;
|
|
let needle_id = NeedleId::from_bytes(&ecj_data[start..start + NEEDLE_ID_SIZE]);
|
|
idx::write_index_entry(
|
|
&mut idx_file,
|
|
needle_id,
|
|
Offset::default(),
|
|
TOMBSTONE_FILE_SIZE,
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::storage::erasure_coding::ec_encoder;
|
|
use crate::storage::needle::needle::Needle;
|
|
use crate::storage::needle_map::NeedleMapKind;
|
|
use crate::storage::volume::Volume;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn test_ec_full_round_trip() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let dir = tmp.path().to_str().unwrap();
|
|
|
|
// Create volume with data
|
|
let mut v = Volume::new(
|
|
dir,
|
|
dir,
|
|
"",
|
|
VolumeId(1),
|
|
NeedleMapKind::InMemory,
|
|
None,
|
|
None,
|
|
0,
|
|
Version::current(),
|
|
)
|
|
.unwrap();
|
|
|
|
let test_data: Vec<(NeedleId, Vec<u8>)> = (1..=3)
|
|
.map(|i| {
|
|
let data = format!("EC round trip data for needle {}", i);
|
|
(NeedleId(i), data.into_bytes())
|
|
})
|
|
.collect();
|
|
|
|
for (id, data) in &test_data {
|
|
let mut n = Needle {
|
|
id: *id,
|
|
cookie: Cookie(id.0 as u32),
|
|
data: data.clone(),
|
|
data_size: data.len() as u32,
|
|
..Needle::default()
|
|
};
|
|
v.write_needle(&mut n, true).unwrap();
|
|
}
|
|
v.sync_to_disk().unwrap();
|
|
let original_dat_size = v.dat_file_size().unwrap();
|
|
v.close();
|
|
|
|
// Read original .dat for comparison
|
|
let original_dat = std::fs::read(format!("{}/1.dat", dir)).unwrap();
|
|
|
|
// Encode to EC
|
|
let data_shards = 10;
|
|
let parity_shards = 4;
|
|
ec_encoder::write_ec_files(dir, dir, "", VolumeId(1), data_shards, parity_shards).unwrap();
|
|
|
|
// Delete original .dat and .idx
|
|
std::fs::remove_file(format!("{}/1.dat", dir)).unwrap();
|
|
std::fs::remove_file(format!("{}/1.idx", dir)).unwrap();
|
|
|
|
// Reconstruct from EC shards
|
|
write_dat_file_from_shards(dir, "", VolumeId(1), original_dat_size as i64, data_shards)
|
|
.unwrap();
|
|
write_idx_file_from_ec_index(dir, "", VolumeId(1)).unwrap();
|
|
|
|
// Verify reconstructed .dat matches original
|
|
let reconstructed_dat = std::fs::read(format!("{}/1.dat", dir)).unwrap();
|
|
assert_eq!(
|
|
original_dat[..original_dat_size as usize],
|
|
reconstructed_dat[..original_dat_size as usize],
|
|
"reconstructed .dat should match original"
|
|
);
|
|
|
|
// Verify we can load and read from reconstructed volume
|
|
let v2 = Volume::new(
|
|
dir,
|
|
dir,
|
|
"",
|
|
VolumeId(1),
|
|
NeedleMapKind::InMemory,
|
|
None,
|
|
None,
|
|
0,
|
|
Version::current(),
|
|
)
|
|
.unwrap();
|
|
|
|
for (id, expected_data) in &test_data {
|
|
let mut n = Needle {
|
|
id: *id,
|
|
..Needle::default()
|
|
};
|
|
v2.read_needle(&mut n).unwrap();
|
|
assert_eq!(&n.data, expected_data, "needle {} data should match", id);
|
|
}
|
|
}
|
|
}
|