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 | namespace { |
22 | struct Receiver |
23 | { |
24 | QPointer<QObject> object; |
25 | const char *member; |
26 | }; |
27 | } |
28 | |
29 | // idea copied from qcache.h |
30 | struct 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 | |
42 | QNetworkAccessCache::CacheableObject::CacheableObject() |
43 | { |
44 | // leave the members uninitialized |
45 | // they must be initialized by the derived class's constructor |
46 | } |
47 | |
48 | QNetworkAccessCache::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 | |
57 | void QNetworkAccessCache::CacheableObject::setExpires(bool enable) |
58 | { |
59 | expires = enable; |
60 | } |
61 | |
62 | void QNetworkAccessCache::CacheableObject::setShareable(bool enable) |
63 | { |
64 | shareable = enable; |
65 | } |
66 | |
67 | QNetworkAccessCache::~QNetworkAccessCache() |
68 | { |
69 | clear(); |
70 | } |
71 | |
72 | void 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 | */ |
98 | void 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 | */ |
159 | bool 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 | |
181 | void 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 | |
200 | bool 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 | |
212 | void 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 | |
231 | void 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 | |
261 | bool QNetworkAccessCache::hasEntry(const QByteArray &key) const |
262 | { |
263 | return hash.contains(key); |
264 | } |
265 | |
266 | QNetworkAccessCache::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 | |
291 | void 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 | |
311 | void 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 | |
330 | QT_END_NAMESPACE |
331 | |
332 | #include "moc_qnetworkaccesscache_p.cpp" |
333 | |