The Gaudi Framework  master (2e57474e)
Loading...
Searching...
No Matches
NTupleDiskBuffer.cpp
Go to the documentation of this file.
1/***********************************************************************************\
2* (c) Copyright 2026 CERN for the benefit of the LHCb and ATLAS collaborations *
3* *
4* This software is distributed under the terms of the Apache version 2 licence, *
5* copied verbatim in the file "LICENSE". *
6* *
7* In applying this licence, CERN does not waive the privileges and immunities *
8* granted to it by virtue of its status as an Intergovernmental Organization *
9* or submit itself to any jurisdiction. *
10\***********************************************************************************/
12
13#include <cerrno>
14#include <cstring>
15#include <unistd.h>
16
17#ifdef GAUDI_USE_ZSTD
18# include <zstd.h>
19#endif
20
21namespace {
22 constexpr char MAGIC[8] = { 'G', 'a', 'u', 'd', 'i', 'N', 'T', 'B' };
23 constexpr std::uint32_t BYTE_ORDER_MARK = 0x01020304u;
24 constexpr std::uint32_t VERSION = 1;
25 constexpr std::uint32_t CODEC_NONE = 0;
26 constexpr std::uint32_t CODEC_ZSTD = 1;
27 constexpr std::size_t READ_BUFFER = 64 * 1024;
28 constexpr std::size_t MAX_RECORD = std::size_t( 1 ) << 30; // keeps a block and its compressed bound in uint32
29 constexpr std::size_t MAX_BLOCK = MAX_RECORD; // a block is at most this plus one record
30 constexpr std::size_t MAX_HEADER = 64 * 1024 * 1024;
31 // a block is one block size plus the record that filled it; anything larger is a corrupt header
32 constexpr std::uint64_t MAX_STORED = std::uint64_t( MAX_BLOCK ) + MAX_RECORD + sizeof( std::uint32_t );
33 constexpr std::size_t PREAMBLE = sizeof( MAGIC ) + 4 * sizeof( std::uint32_t );
34 constexpr std::size_t BLOCK_HEADER = 2 * sizeof( std::uint32_t );
35
36 void put32( char* dest, std::uint32_t v ) { std::memcpy( dest, &v, sizeof( v ) ); }
37 std::uint32_t get32( const char* src ) {
38 std::uint32_t v;
39 std::memcpy( &v, src, sizeof( v ) );
40 return v;
41 }
42} // namespace
43
44using namespace Gaudi::NTuple::DiskBuffer;
45
46std::size_t Gaudi::NTuple::DiskBuffer::maxBlockSize() { return MAX_BLOCK; }
47
49#ifdef GAUDI_USE_ZSTD
50 return true;
51#else
52 return false;
53#endif
54}
55
56Error::Error( const std::filesystem::path& path, std::string_view what, int err )
57 : std::runtime_error( std::string( what ) + ": " + path.string() +
58 ( err ? std::string( " (" ) + std::strerror( err ) + ")" : "" ) )
59 , m_path( path )
60 , m_code( err ) {}
61
62// ---------------------------------------------------------------------------
63Writer::Writer( std::filesystem::path path, std::string_view header, int zstdLevel, std::size_t blockSize )
64 : m_path( std::move( path ) ), m_level( zstdLevel ), m_blockSize( blockSize ) {
65 if ( header.size() > MAX_HEADER ) throw Error( m_path, "header too large" );
66 if ( m_level != 0 && !zstdAvailable() ) throw Error( m_path, "zstd compression is not available in this build" );
67 if ( m_blockSize == 0 || m_blockSize > MAX_BLOCK ) throw Error( m_path, "invalid block size" );
68 m_file.reset( std::fopen( m_path.c_str(), "wb" ) );
69 if ( !m_file ) throw Error( m_path, "cannot create buffer file", errno );
70 std::setvbuf( m_file.get(), nullptr, _IONBF, 0 ); // m_buf is the buffer
71 std::vector<char> preamble( MAGIC, MAGIC + sizeof( MAGIC ) );
72 char words[4 * sizeof( std::uint32_t )];
73 put32( words, BYTE_ORDER_MARK );
74 put32( words + 4, VERSION );
75 put32( words + 8, m_level ? CODEC_ZSTD : CODEC_NONE );
76 put32( words + 12, static_cast<std::uint32_t>( header.size() ) );
77 preamble.insert( preamble.end(), words, words + sizeof( words ) );
78 preamble.insert( preamble.end(), header.begin(), header.end() );
79 write( preamble.data(), preamble.size() );
80 m_stored = 0; // count from here
81 // m_buf grows with the data: reserving m_blockSize up front costs a full block per tuple whether it fills or not
82}
83
84Writer::~Writer() = default; // m_file closes itself; close() is the checked path
85
87 if ( m_inRecord ) throw Error( m_path, "beginRecord called inside a record" );
88 m_recordStart = m_buf.size();
89 m_buf.resize( m_buf.size() + sizeof( std::uint32_t ) ); // length, patched by endRecord
90 m_inRecord = true;
91}
92
93void Writer::append( const void* data, std::size_t bytes ) {
94 if ( !m_inRecord ) throw Error( m_path, "append called outside a record" );
95 if ( bytes == 0 ) return;
96 if ( m_buf.size() - m_recordStart - sizeof( std::uint32_t ) + bytes > MAX_RECORD )
97 throw Error( m_path, "record too large" );
98 const char* src = static_cast<const char*>( data );
99 m_buf.insert( m_buf.end(), src, src + bytes );
100}
101
103 if ( !m_inRecord ) throw Error( m_path, "endRecord called outside a record" );
104 const std::size_t payload = m_buf.size() - m_recordStart - sizeof( std::uint32_t );
105 put32( m_buf.data() + m_recordStart, static_cast<std::uint32_t>( payload ) );
106 m_inRecord = false;
107 ++m_pending;
108 m_pendingBytes += payload;
109 if ( m_buf.size() >= m_blockSize ) flush();
110}
111
112void Writer::write( const void* data, std::size_t bytes ) {
113 if ( std::fwrite( data, 1, bytes, m_file.get() ) != bytes ) throw Error( m_path, "write failed", errno );
114 m_stored += bytes;
115}
116
117// Write m_buf as one block. Only called between records, so a block holds whole records.
119 if ( !m_file ) throw Error( m_path, "buffer file is closed" );
120 if ( m_buf.empty() ) return;
121 // dropped on failure too: the data is lost either way, do not let the buffer grow
122 auto drop = [this] {
123 m_buf.clear();
125 // a single huge record should not pin its memory for the rest of the job
126 if ( m_buf.capacity() > 4 * m_blockSize ) m_buf.shrink_to_fit();
127 };
128 const long start = std::ftell( m_file.get() );
129 const std::uint64_t storedStart = m_stored;
130 try {
131 const char* data = m_buf.data();
132 std::size_t stored = m_buf.size();
133 std::vector<char> out;
134#ifdef GAUDI_USE_ZSTD
135 if ( m_level != 0 ) {
136 out.resize( ZSTD_compressBound( m_buf.size() ) );
137 stored = ZSTD_compress( out.data(), out.size(), m_buf.data(), m_buf.size(), m_level );
138 if ( ZSTD_isError( stored ) )
139 throw Error( m_path, std::string( "zstd compression failed: " ) + ZSTD_getErrorName( stored ) );
140 data = out.data();
141 }
142#endif
143 char hdr[BLOCK_HEADER];
144 put32( hdr, static_cast<std::uint32_t>( stored ) );
145 put32( hdr + 4, static_cast<std::uint32_t>( m_buf.size() ) );
146 write( hdr, sizeof( hdr ) );
147 write( data, stored );
148 } catch ( ... ) {
149 // cut the half-written block off, so the blocks before it stay readable
150 if ( start >= 0 && ::ftruncate( ::fileno( m_file.get() ), start ) == 0 ) {
151 std::fseek( m_file.get(), 0, SEEK_END );
152 m_stored = storedStart;
153 }
154 drop();
155 throw;
156 }
159 drop();
160}
161
163 if ( !m_file ) return;
164 if ( m_inRecord ) throw Error( m_path, "close called inside a record" );
165 flush();
166 bool ok = std::fflush( m_file.get() ) == 0;
167 int err = ok ? 0 : errno;
168 // the block is the buffer, so a deferred write error (quota, NFS) only shows up here
169 if ( std::fclose( m_file.release() ) != 0 && ok ) {
170 ok = false;
171 err = errno;
172 }
173 if ( !ok ) throw Error( m_path, "close failed", err );
174}
175
176// ---------------------------------------------------------------------------
177Reader::Reader( std::filesystem::path path ) : m_path( std::move( path ) ) {
178 m_file.reset( std::fopen( m_path.c_str(), "rb" ) );
179 if ( !m_file ) throw Error( m_path, "cannot open buffer file", errno );
180 std::setvbuf( m_file.get(), nullptr, _IOFBF, READ_BUFFER );
181 std::error_code ec;
182 m_size = std::filesystem::file_size( m_path, ec );
183 if ( ec ) m_size = 0;
184 char preamble[PREAMBLE];
185 if ( read( preamble, PREAMBLE ) != PREAMBLE || std::memcmp( preamble, MAGIC, sizeof( MAGIC ) ) != 0 )
186 throw Error( m_path, "not a Gaudi ntuple buffer file" );
187 const char* words = preamble + sizeof( MAGIC );
188 if ( get32( words ) != BYTE_ORDER_MARK ) throw Error( m_path, "buffer file has foreign byte order" );
189 if ( get32( words + 4 ) != VERSION ) throw Error( m_path, "unsupported buffer file version" );
190 switch ( get32( words + 8 ) ) {
191 case CODEC_NONE:
192 break;
193 case CODEC_ZSTD:
194 if ( !zstdAvailable() ) throw Error( m_path, "compressed buffer file but zstd is not available in this build" );
195 m_compressed = true;
196 break;
197 default:
198 throw Error( m_path, "unknown buffer file compression" );
199 }
200 const std::uint32_t headerLen = get32( words + 12 );
201 if ( headerLen > MAX_HEADER ) throw Error( m_path, "corrupt header length" );
202 m_header.resize( headerLen );
203 if ( read( m_header.data(), headerLen ) != headerLen ) throw Error( m_path, "truncated header" );
204}
205
206Reader::~Reader() = default;
207
208std::size_t Reader::read( void* dest, std::size_t bytes ) {
209 const std::size_t got = std::fread( dest, 1, bytes, m_file.get() );
210 if ( got != bytes && std::ferror( m_file.get() ) ) throw Error( m_path, "read failed", errno );
211 return got;
212}
213
215 char hdr[BLOCK_HEADER];
216 const std::size_t got = read( hdr, sizeof( hdr ) );
217 if ( got == 0 ) return false;
218 if ( got != sizeof( hdr ) ) throw Error( m_path, "truncated block header" );
219 const std::uint32_t stored = get32( hdr ), raw = get32( hdr + 4 );
220 // check the lengths before allocating: a corrupt header must not turn into a huge resize
221 if ( stored > MAX_STORED || raw > MAX_STORED ) throw Error( m_path, "corrupt block header" );
222 const long pos = std::ftell( m_file.get() );
223 if ( m_size > 0 && pos >= 0 && stored > m_size - static_cast<std::uintmax_t>( pos ) )
224 throw Error( m_path, "truncated block" );
225 m_pos = 0;
226 if ( !m_compressed ) {
227 if ( stored != raw ) throw Error( m_path, "corrupt block header" );
228 if ( m_block.size() < raw ) m_block.resize( raw );
229 if ( read( m_block.data(), raw ) != raw ) throw Error( m_path, "truncated block" );
230 m_block.resize( raw );
231 return true;
232 }
233#ifdef GAUDI_USE_ZSTD
234 if ( m_stored.size() < stored ) m_stored.resize( stored );
235 if ( read( m_stored.data(), stored ) != stored ) throw Error( m_path, "truncated block" );
236 // the frame knows what it unpacks to: trust that over the block header
237 const unsigned long long content = ZSTD_getFrameContentSize( m_stored.data(), stored );
238 if ( content != ZSTD_CONTENTSIZE_UNKNOWN && content != ZSTD_CONTENTSIZE_ERROR && content != raw )
239 throw Error( m_path, "corrupt block header" );
240 if ( m_block.size() < raw ) m_block.resize( raw );
241 const std::size_t n = ZSTD_decompress( m_block.data(), raw, m_stored.data(), stored );
242 if ( ZSTD_isError( n ) ) throw Error( m_path, std::string( "corrupt block: " ) + ZSTD_getErrorName( n ) );
243 if ( n != raw ) throw Error( m_path, "corrupt block: size mismatch" );
244 m_block.resize( raw );
245 return true;
246#else
247 throw Error( m_path, "compressed buffer file but zstd is not available in this build" );
248#endif
249}
250
251bool Reader::next( std::span<const char>& record ) {
252 while ( m_pos == m_block.size() ) {
253 if ( !readBlock() ) return false;
254 }
255 if ( m_block.size() - m_pos < sizeof( std::uint32_t ) ) throw Error( m_path, "corrupt block: bad record length" );
256 const std::uint32_t len = get32( m_block.data() + m_pos );
257 m_pos += sizeof( std::uint32_t );
258 if ( m_block.size() - m_pos < len ) throw Error( m_path, "corrupt block: record crosses block end" );
259 record = std::span<const char>( m_block.data() + m_pos, len );
260 m_pos += len;
261 ++m_entries;
262 return true;
263}
Any I/O or format problem, carrying the file it happened on.
Definition DiskBuffer.h:50
const std::filesystem::path & path() const
Definition DiskBuffer.h:53
std::filesystem::path m_path
Definition DiskBuffer.h:57
Error(const std::filesystem::path &path, std::string_view what, int err=0)
std::size_t read(void *dest, std::size_t bytes)
Reader(std::filesystem::path path)
Open path and validate the preamble.
bool next(std::span< const char > &record)
Read the next record; false at a clean end of file. A partial block throws.
const std::filesystem::path & path() const
Definition DiskBuffer.h:122
std::filesystem::path m_path
Definition DiskBuffer.h:128
void write(const void *data, std::size_t bytes)
void append(const void *data, std::size_t bytes)
const std::filesystem::path & path() const
Definition DiskBuffer.h:87
std::filesystem::path m_path
Definition DiskBuffer.h:93
std::size_t blockSize() const
Definition DiskBuffer.h:86
void close()
Flush and close; throws on failure. Safe to call twice.
std::uint64_t bytes() const
Payload bytes of those records.
Definition DiskBuffer.h:82
STL class.
Append-only file of length-prefixed records, used to park ntuple rows on disk during the event loop a...
Definition DiskBuffer.h:33
bool zstdAvailable()
Whether this build can write and read compressed blocks.
std::size_t maxBlockSize()
Largest block size a Writer accepts.
STL namespace.