Coverage Report

Created: 2026-08-13 06:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib-index/mail-index-alloc-cache.c
Line
Count
Source
1
/* Copyright (c) Dovecot authors, see top-level 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
0
  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
0
#define INDEX_CACHE_TIMEOUT 10
15
/* How many closed indexes to keep */
16
0
#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 *
40
mail_index_alloc_cache_add(struct mail_index *index,
41
         const char *mailbox_path, struct stat *st)
42
0
{
43
0
  struct mail_index_alloc_cache_list *list;
44
45
0
  list = i_new(struct mail_index_alloc_cache_list, 1);
46
0
  list->refcount = 1;
47
0
  list->index = index;
48
49
0
  list->mailbox_path = i_strdup(mailbox_path);
50
0
  list->index_dir_dev = st->st_dev;
51
0
  list->index_dir_ino = st->st_ino;
52
53
0
  list->next = indexes;
54
0
  indexes = list;
55
56
0
  MODULE_CONTEXT_SET(index, mail_index_alloc_cache_index_module, list);
57
0
  return list;
58
0
}
59
60
static void
61
mail_index_alloc_cache_list_unref(struct mail_index_alloc_cache_list *list)
62
0
{
63
0
  i_assert(list->referenced);
64
0
  i_assert(indexes_cache_references_count > 0);
65
66
0
  indexes_cache_references_count--;
67
0
  mail_index_close(list->index);
68
0
  list->referenced = FALSE;
69
0
}
70
71
static void
72
mail_index_alloc_cache_list_free(struct mail_index_alloc_cache_list *list)
73
0
{
74
0
  i_assert(list->refcount == 0);
75
76
0
  if (list->referenced)
77
0
    mail_index_alloc_cache_list_unref(list);
78
0
  mail_index_free(&list->index);
79
0
  i_free(list->mailbox_path);
80
0
  i_free(list);
81
0
}
82
83
static struct mail_index_alloc_cache_list *
84
mail_index_alloc_cache_find_and_expire(const char *mailbox_path,
85
               const char *index_dir,
86
               const struct stat *index_st)
87
0
{
88
0
  struct mail_index_alloc_cache_list **indexp, *rec, *match;
89
0
  unsigned int destroy_count;
90
0
  struct stat st;
91
92
0
  destroy_count = 0; match = NULL;
93
0
  for (indexp = &indexes; *indexp != NULL;) {
94
0
    rec = *indexp;
95
96
0
    if (match != NULL) {
97
      /* already found the index. we're just going through
98
         the rest of them to drop 0 refcounts */
99
0
    } else if (rec->refcount == 0 && rec->index->open_count == 0) {
100
      /* index is already closed. don't even try to
101
         reuse it. */
102
0
    } else if (index_dir != NULL && rec->index_dir_ino != 0) {
103
0
      if (index_st->st_ino == rec->index_dir_ino &&
104
0
          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
0
        if (stat(rec->index->dir, &st) < 0 ||
109
0
            st.st_ino != index_st->st_ino ||
110
0
            !CMP_DEV_T(st.st_dev, index_st->st_dev))
111
0
          rec->destroy_time = 0;
112
0
        else
113
0
          match = rec;
114
0
      }
115
0
    } else if (mailbox_path != NULL && rec->mailbox_path != NULL &&
116
0
         index_dir == NULL && rec->index_dir_ino == 0) {
117
0
      if (strcmp(mailbox_path, rec->mailbox_path) == 0)
118
0
        match = rec;
119
0
    }
120
121
0
    if (rec->refcount == 0 && rec != match) {
122
0
      if (rec->destroy_time <= ioloop_time ||
123
0
          destroy_count >= INDEX_CACHE_MAX) {
124
0
        *indexp = rec->next;
125
0
        mail_index_alloc_cache_list_free(rec);
126
0
        continue;
127
0
      } else {
128
0
        destroy_count++;
129
0
      }
130
0
    }
131
132
0
                indexp = &(*indexp)->next;
133
0
  }
134
0
  return match;
135
0
}
136
137
struct mail_index *
138
mail_index_alloc_cache_get(struct event *parent_event, const char *mailbox_path,
139
         const char *index_dir, const char *prefix)
