1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
//! Publish a complete optional index while preserving the old file until replacement.
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use super::{MultiPackIndex, WriteLimits, WriteOptions};
use crate::retention::MaintenanceIsolation;
use crate::{ObjectId, Repository};
/// A standalone MIDX successfully published in the primary object directory.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct IndexWritten {
/// Checksum identifying the encoded index and its optional bitmap sidecars.
pub checksum: ObjectId,
/// Number of unique objects represented.
pub objects: usize,
/// Number of named primary-store packs.
pub packs: usize,
}
/// MIDX installation failure, retaining whether the new index became visible.
#[derive(Debug, thiserror::Error)]
#[error("cannot {operation} multi-pack index")]
#[non_exhaustive]
pub struct InstallError {
/// Whether atomic replacement completed before the failure. Inspect storage before retrying.
pub published: bool,
/// Publication phase that failed.
pub operation: &'static str,
/// Filesystem, isolation, input or encoding failure.
#[source]
pub source: crate::Error,
}
impl Repository {
/// Writes a primary-store MIDX under caller-owned maintenance exclusion and default bounds.
///
/// Equivalent to [`Self::write_multi_pack_index_with`] with default limits and no cancellation.
///
/// # Errors
///
/// Returns [`InstallError`] for unavailable exclusion, a held writer lock or invalid inputs.
pub fn write_multi_pack_index<I: MaintenanceIsolation>(
&self,
isolation: &mut I,
options: WriteOptions,
) -> Result<IndexWritten, InstallError> {
self.write_multi_pack_index_with(
isolation,
options,
WriteLimits::default(),
&AtomicBool::new(false),
)
}
/// Atomically publishes a monolithic MIDX built from one verified primary-store snapshot.
///
/// The isolation guard must exclude other pack writers and retirement from acquisition through
/// return, as required by [`MaintenanceIsolation`]. A separate MIDX writer lock coordinates
/// auxiliary-index publishers. See [`MaintenanceIsolation`] for a caller-owned guard example.
/// Existing packs, incremental layers and checksum-named bitmap
/// sidecars remain untouched; the standalone file takes precedence over incremental chains.
/// A bitmap with another checksum cannot attach to the new index.
///
/// Source indexes verify before encoding, and pinned generations are checked again before
/// replacement. A complete private file is synchronized before atomic replacement. On Unix,
/// the containing directory is synchronized afterwards; other platforms do not promise name
/// durability across power loss. This operation does not change references or create bitmaps.
/// Snapshot opening uses [`crate::PackLimits::default`]; use the object-store encoder for
/// larger caller-managed snapshots. Hostile directory replacement is outside the exclusion
/// contract.
///
/// # Errors
///
/// Before replacement, errors preserve an existing MIDX. Afterwards, directory synchronization
/// or lock-release failure returns `published=true`; inspect current storage before retrying.
/// Filesystem sharing or replacement restrictions can refuse publication with
/// `published=false`. A pre-existing lock is never stolen; establish that no writer owns it
/// before removing it. Cancellation is checked before source opening, during encoding and
/// before replacement.
pub fn write_multi_pack_index_with<I: MaintenanceIsolation>(
&self,
isolation: &mut I,
options: WriteOptions,
limits: WriteLimits,
cancel: &AtomicBool,
) -> Result<IndexWritten, InstallError> {
let mut operation = "acquire maintenance exclusion";
let mut published = false;
let result = (|| -> crate::Result<IndexWritten> {
super::write::check(cancel)?;
let _exclusion = isolation.acquire(self)?;
let _operation = crate::locks::operation();
let directory = self.object_dir().join("pack");
match fs::metadata(&directory) {
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return Err(super::WriteError::NoObjects.into());
}
Err(error) => return Err(error.into()),
}
operation = "acquire writer lock";
let lock_path = directory.join("multi-pack-index.lock");
let lock = WriterLock::acquire(lock_path.clone()).map_err(|error| {
if error.kind() == io::ErrorKind::AlreadyExists {
crate::Error::from(super::WriteError::Locked(lock_path))
} else {
crate::Error::from(error)
}
})?;
operation = "open primary snapshot";
let objects = self.objects_controlled(
crate::PackLimits::default(),
crate::AlternateLimits::default(),
cancel,
)?;
operation = "encode index";
let bytes = objects.encode_multi_pack_index_with(options, limits, cancel)?;
let parsed = MultiPackIndex::parse_with(
&bytes,
self.object_format(),
super::Limits {
max_bytes: limits.max_bytes,
max_objects: limits.max_objects,
max_packs: limits.max_packs,
},
)?;
let checksum = ObjectId::from_bytes(
self.object_format(),
&bytes[bytes.len() - self.object_format().digest_len()..],
)?;
let report = IndexWritten {
checksum,
objects: parsed.len(),
packs: parsed.pack_names().len(),
};
operation = "write private index";
let mut temporary = tempfile::NamedTempFile::new_in(&directory)?;
temporary.write_all(&bytes)?;
temporary.as_file().sync_all()?;
// Encoding checks its own returned artifact; installation checks again after the
// private file has been written and synchronized, immediately before publication.
operation = "revalidate source packs";
for (_, pack) in objects.primary_packs_with_paths() {
pack.check_unchanged()?;
}
lock.registration.check_path()?;
super::write::check(cancel)?;
operation = "publish index";
temporary
.persist(directory.join("multi-pack-index"))
.map_err(|error| error.error)?;
published = true;
#[cfg(unix)]
{
operation = "synchronize pack directory";
fs::File::open(&directory)?.sync_all()?;
}
operation = "release writer lock";
lock.release()?;
Ok(report)
})();
result.map_err(|source| InstallError {
published,
operation,
source,
})
}
}
pub(crate) struct WriterLock {
path: PathBuf,
file: Option<crate::locks::SharedFile>,
registration: crate::locks::Guard,
}
impl WriterLock {
pub(crate) fn acquire(path: PathBuf) -> io::Result<Self> {
let file =
crate::locks::SharedFile::from(crate::locks::unreplaceable_lock_options().open(&path)?);
let identity = crate::locks::capture_identity(&path, &file)?;
let registration = crate::locks::Guard::new(&path, &file, identity);
Ok(Self {
path,
file: Some(file),
registration,
})
}
pub(crate) fn check(&self) -> io::Result<()> {
self.registration.check_path()
}
pub(crate) fn release(mut self) -> io::Result<()> {
self.registration.check_path()?;
drop(self.file.take());
fs::remove_file(&self.path)
}
}
impl Drop for WriterLock {
fn drop(&mut self) {
if self.file.is_some() && self.registration.check_path().is_ok() {
drop(self.file.take());
let _ = fs::remove_file(&self.path);
}
}
}