|
|
@@ -390,6 +390,14 @@ void LmdbDocumentStore::cacheCommittedDbi(std::string_view collection,
|
|
|
dbi_cache_[std::string(collection)] = dbi;
|
|
|
}
|
|
|
|
|
|
+std::optional<unsigned int>
|
|
|
+LmdbDocumentStore::cachedDbi(std::string_view collection) {
|
|
|
+ std::lock_guard<std::mutex> lock(cache_mutex_);
|
|
|
+ auto it = dbi_cache_.find(std::string(collection));
|
|
|
+ if (it == dbi_cache_.end()) return std::nullopt;
|
|
|
+ return it->second;
|
|
|
+}
|
|
|
+
|
|
|
std::optional<unsigned int>
|
|
|
LmdbDocumentStore::try_open_for_read(ReadTxn& rtxn,
|
|
|
std::string_view collection) {
|
|
|
@@ -500,8 +508,21 @@ size_t LmdbDocumentStore::prime_dbi_cache() {
|
|
|
void LmdbDocumentStore::put(std::string_view collection,
|
|
|
std::string_view id,
|
|
|
const smartbotic::database::Document& doc) {
|
|
|
- std::string payload = encode_document(doc);
|
|
|
WriteTxn wtxn(env_);
|
|
|
+ std::vector<std::pair<std::string, unsigned int>> to_cache;
|
|
|
+ put(wtxn, collection, id, doc, to_cache);
|
|
|
+ // Commit BEFORE any handle is cached - the v2.8.0 lesson. Every handle in
|
|
|
+ // to_cache is private to wtxn until this succeeds.
|
|
|
+ wtxn.commit();
|
|
|
+ for (const auto& [sub, d] : to_cache) cacheCommittedDbi(sub, d);
|
|
|
+}
|
|
|
+
|
|
|
+void LmdbDocumentStore::put(WriteTxn& wtxn,
|
|
|
+ std::string_view collection,
|
|
|
+ std::string_view id,
|
|
|
+ const smartbotic::database::Document& doc,
|
|
|
+ std::vector<std::pair<std::string, unsigned int>>& to_cache) {
|
|
|
+ std::string payload = encode_document(doc);
|
|
|
unsigned int dbi = open_for_write(wtxn, collection);
|
|
|
MDB_val k = to_val(id);
|
|
|
|
|
|
@@ -509,7 +530,6 @@ void LmdbDocumentStore::put(std::string_view collection,
|
|
|
// throws after this point rolls the index back with the document. An
|
|
|
// asynchronously-maintained index would let a query read entries for a row
|
|
|
// that was never stored, and return silently wrong rows rather than an error.
|
|
|
- std::vector<std::pair<std::string, unsigned int>> index_dbis;
|
|
|
const bool needs_index = !indexed_fields(collection).empty();
|
|
|
const bool needs_relations = !relations(collection).empty();
|
|
|
if (needs_index || needs_relations) {
|
|
|
@@ -519,18 +539,18 @@ void LmdbDocumentStore::put(std::string_view collection,
|
|
|
const std::string_view old_payload =
|
|
|
rc == MDB_SUCCESS ? to_sv(old) : std::string_view{};
|
|
|
if (needs_index) {
|
|
|
- maintainIndexes(wtxn, collection, id, old_payload, &doc, index_dbis);
|
|
|
+ maintainIndexes(wtxn, collection, id, old_payload, &doc, to_cache);
|
|
|
}
|
|
|
if (needs_relations) {
|
|
|
- maintainRelations(wtxn, collection, id, old_payload, &doc, index_dbis);
|
|
|
+ maintainRelations(wtxn, collection, id, old_payload, &doc, to_cache);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
MDB_val v = to_val(payload);
|
|
|
mdb_check(mdb_put(wtxn.raw(), dbi, &k, &v, 0), "put");
|
|
|
- wtxn.commit();
|
|
|
- cacheCommittedDbi(collection, dbi);
|
|
|
- for (const auto& [sub, d] : index_dbis) cacheCommittedDbi(sub, d);
|
|
|
+ // NOT cached here - see the header contract. The caller caches every
|
|
|
+ // entry in to_cache (this one included) only after ITS commit succeeds.
|
|
|
+ to_cache.emplace_back(std::string(collection), dbi);
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1428,23 +1448,34 @@ bool LmdbDocumentStore::del(std::string_view collection, std::string_view id) {
|
|
|
// Same reasoning as get(): a zero-length key cannot exist, so there is
|
|
|
// nothing to delete rather than an error to raise.
|
|
|
if (id.empty()) return false;
|
|
|
- // Open as a write txn unconditionally so we have MDB_CREATE available
|
|
|
- // if the collection doesn't exist yet — but in that case there's
|
|
|
- // nothing to delete; just probe with a read txn first to avoid
|
|
|
- // accidentally creating an empty sub-db on a no-op delete.
|
|
|
- {
|
|
|
- ReadTxn rtxn(env_);
|
|
|
- auto dbi_opt = try_open_for_read(rtxn, collection);
|
|
|
- if (!dbi_opt) return false;
|
|
|
- }
|
|
|
+ // Probe the cache first to avoid accidentally creating an empty sub-db on
|
|
|
+ // a no-op delete - no transaction needed for this, see cachedDbi().
|
|
|
+ if (!cachedDbi(collection)) return false;
|
|
|
|
|
|
WriteTxn wtxn(env_);
|
|
|
+ std::vector<std::pair<std::string, unsigned int>> to_cache;
|
|
|
+ const bool existed = del(wtxn, collection, id, to_cache);
|
|
|
+ // Commit BEFORE any handle is cached - the v2.8.0 lesson.
|
|
|
+ wtxn.commit();
|
|
|
+ for (const auto& [sub, d] : to_cache) cacheCommittedDbi(sub, d);
|
|
|
+ return existed;
|
|
|
+}
|
|
|
+
|
|
|
+bool LmdbDocumentStore::del(WriteTxn& wtxn,
|
|
|
+ std::string_view collection,
|
|
|
+ std::string_view id,
|
|
|
+ std::vector<std::pair<std::string, unsigned int>>& to_cache) {
|
|
|
+ if (id.empty()) return false;
|
|
|
+ // Same no-op-avoidance as the no-txn form above: don't spring an empty
|
|
|
+ // sub-db into existence for a collection that has never been written.
|
|
|
+ // Cache-only, so this costs nothing extra inside the caller's txn.
|
|
|
+ if (!cachedDbi(collection)) return false;
|
|
|
+
|
|
|
unsigned int dbi = open_for_write(wtxn, collection);
|
|
|
MDB_val k = to_val(id);
|
|
|
|
|
|
// Remove index entries before the row goes, while its stored bytes are
|
|
|
// still readable - they are the only record of which index keys it owns.
|
|
|
- std::vector<std::pair<std::string, unsigned int>> index_dbis;
|
|
|
const bool needs_index = !indexed_fields(collection).empty();
|
|
|
const bool needs_relations = !relations(collection).empty();
|
|
|
if (needs_index || needs_relations) {
|
|
|
@@ -1452,10 +1483,10 @@ bool LmdbDocumentStore::del(std::string_view collection, std::string_view id) {
|
|
|
const int grc = mdb_get(wtxn.raw(), dbi, &k, &old);
|
|
|
if (grc == MDB_SUCCESS) {
|
|
|
if (needs_index) {
|
|
|
- maintainIndexes(wtxn, collection, id, to_sv(old), nullptr, index_dbis);
|
|
|
+ maintainIndexes(wtxn, collection, id, to_sv(old), nullptr, to_cache);
|
|
|
}
|
|
|
if (needs_relations) {
|
|
|
- maintainRelations(wtxn, collection, id, to_sv(old), nullptr, index_dbis);
|
|
|
+ maintainRelations(wtxn, collection, id, to_sv(old), nullptr, to_cache);
|
|
|
}
|
|
|
} else if (grc != MDB_NOTFOUND) {
|
|
|
throw_mdb(grc, "get (pre-index del)");
|
|
|
@@ -1463,15 +1494,11 @@ bool LmdbDocumentStore::del(std::string_view collection, std::string_view id) {
|
|
|
}
|
|
|
|
|
|
int rc = mdb_del(wtxn.raw(), dbi, &k, nullptr);
|
|
|
- if (rc == MDB_NOTFOUND) {
|
|
|
- wtxn.commit();
|
|
|
- cacheCommittedDbi(collection, dbi);
|
|
|
- return false;
|
|
|
- }
|
|
|
+ // NOT cached here - the caller caches every entry in to_cache (this one
|
|
|
+ // included) only after ITS commit succeeds.
|
|
|
+ to_cache.emplace_back(std::string(collection), dbi);
|
|
|
+ if (rc == MDB_NOTFOUND) return false;
|
|
|
if (rc != MDB_SUCCESS) throw_mdb(rc, "del");
|
|
|
- wtxn.commit();
|
|
|
- cacheCommittedDbi(collection, dbi);
|
|
|
- for (const auto& [sub, d] : index_dbis) cacheCommittedDbi(sub, d);
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@@ -2385,34 +2412,53 @@ void LmdbDocumentStore::put_vector(std::string_view collection,
|
|
|
std::string_view id,
|
|
|
const std::vector<float>& vec) {
|
|
|
if (vec.empty()) return; // mirror the migration tool's no-op semantics
|
|
|
- const std::string subdb = vector_subdb_name(collection);
|
|
|
WriteTxn wtxn(env_);
|
|
|
+ std::vector<std::pair<std::string, unsigned int>> to_cache;
|
|
|
+ put_vector(wtxn, collection, id, vec, to_cache);
|
|
|
+ wtxn.commit();
|
|
|
+ for (const auto& [sub, d] : to_cache) cacheCommittedDbi(sub, d);
|
|
|
+}
|
|
|
+
|
|
|
+void LmdbDocumentStore::put_vector(WriteTxn& wtxn,
|
|
|
+ std::string_view collection,
|
|
|
+ std::string_view id,
|
|
|
+ const std::vector<float>& vec,
|
|
|
+ std::vector<std::pair<std::string, unsigned int>>& to_cache) {
|
|
|
+ if (vec.empty()) return; // mirror the migration tool's no-op semantics
|
|
|
+ const std::string subdb = vector_subdb_name(collection);
|
|
|
unsigned int dbi = open_for_write(wtxn, subdb);
|
|
|
MDB_val k = to_val(id);
|
|
|
MDB_val v{vec.size() * sizeof(float),
|
|
|
const_cast<void*>(static_cast<const void*>(vec.data()))};
|
|
|
mdb_check(mdb_put(wtxn.raw(), dbi, &k, &v, 0), "put_vector");
|
|
|
- wtxn.commit();
|
|
|
- cacheCommittedDbi(subdb, dbi);
|
|
|
+ to_cache.emplace_back(subdb, dbi);
|
|
|
}
|
|
|
|
|
|
bool LmdbDocumentStore::del_vector(std::string_view collection, std::string_view id) {
|
|
|
const std::string subdb = vector_subdb_name(collection);
|
|
|
// Probe first so we don't create an empty vectors sub-db just to
|
|
|
- // discover the vector isn't there.
|
|
|
- {
|
|
|
- ReadTxn rtxn(env_);
|
|
|
- auto dbi_opt = try_open_for_read(rtxn, subdb);
|
|
|
- if (!dbi_opt) return false;
|
|
|
- }
|
|
|
+ // discover the vector isn't there. Cache-only, see cachedDbi().
|
|
|
+ if (!cachedDbi(subdb)) return false;
|
|
|
WriteTxn wtxn(env_);
|
|
|
+ std::vector<std::pair<std::string, unsigned int>> to_cache;
|
|
|
+ const bool existed = del_vector(wtxn, collection, id, to_cache);
|
|
|
+ wtxn.commit();
|
|
|
+ for (const auto& [sub, d] : to_cache) cacheCommittedDbi(sub, d);
|
|
|
+ return existed;
|
|
|
+}
|
|
|
+
|
|
|
+bool LmdbDocumentStore::del_vector(WriteTxn& wtxn,
|
|
|
+ std::string_view collection,
|
|
|
+ std::string_view id,
|
|
|
+ std::vector<std::pair<std::string, unsigned int>>& to_cache) {
|
|
|
+ const std::string subdb = vector_subdb_name(collection);
|
|
|
+ if (!cachedDbi(subdb)) return false;
|
|
|
unsigned int dbi = open_for_write(wtxn, subdb);
|
|
|
MDB_val k = to_val(id);
|
|
|
int rc = mdb_del(wtxn.raw(), dbi, &k, nullptr);
|
|
|
+ to_cache.emplace_back(subdb, dbi);
|
|
|
if (rc == MDB_NOTFOUND) return false;
|
|
|
if (rc != MDB_SUCCESS) throw_mdb(rc, "del_vector");
|
|
|
- wtxn.commit();
|
|
|
- cacheCommittedDbi(subdb, dbi);
|
|
|
return true;
|
|
|
}
|
|
|
|