diff --git a/src/blackmisc/datacache.cpp b/src/blackmisc/datacache.cpp index b77600fdd..606a70f13 100644 --- a/src/blackmisc/datacache.cpp +++ b/src/blackmisc/datacache.cpp @@ -11,6 +11,7 @@ #include "blackmisc/logmessage.h" #include "blackmisc/identifier.h" #include +#include namespace BlackMisc { @@ -83,6 +84,21 @@ namespace BlackMisc return enumerateFiles(persistentStore()); } + std::future CDataCache::syncLoad(QObject *pageOwner, const QString &key) + { + auto future = m_revision.promiseLoadedValue(pageOwner, key); + if (future.valid()) + { + return future; + } + else // value is not currently loading, so immediately return the current value + { + std::promise p; + p.set_value(getValueSync(key)); + return p.get_future(); + } + } + QString lockFileError(const QLockFile &lock) { switch (lock.error()) @@ -162,18 +178,47 @@ namespace BlackMisc { if (! m_deferredChanges.isEmpty()) { + auto promises = m_cache->m_revision.loadedValuePromises(); + for (const auto &tuple : promises) + { + QObject *pageOwner = nullptr; + QString key; + std::tie(pageOwner, key, std::ignore) = tuple; + + m_deferredChanges.inhibit(pageOwner, key); // don't fire notification slots for objects waiting on syncLoad futures + } + m_deferredChanges.setSaved(); emit valuesLoadedFromStore(m_deferredChanges, CIdentifier::anonymous()); + deliverPromises(std::move(promises)); m_deferredChanges.clear(); } } + void CDataCacheSerializer::deliverPromises(std::vector>> i_promises) + { + auto changes = m_deferredChanges; + auto promises = std::make_shared(std::move(i_promises)); // \todo use C++14 lambda init-capture + QTimer::singleShot(0, Qt::PreciseTimer, this, [this, changes, promises] + { + for (auto &tuple : *promises) + { + QString key; + std::promise promise; + std::tie(std::ignore, key, promise) = std::move(tuple); + + promise.set_value(changes.value(key).first); + } + }); + } + CDataCacheRevision::LockGuard CDataCacheRevision::beginUpdate(const QMap ×tamps) { QMutexLocker lock(&m_mutex); Q_ASSERT(! m_updateInProgress); Q_ASSERT(! m_lockFile.isLocked()); + Q_ASSERT(m_promises.empty()); if (! m_lockFile.lock()) { @@ -301,6 +346,28 @@ namespace BlackMisc return m_updateInProgress && m_timestamps.contains(key); } + std::future CDataCacheRevision::promiseLoadedValue(QObject *pageOwner, const QString &key) + { + QMutexLocker lock(&m_mutex); + + if (isNewerValueAvailable(key)) + { + std::promise promise; + auto future = promise.get_future(); + m_promises.emplace_back(pageOwner, key, std::move(promise)); + return future; + } + return {}; + } + + std::vector>> CDataCacheRevision::loadedValuePromises() + { + QMutexLocker lock(&m_mutex); + + Q_ASSERT(m_updateInProgress); + return std::move(m_promises); // move into the return value, so m_promises becomes empty + } + QJsonObject CDataCacheRevision::toJson(const QMap ×tamps) { QJsonObject result; diff --git a/src/blackmisc/datacache.h b/src/blackmisc/datacache.h index 7d784367b..56bfb3636 100644 --- a/src/blackmisc/datacache.h +++ b/src/blackmisc/datacache.h @@ -18,6 +18,7 @@ #include #include #include +#include namespace BlackMisc { @@ -65,6 +66,12 @@ namespace BlackMisc //! During update, returns true if the on-disk timestamp of this key is newer than in-memory. bool isNewerValueAvailable(const QString &key) const; + //! Return a future which will be made ready when the value is loaded. Future is invalid if value is not loading. + std::future promiseLoadedValue(QObject *pageOwner, const QString &key); + + //! Returns (by move) the container of promises to load values. + std::vector>> loadedValuePromises(); + private: mutable QMutex m_mutex { QMutex::Recursive }; bool m_updateInProgress = false; @@ -74,6 +81,7 @@ namespace BlackMisc QLockFile m_lockFile { m_basename + "/.lock" }; QUuid m_uuid; QMap m_timestamps; + std::vector>> m_promises; static QJsonObject toJson(const QMap ×tamps); static QMap fromJson(const QJsonObject ×tamps); @@ -108,6 +116,7 @@ namespace BlackMisc private: const QString &persistentStore() const; void applyDeferredChanges(); + void deliverPromises(std::vector>>); CDataCache *const m_cache = nullptr; QUuid m_revision; @@ -137,6 +146,9 @@ namespace BlackMisc //! Return all files where data may be stored. QStringList enumerateStore() const; + //! Method used for implementing CData::syncLoad. + std::future syncLoad(QObject *pageOwner, const QString &key); + private: CDataCache(); @@ -169,7 +181,8 @@ namespace BlackMisc //! Must be a void, non-const member function of the owner. template CData(T *owner, NotifySlot slot = nullptr) : - CData::CCached(CDataCache::instance(), Trait::key(), Trait::isValid, Trait::defaultValue(), owner, slot) + CData::CCached(CDataCache::instance(), Trait::key(), Trait::isValid, Trait::defaultValue(), owner, slot), + m_owner(owner) {} //! Reset the data to its default value. @@ -178,8 +191,15 @@ namespace BlackMisc //! Return the file that is used for persistence for this value. QString getFilename() const { return CDataCache::filenameForKey(this->getKey()); } + //! Return a future providing the value. If the value is still loading, the future will wait for it. + //! If the value is not present, the variant is null. Bypasses async get and inhibits notification slot. + std::future syncLoad() { return CDataCache::instance()->syncLoad(m_owner, this->getKey()); } + //! Data cache doesn't support setAndSave (because set() already causes save anyway). CStatusMessage setAndSave(const typename Trait::type &value, qint64 timestamp = 0) = delete; + + private: + QObject *m_owner = nullptr; }; /*! diff --git a/src/blackmisc/valuecache.h b/src/blackmisc/valuecache.h index 42b12ca27..348158fc8 100644 --- a/src/blackmisc/valuecache.h +++ b/src/blackmisc/valuecache.h @@ -224,6 +224,11 @@ namespace BlackMisc //! Mutex protecting operations which are critical on m_elements. mutable QMutex m_mutex { QMutex::Recursive }; + protected: + //! Synchronously return a current value. + //! \threadsafe + CVariant getValueSync(const QString &key) { return std::get<0>(getValue(key)); } + private: friend class Private::CValuePage; struct Element;