1 /* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
2 
3 #include "lib.h"
4 #include "ioloop.h"
5 #include "module-context.h"
6 #include "eacces-error.h"
7 #include "mail-index-private.h"
8 #include "mail-index-alloc-cache.h"
9 
10 #define MAIL_INDEX_ALLOC_CACHE_CONTEXT(obj) \
11 	MODULE_CONTEXT(obj, mail_index_alloc_cache_index_module)
12 
13 /* How many seconds to keep index opened for reuse after it's been closed */
14 #define INDEX_CACHE_TIMEOUT 10
15 /* How many closed indexes to keep */
16 #define INDEX_CACHE_MAX 3
17 
18 struct mail_index_alloc_cache_list {
19 	union mail_index_module_context module_ctx;
20 	struct mail_index_alloc_cache_list *next;
21 
22 	struct mail_index *index;
23 	char *mailbox_path;
24 	int refcount;
25 	bool referenced;
26 
27 	dev_t index_dir_dev;
28 	ino_t index_dir_ino;
29 
30 	time_t destroy_time;
31 };
32 
33 static MODULE_CONTEXT_DEFINE_INIT(mail_index_alloc_cache_index_module,
34 				  &mail_index_module_register);
35 static struct mail_index_alloc_cache_list *indexes = NULL;
36 static unsigned int indexes_cache_references_count = 0;
37 static struct timeout *to_index = NULL;
38 
39 static struct mail_index_alloc_cache_list *
mail_index_alloc_cache_add(struct mail_index * index,const char * mailbox_path,struct stat * st)40 mail_index_alloc_cache_add(struct mail_index *index,
41 			   const char *mailbox_path, struct stat *st)
42 {
43 	struct mail_index_alloc_cache_list *list;
44 
45 	list = i_new(struct mail_index_alloc_cache_list, 1);
46 	list->refcount = 1;
47 	list->index = index;
48 
49 	list->mailbox_path = i_strdup(mailbox_path);
50 	list->index_dir_dev = st->st_dev;
51 	list->index_dir_ino = st->st_ino;
52 
53 	list->next = indexes;
54 	indexes = list;
55 
56 	MODULE_CONTEXT_SET(index, mail_index_alloc_cache_index_module, list);
57 	return list;
58 }
59 
60 static void
mail_index_alloc_cache_list_unref(struct mail_index_alloc_cache_list * list)61 mail_index_alloc_cache_list_unref(struct mail_index_alloc_cache_list *list)
62 {
63 	i_assert(list->referenced);
64 	i_assert(indexes_cache_references_count > 0);
65 
66 	indexes_cache_references_count--;
67 	mail_index_close(list->index);
68 	list->referenced = FALSE;
69 }
70 
71 static void
mail_index_alloc_cache_list_free(struct mail_index_alloc_cache_list * list)72 mail_index_alloc_cache_list_free(struct mail_index_alloc_cache_list *list)
73 {
74 	i_assert(list->refcount == 0);
75 
76 	if (list->referenced)
77 		mail_index_alloc_cache_list_unref(list);
78 	mail_index_free(&list->index);
79 	i_free(list->mailbox_path);
80 	i_free(list);
81 }
82 
83 static struct mail_index_alloc_cache_list *
mail_index_alloc_cache_find_and_expire(const char * mailbox_path,const char * index_dir,const struct stat * index_st)84 mail_index_alloc_cache_find_and_expire(const char *mailbox_path,
85 				       const char *index_dir,
86 				       const struct stat *index_st)
87 {
88 	struct mail_index_alloc_cache_list **indexp, *rec, *match;
89 	unsigned int destroy_count;
90 	struct stat st;
91 
92 	destroy_count = 0; match = NULL;
93 	for (indexp = &indexes; *indexp != NULL;) {
94 		rec = *indexp;
95 
96 		if (match != NULL) {
97 			/* already found the index. we're just going through
98 			   the rest of them to drop 0 refcounts */
99 		} else if (rec->refcount == 0 && rec->index->open_count == 0) {
100 			/* index is already closed. don't even try to
101 			   reuse it. */
102 		} else if (index_dir != NULL && rec->index_dir_ino != 0) {
103 			if (index_st->st_ino == rec->index_dir_ino &&
104 			    CMP_DEV_T(index_st->st_dev, rec->index_dir_dev)) {
105 				/* make sure the directory still exists.
106 				   it might have been renamed and we're trying
107 				   to access it via its new path now. */
108 				if (stat(rec->index->dir, &st) < 0 ||
109 				    st.st_ino != index_st->st_ino ||
110 				    !CMP_DEV_T(st.st_dev, index_st->st_dev))
111 					rec->destroy_time = 0;
112 				else
113 					match = rec;
114 			}
115 		} else if (mailbox_path != NULL && rec->mailbox_path != NULL &&
116 			   index_dir == NULL && rec->index_dir_ino == 0) {
117 			if (strcmp(mailbox_path, rec->mailbox_path) == 0)
118 				match = rec;
119 		}
120 
121 		if (rec->refcount == 0 && rec != match) {
122 			if (rec->destroy_time <= ioloop_time ||
123 			    destroy_count >= INDEX_CACHE_MAX) {
124 				*indexp = rec->next;
125 				mail_index_alloc_cache_list_free(rec);
126 				continue;
127 			} else {
128 				destroy_count++;
129 			}
130 		}
131 
132                 indexp = &(*indexp)->next;
133 	}
134 	return match;
135 }
136 
137 struct mail_index *
mail_index_alloc_cache_get(struct event * parent_event,const char * mailbox_path,const char * index_dir,const char * prefix)138 mail_index_alloc_cache_get(struct event *parent_event, const char *mailbox_path,
139 			   const char *index_dir, const char *prefix)
140 {
141 	struct mail_index_alloc_cache_list *match;
142 	struct stat st;
143 
144 	/* compare index_dir inodes so we don't break even with symlinks.
145 	   if index_dir doesn't exist yet or if using in-memory indexes, just
146 	   compare mailbox paths */
147 	i_zero(&st);
148 	if (index_dir == NULL) {
149 		/* in-memory indexes */
150 	} else if (stat(index_dir, &st) < 0) {
151 		if (errno == ENOENT) {
152 			/* it'll be created later */
153 		} else if (errno == EACCES) {
154 			e_error(parent_event, "%s",
155 				eacces_error_get("stat", index_dir));
156 		} else {
157 			e_error(parent_event, "stat(%s) failed: %m", index_dir);
158 		}
159 	}
160 
161 	match = mail_index_alloc_cache_find_and_expire(mailbox_path,
162 						       index_dir, &st);
163 	if (match == NULL) {
164 		struct mail_index *index =
165 			mail_index_alloc(parent_event, index_dir, prefix);
166 		match = mail_index_alloc_cache_add(index, mailbox_path, &st);
167 	} else {
168 		match->refcount++;
169 	}
170 	i_assert(match->index != NULL);
171 	return match->index;
172 }
173 
174 struct mail_index *
mail_index_alloc_cache_find(const char * index_dir)175 mail_index_alloc_cache_find(const char *index_dir)
176 {
177 	struct mail_index_alloc_cache_list *rec;
178 	struct stat st;
179 
180 	if (stat(index_dir, &st) < 0) {
181 		if (errno != ENOENT)
182 			i_error("stat(%s) failed: %m", index_dir);
183 		return NULL;
184 	}
185 
186 	for (rec = indexes; rec != NULL; rec = rec->next) {
187 		if (st.st_ino == rec->index_dir_ino &&
188 		    CMP_DEV_T(st.st_dev, rec->index_dir_dev))
189 			return rec->index;
190 	}
191 	return NULL;
192 }
193 
destroy_unrefed(unsigned int min_destroy_count)194 static bool destroy_unrefed(unsigned int min_destroy_count)
195 {
196 	struct mail_index_alloc_cache_list **list, *rec;
197 	bool destroyed = FALSE;
198 	bool seen_ref0 = FALSE;
199 
200 	for (list = &indexes; *list != NULL;) {
201 		rec = *list;
202 
203 		if (rec->refcount == 0 &&
204 		    (min_destroy_count > 0 || rec->destroy_time <= ioloop_time)) {
205 			*list = rec->next;
206 			destroyed = TRUE;
207 			mail_index_alloc_cache_list_free(rec);
208 			if (min_destroy_count > 0)
209 				min_destroy_count--;
210 		} else {
211 			if (rec->refcount == 0)
212 				seen_ref0 = TRUE;
213 			if (min_destroy_count > 0 &&
214 			    rec->index->open_count == 1 &&
215 			    rec->referenced) {
216 				/* we're the only one keeping this index open.
217 				   we might be here, because the caller is
218 				   deleting this mailbox and wants its indexes
219 				   to be closed. so close it. */
220 				destroyed = TRUE;
221 				mail_index_alloc_cache_list_unref(rec);
222 			}
223 			list = &(*list)->next;
224 		}
225 	}
226 
227 	if (!seen_ref0 && to_index != NULL)
228 		timeout_remove(&to_index);
229 	return destroyed;
230 }
231 
232 static void ATTR_NULL(1)
index_removal_timeout(void * context ATTR_UNUSED)233 index_removal_timeout(void *context ATTR_UNUSED)
234 {
235 	destroy_unrefed(0);
236 }
237 
mail_index_alloc_cache_unref(struct mail_index ** _index)238 void mail_index_alloc_cache_unref(struct mail_index **_index)
239 {
240 	struct mail_index *index = *_index;
241 	struct mail_index_alloc_cache_list *list, **listp;
242 
243 	*_index = NULL;
244 	list = NULL;
245 	for (listp = &indexes; *listp != NULL; listp = &(*listp)->next) {
246 		if ((*listp)->index == index) {
247 			list = *listp;
248 			break;
249 		}
250 	}
251 
252 	i_assert(list != NULL);
253 	i_assert(list->refcount > 0);
254 
255 	list->refcount--;
256 	list->destroy_time = ioloop_time + INDEX_CACHE_TIMEOUT;
257 
258 	if (list->refcount == 0 && index->open_count == 0) {
259 		/* index was already closed. don't even try to cache it. */
260 		*listp = list->next;
261 		mail_index_alloc_cache_list_free(list);
262 	} else if (to_index == NULL) {
263 		/* Add to root ioloop in case we got here from an inner
264 		   ioloop which gets destroyed too early. */
265 		to_index = timeout_add_to(io_loop_get_root(),
266 					  INDEX_CACHE_TIMEOUT*1000/2,
267 					  index_removal_timeout, NULL);
268 	}
269 }
270 
mail_index_alloc_cache_destroy_unrefed(void)271 void mail_index_alloc_cache_destroy_unrefed(void)
272 {
273 	destroy_unrefed(UINT_MAX);
274 }
275 
mail_index_alloc_cache_index_opened(struct mail_index * index)276 void mail_index_alloc_cache_index_opened(struct mail_index *index)
277 {
278 	struct mail_index_alloc_cache_list *list =
279 		MAIL_INDEX_ALLOC_CACHE_CONTEXT(index);
280 	struct stat st;
281 
282 	if (list != NULL && list->index_dir_ino == 0 &&
283 	    !MAIL_INDEX_IS_IN_MEMORY(index)) {
284 		/* newly created index directory. update its stat. */
285 		if (stat(index->dir, &st) == 0) {
286 			list->index_dir_ino = st.st_ino;
287 			list->index_dir_dev = st.st_dev;
288 		}
289 	}
290 }
291 
mail_index_alloc_cache_index_closing(struct mail_index * index)292 void mail_index_alloc_cache_index_closing(struct mail_index *index)
293 {
294 	struct mail_index_alloc_cache_list *list =
295 		MAIL_INDEX_ALLOC_CACHE_CONTEXT(index);
296 
297 	i_assert(index->open_count > 0);
298 	if (index->open_count > 1 || list == NULL)
299 		return;
300 
301 	if (list->referenced) {
302 		/* we're closing our referenced index */
303 		return;
304 	}
305 	while (indexes_cache_references_count > INDEX_CACHE_MAX) {
306 		if (!destroy_unrefed(1)) {
307 			/* our cache is full already, don't keep more */
308 			return;
309 		}
310 	}
311 	/* keep the index referenced for caching */
312 	indexes_cache_references_count++;
313 	list->referenced = TRUE;
314 	index->open_count++;
315 }
316