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

source code of qtbase/src/network/access/qnetworkaccesscache.cpp