140
0
{
141
0
  struct mail_index_alloc_cache_list *match;
142
0
  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
0
  i_zero(&st);
148
0
  if (index_dir == NULL) {
149
    /* in-memory indexes */
150
0
  } else if (stat(index_dir, &st) < 0) {
151
0
    if (errno == ENOENT) {
152
      /* it'll be created later */
153
0
    } else if (ENOACCESS(errno)) {
154
0
      e_error(parent_event, "%s",
155
0
        eacces_error_get("stat", index_dir));
156
0
    } else {
157
0
      e_error(parent_event, "stat(%s) failed: %m", index_dir);
158
0
    }
159
0
  }
160
161
0
  match = mail_index_alloc_cache_find_and_expire(mailbox_path,
162
0
                   index_dir, &st);
163
0
  if (match == NULL) {
164
0
    struct mail_index *index =
165
0
      mail_index_alloc(parent_event, index_dir, prefix);
166
0
    match = mail_index_alloc_cache_add(index, mailbox_path, &st);
167
0
  } else {
168
0
    match->refcount++;
169
0
  }
170
0
  i_assert(match->index != NULL);
171
0
  return match->index;
172
0
}
173
174
struct mail_index *
175
mail_index_alloc_cache_find(const char *index_dir)
176
0
{
177
0
  struct mail_index_alloc_cache_list *rec;
178
0
  struct stat st;
179
180
0
  if (stat(index_dir, &st) < 0) {
181
0
    if (errno != ENOENT)
182
0
      i_error("stat(%s) failed: %m", index_dir);
183
0
    return NULL;
184
0
  }
185
186
0
  for (rec = indexes; rec != NULL; rec = rec->next) {
187
0
    if (st.st_ino == rec->index_dir_ino &&
188
0
        CMP_DEV_T(st.st_dev, rec->index_dir_dev))
189
0
      return rec->index;
190
0
  }
191
0
  return NULL;
192
0
}
193
194
static bool destroy_unrefed(unsigned int min_destroy_count)
195
0
{
196
0
  struct mail_index_alloc_cache_list **list, *rec;
197
0
  bool destroyed = FALSE;
198
0
  bool seen_ref0 = FALSE;
199
200
0
  for (list = &indexes; *list != NULL;) {
201
0
    rec = *list;
202
203
0
    if (rec->refcount == 0 &&
204
0
        (min_destroy_count > 0 || rec->destroy_time <= ioloop_time)) {
205
0
      *list = rec->next;
206
0
      destroyed = TRUE;
207
0
      mail_index_alloc_cache_list_free(rec);
208
0
      if (min_destroy_count > 0)
209
0
        min_destroy_count--;
210
0
    } else {
211
0
      if (rec->refcount == 0)
212
0
        seen_ref0 = TRUE;
213
0
      if (min_destroy_count > 0 &&
214
0
          rec->index->open_count == 1 &&
215
0
          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
0
        destroyed = TRUE;
221
0
        mail_index_alloc_cache_list_unref(rec);
222
0
      }
223
0
      list = &(*list)->next;
224
0
    }
225
0
  }
226
227
0
  if (!seen_ref0 && to_index != NULL)
228
0
    timeout_remove(&to_index);
229
0
  return destroyed;
230
0
}
231
232
static void ATTR_NULL(1)
233
index_removal_timeout(void *context ATTR_UNUSED)
234
0
{
235
0
  destroy_unrefed(0);
236
0
}
237
238
void mail_index_alloc_cache_unref(struct mail_index **_index)
239
0
{
240
0
  struct mail_index *index = *_index;
241
0
  struct mail_index_alloc_cache_list *list, **listp;
242
243
0
  *_index = NULL;
244
0
  list = NULL;
245
0
  for (listp = &indexes; *listp != NULL; listp = &(*listp)->next) {
246
0
    if ((*listp)->index == index) {
247
0
      list = *listp;
248
0
      break;
249
0
    }
250
0
  }
251
252
0
  i_assert(list != NULL);
253
0
  i_assert(list->refcount > 0);
254
255
0
  list->refcount--;
256
0
  list->destroy_time = ioloop_time + INDEX_CACHE_TIMEOUT;
257
258
0
  if (list->refcount == 0 && index->open_count == 0) {
259
    /* index was already closed. don't even try to cache it. */
260
0
    *listp = list->next;
261
0
    mail_index_alloc_cache_list_free(list);
262
0
  } 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
0
    to_index = timeout_add_to(io_loop_get_root(),
266
0
            INDEX_CACHE_TIMEOUT*1000/2,
267
0
            index_removal_timeout, NULL);
268
0
  }
269
0
}
270
271
void mail_index_alloc_cache_destroy_unrefed(void)
272
0
{
273
0
  destroy_unrefed(UINT_MAX);
274
0
}
275
276
void mail_index_alloc_cache_index_opened(struct mail_index *index)
277
0
{
278
0
  struct mail_index_alloc_cache_list *list =
279
0
    MAIL_INDEX_ALLOC_CACHE_CONTEXT(index);
280
0
  struct stat st;
281
282
0
  if (list != NULL && list->index_dir_ino == 0 &&
283
0
      !MAIL_INDEX_IS_IN_MEMORY(index)) {
284
    /* newly created index directory. update its stat. */
285
0
    if (stat(index->dir, &st) == 0) {
286
0
      list->index_dir_ino = st.st_ino;
287
0
      list->index_dir_dev = st.st_dev;
288
0
    }
289
0
  }
290
0
}
291
292
void mail_index_alloc_cache_index_closing(struct mail_index *index)
293
0
{
294
0
  struct mail_index_alloc_cache_list *list =
295
0
    MAIL_INDEX_ALLOC_CACHE_CONTEXT(index);
296
297
0
  i_assert(index->open_count > 0);
298
0
  if (index->open_count > 1 || list == NULL)
299
0
    return;
300
301
0
  if (list->referenced) {
302
    /* we're closing our referenced index */
303
0
    return;
304
0
  }
305
0
  while (indexes_cache_references_count > INDEX_CACHE_MAX) {
306
0
    if (!destroy_unrefed(1)) {
307
      /* our cache is full already, don't keep more */
308
0
      return;
309
0
    }
310
0
  }
311
  /* keep the index referenced for caching */
312
0
  indexes_cache_references_count++;
313
0
  list->referenced = TRUE;
314
0
  index->open_count++;
315
0
}