Files
2025-12-10 14:38:26 -08:00

208 lines
8.0 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#ifndef STREAM_BUFFER_H_08492572089560298
#define STREAM_BUFFER_H_08492572089560298
#include <thread>
#include "ring_buffer.h"
#include "string_tools.h"
namespace zen
{
/* implement streaming API on top of libcurl's icky callback-based design
+ curl uses READBUFFER_SIZE download buffer size, but returns via a retarded sendf.c::chop_write() writing in small junks of CURL_MAX_WRITE_SIZE (16 kB)
=> support copying arbitrarily-large files: https://freefilesync.org/forum/viewtopic.php?t=4471
=> maximum performance through async processing (prefetching + output buffer!)
=> cost per worker thread creation ~ 1/20 ms */
class AsyncStreamBuffer
{
public:
explicit AsyncStreamBuffer(size_t capacity) { ringBuf_.reserve(capacity); }
//context of input thread, blocking
size_t read(void* buffer, size_t bytesToRead) //throw <write error>; return "bytesToRead" bytes unless end of stream!
{
std::unique_lock dummy(lockStream_);
const auto bufStart = buffer;
while (bytesToRead > 0)
{
const size_t bytesRead = tryReadImpl(dummy, buffer, bytesToRead); //throw <write error>
if (bytesRead == 0) //end of file
break;
conditionBytesRead_.notify_all();
buffer = static_cast<std::byte*>(buffer) + bytesRead;
bytesToRead -= bytesRead;
}
return static_cast<std::byte*>(buffer) -
static_cast<std::byte*>(bufStart);
}
//context of input thread, blocking
size_t tryRead(void* buffer, size_t bytesToRead) //throw <write error>; may return short; only 0 means EOF! CONTRACT: bytesToRead > 0!
{
size_t bytesRead = 0;
{
std::unique_lock dummy(lockStream_);
bytesRead = tryReadImpl(dummy, buffer, bytesToRead);
}
if (bytesRead > 0)
conditionBytesRead_.notify_all(); //...*outside* the lock
return bytesRead;
}
//context of output thread, blocking
void write(const void* buffer, size_t bytesToWrite) //throw <read error>
{
std::unique_lock dummy(lockStream_);
while (bytesToWrite > 0)
{
const size_t bytesWritten = tryWriteWhileImpl(dummy, buffer, bytesToWrite); //throw <read error>
conditionBytesWritten_.notify_all();
buffer = static_cast<const std::byte*>(buffer) + bytesWritten;
bytesToWrite -= bytesWritten;
}
}
//context of output thread, blocking
size_t tryWrite(const void* buffer, size_t bytesToWrite) //throw <read error>; may return short! CONTRACT: bytesToWrite > 0
{
size_t bytesWritten = 0;
{
std::unique_lock dummy(lockStream_);
bytesWritten = tryWriteWhileImpl(dummy, buffer, bytesToWrite);
}
conditionBytesWritten_.notify_all(); //...*outside* the lock
return bytesWritten;
}
//context of output thread
void closeStream()
{
{
std::lock_guard dummy(lockStream_);
assert(!eof_ && !errorWrite_);
eof_ = true;
}
conditionBytesWritten_.notify_all();
}
//context of input thread
void setReadError(const std::exception_ptr& error)
{
{
std::lock_guard dummy(lockStream_);
assert(error && !errorRead_);
if (!errorRead_)
errorRead_ = error;
}
conditionBytesRead_.notify_all();
}
//context of output thread
void setWriteError(const std::exception_ptr& error)
{
{
std::lock_guard dummy(lockStream_);
assert(error && !errorWrite_);
if (!errorWrite_)
errorWrite_ = error;
}
conditionBytesWritten_.notify_all();
}
#if 0
//function not needed: after file upload completed successfully, no further error can occur!
// => caveat: writing is NOT done (yet) when closeStream() is called!
//context of *output* thread
void checkReadErrors() //throw <read error>
{
std::lock_guard dummy(lockStream_);
if (errorRead_)
std::rethrow_exception(errorRead_); //throw <read error>
}
//function not needed: when EOF is reached (without errors), reading is done => no further error can occur!
//context of *input* thread
void checkWriteErrors() //throw <write error>
{
std::lock_guard dummy(lockStream_);
if (errorWrite_)
std::rethrow_exception(errorWrite_); //throw <write error>
}
#endif
uint64_t getTotalBytesWritten() const { return totalBytesWritten_; }
uint64_t getTotalBytesRead () const { return totalBytesRead_; }
private:
AsyncStreamBuffer (const AsyncStreamBuffer&) = delete;
AsyncStreamBuffer& operator=(const AsyncStreamBuffer&) = delete;
//context of input thread, blocking
size_t tryReadImpl(std::unique_lock<std::mutex>& ul, void* buffer, size_t bytesToRead) //throw <write error>; may return short; only 0 means EOF! CONTRACT: bytesToRead > 0!
{
if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check!
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
assert(isLocked(lockStream_));
assert(!errorRead_);
conditionBytesWritten_.wait(ul, [this] { return errorWrite_ || !ringBuf_.empty() || eof_; });
if (errorWrite_)
std::rethrow_exception(errorWrite_); //throw <write error>
const size_t junkSize = std::min(bytesToRead, ringBuf_.size());
ringBuf_.extract_front(static_cast<std::byte*>(buffer),
static_cast<std::byte*>(buffer)+ junkSize);
totalBytesRead_ += junkSize;
return junkSize;
}
//context of output thread, blocking
size_t tryWriteWhileImpl(std::unique_lock<std::mutex>& ul, const void* buffer, size_t bytesToWrite) //throw <read error>; may return short! CONTRACT: bytesToWrite > 0
{
if (bytesToWrite == 0)
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
assert(isLocked(lockStream_));
assert(!eof_ && !errorWrite_);
/* => can't use InterruptibleThread's interruptibleWait() :(
-> AsyncStreamBuffer is used for input and output streaming
=> both AsyncStreamBuffer::write()/read() would have to implement interruptibleWait()
=> one of these usually called from main thread
=> but interruptibleWait() cannot be called from main thread! */
conditionBytesRead_.wait(ul, [this] { return errorRead_ || ringBuf_.size() < ringBuf_.capacity(); });
if (errorRead_)
std::rethrow_exception(errorRead_); //throw <read error>
const size_t junkSize = std::min(bytesToWrite, ringBuf_.capacity() - ringBuf_.size());
ringBuf_.insert_back(static_cast<const std::byte*>(buffer),
static_cast<const std::byte*>(buffer) + junkSize);
totalBytesWritten_ += junkSize;
return junkSize;
}
std::mutex lockStream_;
RingBuffer<std::byte> ringBuf_; //prefetch/output buffer
bool eof_ = false;
std::exception_ptr errorWrite_;
std::exception_ptr errorRead_;
std::condition_variable conditionBytesWritten_;
std::condition_variable conditionBytesRead_;
std::atomic<uint64_t> totalBytesWritten_{0}; //std:atomic is uninitialized by default!
std::atomic<uint64_t> totalBytesRead_ {0}; //
};
}
#endif //STREAM_BUFFER_H_08492572089560298