| 1 | // Copyright (C) 2016 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qnetworkaccesscache_p.h" |
| 5 | #include "QtCore/qpointer.h" |
| 6 | #include "QtCore/qdeadlinetimer.h" |
| 7 | #include "qnetworkaccessmanager_p.h" |
| 8 | #include "qnetworkreply_p.h" |
| 9 | #include "qnetworkrequest.h" |
| 10 | |
| 11 | #include <vector> |
| 12 | |
| 13 | //#define DEBUG_ACCESSCACHE |
| 14 | |
| 15 | QT_BEGIN_NAMESPACE |
| 16 | |
| 17 | enum ExpiryTimeEnum { |
| 18 | ExpiryTime = 120 |
| 19 | }; |
| 20 | |
| 21 | // idea copied from qcache.h |
| 22 | struct QNetworkAccessCache::Node |
| 23 | { |
| 24 | QDeadlineTimer timer; |
| 25 | QByteArray key; |
| 26 | |
| 27 | Node *previous = nullptr; // "previous" nodes expire "previous"ly (before us) |
| 28 | Node *next = nullptr; // "next" nodes expire "next" (after us) |
| 29 | CacheableObject *object = nullptr; |
| 30 | |
| 31 | int useCount = 0; |
| 32 | }; |
| 33 | |
| 34 | QNetworkAccessCache::CacheableObject::CacheableObject(Options options) |
| 35 | : expires(options & Option::Expires), |
| 36 | shareable(options & Option::Shareable) |
| 37 | { |
| 38 | |
| 39 | } |
| 40 | |
| 41 | QNetworkAccessCache::CacheableObject::~CacheableObject() |
| 42 | { |
| 43 | #if 0 //def QT_DEBUG |
| 44 | if (!key.isEmpty() && Ptr()->hasEntry(key)) |
| 45 | qWarning() << "QNetworkAccessCache: object" << (void*)this << "key" << key |
| 46 | << "destroyed without being removed from cache first!" ; |
| 47 | #endif |
| 48 | } |
| 49 | |
| 50 | QNetworkAccessCache::~QNetworkAccessCache() |
| 51 | { |
| 52 | clear(); |
| 53 | } |
| 54 | |
| 55 | void QNetworkAccessCache::clear() |
| 56 | { |
| 57 | NodeHash hashCopy = hash; |
| 58 | hash.clear(); |
| 59 | |
| 60 | // remove all entries |
| 61 | NodeHash::Iterator it = hashCopy.begin(); |
| 62 | NodeHash::Iterator end = hashCopy.end(); |
| 63 | for ( ; it != end; ++it) { |
| 64 | (*it)->object->key.clear(); |
| 65 | (*it)->object->dispose(); |
| 66 | delete (*it); |
| 67 | } |
| 68 | |
| 69 | // now delete: |
| 70 | hashCopy.clear(); |
| 71 | |
| 72 | timer.stop(); |
| 73 | |
| 74 | firstExpiringNode = lastExpiringNode = nullptr; |
| 75 | } |
| 76 | |
| 77 | /*! |
| 78 | Appends the entry given by \a key to the end of the linked list. |
| 79 | (i.e., makes it the newest entry) |
| 80 | */ |
| 81 | void QNetworkAccessCache::linkEntry(const QByteArray &key) |
| 82 | { |
| 83 | Node * const node = hash.value(key); |
| 84 | if (!node) |
| 85 | return; |
| 86 | |
| 87 | Q_ASSERT(node != firstExpiringNode && node != lastExpiringNode); |
| 88 | Q_ASSERT(node->previous == nullptr && node->next == nullptr); |
| 89 | Q_ASSERT(node->useCount == 0); |
| 90 | |
| 91 | |
| 92 | node->timer.setPreciseRemainingTime(secs: node->object->expiryTimeoutSeconds); |
| 93 | #ifdef DEBUG_ACCESSCACHE |
| 94 | qDebug() << "QNetworkAccessCache case trying to insert=" << QString::fromUtf8(key) |
| 95 | << node->timer.remainingTime() << "milliseconds" ; |
| 96 | Node *current = lastExpiringNode; |
| 97 | while (current) { |
| 98 | qDebug() << "QNetworkAccessCache item=" << QString::fromUtf8(current->key) |
| 99 | << current->timer.remainingTime() << "milliseconds" |
| 100 | << (current == lastExpiringNode ? "[last to expire]" : "" ) |
| 101 | << (current == firstExpiringNode ? "[first to expire]" : "" ); |
| 102 | current = current->previous; |
| 103 | } |
| 104 | #endif |
| 105 | |
| 106 | if (lastExpiringNode) { |
| 107 | Q_ASSERT(lastExpiringNode->next == nullptr); |
| 108 | if (lastExpiringNode->timer < node->timer) { |
| 109 | // Insert as new last-to-expire node. |
| 110 | node->previous = lastExpiringNode; |
| 111 | lastExpiringNode->next = node; |
| 112 | lastExpiringNode = node; |
| 113 | } else { |
| 114 | // Insert in a sorted way, as different nodes might have had different expiryTimeoutSeconds set. |
| 115 | Node *current = lastExpiringNode; |
| 116 | while (current->previous != nullptr && current->previous->timer >= node->timer) |
| 117 | current = current->previous; |
| 118 | node->previous = current->previous; |
| 119 | if (node->previous) |
| 120 | node->previous->next = node; |
| 121 | node->next = current; |
| 122 | current->previous = node; |
| 123 | if (node->previous == nullptr) |
| 124 | firstExpiringNode = node; |
| 125 | } |
| 126 | } else { |
| 127 | // no current last-to-expire node |
| 128 | lastExpiringNode = node; |
| 129 | } |
| 130 | if (!firstExpiringNode) { |
| 131 | // there are no entries, so this is the next-to-expire too |
| 132 | firstExpiringNode = node; |
| 133 | } |
| 134 | Q_ASSERT(firstExpiringNode->previous == nullptr); |
| 135 | Q_ASSERT(lastExpiringNode->next == nullptr); |
| 136 | } |
| 137 | |
| 138 | /*! |
| 139 | Removes the entry pointed by \a key from the linked list. |
| 140 | Returns \c true if the entry removed was the next to expire. |
| 141 | */ |
| 142 | bool QNetworkAccessCache::unlinkEntry(const QByteArray &key) |
| 143 | { |
| 144 | Node * const node = hash.value(key); |
| 145 | if (!node) |
| 146 | return false; |
| 147 | |
| 148 | bool wasFirst = false; |
| 149 | if (node == firstExpiringNode) { |
| 150 | firstExpiringNode = node->next; |
| 151 | wasFirst = true; |
| 152 | } |
| 153 | if (node == lastExpiringNode) |
| 154 | lastExpiringNode = node->previous; |
| 155 | if (node->previous) |
| 156 | node->previous->next = node->next; |
| 157 | if (node->next) |
| 158 | node->next->previous = node->previous; |
| 159 | |
| 160 | node->next = node->previous = nullptr; |
| 161 | return wasFirst; |
| 162 | } |
| 163 | |
| 164 | void QNetworkAccessCache::updateTimer() |
| 165 | { |
| 166 | timer.stop(); |
| 167 | |
| 168 | if (!firstExpiringNode) |
| 169 | return; |
| 170 | |
| 171 | qint64 interval = firstExpiringNode->timer.remainingTime(); |
| 172 | if (interval <= 0) { |
| 173 | interval = 0; |
| 174 | } |
| 175 | |
| 176 | // Plus 10 msec so we don't spam timer events if date comparisons are too fuzzy. |
| 177 | // This code used to do (broken) rounding, but for ConnectionCacheExpiryTimeoutSecondsAttribute |
| 178 | // to work we cannot do this. |
| 179 | // See discussion in https://codereview.qt-project.org/c/qt/qtbase/+/337464 |
| 180 | timer.start(msec: interval + 10, obj: this); |
| 181 | } |
| 182 | |
| 183 | bool QNetworkAccessCache::emitEntryReady(Node *node, QObject *target, const char *member) |
| 184 | { |
| 185 | if (!connect(sender: this, SIGNAL(entryReady(QNetworkAccessCache::CacheableObject*)), |
| 186 | receiver: target, member, Qt::QueuedConnection)) |
| 187 | return false; |
| 188 | |
| 189 | emit entryReady(node->object); |
| 190 | disconnect(SIGNAL(entryReady(QNetworkAccessCache::CacheableObject*))); |
| 191 | |
| 192 | return true; |
| 193 | } |
| 194 | |
| 195 | void QNetworkAccessCache::timerEvent(QTimerEvent *) |
| 196 | { |
| 197 | while (firstExpiringNode && firstExpiringNode->timer.hasExpired()) { |
| 198 | Node *next = firstExpiringNode->next; |
| 199 | firstExpiringNode->object->dispose(); |
| 200 | hash.remove(key: firstExpiringNode->key); // `firstExpiringNode` gets deleted |
| 201 | delete firstExpiringNode; |
| 202 | firstExpiringNode = next; |
| 203 | } |
| 204 | |
| 205 | // fixup the list |
| 206 | if (firstExpiringNode) |
| 207 | firstExpiringNode->previous = nullptr; |
| 208 | else |
| 209 | lastExpiringNode = nullptr; |
| 210 | |
| 211 | updateTimer(); |
| 212 | } |
| 213 | |
| 214 | void QNetworkAccessCache::addEntry(const QByteArray &key, CacheableObject *entry, qint64 connectionCacheExpiryTimeoutSeconds) |
| 215 | { |
| 216 | Q_ASSERT(!key.isEmpty()); |
| 217 | |
| 218 | if (unlinkEntry(key)) |
| 219 | updateTimer(); |
| 220 | |
| 221 | Node *node = hash.value(key); |
| 222 | if (!node) { |
| 223 | node = new Node; |
| 224 | hash.insert(key, value: node); |
| 225 | } |
| 226 | |
| 227 | if (node->useCount) |
| 228 | qWarning(msg: "QNetworkAccessCache::addEntry: overriding active cache entry '%s'" , key.constData()); |
| 229 | if (node->object) |
| 230 | node->object->dispose(); |
| 231 | node->object = entry; |
| 232 | node->object->key = key; |
| 233 | if (connectionCacheExpiryTimeoutSeconds > -1) { |
| 234 | node->object->expiryTimeoutSeconds = connectionCacheExpiryTimeoutSeconds; // via ConnectionCacheExpiryTimeoutSecondsAttribute |
| 235 | } else { |
| 236 | node->object->expiryTimeoutSeconds = ExpiryTime; |
| 237 | } |
| 238 | node->key = key; |
| 239 | node->useCount = 1; |
| 240 | |
| 241 | // It gets only put into the expiry list in linkEntry (from releaseEntry), when it is not used anymore. |
| 242 | } |
| 243 | |
| 244 | bool QNetworkAccessCache::hasEntry(const QByteArray &key) const |
| 245 | { |
| 246 | return hash.contains(key); |
| 247 | } |
| 248 | |
| 249 | QNetworkAccessCache::CacheableObject *QNetworkAccessCache::requestEntryNow(const QByteArray &key) |
| 250 | { |
| 251 | Node *node = hash.value(key); |
| 252 | if (!node) |
| 253 | return nullptr; |
| 254 | |
| 255 | if (node->useCount > 0) { |
| 256 | if (node->object->shareable) { |
| 257 | ++node->useCount; |
| 258 | return node->object; |
| 259 | } |
| 260 | |
| 261 | // object in use and not shareable |
| 262 | return nullptr; |
| 263 | } |
| 264 | |
| 265 | // entry not in use, let the caller have it |
| 266 | bool wasNext = unlinkEntry(key); |
| 267 | ++node->useCount; |
| 268 | |
| 269 | if (wasNext) |
| 270 | updateTimer(); |
| 271 | return node->object; |
| 272 | } |
| 273 | |
| 274 | void QNetworkAccessCache::releaseEntry(const QByteArray &key) |
| 275 | { |
| 276 | Node *node = hash.value(key); |
| 277 | if (!node) { |
| 278 | qWarning(msg: "QNetworkAccessCache::releaseEntry: trying to release key '%s' that is not in cache" , key.constData()); |
| 279 | return; |
| 280 | } |
| 281 | |
| 282 | Q_ASSERT(node->useCount > 0); |
| 283 | |
| 284 | if (!--node->useCount) { |
| 285 | // no objects waiting; add it back to the expiry list |
| 286 | if (node->object->expires) |
| 287 | linkEntry(key); |
| 288 | |
| 289 | if (firstExpiringNode == node) |
| 290 | updateTimer(); |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | void QNetworkAccessCache::removeEntry(const QByteArray &key) |
| 295 | { |
| 296 | Node *node = hash.value(key); |
| 297 | if (!node) { |
| 298 | qWarning(msg: "QNetworkAccessCache::removeEntry: trying to remove key '%s' that is not in cache" , key.constData()); |
| 299 | return; |
| 300 | } |
| 301 | |
| 302 | if (unlinkEntry(key)) |
| 303 | updateTimer(); |
| 304 | if (node->useCount > 1) |
| 305 | qWarning(msg: "QNetworkAccessCache::removeEntry: removing active cache entry '%s'" , |
| 306 | key.constData()); |
| 307 | |
| 308 | node->object->key.clear(); |
| 309 | hash.remove(key: node->key); |
| 310 | delete node; |
| 311 | } |
| 312 | |
| 313 | QT_END_NAMESPACE |
| 314 | |
| 315 | #include "moc_qnetworkaccesscache_p.cpp" |
| 316 | |