File indexing completed on 2024-05-12 03:54:54

0001 /*
0002     This file is part of the KDE project.
0003 
0004     SPDX-FileCopyrightText: 2010, 2012 Michael Pyne <mpyne@kde.org>
0005     SPDX-FileCopyrightText: 2012 Ralf Jung <ralfjung-e@gmx.de>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-only
0008 */
0009 
0010 #include "kshareddatacache.h"
0011 #include "kcoreaddons_debug.h"
0012 #include "ksdcmapping_p.h"
0013 #include "ksdcmemory_p.h"
0014 
0015 #include "kshareddatacache_p.h" // Various auxiliary support code
0016 
0017 #include <QByteArray>
0018 #include <QDir>
0019 #include <QFile>
0020 #include <QRandomGenerator>
0021 #include <QStandardPaths>
0022 
0023 // The per-instance private data, such as map size, whether
0024 // attached or not, pointer to shared memory, etc.
0025 class Q_DECL_HIDDEN KSharedDataCache::Private
0026 {
0027 public:
0028     Private(const QString &name, unsigned defaultCacheSize, unsigned expectedItemSize)
0029         : m_cacheName(name)
0030         , shm(nullptr)
0031         , m_mapping(nullptr)
0032         , m_defaultCacheSize(defaultCacheSize)
0033         , m_expectedItemSize(expectedItemSize)
0034     {
0035         createMemoryMapping();
0036     }
0037 
0038     void createMemoryMapping()
0039     {
0040         shm = nullptr;
0041         m_mapping.reset();
0042 
0043         // 0-sized caches are fairly useless.
0044         unsigned cacheSize = qMax(m_defaultCacheSize, uint(SharedMemory::MINIMUM_CACHE_SIZE));
0045         unsigned pageSize = SharedMemory::equivalentPageSize(m_expectedItemSize);
0046 
0047         // Ensure that the cache is sized such that there is a minimum number of
0048         // pages available. (i.e. a cache consisting of only 1 page is fairly
0049         // useless and probably crash-prone).
0050         cacheSize = qMax(pageSize * 256, cacheSize);
0051 
0052         // The m_cacheName is used to find the file to store the cache in.
0053         const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
0054         QString cacheName = cacheDir + QLatin1String("/") + m_cacheName + QLatin1String(".kcache");
0055         QFile file(cacheName);
0056         QFileInfo fileInfo(file);
0057         if (!QDir().mkpath(fileInfo.absolutePath())) {
0058             qCWarning(KCOREADDONS_DEBUG) << "Failed to create cache dir" << fileInfo.absolutePath();
0059         }
0060 
0061         // The basic idea is to open the file that we want to map into shared
0062         // memory, and then actually establish the mapping. Once we have mapped the
0063         // file into shared memory we can close the file handle, the mapping will
0064         // still be maintained (unless the file is resized to be shorter than
0065         // expected, which we don't handle yet :-( )
0066 
0067         // size accounts for the overhead over the desired cacheSize
0068         uint size = SharedMemory::totalSize(cacheSize, pageSize);
0069         Q_ASSERT(size >= cacheSize);
0070 
0071         // Open the file and resize to some sane value if the file is too small.
0072         if (file.open(QIODevice::ReadWrite) && (file.size() >= size || (ensureFileAllocated(file.handle(), size) && file.resize(size)))) {
0073             try {
0074                 m_mapping.reset(new KSDCMapping(&file, size, cacheSize, pageSize));
0075                 shm = m_mapping->m_mapped;
0076             } catch (KSDCCorrupted) {
0077                 shm = nullptr;
0078                 m_mapping.reset();
0079 
0080                 qCWarning(KCOREADDONS_DEBUG) << "Deleting corrupted cache" << cacheName;
0081                 file.remove();
0082                 QFile file(cacheName);
0083                 if (file.open(QIODevice::ReadWrite) && ensureFileAllocated(file.handle(), size) && file.resize(size)) {
0084                     try {
0085                         m_mapping.reset(new KSDCMapping(&file, size, cacheSize, pageSize));
0086                     } catch (KSDCCorrupted) {
0087                         m_mapping.reset();
0088                         qCCritical(KCOREADDONS_DEBUG) << "Even a brand-new cache starts off corrupted, something is"
0089                                                       << "seriously wrong. :-(";
0090                     }
0091                 }
0092             }
0093         }
0094 
0095         if (!m_mapping) {
0096             m_mapping.reset(new KSDCMapping(nullptr, size, cacheSize, pageSize));
0097             shm = m_mapping->m_mapped;
0098         }
0099     }
0100 
0101     // Called whenever the cache is apparently corrupt (for instance, a timeout trying to
0102     // lock the cache). In this situation it is safer just to destroy it all and try again.
0103     void recoverCorruptedCache()
0104     {
0105         qCWarning(KCOREADDONS_DEBUG) << "Deleting corrupted cache" << m_cacheName;
0106 
0107         KSharedDataCache::deleteCache(m_cacheName);
0108 
0109         createMemoryMapping();
0110     }
0111 
0112     class CacheLocker
0113     {
0114         mutable Private *d;
0115 
0116         bool cautiousLock()
0117         {
0118             int lockCount = 0;
0119 
0120             // Locking can fail due to a timeout. If it happens too often even though
0121             // we're taking corrective action assume there's some disastrous problem
0122             // and give up.
0123             while (!d->m_mapping->lock() && !d->m_mapping->isLockedCacheSafe()) {
0124                 d->recoverCorruptedCache();
0125 
0126                 if (!d->m_mapping->isValid()) {
0127                     qCWarning(KCOREADDONS_DEBUG) << "Lost the connection to shared memory for cache" << d->m_cacheName;
0128                     return false;
0129                 }
0130 
0131                 if (lockCount++ > 4) {
0132                     qCCritical(KCOREADDONS_DEBUG) << "There is a very serious problem with the KDE data cache" << d->m_cacheName
0133                                                   << "giving up trying to access cache.";
0134                     return false;
0135                 }
0136             }
0137 
0138             return true;
0139         }
0140 
0141     public:
0142         CacheLocker(const Private *_d)
0143             : d(const_cast<Private *>(_d))
0144         {
0145             if (Q_UNLIKELY(!d || !cautiousLock())) {
0146                 d = nullptr;
0147             }
0148         }
0149 
0150         ~CacheLocker()
0151         {
0152             if (d) {
0153                 d->m_mapping->unlock();
0154             }
0155         }
0156 
0157         CacheLocker(const CacheLocker &) = delete;
0158         CacheLocker &operator=(const CacheLocker &) = delete;
0159 
0160         bool failed() const
0161         {
0162             return !d;
0163         }
0164     };
0165 
0166     QString m_cacheName;
0167     SharedMemory *shm;
0168     std::unique_ptr<KSDCMapping> m_mapping;
0169     uint m_defaultCacheSize;
0170     uint m_expectedItemSize;
0171 };
0172 
0173 KSharedDataCache::KSharedDataCache(const QString &cacheName, unsigned defaultCacheSize, unsigned expectedItemSize)
0174     : d(nullptr)
0175 {
0176     try {
0177         d = new Private(cacheName, defaultCacheSize, expectedItemSize);
0178     } catch (KSDCCorrupted) {
0179         qCCritical(KCOREADDONS_DEBUG) << "Failed to initialize KSharedDataCache!";
0180         d = nullptr; // Just in case
0181     }
0182 }
0183 
0184 KSharedDataCache::~KSharedDataCache()
0185 {
0186     if (!d) {
0187         return;
0188     }
0189 
0190     delete d;
0191 }
0192 
0193 bool KSharedDataCache::insert(const QString &key, const QByteArray &data)
0194 {
0195     try {
0196         Private::CacheLocker lock(d);
0197         if (lock.failed()) {
0198             return false;
0199         }
0200 
0201         QByteArray encodedKey = key.toUtf8();
0202         uint keyHash = SharedMemory::generateHash(encodedKey);
0203         uint position = keyHash % d->shm->indexTableSize();
0204 
0205         // See if we're overwriting an existing entry.
0206         IndexTableEntry *indices = d->shm->indexTable();
0207 
0208         // In order to avoid the issue of a very long-lived cache having items
0209         // with a use count of 1 near-permanently, we attempt to artifically
0210         // reduce the use count of long-lived items when there is high load on
0211         // the cache. We do this randomly, with a weighting that makes the event
0212         // impossible if load < 0.5, and guaranteed if load >= 0.96.
0213         const static double startCullPoint = 0.5l;
0214         const static double mustCullPoint = 0.96l;
0215 
0216         // cacheAvail is in pages, cacheSize is in bytes.
0217         double loadFactor = 1.0 - (1.0l * d->shm->cacheAvail * d->shm->cachePageSize() / d->shm->cacheSize);
0218         bool cullCollisions = false;
0219 
0220         if (Q_UNLIKELY(loadFactor >= mustCullPoint)) {
0221             cullCollisions = true;
0222         } else if (loadFactor > startCullPoint) {
0223             const int tripWireValue = RAND_MAX * (loadFactor - startCullPoint) / (mustCullPoint - startCullPoint);
0224             if (QRandomGenerator::global()->bounded(RAND_MAX) >= tripWireValue) {
0225                 cullCollisions = true;
0226             }
0227         }
0228 
0229         // In case of collisions in the index table (i.e. identical positions), use
0230         // quadratic chaining to attempt to find an empty slot. The equation we use
0231         // is:
0232         // position = (hash + (i + i*i) / 2) % size, where i is the probe number.
0233         uint probeNumber = 1;
0234         while (indices[position].useCount > 0 && probeNumber < SharedMemory::MAX_PROBE_COUNT) {
0235             // If we actually stumbled upon an old version of the key we are
0236             // overwriting, then use that position, do not skip over it.
0237 
0238             if (Q_UNLIKELY(indices[position].fileNameHash == keyHash)) {
0239                 break;
0240             }
0241 
0242             // If we are "culling" old entries, see if this one is old and if so
0243             // reduce its use count. If it reduces to zero then eliminate it and
0244             // use its old spot.
0245 
0246             if (cullCollisions && (::time(nullptr) - indices[position].lastUsedTime) > 60) {
0247                 indices[position].useCount >>= 1;
0248                 if (indices[position].useCount == 0) {
0249                     qCDebug(KCOREADDONS_DEBUG) << "Overwriting existing old cached entry due to collision.";
0250                     d->shm->removeEntry(position); // Remove it first
0251                     break;
0252                 }
0253             }
0254 
0255             position = (keyHash + (probeNumber + probeNumber * probeNumber) / 2) % d->shm->indexTableSize();
0256             probeNumber++;
0257         }
0258 
0259         if (indices[position].useCount > 0 && indices[position].firstPage >= 0) {
0260             qCDebug(KCOREADDONS_DEBUG) << "Overwriting existing cached entry due to collision.";
0261             d->shm->removeEntry(position); // Remove it first
0262         }
0263 
0264         // Data will be stored as fileNamefoo\0PNGimagedata.....
0265         // So total size required is the length of the encoded file name + 1
0266         // for the trailing null, and then the length of the image data.
0267         uint fileNameLength = 1 + encodedKey.length();
0268         uint requiredSize = fileNameLength + data.size();
0269         uint pagesNeeded = SharedMemory::intCeil(requiredSize, d->shm->cachePageSize());
0270         uint firstPage(-1);
0271 
0272         if (pagesNeeded >= d->shm->pageTableSize()) {
0273             qCWarning(KCOREADDONS_DEBUG) << key << "is too large to be cached.";
0274             return false;
0275         }
0276 
0277         // If the cache has no room, or the fragmentation is too great to find
0278         // the required number of consecutive free pages, take action.
0279         if (pagesNeeded > d->shm->cacheAvail || (firstPage = d->shm->findEmptyPages(pagesNeeded)) >= d->shm->pageTableSize()) {
0280             // If we have enough free space just defragment
0281             uint freePagesDesired = 3 * qMax(1u, pagesNeeded / 2);
0282 
0283             if (d->shm->cacheAvail > freePagesDesired) {
0284                 // TODO: How the hell long does this actually take on real
0285                 // caches?
0286                 d->shm->defragment();
0287                 firstPage = d->shm->findEmptyPages(pagesNeeded);
0288             } else {
0289                 // If we already have free pages we don't want to remove a ton
0290                 // extra. However we can't rely on the return value of
0291                 // removeUsedPages giving us a good location since we're not
0292                 // passing in the actual number of pages that we need.
0293                 d->shm->removeUsedPages(qMin(2 * freePagesDesired, d->shm->pageTableSize()) - d->shm->cacheAvail);
0294                 firstPage = d->shm->findEmptyPages(pagesNeeded);
0295             }
0296 
0297             if (firstPage >= d->shm->pageTableSize() || d->shm->cacheAvail < pagesNeeded) {
0298                 qCCritical(KCOREADDONS_DEBUG) << "Unable to free up memory for" << key;
0299                 return false;
0300             }
0301         }
0302 
0303         // Update page table
0304         PageTableEntry *table = d->shm->pageTable();
0305         for (uint i = 0; i < pagesNeeded; ++i) {
0306             table[firstPage + i].index = position;
0307         }
0308 
0309         // Update index
0310         indices[position].fileNameHash = keyHash;
0311         indices[position].totalItemSize = requiredSize;
0312         indices[position].useCount = 1;
0313         indices[position].addTime = ::time(nullptr);
0314         indices[position].lastUsedTime = indices[position].addTime;
0315         indices[position].firstPage = firstPage;
0316 
0317         // Update cache
0318         d->shm->cacheAvail -= pagesNeeded;
0319 
0320         // Actually move the data in place
0321         void *dataPage = d->shm->page(firstPage);
0322         if (Q_UNLIKELY(!dataPage)) {
0323             throw KSDCCorrupted();
0324         }
0325 
0326         // Verify it will all fit
0327         d->m_mapping->verifyProposedMemoryAccess(dataPage, requiredSize);
0328 
0329         // Cast for byte-sized pointer arithmetic
0330         uchar *startOfPageData = reinterpret_cast<uchar *>(dataPage);
0331         ::memcpy(startOfPageData, encodedKey.constData(), fileNameLength);
0332         ::memcpy(startOfPageData + fileNameLength, data.constData(), data.size());
0333 
0334         return true;
0335     } catch (KSDCCorrupted) {
0336         d->recoverCorruptedCache();
0337         return false;
0338     }
0339 }
0340 
0341 bool KSharedDataCache::find(const QString &key, QByteArray *destination) const
0342 {
0343     try {
0344         Private::CacheLocker lock(d);
0345         if (lock.failed()) {
0346             return false;
0347         }
0348 
0349         // Search in the index for our data, hashed by key;
0350         QByteArray encodedKey = key.toUtf8();
0351         qint32 entry = d->shm->findNamedEntry(encodedKey);
0352 
0353         if (entry >= 0) {
0354             const IndexTableEntry *header = &d->shm->indexTable()[entry];
0355             const void *resultPage = d->shm->page(header->firstPage);
0356             if (Q_UNLIKELY(!resultPage)) {
0357                 throw KSDCCorrupted();
0358             }
0359 
0360             d->m_mapping->verifyProposedMemoryAccess(resultPage, header->totalItemSize);
0361 
0362             header->useCount++;
0363             header->lastUsedTime = ::time(nullptr);
0364 
0365             // Our item is the key followed immediately by the data, so skip
0366             // past the key.
0367             const char *cacheData = reinterpret_cast<const char *>(resultPage);
0368             cacheData += encodedKey.size();
0369             cacheData++; // Skip trailing null -- now we're pointing to start of data
0370 
0371             if (destination) {
0372                 *destination = QByteArray(cacheData, header->totalItemSize - encodedKey.size() - 1);
0373             }
0374 
0375             return true;
0376         }
0377     } catch (KSDCCorrupted) {
0378         d->recoverCorruptedCache();
0379     }
0380 
0381     return false;
0382 }
0383 
0384 void KSharedDataCache::clear()
0385 {
0386     try {
0387         Private::CacheLocker lock(d);
0388 
0389         if (!lock.failed()) {
0390             d->shm->clear();
0391         }
0392     } catch (KSDCCorrupted) {
0393         d->recoverCorruptedCache();
0394     }
0395 }
0396 
0397 bool KSharedDataCache::contains(const QString &key) const
0398 {
0399     try {
0400         Private::CacheLocker lock(d);
0401         if (lock.failed()) {
0402             return false;
0403         }
0404 
0405         return d->shm->findNamedEntry(key.toUtf8()) >= 0;
0406     } catch (KSDCCorrupted) {
0407         d->recoverCorruptedCache();
0408         return false;
0409     }
0410 }
0411 
0412 void KSharedDataCache::deleteCache(const QString &cacheName)
0413 {
0414     QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/") + cacheName + QLatin1String(".kcache");
0415 
0416     // Note that it is important to simply unlink the file, and not truncate it
0417     // smaller first to avoid SIGBUS errors and similar with shared memory
0418     // attached to the underlying inode.
0419     qCDebug(KCOREADDONS_DEBUG) << "Removing cache at" << cachePath;
0420     QFile::remove(cachePath);
0421 }
0422 
0423 unsigned KSharedDataCache::totalSize() const
0424 {
0425     try {
0426         Private::CacheLocker lock(d);
0427         if (lock.failed()) {
0428             return 0u;
0429         }
0430 
0431         return d->shm->cacheSize;
0432     } catch (KSDCCorrupted) {
0433         d->recoverCorruptedCache();
0434         return 0u;
0435     }
0436 }
0437 
0438 unsigned KSharedDataCache::freeSize() const
0439 {
0440     try {
0441         Private::CacheLocker lock(d);
0442         if (lock.failed()) {
0443             return 0u;
0444         }
0445 
0446         return d->shm->cacheAvail * d->shm->cachePageSize();
0447     } catch (KSDCCorrupted) {
0448         d->recoverCorruptedCache();
0449         return 0u;
0450     }
0451 }
0452 
0453 KSharedDataCache::EvictionPolicy KSharedDataCache::evictionPolicy() const
0454 {
0455     if (d && d->shm) {
0456         return static_cast<EvictionPolicy>(d->shm->evictionPolicy.fetchAndAddAcquire(0));
0457     }
0458 
0459     return NoEvictionPreference;
0460 }
0461 
0462 void KSharedDataCache::setEvictionPolicy(EvictionPolicy newPolicy)
0463 {
0464     if (d && d->shm) {
0465         d->shm->evictionPolicy.fetchAndStoreRelease(static_cast<int>(newPolicy));
0466     }
0467 }
0468 
0469 unsigned KSharedDataCache::timestamp() const
0470 {
0471     if (d && d->shm) {
0472         return static_cast<unsigned>(d->shm->cacheTimestamp.fetchAndAddAcquire(0));
0473     }
0474 
0475     return 0;
0476 }
0477 
0478 void KSharedDataCache::setTimestamp(unsigned newTimestamp)
0479 {
0480     if (d && d->shm) {
0481         d->shm->cacheTimestamp.fetchAndStoreRelease(static_cast<int>(newTimestamp));
0482     }
0483 }