← Documentation
ADS-IDAC Engine 5.0.0
PEOutput.h
Go to the documentation of this file.
1
7//
8// PE node output layout: one file per node (default) or records in one file.
9//
10// PURPOSE. A DET writes one ZPENode file per branch node, and at long mission
11// times those files dominate the run's FILE COUNT (tens of files per
12// sequence), which is what a file-count quota limits, not capacity. When such
13// a quota is exhausted the run fails with "write failed (disk full or I/O
14// error)" from a worker rank even though space is free.
15//
16// Aggregating the same bytes into one file per rank makes the PE file count
17// track the rank count instead of the node count. It is OPT-IN
18// (`--aggregate-pe`, default off) because the per-file layout is what the gold
19// baselines and the downstream readers (pe-plotter, the clustering tools) read.
20//
21// RECORD FORMAT. Self-delimiting so that concatenating two aggregate files is
22// a valid aggregate file, which lets the MPI merge treat the aggregate as one
23// more append file instead of needing rank-unique names:
24//
25// #PE <branch_id> <content_bytes>\n
26// <exactly content_bytes of content>\n
27//
28// The explicit byte count (rather than a sentinel line) keeps parsing
29// unambiguous even if node content ever contains a line beginning "#PE ".
30// Ids may repeat: MPI clients re-save a whole lineage after every sequence, so
31// a later record for an id supersedes an earlier one, exactly as a rewritten
32// file supersedes its former contents.
33//
34// Directory arguments named `pe_dir` are strings that the functions extend by
35// plain concatenation, so pass them WITH a trailing '/' (as
36// Scheduler::getPEDirectory() returns them). Write failures are not reported
37// to the caller, and no function is safe to run concurrently with another one
38// on the same directory.
39//
40#ifndef ADSIDAC_PEOUTPUT_H
41#define ADSIDAC_PEOUTPUT_H
42
43#include <algorithm>
44#include <filesystem>
45#include <fstream>
46#include <map>
47#include <sstream>
48#include <string>
49#include <utility>
50#include <vector>
51
52namespace adsidac::pe {
53
54namespace fs = std::filesystem;
55
59inline const char* aggregate_basename() { return "ZPENodes_agg.txt"; }
60
70inline void append_record(const std::string& pe_dir, int branch_id,
71 const std::string& content) {
72 std::error_code ec;
73 fs::create_directories(pe_dir, ec);
74 std::ofstream fout(pe_dir + aggregate_basename(), std::ios::app | std::ios::binary);
75 if (!fout) return;
76 fout << "#PE " << branch_id << ' ' << content.size() << '\n';
77 fout.write(content.data(), static_cast<std::streamsize>(content.size()));
78 fout << '\n';
79}
80
88inline void parse_aggregate(const fs::path& path,
89 std::vector<std::pair<int, std::string>>& out) {
90 std::ifstream fin(path, std::ios::binary);
91 if (!fin) return;
92 std::string line;
93 while (std::getline(fin, line)) {
94 if (line.rfind("#PE ", 0) != 0) continue;
95 std::istringstream hs(line.substr(4));
96 int id = 0;
97 long long nbytes = -1;
98 if (!(hs >> id >> nbytes) || nbytes < 0) break;
99 std::string content(static_cast<size_t>(nbytes), '\0');
100 fin.read(&content[0], nbytes);
101 if (fin.gcount() != nbytes) break; // truncated final record
102 fin.get(); // trailing newline
103 out.emplace_back(id, std::move(content));
104 }
105}
106
123inline std::pair<size_t, size_t> compact_aggregate(const fs::path& agg_path) {
124 std::vector<std::pair<int, std::string>> raw;
125 parse_aggregate(agg_path, raw);
126 if (raw.empty()) return {0, 0};
127 std::map<int, std::string> latest;
128 for (auto& [id, content] : raw)
129 latest[id] = std::move(content);
130 if (latest.size() == raw.size()) return {raw.size(), latest.size()};
131 const fs::path tmp = agg_path.string() + ".compact";
132 {
133 std::ofstream fout(tmp, std::ios::binary | std::ios::trunc);
134 if (!fout) return {raw.size(), latest.size()};
135 for (auto& [id, content] : latest) {
136 fout << "#PE " << id << ' ' << content.size() << '\n';
137 fout.write(content.data(), static_cast<std::streamsize>(content.size()));
138 fout << '\n';
139 }
140 }
141 std::error_code ec;
142 fs::rename(tmp, agg_path, ec);
143 if (ec) fs::remove(tmp, ec);
144 return {raw.size(), latest.size()};
145}
146
156inline std::vector<std::pair<int, std::string>> collect(const std::string& pe_dir) {
157 std::vector<std::pair<int, std::string>> raw;
158 std::error_code ec;
159 if (!fs::exists(pe_dir, ec)) return {};
160 for (auto& entry : fs::recursive_directory_iterator(pe_dir, ec)) {
161 if (ec) break;
162 if (!entry.is_regular_file()) continue;
163 const std::string fname = entry.path().filename().string();
164 if (fname == aggregate_basename()) {
165 parse_aggregate(entry.path(), raw);
166 } else if (fname.size() > 7 && fname.compare(0, 7, "ZPENode") == 0
167 && fname.back() == 't') {
168 const size_t us = fname.find('_'), dot = fname.find('.');
169 if (us == std::string::npos || dot == std::string::npos || dot <= us) continue;
170 int id = 0;
171 try { id = std::stoi(fname.substr(us + 1, dot - us - 1)); }
172 catch (...) { continue; }
173 std::ifstream fin(entry.path());
174 std::string content((std::istreambuf_iterator<char>(fin)),
175 std::istreambuf_iterator<char>());
176 raw.emplace_back(id, std::move(content));
177 }
178 }
179 std::map<int, std::string> latest; // last write wins, ordered by id
180 for (auto& [id, content] : raw) {
181 if (id < 0) continue;
182 latest[id] = std::move(content);
183 }
184 return {latest.begin(), latest.end()};
185}
186
199inline size_t rewrite_sequential(const std::string& pe_dir,
200 std::vector<std::pair<int, std::string>> entries,
201 bool aggregate) {
202 std::sort(entries.begin(), entries.end(),
203 [](const auto& a, const auto& b) { return a.first < b.first; });
204 std::error_code ec;
205 fs::remove_all(pe_dir, ec);
206 const std::string folder = pe_dir + "PEFolder_0/";
207 fs::create_directories(folder, ec);
208 if (aggregate) {
209 std::ofstream fout(folder + aggregate_basename(), std::ios::binary);
210 for (size_t i = 0; i < entries.size(); ++i) {
211 fout << "#PE " << i << ' ' << entries[i].second.size() << '\n';
212 fout.write(entries[i].second.data(),
213 static_cast<std::streamsize>(entries[i].second.size()));
214 fout << '\n';
215 }
216 } else {
217 for (size_t i = 0; i < entries.size(); ++i) {
218 std::ofstream fout(folder + "ZPENode_" + std::to_string(i) + ".txt");
219 fout << entries[i].second;
220 }
221 }
222 return entries.size();
223}
224
254inline size_t finalize_sequential(const std::string& pe_dir, bool aggregate) {
255 std::error_code ec;
256 if (!fs::exists(pe_dir, ec)) return 0;
257 if (aggregate)
258 return rewrite_sequential(pe_dir, collect(pe_dir), true);
259
260 const std::string journal_path = pe_dir + "/renumber.map";
261 const std::string folder = (fs::path(pe_dir) / "PEFolder_0").string() + "/";
262 std::vector<std::pair<int, int>> mapping; // old id -> new id
263
264 std::ifstream jin(journal_path);
265 if (jin) {
266 // Crash recovery: resume the journaled mapping, never recompute it.
267 std::string header;
268 std::getline(jin, header);
269 int o = 0, n = 0;
270 while (jin >> o >> n) mapping.emplace_back(o, n);
271 if (header.rfind("# adsidac pe renumber v1", 0) != 0 || mapping.empty())
272 return rewrite_sequential(pe_dir, collect(pe_dir), false);
273 } else {
274 // Fresh scan: filenames only. Any aggregate file or duplicate id means
275 // mixed/unexpected state - take the content path instead.
276 std::map<int, fs::path> ids;
277 for (auto& entry : fs::recursive_directory_iterator(pe_dir, ec)) {
278 if (ec) break;
279 if (!entry.is_regular_file()) continue;
280 const std::string fname = entry.path().filename().string();
281 if (fname == aggregate_basename())
282 return rewrite_sequential(pe_dir, collect(pe_dir), false);
283 if (fname.size() > 7 && fname.compare(0, 7, "ZPENode") == 0
284 && fname.back() == 't') {
285 const size_t us = fname.find('_'), dot = fname.find('.');
286 if (us == std::string::npos || dot == std::string::npos || dot <= us) continue;
287 int id = 0;
288 try { id = std::stoi(fname.substr(us + 1, dot - us - 1)); }
289 catch (...) { continue; }
290 if (id < 0) continue; // sentinels removed in the cleanup pass
291 if (!ids.emplace(id, entry.path()).second)
292 return rewrite_sequential(pe_dir, collect(pe_dir), false);
293 }
294 }
295 if (ids.empty()) return 0;
296 // Idempotency: already finalized (dense 0..N-1, everything in PEFolder_0).
297 bool done = ids.rbegin()->first == static_cast<int>(ids.size()) - 1;
298 if (done)
299 for (auto& [id, p] : ids)
300 if (p.parent_path().filename() != "PEFolder_0") { done = false; break; }
301 if (!done) {
302 int next = 0;
303 for (auto& [id, p] : ids) mapping.emplace_back(id, next++);
304 std::ofstream jout(journal_path, std::ios::trunc);
305 jout << "# adsidac pe renumber v1 " << mapping.size() << '\n';
306 for (auto& [o, n] : mapping) jout << o << ' ' << n << '\n';
307 jout.flush();
308 if (!jout) return rewrite_sequential(pe_dir, collect(pe_dir), false);
309 }
310 }
311
312 if (!mapping.empty()) {
313 fs::create_directories(folder, ec);
314 // Pass 1: old name -> tmp_<new>. Existence checks make recovery re-runs
315 // and fresh runs the same loop.
316 for (auto& [oldid, newid] : mapping) {
317 const std::string final_name = folder + "ZPENode_" + std::to_string(newid) + ".txt";
318 const std::string tmp_name = folder + "tmp_" + std::to_string(newid);
319 if (fs::exists(final_name, ec) || fs::exists(tmp_name, ec)) continue;
320 const std::string old_name = (fs::path(pe_dir) / ("PEFolder_" + std::to_string(oldid / 1000))
321 / ("ZPENode_" + std::to_string(oldid) + ".txt")).string();
322 if (fs::exists(old_name, ec)) {
323 fs::rename(old_name, tmp_name, ec);
324 } else {
325 // Bucket layouts can differ (a serial resume writes epoch ids into
326 // their own buckets); find the file wherever it is.
327 for (auto& entry : fs::recursive_directory_iterator(pe_dir, ec)) {
328 if (!entry.is_regular_file()) continue;
329 if (entry.path().filename() == "ZPENode_" + std::to_string(oldid) + ".txt") {
330 fs::rename(entry.path(), tmp_name, ec);
331 break;
332 }
333 }
334 }
335 }
336 // Pass 2: tmp_<new> -> ZPENode_<new>.txt.
337 for (auto& [oldid, newid] : mapping) {
338 const std::string tmp_name = folder + "tmp_" + std::to_string(newid);
339 if (fs::exists(tmp_name, ec))
340 fs::rename(tmp_name, folder + "ZPENode_" + std::to_string(newid) + ".txt", ec);
341 }
342 }
343
344 // Cleanup: negative-id sentinels, emptied buckets, the journal. The
345 // content path gets the same result from rewrite_sequential's remove_all;
346 // the finalized-run detector relies on the sentinels being gone.
347 size_t count = 0;
348 std::vector<fs::path> doomed;
349 for (auto& entry : fs::recursive_directory_iterator(pe_dir, ec)) {
350 if (!entry.is_regular_file()) continue;
351 const std::string fname = entry.path().filename().string();
352 if (fname.size() > 8 && fname.compare(0, 8, "ZPENode_") == 0 && fname[8] == '-')
353 doomed.push_back(entry.path());
354 else if (fname.compare(0, 7, "ZPENode") == 0 && fname.back() == 't')
355 ++count;
356 }
357 for (auto& p : doomed) fs::remove(p, ec);
358 for (auto& entry : fs::directory_iterator(pe_dir, ec)) {
359 if (entry.is_directory() && entry.path().filename() != "PEFolder_0"
360 && fs::is_empty(entry.path(), ec))
361 fs::remove(entry.path(), ec);
362 }
363 fs::remove(journal_path, ec);
364 return count;
365}
366
367} // namespace adsidac::pe
368
369#endif // ADSIDAC_PEOUTPUT_H
void parse_aggregate(const fs::path &path, std::vector< std::pair< int, std::string > > &out)
Parse one aggregate file, appending (id, content) to out in file order, duplicates and negative ids i...
Definition: PEOutput.h:88
std::vector< std::pair< int, std::string > > collect(const std::string &pe_dir)
Collect every PE node under pe_dir (recursively), from either layout, reading all node content into m...
Definition: PEOutput.h:156
void append_record(const std::string &pe_dir, int branch_id, const std::string &content)
Append one node record to <pe_dir>ZPENodes_agg.txt.
Definition: PEOutput.h:70
size_t rewrite_sequential(const std::string &pe_dir, std::vector< std::pair< int, std::string > > entries, bool aggregate)
Replace the contents of pe_dir with entries renumbered 0..N-1 in ascending id order (the content path...
Definition: PEOutput.h:199
const char * aggregate_basename()
Basename of the aggregate file ("ZPENodes_agg.txt"). It is in the MPI merge's append-files list,...
Definition: PEOutput.h:59
std::pair< size_t, size_t > compact_aggregate(const fs::path &agg_path)
Rewrite one rank's aggregate file last-wins per id, sorted by id.
Definition: PEOutput.h:123
size_t finalize_sequential(const std::string &pe_dir, bool aggregate)
End-of-run finalize: renumber every PE node 0..N-1 in id order, ending in the same layout rewrite_seq...
Definition: PEOutput.h:254