Refactor concurrency handling and file operations for improved performance and thread safety
This commit is contained in:
@@ -20,6 +20,8 @@ add_executable(main
|
|||||||
src/Public/sweepstore/concurrency.h
|
src/Public/sweepstore/concurrency.h
|
||||||
src/Private/sweepstore/concurrency.cpp
|
src/Private/sweepstore/concurrency.cpp
|
||||||
src/Public/sweepstore/utils/file_lock.h
|
src/Public/sweepstore/utils/file_lock.h
|
||||||
|
src/Private/sweepstore/utils/fd_pool.h
|
||||||
|
src/Private/sweepstore/utils/fd_pool.cpp
|
||||||
src/Public/sweepstore/utils/file_handle.h
|
src/Public/sweepstore/utils/file_handle.h
|
||||||
src/Private/sweepstore/utils/file_handle.cpp
|
src/Private/sweepstore/utils/file_handle.cpp
|
||||||
src/Public/sweepstore/header.h
|
src/Public/sweepstore/header.h
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
#include "sweepstore/utils/helpers.h"
|
#include "sweepstore/utils/helpers.h"
|
||||||
#include "sweepstore/utils/file_handle.h"
|
#include "sweepstore/utils/file_handle.h"
|
||||||
|
#include "sweepstore/structures.h"
|
||||||
|
#include "sweepstore/concurrency.h"
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
@@ -35,16 +37,17 @@ int main() {
|
|||||||
|
|
||||||
SweepstoreConcurrency::initialiseMasterAsync(filePath);
|
SweepstoreConcurrency::initialiseMasterAsync(filePath);
|
||||||
|
|
||||||
int iterations = 100;
|
int iterations = 32;
|
||||||
int currentIteration = 0;
|
int currentIteration = 0;
|
||||||
|
|
||||||
|
int concurrencyTest = 1;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
if (++currentIteration > iterations) {
|
if (++currentIteration > iterations) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
int concurrencyTest = 256;
|
|
||||||
std::atomic<int> completedJobs = 0;
|
std::atomic<int> completedJobs = 0;
|
||||||
|
|
||||||
// Worker pool infrastructure
|
// Worker pool infrastructure
|
||||||
@@ -82,11 +85,11 @@ int main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue 256 tasks
|
// Queue 256 tasks - each will open its own handle
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lock(queueMutex);
|
std::unique_lock<std::mutex> lock(queueMutex);
|
||||||
for (int i = 0; i < concurrencyTest; i++) {
|
for (int i = 0; i < concurrencyTest; i++) {
|
||||||
taskQueue.push([&sweepstore, i]() {
|
taskQueue.push([filePath, i, &sweepstore]() {
|
||||||
sweepstore["key_" + std::to_string(i)] = "value_" + std::to_string(i);
|
sweepstore["key_" + std::to_string(i)] = "value_" + std::to_string(i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -102,7 +105,7 @@ int main() {
|
|||||||
auto end = std::chrono::high_resolution_clock::now();
|
auto end = std::chrono::high_resolution_clock::now();
|
||||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
|
||||||
|
|
||||||
std::cout << "Completed " << concurrencyTest << " operations in " << duration << " ms." << std::endl;
|
std::cout << "[" << currentIteration << "/" << iterations << "] Completed " << concurrencyTest << " operations in " << duration << " ms." << std::endl;
|
||||||
|
|
||||||
// Shutdown workers
|
// Shutdown workers
|
||||||
shutdown = true;
|
shutdown = true;
|
||||||
@@ -110,6 +113,8 @@ int main() {
|
|||||||
for (auto& worker : workers) {
|
for (auto& worker : workers) {
|
||||||
worker.join();
|
worker.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
concurrencyTest *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ int randomId() {
|
|||||||
return (time ^ random) & 0x7FFFFFFF;
|
return (time ^ random) & 0x7FFFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SweepstoreConcurrency::spawnTicket(std::string filePath,
|
void SweepstoreConcurrency::spawnTicket(SweepstoreFileHandle* _file,
|
||||||
const SweepstoreTicketOperation& operation,
|
const SweepstoreTicketOperation& operation,
|
||||||
const uint32_t keyHash,
|
const uint32_t keyHash,
|
||||||
const uint32_t targetSize,
|
const uint32_t targetSize,
|
||||||
@@ -38,7 +38,9 @@ void SweepstoreConcurrency::spawnTicket(std::string filePath,
|
|||||||
std::string debugLabel
|
std::string debugLabel
|
||||||
) {
|
) {
|
||||||
|
|
||||||
SweepstoreFileHandle file(filePath, std::ios::binary | std::ios::in | std::ios::out);
|
// FileHandle now uses thread-local streams internally - no need to create new handle!
|
||||||
|
// Each thread automatically gets its own fstream from the shared file handle
|
||||||
|
SweepstoreFileHandle* file = new SweepstoreFileHandle(_file->getPath(), std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Useful Functions
|
Useful Functions
|
||||||
@@ -47,7 +49,7 @@ void SweepstoreConcurrency::spawnTicket(std::string filePath,
|
|||||||
/// Logging function
|
/// Logging function
|
||||||
auto log = [&](const std::string &message) {
|
auto log = [&](const std::string &message) {
|
||||||
std::string prefix = !debugLabel.empty() ? "\033[38;5;208m[Ticket Spawner - " + debugLabel + "]:\033[0m " : "\033[38;5;208m[Ticket Spawner]:\033[0m ";
|
std::string prefix = !debugLabel.empty() ? "\033[38;5;208m[Ticket Spawner - " + debugLabel + "]:\033[0m " : "\033[38;5;208m[Ticket Spawner]:\033[0m ";
|
||||||
debugPrint(prefix + message);
|
// debugPrint(prefix + message);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sleep with variance (additive only)
|
// Sleep with variance (additive only)
|
||||||
@@ -71,9 +73,9 @@ void SweepstoreConcurrency::spawnTicket(std::string filePath,
|
|||||||
expSleepTracker[label] = count + 1;
|
expSleepTracker[label] = count + 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the header(s)
|
// Get the header(s) - using the shared file handle directly
|
||||||
SweepstoreHeader header(file);
|
SweepstoreHeader header(*file);
|
||||||
SweepstoreConcurrencyHeader concurrencyHeader(file);
|
SweepstoreConcurrencyHeader concurrencyHeader(*file);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Ticket Acquisition
|
Ticket Acquisition
|
||||||
@@ -91,7 +93,7 @@ void SweepstoreConcurrency::spawnTicket(std::string filePath,
|
|||||||
|
|
||||||
for (uint32_t i = 0; i < concurrentWorkers; i++) {
|
for (uint32_t i = 0; i < concurrentWorkers; i++) {
|
||||||
|
|
||||||
SweepstoreWorkerTicket ticket = SweepstoreWorkerTicket(i, file);
|
SweepstoreWorkerTicket ticket = SweepstoreWorkerTicket(i, *file);
|
||||||
|
|
||||||
if (!ticket.writable()) {
|
if (!ticket.writable()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -165,21 +167,24 @@ void SweepstoreConcurrency::spawnTicket(std::string filePath,
|
|||||||
// Re-verify we lost the ticket
|
// Re-verify we lost the ticket
|
||||||
SweepstoreWorkerTicketSnapshot recheckSnapshot = myTicket.snapshot();
|
SweepstoreWorkerTicketSnapshot recheckSnapshot = myTicket.snapshot();
|
||||||
if (recheckSnapshot.identifier != myIdentifier) {
|
if (recheckSnapshot.identifier != myIdentifier) {
|
||||||
log("Lost ownership of ticket " + std::to_string(myTicket.getTicketIndex()) + ", was expecting identifier " + std::to_string(myIdentifier) + " but found " + std::to_string(recheckSnapshot.identifier) + ".");
|
// log("Lost ownership of ticket " + std::to_string(myTicket.getTicketIndex()) + ", was expecting identifier " + std::to_string(myIdentifier) + " but found " + std::to_string(recheckSnapshot.identifier) + ".");
|
||||||
|
std::cout << "\033[38;5;82m[Ticket Spawner - " << debugLabel << "]:\033[0m Lost ticket " << myTicket.getTicketIndex() << ", respawning..." << std::endl;
|
||||||
|
|
||||||
// ReSharper disable once CppDFAInfiniteRecursion
|
// ReSharper disable once CppDFAInfiniteRecursion
|
||||||
return spawnTicket(
|
spawnTicket(
|
||||||
filePath,
|
_file,
|
||||||
operation,
|
operation,
|
||||||
keyHash,
|
keyHash,
|
||||||
targetSize,
|
targetSize,
|
||||||
onApproved,
|
onApproved,
|
||||||
debugLabel
|
debugLabel
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// False alarm, continue waiting
|
// False alarm, continue waiting
|
||||||
log("False alarm, still own ticket " + std::to_string(myTicket.getTicketIndex()) + ".");
|
// log("False alarm, still own ticket " + std::to_string(myTicket.getTicketIndex()) + ".");
|
||||||
|
std::cout << "\033[38;5;82m[Ticket Spawner - " << debugLabel << "]:\033[0m False alarm, still own ticket " << myTicket.getTicketIndex() << "." << std::endl;
|
||||||
snapshot = recheckSnapshot;
|
snapshot = recheckSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +204,7 @@ void SweepstoreConcurrency::spawnTicket(std::string filePath,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// std::cout << "\033[38;5;82m[Ticket Spawner - " << debugLabel << "]:\033[0m Completed ticket " << myTicket.getTicketIndex() << "." << std::endl;
|
// std::cout << "\033[38;5;82m[Ticket Spawner - " << debugLabel << "]:\033[0m Completed ticket " << myTicket.getTicketIndex() << "." << std::endl;
|
||||||
|
delete file;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SweepstoreConcurrency::initialiseMaster(std::string filePath) {
|
void SweepstoreConcurrency::initialiseMaster(std::string filePath) {
|
||||||
@@ -212,6 +218,8 @@ void SweepstoreConcurrency::initialiseMaster(std::string filePath) {
|
|||||||
SweepstoreHeader header(file);
|
SweepstoreHeader header(file);
|
||||||
SweepstoreConcurrencyHeader concurrencyHeader(file);
|
SweepstoreConcurrencyHeader concurrencyHeader(file);
|
||||||
|
|
||||||
|
std::cout << "[Master] Starting master loop" << std::endl;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
int concurrentWorkers = concurrencyHeader.readNumberOfWorkers();
|
int concurrentWorkers = concurrencyHeader.readNumberOfWorkers();
|
||||||
|
|||||||
@@ -145,7 +145,8 @@ void SweepstoreConcurrencyHeader::initialise(int concurrentWorkers) {
|
|||||||
writeMasterHeartbeat(0);
|
writeMasterHeartbeat(0);
|
||||||
writeNumberOfWorkers(concurrentWorkers);
|
writeNumberOfWorkers(concurrentWorkers);
|
||||||
writeIsReadAllowed(true);
|
writeIsReadAllowed(true);
|
||||||
for (uint32_t i = 0; i < readNumberOfWorkers(); i++) {
|
uint32_t verifyWorkers = readNumberOfWorkers();
|
||||||
|
for (uint32_t i = 0; i < verifyWorkers; i++) {
|
||||||
SweepstoreWorkerTicketSnapshot ticket = SweepstoreWorkerTicketSnapshot();
|
SweepstoreWorkerTicketSnapshot ticket = SweepstoreWorkerTicketSnapshot();
|
||||||
ticket.identifier = 0;
|
ticket.identifier = 0;
|
||||||
ticket.workerHeartbeat = 0;
|
ticket.workerHeartbeat = 0;
|
||||||
@@ -164,8 +165,7 @@ void SweepstoreConcurrencyHeader::initialise(int concurrentWorkers) {
|
|||||||
void SweepstoreWorkerTicket::write(SweepstoreWorkerTicketSnapshot &snapshot) {
|
void SweepstoreWorkerTicket::write(SweepstoreWorkerTicketSnapshot &snapshot) {
|
||||||
RandomAccessMemory buffer;
|
RandomAccessMemory buffer;
|
||||||
|
|
||||||
SweepstoreFileLock lock(file.getPath(), SweepstoreFileLock::Mode::Exclusive);
|
uint64_t offset = getOffset();
|
||||||
SweepstoreFileLock::Scoped scopedLock(lock);
|
|
||||||
|
|
||||||
buffer.setPositionSync(0);
|
buffer.setPositionSync(0);
|
||||||
buffer.writeIntSync(snapshot.identifier, 4);
|
buffer.writeIntSync(snapshot.identifier, 4);
|
||||||
@@ -187,23 +187,20 @@ void SweepstoreWorkerTicket::write(SweepstoreWorkerTicketSnapshot &snapshot) {
|
|||||||
char* dataPtr = reinterpret_cast<char*>(data.data());
|
char* dataPtr = reinterpret_cast<char*>(data.data());
|
||||||
|
|
||||||
// Write to file
|
// Write to file
|
||||||
file.writeSeek(getOffset());
|
file.writeSeek(offset);
|
||||||
file.writeBytes(dataPtr, data.size());
|
file.writeBytes(dataPtr, data.size());
|
||||||
file.flush();
|
file.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SweepstoreWorkerTicket::writable() {
|
bool SweepstoreWorkerTicket::writable() {
|
||||||
SweepstoreFileLock lock(file.getPath(), SweepstoreFileLock::Mode::Exclusive);
|
return true;
|
||||||
return lock.isLocked() == false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SweepstoreWorkerTicketSnapshot SweepstoreWorkerTicket::snapshot() {
|
SweepstoreWorkerTicketSnapshot SweepstoreWorkerTicket::snapshot() {
|
||||||
SweepstoreFileLock lock(file.getPath(), SweepstoreFileLock::Mode::Shared);
|
uint64_t offset = getOffset();
|
||||||
lock.lock();
|
file.readSeek(offset);
|
||||||
file.readSeek(getOffset());
|
|
||||||
std::unique_ptr<char[]> buffer(new char[TICKET_SIZE]);
|
std::unique_ptr<char[]> buffer(new char[TICKET_SIZE]);
|
||||||
file.readBytes(buffer.get(), TICKET_SIZE);
|
file.readBytes(buffer.get(), TICKET_SIZE);
|
||||||
lock.unlock();
|
|
||||||
RandomAccessMemory ram(reinterpret_cast<uint8_t*>(buffer.get()), TICKET_SIZE);
|
RandomAccessMemory ram(reinterpret_cast<uint8_t*>(buffer.get()), TICKET_SIZE);
|
||||||
|
|
||||||
SweepstoreWorkerTicketSnapshot snapshot;
|
SweepstoreWorkerTicketSnapshot snapshot;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#include "sweepstore/utils/file_handle.h"
|
#include "sweepstore/utils/file_handle.h"
|
||||||
|
|
||||||
// Constructor
|
// Constructor - just stores path and mode, actual stream is created per-thread
|
||||||
SweepstoreFileHandle::SweepstoreFileHandle(const std::string& p, std::ios::openmode mode)
|
SweepstoreFileHandle::SweepstoreFileHandle(const std::string& p, std::ios::openmode mode)
|
||||||
: path(p)
|
: path(p)
|
||||||
|
, openMode(mode)
|
||||||
#ifdef WITH_UNREAL
|
#ifdef WITH_UNREAL
|
||||||
{
|
{
|
||||||
IPlatformFile& platformFile = FPlatformFileManager::Get().GetPlatformFile();
|
IPlatformFile& platformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||||||
@@ -24,11 +25,30 @@ SweepstoreFileHandle::SweepstoreFileHandle(const std::string& p, std::ios::openm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
, stream(std::make_unique<std::fstream>(p, mode))
|
|
||||||
{
|
{
|
||||||
if (!stream->is_open()) {
|
// Thread-local streams created on demand in getThreadStream()
|
||||||
throw std::runtime_error("Failed to open file: " + path);
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef WITH_UNREAL
|
||||||
|
// Get or create the fstream for this thread
|
||||||
|
std::fstream& SweepstoreFileHandle::getThreadStream() {
|
||||||
|
auto it = streamCache.find(path);
|
||||||
|
if (it == streamCache.end() || !it->second || !it->second->is_open()) {
|
||||||
|
// Create new stream for this thread
|
||||||
|
auto stream = std::make_unique<std::fstream>(path, openMode);
|
||||||
|
if (!stream->is_open()) {
|
||||||
|
throw std::runtime_error("Failed to open file: " + path);
|
||||||
|
}
|
||||||
|
streamCache[path] = std::move(stream);
|
||||||
|
return *streamCache[path];
|
||||||
}
|
}
|
||||||
|
return *it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::fstream& SweepstoreFileHandle::getThreadStream() const {
|
||||||
|
// Use const_cast to reuse the non-const version
|
||||||
|
return const_cast<SweepstoreFileHandle*>(this)->getThreadStream();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -37,7 +57,8 @@ bool SweepstoreFileHandle::isOpen() const {
|
|||||||
#ifdef WITH_UNREAL
|
#ifdef WITH_UNREAL
|
||||||
return unrealHandle != nullptr;
|
return unrealHandle != nullptr;
|
||||||
#else
|
#else
|
||||||
return stream && stream->is_open();
|
auto it = streamCache.find(path);
|
||||||
|
return it != streamCache.end() && it->second && it->second->is_open();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +70,10 @@ void SweepstoreFileHandle::close() {
|
|||||||
unrealHandle = nullptr;
|
unrealHandle = nullptr;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if (stream) {
|
// Close this thread's stream if it exists
|
||||||
stream->close();
|
auto it = streamCache.find(path);
|
||||||
|
if (it != streamCache.end() && it->second && it->second->is_open()) {
|
||||||
|
it->second->close();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -63,11 +86,10 @@ void SweepstoreFileHandle::flush() {
|
|||||||
unrealHandle->Flush();
|
unrealHandle->Flush();
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if (stream) {
|
auto& stream = getThreadStream();
|
||||||
stream->flush();
|
stream.flush();
|
||||||
// On Windows, also sync to ensure data hits disk
|
// On Windows, also sync to ensure data hits disk
|
||||||
stream->sync();
|
stream.sync();
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,13 +107,14 @@ void SweepstoreFileHandle::readSeek(std::streampos pos, std::ios::seekdir dir) {
|
|||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Windows
|
// Windows
|
||||||
|
auto& stream = getThreadStream();
|
||||||
// On Windows, flush and sync to disk, then invalidate buffers
|
// On Windows, flush and sync to disk, then invalidate buffers
|
||||||
stream->flush();
|
stream.flush();
|
||||||
stream->sync();
|
stream.sync();
|
||||||
stream->clear();
|
stream.clear();
|
||||||
// Sync both pointers to same position
|
// Sync both pointers to same position
|
||||||
stream->seekp(pos, dir);
|
stream.seekp(pos, dir);
|
||||||
stream->seekg(pos, dir);
|
stream.seekg(pos, dir);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +125,10 @@ void SweepstoreFileHandle::writeSeek(std::streampos pos, std::ios::seekdir dir)
|
|||||||
readSeek(pos, dir);
|
readSeek(pos, dir);
|
||||||
#else
|
#else
|
||||||
// Windows
|
// Windows
|
||||||
stream->flush();
|
auto& stream = getThreadStream();
|
||||||
stream->sync();
|
stream.flush();
|
||||||
stream->seekp(pos, dir);
|
stream.sync();
|
||||||
|
stream.seekp(pos, dir);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,10 +138,11 @@ void SweepstoreFileHandle::readBytes(char* buffer, std::streamsize size) {
|
|||||||
unrealHandle->Read(reinterpret_cast<uint8*>(buffer), size);
|
unrealHandle->Read(reinterpret_cast<uint8*>(buffer), size);
|
||||||
#else
|
#else
|
||||||
// Windows
|
// Windows
|
||||||
stream->read(buffer, size);
|
auto& stream = getThreadStream();
|
||||||
|
stream.read(buffer, size);
|
||||||
// Check for read errors on Windows
|
// Check for read errors on Windows
|
||||||
if (stream->fail() && !stream->eof()) {
|
if (stream.fail() && !stream.eof()) {
|
||||||
stream->clear();
|
stream.clear();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -129,10 +154,11 @@ void SweepstoreFileHandle::writeBytes(const char* buffer, std::streamsize size)
|
|||||||
unrealHandle->Flush(); // Unreal requires explicit flush
|
unrealHandle->Flush(); // Unreal requires explicit flush
|
||||||
#else
|
#else
|
||||||
// Windows
|
// Windows
|
||||||
stream->write(buffer, size);
|
auto& stream = getThreadStream();
|
||||||
|
stream.write(buffer, size);
|
||||||
// Check for write errors on Windows
|
// Check for write errors on Windows
|
||||||
if (stream->fail()) {
|
if (stream.fail()) {
|
||||||
stream->clear();
|
stream.clear();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,16 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#define STALE_HEARTBEAT_THRESHOLD_MS 5000
|
#define STALE_HEARTBEAT_THRESHOLD_MS 5000
|
||||||
|
|
||||||
enum SweepstoreTicketOperation : int;
|
enum SweepstoreTicketOperation : int;
|
||||||
|
class SweepstoreFileHandle;
|
||||||
|
|
||||||
namespace SweepstoreConcurrency {
|
namespace SweepstoreConcurrency {
|
||||||
|
|
||||||
void spawnTicket(std::string filePath,
|
void spawnTicket(SweepstoreFileHandle* file,
|
||||||
const SweepstoreTicketOperation& operation,
|
const SweepstoreTicketOperation& operation,
|
||||||
const uint32_t keyHash,
|
const uint32_t keyHash,
|
||||||
const uint32_t targetSize,
|
const uint32_t targetSize,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public:
|
|||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void operator=(const T& value) {
|
void operator=(const T& value) {
|
||||||
SweepstoreConcurrency::spawnTicket(sweepstore->filePath,
|
SweepstoreConcurrency::spawnTicket(&sweepstore->file,
|
||||||
SweepstoreTicketOperation::WRITE,
|
SweepstoreTicketOperation::WRITE,
|
||||||
bt_hash(key),
|
bt_hash(key),
|
||||||
sizeof(T),
|
sizeof(T),
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <iostream>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#include <io.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef WITH_UNREAL
|
#ifdef WITH_UNREAL
|
||||||
#include "HAL/PlatformFileManager.h"
|
#include "HAL/PlatformFileManager.h"
|
||||||
@@ -12,10 +21,16 @@
|
|||||||
class SweepstoreFileHandle {
|
class SweepstoreFileHandle {
|
||||||
private:
|
private:
|
||||||
std::string path;
|
std::string path;
|
||||||
|
std::ios::openmode openMode;
|
||||||
#ifdef WITH_UNREAL
|
#ifdef WITH_UNREAL
|
||||||
IFileHandle* unrealHandle;
|
IFileHandle* unrealHandle;
|
||||||
#else
|
#else
|
||||||
std::unique_ptr<std::fstream> stream;
|
// Thread-local cache: each thread gets its own fstream per path
|
||||||
|
static thread_local std::unordered_map<std::string, std::unique_ptr<std::fstream>> streamCache;
|
||||||
|
|
||||||
|
// Get or create the fstream for this thread
|
||||||
|
std::fstream& getThreadStream();
|
||||||
|
const std::fstream& getThreadStream() const;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -24,15 +39,15 @@ public:
|
|||||||
const std::string& getPath() const { return path; }
|
const std::string& getPath() const { return path; }
|
||||||
|
|
||||||
#ifndef WITH_UNREAL
|
#ifndef WITH_UNREAL
|
||||||
std::fstream& getStream() { return *stream; }
|
std::fstream& getStream() { return getThreadStream(); }
|
||||||
const std::fstream& getStream() const { return *stream; }
|
const std::fstream& getStream() const { return getThreadStream(); }
|
||||||
|
|
||||||
// Smart pointer-like interface
|
// Smart pointer-like interface
|
||||||
std::fstream* operator->() { return stream.get(); }
|
std::fstream* operator->() { return &getThreadStream(); }
|
||||||
const std::fstream* operator->() const { return stream.get(); }
|
const std::fstream* operator->() const { return &getThreadStream(); }
|
||||||
|
|
||||||
std::fstream& operator*() { return *stream; }
|
std::fstream& operator*() { return getThreadStream(); }
|
||||||
const std::fstream& operator*() const { return *stream; }
|
const std::fstream& operator*() const { return getThreadStream(); }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool isOpen() const;
|
bool isOpen() const;
|
||||||
@@ -48,26 +63,46 @@ public:
|
|||||||
#else
|
#else
|
||||||
// Inline for non-Windows to avoid overhead
|
// Inline for non-Windows to avoid overhead
|
||||||
inline void flush() {
|
inline void flush() {
|
||||||
if (stream) {
|
getThreadStream().flush();
|
||||||
stream->flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
inline void readSeek(std::streampos pos, std::ios::seekdir dir = std::ios::beg) {
|
inline void readSeek(std::streampos pos, std::ios::seekdir dir = std::ios::beg) {
|
||||||
stream->seekg(pos, dir);
|
getThreadStream().seekg(pos, dir);
|
||||||
}
|
}
|
||||||
inline void writeSeek(std::streampos pos, std::ios::seekdir dir = std::ios::beg) {
|
inline void writeSeek(std::streampos pos, std::ios::seekdir dir = std::ios::beg) {
|
||||||
stream->seekp(pos, dir);
|
getThreadStream().seekp(pos, dir);
|
||||||
}
|
}
|
||||||
inline void readBytes(char* buffer, std::streamsize size) {
|
inline void readBytes(char* buffer, std::streamsize size) {
|
||||||
stream->read(buffer, size);
|
getThreadStream().read(buffer, size);
|
||||||
}
|
}
|
||||||
inline void writeBytes(const char* buffer, std::streamsize size) {
|
inline void writeBytes(const char* buffer, std::streamsize size) {
|
||||||
stream->write(buffer, size);
|
getThreadStream().write(buffer, size);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
SweepstoreFileHandle(SweepstoreFileHandle&&) noexcept = default;
|
SweepstoreFileHandle(SweepstoreFileHandle&& other) noexcept
|
||||||
SweepstoreFileHandle& operator=(SweepstoreFileHandle&&) noexcept = default;
|
: path(std::move(other.path))
|
||||||
|
, openMode(other.openMode)
|
||||||
|
#ifdef WITH_UNREAL
|
||||||
|
, unrealHandle(other.unrealHandle)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
#ifdef WITH_UNREAL
|
||||||
|
other.unrealHandle = nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
SweepstoreFileHandle& operator=(SweepstoreFileHandle&& other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
close();
|
||||||
|
path = std::move(other.path);
|
||||||
|
openMode = other.openMode;
|
||||||
|
#ifdef WITH_UNREAL
|
||||||
|
unrealHandle = other.unrealHandle;
|
||||||
|
other.unrealHandle = nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
SweepstoreFileHandle(const SweepstoreFileHandle&) = delete;
|
SweepstoreFileHandle(const SweepstoreFileHandle&) = delete;
|
||||||
SweepstoreFileHandle& operator=(const SweepstoreFileHandle&) = delete;
|
SweepstoreFileHandle& operator=(const SweepstoreFileHandle&) = delete;
|
||||||
|
|||||||
@@ -1,159 +1,103 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#else
|
#else
|
||||||
#include <sys/file.h>
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <sys/file.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Simple file lock using flock() with thread-local FD cache
|
||||||
|
// Each thread has its own FD, flock() is per-FD, so threads don't conflict
|
||||||
|
// Matches Dart's paradigm: each isolate has its own RandomAccessFile
|
||||||
class SweepstoreFileLock {
|
class SweepstoreFileLock {
|
||||||
public:
|
public:
|
||||||
enum class Mode {
|
enum class Mode { Shared, Exclusive };
|
||||||
Shared,
|
|
||||||
Exclusive
|
|
||||||
};
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
#ifdef _WIN32
|
std::string filePath;
|
||||||
HANDLE handle;
|
|
||||||
OVERLAPPED overlapped;
|
|
||||||
#else
|
|
||||||
int fd;
|
|
||||||
#endif
|
|
||||||
std::string path;
|
|
||||||
Mode mode;
|
Mode mode;
|
||||||
bool locked;
|
bool locked = false;
|
||||||
|
|
||||||
|
// Thread-local FD cache - each thread has its own FD per file
|
||||||
|
static thread_local std::unordered_map<std::string, int> fdCache;
|
||||||
|
|
||||||
|
static int getOrOpenFD(const std::string& path) {
|
||||||
|
auto it = fdCache.find(path);
|
||||||
|
if (it != fdCache.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd = open(path.c_str(), O_RDWR);
|
||||||
|
if (fd == -1) {
|
||||||
|
throw std::runtime_error("Failed to open file for locking: " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
fdCache[path] = fd;
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
void acquire() {
|
void acquire() {
|
||||||
#ifdef _WIN32
|
int fd = getOrOpenFD(filePath);
|
||||||
handle = CreateFileA(path.c_str(), GENERIC_READ | GENERIC_WRITE,
|
int operation = (mode == Mode::Exclusive) ? LOCK_EX : LOCK_SH;
|
||||||
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
|
|
||||||
FILE_ATTRIBUTE_NORMAL, NULL);
|
|
||||||
|
|
||||||
if (handle == INVALID_HANDLE_VALUE) {
|
if (flock(fd, operation) == -1) {
|
||||||
throw std::runtime_error("Failed to open file");
|
throw std::runtime_error("Failed to acquire file lock");
|
||||||
}
|
}
|
||||||
|
|
||||||
memset(&overlapped, 0, sizeof(overlapped));
|
|
||||||
DWORD flags = (mode == Mode::Exclusive) ? LOCKFILE_EXCLUSIVE_LOCK : 0;
|
|
||||||
|
|
||||||
if (!LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped)) {
|
|
||||||
CloseHandle(handle);
|
|
||||||
throw std::runtime_error("Failed to lock");
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
fd = open(path.c_str(), O_RDWR);
|
|
||||||
if (fd == -1) throw std::runtime_error("Failed to open file");
|
|
||||||
|
|
||||||
int op = (mode == Mode::Exclusive) ? LOCK_EX : LOCK_SH;
|
|
||||||
if (flock(fd, op) != 0) {
|
|
||||||
close(fd);
|
|
||||||
throw std::runtime_error("Failed to lock");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
locked = true;
|
locked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void release() {
|
void release() {
|
||||||
if (locked) {
|
if (locked) {
|
||||||
#ifdef _WIN32
|
int fd = getOrOpenFD(filePath);
|
||||||
UnlockFileEx(handle, 0, MAXDWORD, MAXDWORD, &overlapped);
|
|
||||||
CloseHandle(handle);
|
|
||||||
#else
|
|
||||||
flock(fd, LOCK_UN);
|
flock(fd, LOCK_UN);
|
||||||
close(fd);
|
|
||||||
#endif
|
|
||||||
locked = false;
|
locked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SweepstoreFileLock(const std::string& p, Mode m)
|
// Constructor accepts offset/length for API compatibility (unused with flock)
|
||||||
: path(p), mode(m), locked(false) {
|
SweepstoreFileLock(const std::string& path, uint64_t, uint64_t, Mode m)
|
||||||
#ifdef _WIN32
|
: filePath(path), mode(m) {}
|
||||||
handle = INVALID_HANDLE_VALUE;
|
|
||||||
memset(&overlapped, 0, sizeof(overlapped));
|
|
||||||
#else
|
|
||||||
fd = -1;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
~SweepstoreFileLock() {
|
~SweepstoreFileLock() { release(); }
|
||||||
release();
|
|
||||||
}
|
|
||||||
|
|
||||||
void lock() {
|
void lock() {
|
||||||
if (!locked) {
|
if (!locked) acquire();
|
||||||
acquire();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void unlock() {
|
void unlock() {
|
||||||
release();
|
release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if THIS instance holds the lock
|
bool holdsLock() const {
|
||||||
bool holdsLock() const { return locked; }
|
return locked;
|
||||||
|
|
||||||
// Check if the file is locked by ANYONE (including this instance)
|
|
||||||
bool isLocked() const {
|
|
||||||
#ifdef _WIN32
|
|
||||||
HANDLE testHandle = CreateFileA(path.c_str(), GENERIC_READ | GENERIC_WRITE,
|
|
||||||
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
|
|
||||||
FILE_ATTRIBUTE_NORMAL, NULL);
|
|
||||||
|
|
||||||
if (testHandle == INVALID_HANDLE_VALUE) {
|
|
||||||
return false; // Can't even open file
|
|
||||||
}
|
|
||||||
|
|
||||||
OVERLAPPED testOverlapped;
|
|
||||||
memset(&testOverlapped, 0, sizeof(testOverlapped));
|
|
||||||
DWORD flags = (mode == Mode::Exclusive) ? LOCKFILE_EXCLUSIVE_LOCK : 0;
|
|
||||||
flags |= LOCKFILE_FAIL_IMMEDIATELY; // Non-blocking
|
|
||||||
|
|
||||||
bool isLocked = !LockFileEx(testHandle, flags, 0, MAXDWORD, MAXDWORD, &testOverlapped);
|
|
||||||
|
|
||||||
if (!isLocked) {
|
|
||||||
// We got the lock, release it
|
|
||||||
UnlockFileEx(testHandle, 0, MAXDWORD, MAXDWORD, &testOverlapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
CloseHandle(testHandle);
|
|
||||||
return isLocked;
|
|
||||||
#else
|
|
||||||
int testFd = open(path.c_str(), O_RDWR);
|
|
||||||
if (testFd == -1) {
|
|
||||||
return false; // Can't open file
|
|
||||||
}
|
|
||||||
|
|
||||||
int op = (mode == Mode::Exclusive) ? LOCK_EX : LOCK_SH;
|
|
||||||
op |= LOCK_NB; // Non-blocking
|
|
||||||
|
|
||||||
bool isLocked = (flock(testFd, op) != 0);
|
|
||||||
|
|
||||||
if (!isLocked) {
|
|
||||||
// We got the lock, release it
|
|
||||||
flock(testFd, LOCK_UN);
|
|
||||||
}
|
|
||||||
|
|
||||||
close(testFd);
|
|
||||||
return isLocked;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SweepstoreFileLock(const SweepstoreFileLock&) = delete;
|
// Check if file is currently locked (non-blocking test)
|
||||||
SweepstoreFileLock& operator=(const SweepstoreFileLock&) = delete;
|
bool isLocked() const {
|
||||||
SweepstoreFileLock(SweepstoreFileLock&&) = default;
|
int fd = getOrOpenFD(filePath);
|
||||||
SweepstoreFileLock& operator=(SweepstoreFileLock&&) = default;
|
int operation = (mode == Mode::Exclusive) ? LOCK_EX : LOCK_SH;
|
||||||
|
|
||||||
|
// Try non-blocking lock
|
||||||
|
if (flock(fd, operation | LOCK_NB) == -1) {
|
||||||
|
return true; // Already locked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Got the lock, release immediately
|
||||||
|
flock(fd, LOCK_UN);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAII helper for scoped locking
|
||||||
class Scoped {
|
class Scoped {
|
||||||
private:
|
|
||||||
SweepstoreFileLock& lock;
|
SweepstoreFileLock& lock;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Scoped(SweepstoreFileLock& l) : lock(l) {
|
Scoped(SweepstoreFileLock& l) : lock(l) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
@@ -166,4 +110,8 @@ public:
|
|||||||
Scoped(const Scoped&) = delete;
|
Scoped(const Scoped&) = delete;
|
||||||
Scoped& operator=(const Scoped&) = delete;
|
Scoped& operator=(const Scoped&) = delete;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Disable copying
|
||||||
|
SweepstoreFileLock(const SweepstoreFileLock&) = delete;
|
||||||
|
SweepstoreFileLock& operator=(const SweepstoreFileLock&) = delete;
|
||||||
};
|
};
|
||||||
@@ -67,22 +67,25 @@ Future<void> main() async {
|
|||||||
print(binaryDump(file.readAsBytesSync()));
|
print(binaryDump(file.readAsBytesSync()));
|
||||||
|
|
||||||
int iteration = 0;
|
int iteration = 0;
|
||||||
|
int maxIterations = 16;
|
||||||
|
|
||||||
print("Concurrent Workers: ${store._concurrencyHeader.numberOfWorkers}");
|
print("Concurrent Workers: ${store._concurrencyHeader.numberOfWorkers}");
|
||||||
print("Stale Ticket Threshold: ${STALE_HEARTBEAT_THRESHOLD_MS}ms");
|
print("Stale Ticket Threshold: ${STALE_HEARTBEAT_THRESHOLD_MS}ms");
|
||||||
|
|
||||||
|
int concurrencyTest = 1;
|
||||||
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
int concurrencyTest = 256;
|
|
||||||
final receivePort = ReceivePort();
|
final receivePort = ReceivePort();
|
||||||
int completedJobs = 0;
|
int completedJobs = 0;
|
||||||
|
|
||||||
if (iteration > 0) {
|
if (iteration > maxIterations) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
print('\x1B[95mStarting iteration #$iteration with $concurrencyTest concurrent jobs...\x1B[0m');
|
// print('\x1B[95mStarting iteration #$iteration with $concurrencyTest concurrent jobs...\x1B[0m');
|
||||||
for (int i = 0; i < concurrencyTest; i++) {
|
for (int i = 0; i < concurrencyTest; i++) {
|
||||||
await Isolate.spawn((message) {
|
await Isolate.spawn((message) {
|
||||||
final index = message['index'] as int;
|
final index = message['index'] as int;
|
||||||
@@ -107,11 +110,10 @@ Future<void> main() async {
|
|||||||
|
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
|
|
||||||
print('\x1B[95mAll jobs completed!\x1B[0m');
|
print("[$iteration/$maxIterations] Completed $concurrencyTest operation in ${stopwatch.elapsedMilliseconds} ms");
|
||||||
print('\x1B[95mTime taken: ${stopwatch.elapsedMilliseconds}ms (${stopwatch.elapsed.inSeconds}s)\x1B[0m');
|
|
||||||
print(" ");
|
|
||||||
|
|
||||||
// sleep(Duration(seconds: 2));
|
|
||||||
iteration++;
|
iteration++;
|
||||||
|
|
||||||
|
concurrencyTest *= 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user