Loading debian/changelog +10 −0 Original line number Diff line number Diff line mediadb (20260419+27) unstable; urgency=critical * Fix catastrophic cluster data loss ("stores disappeared") caused by nodes pushing an empty local memory state to the global index when network fetches timed out. Replication loops (`repair_replication` and edits) are now forcefully blocked (`HTTP 503`/`std::runtime_error`) if a node has not yet successfully completed an initial sync (`initial_sync_ok_`). -- Jan Koester <jan.koester@tuxist.de> Sun, 19 Apr 2026 15:30:00 +0200 mediadb (20260419+26) unstable; urgency=critical * Fix preview std::future_error exception caused by invalidating future read states. Loading src/backend.cpp +23 −0 Original line number Diff line number Diff line Loading @@ -2489,6 +2489,9 @@ std::vector<StoreRecord> ClusterMediaBackend::list_stores() const { } bool ClusterMediaBackend::delete_store(const std::string& id) { if (!initial_sync_ok_.load()) { throw std::runtime_error("Cluster node isolated or syncing. Try again later."); } std::unique_lock<std::shared_mutex> cguard(cluster_op_mutex_); // Collect all media and albums to remove their cluster objects Loading Loading @@ -2604,6 +2607,9 @@ std::vector<AlbumRecord> ClusterMediaBackend::list_albums(const std::string& sto } bool ClusterMediaBackend::delete_album(const std::string& id) { if (!initial_sync_ok_.load()) { throw std::runtime_error("Cluster node isolated or syncing. Try again later."); } std::unique_lock<std::shared_mutex> cguard(cluster_op_mutex_); auto album = local_.get_album(id); if (!album) return false; Loading Loading @@ -2681,6 +2687,9 @@ std::vector<MediaRecord> ClusterMediaBackend::list_media(const std::string& albu } bool ClusterMediaBackend::delete_media(const std::string& id) { if (!initial_sync_ok_.load()) { throw std::runtime_error("Cluster node isolated or syncing. Try again later."); } std::unique_lock<std::shared_mutex> cguard(cluster_op_mutex_); auto media = local_.get_media(id); if (!media) return false; Loading Loading @@ -2976,13 +2985,27 @@ void ClusterMediaBackend::sync_from_cluster() { return; } uint64_t index_gid = cluster_group_id("index"); bool has_index = false; for (const auto& pg : cluster_.list_peer_groups()) { if (std::find(pg.groups.begin(), pg.groups.end(), index_gid) != pg.groups.end()) { has_index = true; break; } } // Fetch index from cluster (contains store/album/ACL metadata, no media data) std::vector<uint8_t> index_data; bool fetch_ok = cluster_.fetch("index", index_data); if (!fetch_ok || index_data.empty()) { if (!has_index) { std::cerr << "[CLUSTER-SYNC] fresh empty cluster detected\n"; initial_sync_ok_.store(true); } else { std::cerr << "[CLUSTER-SYNC] index fetch " << (fetch_ok ? "returned empty data" : "failed (exception in cluster layer)") << "\n"; } return; } { Loading src/backend.cpp.rej 0 → 100644 +12 −0 Original line number Diff line number Diff line --- src/backend.cpp +++ src/backend.cpp @@ -2578,6 +2578,9 @@ std::optional<AlbumRecord> ClusterMediaBackend::create_album(const std::string& store_id, const std::string& name) { + if (!initial_sync_ok_.load()) { + throw std::runtime_error("Cluster node isolated or syncing. Try again later."); + } std::unique_lock<std::shared_mutex> cguard(cluster_op_mutex_); auto a = local_.create_album(store_id, name); if (a) replicate_store(store_id); src/backend.h +1 −0 Original line number Diff line number Diff line Loading @@ -337,6 +337,7 @@ private: Cluster& cluster_; std::atomic<bool> sync_running_{false}; std::atomic<bool> importing_{false}; std::atomic<bool> initial_sync_ok_{false}; std::thread sync_thread_; std::mutex sync_mutex_; std::condition_variable sync_cv_; Loading Loading
debian/changelog +10 −0 Original line number Diff line number Diff line mediadb (20260419+27) unstable; urgency=critical * Fix catastrophic cluster data loss ("stores disappeared") caused by nodes pushing an empty local memory state to the global index when network fetches timed out. Replication loops (`repair_replication` and edits) are now forcefully blocked (`HTTP 503`/`std::runtime_error`) if a node has not yet successfully completed an initial sync (`initial_sync_ok_`). -- Jan Koester <jan.koester@tuxist.de> Sun, 19 Apr 2026 15:30:00 +0200 mediadb (20260419+26) unstable; urgency=critical * Fix preview std::future_error exception caused by invalidating future read states. Loading
src/backend.cpp +23 −0 Original line number Diff line number Diff line Loading @@ -2489,6 +2489,9 @@ std::vector<StoreRecord> ClusterMediaBackend::list_stores() const { } bool ClusterMediaBackend::delete_store(const std::string& id) { if (!initial_sync_ok_.load()) { throw std::runtime_error("Cluster node isolated or syncing. Try again later."); } std::unique_lock<std::shared_mutex> cguard(cluster_op_mutex_); // Collect all media and albums to remove their cluster objects Loading Loading @@ -2604,6 +2607,9 @@ std::vector<AlbumRecord> ClusterMediaBackend::list_albums(const std::string& sto } bool ClusterMediaBackend::delete_album(const std::string& id) { if (!initial_sync_ok_.load()) { throw std::runtime_error("Cluster node isolated or syncing. Try again later."); } std::unique_lock<std::shared_mutex> cguard(cluster_op_mutex_); auto album = local_.get_album(id); if (!album) return false; Loading Loading @@ -2681,6 +2687,9 @@ std::vector<MediaRecord> ClusterMediaBackend::list_media(const std::string& albu } bool ClusterMediaBackend::delete_media(const std::string& id) { if (!initial_sync_ok_.load()) { throw std::runtime_error("Cluster node isolated or syncing. Try again later."); } std::unique_lock<std::shared_mutex> cguard(cluster_op_mutex_); auto media = local_.get_media(id); if (!media) return false; Loading Loading @@ -2976,13 +2985,27 @@ void ClusterMediaBackend::sync_from_cluster() { return; } uint64_t index_gid = cluster_group_id("index"); bool has_index = false; for (const auto& pg : cluster_.list_peer_groups()) { if (std::find(pg.groups.begin(), pg.groups.end(), index_gid) != pg.groups.end()) { has_index = true; break; } } // Fetch index from cluster (contains store/album/ACL metadata, no media data) std::vector<uint8_t> index_data; bool fetch_ok = cluster_.fetch("index", index_data); if (!fetch_ok || index_data.empty()) { if (!has_index) { std::cerr << "[CLUSTER-SYNC] fresh empty cluster detected\n"; initial_sync_ok_.store(true); } else { std::cerr << "[CLUSTER-SYNC] index fetch " << (fetch_ok ? "returned empty data" : "failed (exception in cluster layer)") << "\n"; } return; } { Loading
src/backend.cpp.rej 0 → 100644 +12 −0 Original line number Diff line number Diff line --- src/backend.cpp +++ src/backend.cpp @@ -2578,6 +2578,9 @@ std::optional<AlbumRecord> ClusterMediaBackend::create_album(const std::string& store_id, const std::string& name) { + if (!initial_sync_ok_.load()) { + throw std::runtime_error("Cluster node isolated or syncing. Try again later."); + } std::unique_lock<std::shared_mutex> cguard(cluster_op_mutex_); auto a = local_.create_album(store_id, name); if (a) replicate_store(store_id);
src/backend.h +1 −0 Original line number Diff line number Diff line Loading @@ -337,6 +337,7 @@ private: Cluster& cluster_; std::atomic<bool> sync_running_{false}; std::atomic<bool> importing_{false}; std::atomic<bool> initial_sync_ok_{false}; std::thread sync_thread_; std::mutex sync_mutex_; std::condition_variable sync_cv_; Loading