Skip to content

Commit 2e74c8e

Browse files
committed
Fix TFile gap coalescing
Prevent merged free segments from becoming bigger than 2GB. Requires additional code to ensure that gaps at the end of the file are always merged.
1 parent f43cdef commit 2e74c8e

4 files changed

Lines changed: 191 additions & 27 deletions

File tree

io/io/inc/TFree.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@
2323

2424
#include "TObject.h"
2525

26-
2726
class TFree : public TObject {
28-
2927
protected:
3028
Long64_t fFirst; ///<First free word of segment
3129
Long64_t fLast; ///<Last free word of segment
3230

3331
public:
32+
/// Prevent gap coalescing from exceeding 2 GB, since the gap size is stored as a signed 4 bytes integer.
33+
/// The true limit is 2 GiB but for historical reasons we stick to the 2GB limit.
34+
static constexpr Int_t kMaxGapSize = 2000 * 1000 * 1000;
35+
3436
TFree();
3537
TFree(TList *lfree, Long64_t first, Long64_t last);
3638
~TFree() override;

io/io/src/TFile.cxx

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,27 +1474,58 @@ Bool_t TFile::IsOpen() const
14741474

14751475
void TFile::MakeFree(Long64_t first, Long64_t last)
14761476
{
1477-
TFree *f1 = (TFree*)fFree->First();
1478-
if (!f1) return;
1479-
TFree *newfree = f1->AddFree(fFree,first,last);
1480-
if(!newfree) return;
1477+
TFree *f1 = static_cast<TFree *>(fFree->First());
1478+
if (!f1)
1479+
return;
1480+
1481+
TFree *newfree = f1->AddFree(fFree, first, last);
1482+
if (!newfree)
1483+
return;
14811484
Long64_t nfirst = newfree->GetFirst();
14821485
Long64_t nlast = newfree->GetLast();
1483-
Long64_t nbytesl= nlast-nfirst+1;
1484-
if (nbytesl > 2000000000) nbytesl = 2000000000;
1485-
Int_t nbytes = -Int_t (nbytesl);
1486-
char buffer[sizeof(Int_t)];
1487-
char *pbuffer = buffer;
1488-
tobuf(pbuffer, nbytes);
1489-
if (last == fEND-1) fEND = nfirst;
1490-
Seek(nfirst);
1491-
// We could not update the meta data for this block on the file.
1492-
// This is not fatal as this only means that we won't get it 'right'
1493-
// if we ever need to Recover the file before the block is actually
1494-
// (attempted to be reused.
1495-
// coverity[unchecked_value]
1496-
WriteBuffer(buffer, sizeof(buffer));
1497-
if (fMustFlush) Flush();
1486+
1487+
// The new free segment may close a series of consecutive free segments at the end of the file.
1488+
// These segments need to be merged so that the last free segment is [fEND + 1 .. max file size].
1489+
auto tail = static_cast<TFree *>(fFree->Last());
1490+
while (tail != fFree->First()) {
1491+
// Starting from the last segment, merge in previous segments if they are adjacent
1492+
auto prev = (TFree *)fFree->Before(tail);
1493+
if (prev->GetLast() + 1 < tail->GetFirst())
1494+
break;
1495+
assert(prev->GetLast() + 1 == tail->GetFirst());
1496+
tail->SetFirst(prev->GetFirst());
1497+
fFree->Remove(prev);
1498+
delete prev;
1499+
}
1500+
if (tail->GetFirst() <= nfirst) {
1501+
nfirst = tail->GetFirst();
1502+
nlast = tail->GetLast();
1503+
}
1504+
1505+
// The file will get smaller if a free segment is added at the end. In this case, we are done and we don't need
1506+
// to write the free segment size past the new end of the file (besides, the length of this "virtual" free
1507+
// segment may anyway be larger than TFree::kMaxGapSize).
1508+
if (last == fEND - 1) {
1509+
fEND = nfirst;
1510+
} else {
1511+
Long64_t nbytesl = nlast - nfirst + 1;
1512+
assert(nbytesl <= TFree::kMaxGapSize);
1513+
1514+
Int_t nbytes = -static_cast<Int_t>(nbytesl);
1515+
char buffer[sizeof(Int_t)];
1516+
char *pbuffer = buffer;
1517+
tobuf(pbuffer, nbytes);
1518+
1519+
Seek(nfirst);
1520+
// We could not update the meta data for this block on the file.
1521+
// This is not fatal as this only means that we won't get it 'right'
1522+
// if we ever need to Recover the file before the block is actually
1523+
// (attempted to be reused.
1524+
// coverity[unchecked_value]
1525+
WriteBuffer(buffer, sizeof(buffer));
1526+
if (fMustFlush)
1527+
Flush();
1528+
}
14981529
}
14991530

15001531
////////////////////////////////////////////////////////////////////////////////

io/io/src/TFree.cxx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,23 @@ TFree *TFree::AddFree(TList *lfree, Long64_t first, Long64_t last)
7070
while (idcur) {
7171
Long64_t curfirst = idcur->GetFirst();
7272
Long64_t curlast = idcur->GetLast();
73-
if (curlast == first-1) {
73+
if ((curlast == first - 1) && (last - curfirst + 1 <= kMaxGapSize)) {
7474
idcur->SetLast(last);
75-
TFree *idnext = (TFree*)lfree->After(idcur);
76-
if (idnext == 0) return idcur;
77-
if (idnext->GetFirst() > last+1) return idcur;
78-
idcur->SetLast( idnext->GetLast() );
75+
// Merged new segment with previous one; is there a next segment?
76+
TFree *idnext = static_cast<TFree *>(lfree->After(idcur));
77+
if (idnext == 0)
78+
return idcur;
79+
80+
// Is the next segment adjacent to the newly merged one (and not too big)
81+
if ((idnext->GetFirst() > last + 1) || (idnext->GetLast() - curfirst + 1 > kMaxGapSize))
82+
return idcur;
83+
84+
idcur->SetLast(idnext->GetLast());
7985
lfree->Remove(idnext);
8086
delete idnext;
8187
return idcur;
8288
}
83-
if (curfirst == last+1) {
89+
if ((curfirst == last + 1) && (curlast - first + 1 <= kMaxGapSize)) {
8490
idcur->SetFirst(first);
8591
return idcur;
8692
}

io/io/test/TFileTests.cxx

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,128 @@ TEST(TFile, WalkTKeys)
251251
EXPECT_EQ(it->fKeyName, kLongerKey);
252252
EXPECT_EQ(it->fClassName, "string");
253253
}
254+
255+
TEST(TFile, DeleteKey)
256+
{
257+
struct FileRaii {
258+
std::string fFilename;
259+
FileRaii(std::string_view fname) : fFilename(fname) {}
260+
~FileRaii() { gSystem->Unlink(fFilename.c_str()); }
261+
} fileGuard("tfile_test_delete_keys.root");
262+
263+
auto fnCountGaps = [](const std::string &fileName) {
264+
auto f = std::unique_ptr<TFile>(TFile::Open(fileName.c_str()));
265+
std::uint64_t nGaps = 0;
266+
for (const auto &k : f->WalkTKeys()) {
267+
if (k.fType == ROOT::Detail::TKeyMapNode::kGap)
268+
nGaps++;
269+
}
270+
return nGaps;
271+
};
272+
273+
auto f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "RECREATE"));
274+
f->SetCompressionSettings(0);
275+
f->Write();
276+
f->Close();
277+
278+
// The empty file should have no gaps. Note that gaps are created temporarily when certain keys are overwritten.
279+
EXPECT_EQ(0, fnCountGaps(fileGuard.fFilename));
280+
281+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
282+
std::vector<char> v;
283+
f->WriteObject(&v, "va0");
284+
f->WriteObject(&v, "va1");
285+
f->WriteObject(&v, "va2");
286+
f->Write();
287+
f->Close();
288+
// 2 gaps: new (larger) keys list and free list are written
289+
EXPECT_EQ(2, fnCountGaps(fileGuard.fFilename));
290+
291+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
292+
f->Delete("va1;*"); // should create small gap that cannot be merged, trapped between v0 and v2
293+
f->Write();
294+
f->Close();
295+
296+
EXPECT_EQ(3, fnCountGaps(fileGuard.fFilename));
297+
298+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
299+
f->Delete("va2;*"); // gaps at the tail should merge
300+
f->Write();
301+
f->Close();
302+
303+
EXPECT_EQ(2, fnCountGaps(fileGuard.fFilename));
304+
305+
v.resize(1000 * 1000 * 1000 - 100, 'x'); // almost 1GB
306+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
307+
f->WriteObject(&v, "vb0");
308+
f->WriteObject(&v, "vb1");
309+
v.resize(1000 * 1000); // truncate next objects to 1MB
310+
f->WriteObject(&v, "vc0");
311+
f->WriteObject(&v, "vc1");
312+
f->WriteObject(&v, "vc2");
313+
f->WriteObject(&v, "vc3");
314+
f->Write();
315+
EXPECT_GT(f->GetEND(), TFile::kStartBigFile);
316+
f->Close();
317+
318+
// New keys list, hence 3 gaps
319+
EXPECT_EQ(3, fnCountGaps(fileGuard.fFilename));
320+
321+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
322+
f->Delete("vb0;*"); // |
323+
f->Delete("vb1;*"); // |---> First merged gap
324+
f->Delete("vc0;*"); // |
325+
f->Delete("vc1;*"); // |---> Second merged gap
326+
f->Write();
327+
f->Close();
328+
329+
// Two merged gaps, the new keys list fits into either of them
330+
EXPECT_EQ(4, fnCountGaps(fileGuard.fFilename));
331+
332+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
333+
// Delete the remaining data at the tail of the file in reverse order
334+
f->Delete("vc2;*");
335+
f->Delete("vc3;*");
336+
f->Write();
337+
// Back to small file
338+
EXPECT_LT(f->GetEND(), TFile::kStartBigFile);
339+
f->Close();
340+
341+
// Only the original 2 gaps from the first keys list and free list overwrite
342+
EXPECT_EQ(2, fnCountGaps(fileGuard.fFilename));
343+
344+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
345+
v.resize(700 * 1000 * 1000); // construct objects such that 3 consecutive gaps surpass 2GB (but not 2)
346+
f->WriteObject(&v, "vd0");
347+
f->WriteObject(&v, "vd1");
348+
f->WriteObject(&v, "vd2");
349+
f->WriteObject(&v, "vd3");
350+
f->WriteObject(&v, "vd4");
351+
f->Write();
352+
f->Close();
353+
354+
// New keys list --> 3 gaps
355+
EXPECT_EQ(3, fnCountGaps(fileGuard.fFilename));
356+
357+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
358+
f->Delete("vd1;*");
359+
f->Delete("vd3;*");
360+
f->Write();
361+
f->Close();
362+
363+
// Nothing mergable, 2 more gaps
364+
EXPECT_EQ(5, fnCountGaps(fileGuard.fFilename));
365+
366+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
367+
auto theEnd = f->GetEND();
368+
f->Delete("vd2;*");
369+
f->Write();
370+
f->Close();
371+
372+
// We can only merge the gaps of v1 and v2, not all three (vd1, vd2, vd3) due to the gap size
373+
EXPECT_EQ(5, fnCountGaps(fileGuard.fFilename));
374+
375+
// Ensure that the file's tail is still intact
376+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str()));
377+
EXPECT_EQ(f->GetEND(), theEnd);
378+
}

0 commit comments

Comments
 (0)