chiark / gitweb /
resolved: rework logic so that we can share transactions between queries of different...
[elogind.git] / src / resolve / resolved-dns-cache.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4   This file is part of systemd.
5
6   Copyright 2014 Lennart Poettering
7
8   systemd is free software; you can redistribute it and/or modify it
9   under the terms of the GNU Lesser General Public License as published by
10   the Free Software Foundation; either version 2.1 of the License, or
11   (at your option) any later version.
12
13   systemd is distributed in the hope that it will be useful, but
14   WITHOUT ANY WARRANTY; without even the implied warranty of
15   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16   Lesser General Public License for more details.
17
18   You should have received a copy of the GNU Lesser General Public License
19   along with systemd; If not, see <http://www.gnu.org/licenses/>.
20 ***/
21
22 #include "resolved-dns-cache.h"
23
24 /* Never cache more than 1K entries */
25 #define CACHE_MAX 1024
26
27 /* We never keep any item longer than 10min in our cache */
28 #define CACHE_TTL_MAX_USEC (10 * USEC_PER_MINUTE)
29
30 static void dns_cache_item_free(DnsCacheItem *i) {
31         if (!i)
32                 return;
33
34         dns_resource_record_unref(i->rr);
35         free(i);
36 }
37
38 DEFINE_TRIVIAL_CLEANUP_FUNC(DnsCacheItem*, dns_cache_item_free);
39
40 static void dns_cache_item_remove_and_free(DnsCache *c, DnsCacheItem *i) {
41         DnsCacheItem *first;
42
43         assert(c);
44
45         if (!i)
46                 return;
47
48         first = hashmap_get(c->rrsets, i->rr->key);
49         LIST_REMOVE(rrsets, first, i);
50
51         if (first)
52                 assert_se(hashmap_replace(c->rrsets, first->rr->key, first) >= 0);
53         else
54                 hashmap_remove(c->rrsets, i->rr->key);
55
56         prioq_remove(c->expire, i, &i->expire_prioq_idx);
57
58         dns_cache_item_free(i);
59 }
60
61 void dns_cache_flush(DnsCache *c) {
62         DnsCacheItem *i;
63
64         assert(c);
65
66         while ((i = hashmap_first(c->rrsets)))
67                 dns_cache_item_remove_and_free(c, i);
68
69         assert(hashmap_size(c->rrsets) == 0);
70         assert(prioq_size(c->expire) == 0);
71
72         hashmap_free(c->rrsets);
73         c->rrsets = NULL;
74
75         prioq_free(c->expire);
76         c->expire = NULL;
77 }
78
79 void dns_cache_remove(DnsCache *c, DnsResourceKey *key) {
80         DnsCacheItem *i;
81
82         assert(c);
83         assert(key);
84
85         while ((i = hashmap_get(c->rrsets, key)))
86                 dns_cache_item_remove_and_free(c, i);
87 }
88
89 static void dns_cache_make_space(DnsCache *c, unsigned add) {
90         assert(c);
91
92         if (add <= 0)
93                 return;
94
95         /* Makes space for n new entries. Note that we actually allow
96          * the cache to grow beyond CACHE_MAX, but only when we shall
97          * add more RRs to the cache than CACHE_MAX at once. In that
98          * case the cache will be emptied completely otherwise. */
99
100         for (;;) {
101                 _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
102                 DnsCacheItem *i;
103
104                 if (prioq_size(c->expire) <= 0)
105                         break;
106
107                 if (prioq_size(c->expire) + add < CACHE_MAX)
108                         break;
109
110                 i = prioq_peek(c->expire);
111                 assert(i);
112
113                 /* Take an extra reference to the key so that it
114                  * doesn't go away in the middle of the remove call */
115                 key = dns_resource_key_ref(i->rr->key);
116                 dns_cache_remove(c, key);
117         }
118 }
119
120 void dns_cache_prune(DnsCache *c) {
121         usec_t t = 0;
122
123         assert(c);
124
125         /* Remove all entries that are past their TTL */
126
127         for (;;) {
128                 _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
129                 DnsCacheItem *i;
130                 usec_t ttl;
131
132                 i = prioq_peek(c->expire);
133                 if (!i)
134                         break;
135
136                 ttl = i->rr->ttl * USEC_PER_SEC;
137                 if (ttl > CACHE_TTL_MAX_USEC)
138                         ttl = CACHE_TTL_MAX_USEC;
139
140                 if (t <= 0)
141                         t = now(CLOCK_MONOTONIC);
142
143                 if (i->timestamp + ttl > t)
144                         break;
145
146                 /* Take an extra reference to the key so that it
147                  * doesn't go away in the middle of the remove call */
148                 key = dns_resource_key_ref(i->rr->key);
149                 dns_cache_remove(c, key);
150         }
151 }
152
153 static int dns_cache_item_prioq_compare_func(const void *a, const void *b) {
154         usec_t t, z;
155         const DnsCacheItem *x = a, *y = b;
156
157         t = x->timestamp + x->rr->ttl * USEC_PER_SEC;
158         z = y->timestamp + y->rr->ttl * USEC_PER_SEC;
159
160         if (t < z)
161                 return -1;
162         if (t > z)
163                 return 1;
164         return 0;
165 }
166
167 static void dns_cache_item_update(DnsCache *c, DnsCacheItem *i, DnsResourceRecord *rr, usec_t timestamp) {
168         assert(c);
169         assert(i);
170         assert(rr);
171
172         if (!i->rrsets_prev) {
173                 /* We are the first item in the list, we need to
174                  * update the key used in the hashmap */
175
176                 assert_se(hashmap_replace(c->rrsets, rr->key, i) >= 0);
177         }
178
179         dns_resource_record_ref(rr);
180         dns_resource_record_unref(i->rr);
181         i->rr = rr;
182
183         i->timestamp = timestamp;
184         prioq_reshuffle(c->expire, i, &i->expire_prioq_idx);
185 }
186
187 static DnsCacheItem* dns_cache_get(DnsCache *c, DnsResourceRecord *rr) {
188         DnsCacheItem *i;
189
190         assert(c);
191         assert(rr);
192
193         LIST_FOREACH(rrsets, i, hashmap_get(c->rrsets, rr->key))
194                 if (dns_resource_record_equal(i->rr, rr))
195                         return i;
196
197         return NULL;
198 }
199
200 int dns_cache_put(DnsCache *c, DnsResourceRecord *rr, usec_t timestamp) {
201         _cleanup_(dns_cache_item_freep) DnsCacheItem *i = NULL;
202         DnsCacheItem *first = NULL, *existing;
203         int r;
204
205         assert(c);
206         assert(rr);
207
208         /* New TTL is 0? Delete the entry... */
209         if (rr->ttl <= 0) {
210                 dns_cache_remove(c, rr->key);
211                 return 0;
212         }
213
214         /* Entry exists already? Update TTL and timestamp */
215         existing = dns_cache_get(c, rr);
216         if (existing) {
217                 dns_cache_item_update(c, existing, rr, timestamp);
218                 return 0;
219         }
220
221         /* Otherwise, add the new RR */
222         r = prioq_ensure_allocated(&c->expire, dns_cache_item_prioq_compare_func);
223         if (r < 0)
224                 return r;
225
226         r = hashmap_ensure_allocated(&c->rrsets, dns_resource_key_hash_func, dns_resource_key_compare_func);
227         if (r < 0)
228                 return r;
229
230         dns_cache_make_space(c, 1);
231
232         i = new0(DnsCacheItem, 1);
233         if (!i)
234                 return -ENOMEM;
235
236         i->rr = dns_resource_record_ref(rr);
237         i->timestamp = timestamp;
238         i->expire_prioq_idx = PRIOQ_IDX_NULL;
239
240         r = prioq_put(c->expire, i, &i->expire_prioq_idx);
241         if (r < 0)
242                 return r;
243
244         first = hashmap_get(c->rrsets, i->rr->key);
245         if (first) {
246                 LIST_PREPEND(rrsets, first, i);
247                 assert_se(hashmap_replace(c->rrsets, first->rr->key, first) >= 0);
248         } else {
249                 r = hashmap_put(c->rrsets, i->rr->key, i);
250                 if (r < 0) {
251                         prioq_remove(c->expire, i, &i->expire_prioq_idx);
252                         return r;
253                 }
254         }
255
256         i = NULL;
257
258         return 0;
259 }
260
261 int dns_cache_put_answer(DnsCache *c, DnsAnswer *answer, usec_t timestamp) {
262         unsigned i, added = 0;
263         int r;
264
265         assert(c);
266         assert(answer);
267
268         /* First iteration, delete all matching old RRs, so that we
269          * only keep complete rrsets in place. */
270         for (i = 0; i < answer->n_rrs; i++)
271                 dns_cache_remove(c, answer->rrs[i]->key);
272
273         dns_cache_make_space(c, answer->n_rrs);
274
275         /* Second iteration, add in new RRs */
276         for (added = 0; added < answer->n_rrs; added++) {
277                 if (timestamp <= 0)
278                         timestamp = now(CLOCK_MONOTONIC);
279
280                 r = dns_cache_put(c, answer->rrs[added], timestamp);
281                 if (r < 0)
282                         goto fail;
283         }
284
285         return 0;
286
287 fail:
288         /* Adding all RRs failed. Let's clean up what we already
289          * added, just in case */
290
291         for (i = 0; i < added; i++)
292                 dns_cache_remove(c, answer->rrs[i]->key);
293
294         return r;
295 }
296
297 int dns_cache_lookup(DnsCache *c, DnsQuestion *q, DnsAnswer **ret) {
298         _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
299         unsigned i, n = 0;
300         int r;
301
302         assert(c);
303         assert(q);
304         assert(ret);
305
306         if (q->n_keys <= 0) {
307                 *ret = NULL;
308                 return 0;
309         }
310
311         for (i = 0; i < q->n_keys; i++) {
312                 DnsCacheItem *j;
313
314                 j = hashmap_get(c->rrsets, q->keys[i]);
315                 if (!j) {
316                         /* If one question cannot be answered we need to refresh */
317                         *ret = NULL;
318                         return 0;
319                 }
320
321                 LIST_FOREACH(rrsets, j, j)
322                         n++;
323         }
324
325         assert(n > 0);
326
327         answer = dns_answer_new(n);
328         if (!answer)
329                 return -ENOMEM;
330
331         for (i = 0; i < q->n_keys; i++) {
332                 DnsCacheItem *j;
333
334                 j = hashmap_get(c->rrsets, q->keys[i]);
335                 LIST_FOREACH(rrsets, j, j) {
336                         r = dns_answer_add(answer, j->rr);
337                         if (r < 0)
338                                 return r;
339                 }
340         }
341
342         assert(n >= answer->n_rrs);
343
344         *ret = answer;
345         answer = NULL;
346
347         return n;
348 }