/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" #include "ioloop.h" #include "array.h" #include "buffer.h" #include "module-context.h" #include "file-cache.h" #include "file-set-size.h" #include "read-full.h" #include "write-full.h" #include "mail-cache-private.h" #include "mail-index-transaction-private.h" #include #include #define MAIL_CACHE_INIT_WRITE_BUFFER (1024*16) #define CACHE_TRANS_CONTEXT(obj) \ MODULE_CONTEXT(obj, cache_mail_index_transaction_module) #define CACHE_TRANS_CONTEXT_REQUIRE(obj) \ MODULE_CONTEXT_REQUIRE(obj, cache_mail_index_transaction_module) struct mail_cache_transaction_rec { uint32_t seq; uint32_t cache_data_pos; }; struct mail_cache_transaction_ctx { union mail_index_transaction_module_context module_ctx; struct mail_index_transaction_vfuncs super; struct mail_cache *cache; struct mail_cache_view *view; struct mail_index_transaction *trans; uint32_t cache_file_seq; uint32_t first_new_seq; buffer_t *cache_data; ARRAY(uint8_t) cache_field_idx_used; ARRAY(struct mail_cache_transaction_rec) cache_data_seq; ARRAY_TYPE(seq_range) cache_data_wanted_seqs; uint32_t prev_seq, min_seq; size_t last_rec_pos; unsigned int records_written; bool tried_purging:1; bool decisions_refreshed:1; bool have_noncommited_mails:1; bool changes:1; }; static MODULE_CONTEXT_DEFINE_INIT(cache_mail_index_transaction_module, &mail_index_module_register); static int mail_cache_transaction_lock(struct mail_cache_transaction_ctx *ctx); static bool mail_cache_transaction_update_last_rec_size(struct mail_cache_transaction_ctx *ctx, size_t *size_r); static int mail_cache_header_rewrite_fields(struct mail_cache *cache); static void mail_index_transaction_cache_reset(struct mail_index_transaction *t) { struct mail_cache_transaction_ctx *ctx = CACHE_TRANS_CONTEXT_REQUIRE(t); struct mail_index_transaction_vfuncs super = ctx->super; mail_cache_transaction_reset(ctx); super.reset(t); } static int mail_index_transaction_cache_commit(struct mail_index_transaction *t, struct mail_index_transaction_commit_result *result_r) { struct mail_cache_transaction_ctx *ctx = CACHE_TRANS_CONTEXT_REQUIRE(t); struct mail_index_transaction_vfuncs super = ctx->super; /* a failed cache commit isn't important enough to fail the entire index transaction, so we'll just ignore it */ (void)mail_cache_transaction_commit(&ctx); return super.commit(t, result_r); } static void mail_index_transaction_cache_rollback(struct mail_index_transaction *t) { struct mail_cache_transaction_ctx *ctx = CACHE_TRANS_CONTEXT_REQUIRE(t); struct mail_index_transaction_vfuncs super = ctx->super; mail_cache_transaction_rollback(&ctx); super.rollback(t); } struct mail_cache_transaction_ctx * mail_cache_get_transaction(struct mail_cache_view *view, struct mail_index_transaction *t) { struct mail_cache_transaction_ctx *ctx; ctx = !cache_mail_index_transaction_module.id.module_id_set ? NULL : CACHE_TRANS_CONTEXT(t); if (ctx != NULL) return ctx; ctx = i_new(struct mail_cache_transaction_ctx, 1); ctx->cache = view->cache; ctx->view = view; ctx->trans = t; i_assert(view->transaction == NULL); view->transaction = ctx; view->trans_view = mail_index_transaction_open_updated_view(t); ctx->super = t->v; t->v.reset = mail_index_transaction_cache_reset; t->v.commit = mail_index_transaction_cache_commit; t->v.rollback = mail_index_transaction_cache_rollback; MODULE_CONTEXT_SET(t, cache_mail_index_transaction_module, ctx); return ctx; } static void mail_cache_transaction_forget_flushed(struct mail_cache_transaction_ctx *ctx, bool reset_id_changed) { uint32_t new_cache_file_seq = MAIL_CACHE_IS_UNUSABLE(ctx->cache) ? 0 : ctx->cache->hdr->file_seq; if (reset_id_changed && ctx->records_written > 0) { e_warning(ctx->cache->event, "Purging lost %u written cache records " "(reset_id changed %u -> %u)", ctx->records_written, ctx->cache_file_seq, new_cache_file_seq); /* don't increase deleted_record_count in the new file */ ctx->records_written = 0; } ctx->cache_file_seq = new_cache_file_seq; /* forget all cache extension updates even if reset_id doesn't change */ mail_index_ext_set_reset_id(ctx->trans, ctx->cache->ext_id, ctx->cache_file_seq); } void mail_cache_transaction_reset(struct mail_cache_transaction_ctx *ctx) { mail_cache_transaction_forget_flushed(ctx, FALSE); if (ctx->cache_data != NULL) buffer_set_used_size(ctx->cache_data, 0); if (array_is_created(&ctx->cache_data_seq)) array_clear(&ctx->cache_data_seq); ctx->prev_seq = 0; ctx->last_rec_pos = 0; ctx->changes = FALSE; } void mail_cache_transaction_rollback(struct mail_cache_transaction_ctx **_ctx) { struct mail_cache_transaction_ctx *ctx = *_ctx; *_ctx = NULL; if (ctx->records_written > 0) { /* we already wrote to the cache file. we can't (or don't want to) delete that data, so just mark it as deleted space */ if (mail_cache_transaction_lock(ctx) > 0) { ctx->cache->hdr_copy.deleted_record_count += ctx->records_written; ctx->cache->hdr_modified = TRUE; (void)mail_cache_flush_and_unlock(ctx->cache); } } MODULE_CONTEXT_UNSET(ctx->trans, cache_mail_index_transaction_module); ctx->view->transaction = NULL; ctx->view->trans_seq1 = ctx->view->trans_seq2 = 0; mail_index_view_close(&ctx->view->trans_view); buffer_free(&ctx->cache_data); if (array_is_created(&ctx->cache_data_seq)) array_free(&ctx->cache_data_seq); if (array_is_created(&ctx->cache_data_wanted_seqs)) array_free(&ctx->cache_data_wanted_seqs); array_free(&ctx->cache_field_idx_used); i_free(ctx); } bool mail_cache_transactions_have_changes(struct mail_cache *cache) { struct mail_cache_view *view; for (view = cache->views; view != NULL; view = view->next) { if (view->transaction != NULL && view->transaction->changes) return TRUE; } return FALSE; } static int mail_cache_transaction_purge(struct mail_cache_transaction_ctx *ctx, const char *reason) { struct mail_cache *cache = ctx->cache; ctx->tried_purging = TRUE; uint32_t purge_file_seq = MAIL_CACHE_IS_UNUSABLE(cache) ? 0 : cache->hdr->file_seq; int ret = mail_cache_purge(cache, purge_file_seq, reason); /* already written cache records must be forgotten, but records in memory can still be written to the new cache file */ mail_cache_transaction_forget_flushed(ctx, TRUE); return ret; } static int mail_cache_transaction_lock(struct mail_cache_transaction_ctx *ctx) { struct mail_cache *cache = ctx->cache; const uoff_t cache_max_size = cache->index->optimization_set.cache.max_size; int ret; if ((ret = mail_cache_lock(cache)) <= 0) { if (ret < 0) return -1; if (!ctx->tried_purging) { if (mail_cache_transaction_purge(ctx, "creating cache") < 0) return -1; return mail_cache_transaction_lock(ctx); } else { return 0; } } i_assert(!MAIL_CACHE_IS_UNUSABLE(cache)); if (!ctx->tried_purging && ctx->cache_data != NULL && cache->last_stat_size + ctx->cache_data->used > cache_max_size) { /* Looks like cache file is becoming too large. Try to purge it to free up some space. */ if (cache->hdr->continued_record_count > 0 || cache->hdr->deleted_record_count > 0) { mail_cache_unlock(cache); (void)mail_cache_transaction_purge(ctx, "cache is too large"); return mail_cache_transaction_lock(ctx); } } if (ctx->cache_file_seq == 0) ctx->cache_file_seq = cache->hdr->file_seq; else if (ctx->cache_file_seq != cache->hdr->file_seq) { /* already written cache records must be forgotten, but records in memory can still be written to the new cache file */ mail_cache_transaction_forget_flushed(ctx, TRUE); i_assert(ctx->cache_file_seq == cache->hdr->file_seq); } return 1; } const struct mail_cache_record * mail_cache_transaction_lookup_rec(struct mail_cache_transaction_ctx *ctx, unsigned int seq, unsigned int *trans_next_idx) { const struct mail_cache_transaction_rec *recs; unsigned int i, count; recs = array_get(&ctx->cache_data_seq, &count); for (i = *trans_next_idx; i < count; i++) { if (recs[i].seq == seq) { *trans_next_idx = i + 1; return CONST_PTR_OFFSET(ctx->cache_data->data, recs[i].cache_data_pos); } } *trans_next_idx = i + 1; if (seq == ctx->prev_seq && i == count) { /* update the unfinished record's (temporary) size and return it */ size_t size; if (!mail_cache_transaction_update_last_rec_size(ctx, &size)) return NULL; return CONST_PTR_OFFSET(ctx->cache_data->data, ctx->last_rec_pos); } return NULL; } static void mail_cache_transaction_update_index(struct mail_cache_transaction_ctx *ctx, uint32_t write_offset, bool committing) { struct mail_cache *cache = ctx->cache; struct mail_index_transaction *trans; const struct mail_cache_record *rec = ctx->cache_data->data; const struct mail_cache_transaction_rec *recs; uint32_t i, seq_count; if (committing) { /* The transaction is being committed now. Use it. */ trans = ctx->trans; } else if (ctx->have_noncommited_mails) { /* Some of the mails haven't been committed yet. We must use the provided transaction to update the cache records. */ trans = ctx->trans; } else { /* We can commit these changes immediately. This way even if the provided transaction runs for a very long time, we still once in a while commit the cache changes so they become visible to other processes as well. */ trans = mail_index_transaction_begin(ctx->view->trans_view, MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL); } mail_index_ext_using_reset_id(trans, ctx->cache->ext_id, ctx->cache_file_seq); /* write the cache_offsets to index file. records' prev_offset is updated to point to old cache record when index is being synced. */ recs = array_get(&ctx->cache_data_seq, &seq_count); for (i = 0; i < seq_count; i++) { mail_index_update_ext(trans, recs[i].seq, cache->ext_id, &write_offset, NULL); write_offset += rec->size; rec = CONST_PTR_OFFSET(rec, rec->size); ctx->records_written++; } if (trans != ctx->trans) { i_assert(cache->index->log_sync_locked); if (mail_index_transaction_commit(&trans) < 0) { /* failed, but can't really do anything */ } else { ctx->records_written = 0; } } } static int mail_cache_link_records(struct mail_cache_transaction_ctx *ctx, uint32_t write_offset) { struct mail_index_map *map; struct mail_cache_record *rec; const struct mail_cache_transaction_rec *recs; const uint32_t *prev_offsetp; ARRAY_TYPE(uint32_t) seq_offsets; uint32_t i, seq_count, reset_id, prev_offset, *offsetp; const void *data; i_assert(ctx->min_seq != 0); i_array_init(&seq_offsets, 64); recs = array_get(&ctx->cache_data_seq, &seq_count); rec = buffer_get_modifiable_data(ctx->cache_data, NULL); for (i = 0; i < seq_count; i++) { offsetp = array_idx_get_space(&seq_offsets, recs[i].seq - ctx->min_seq); if (*offsetp != 0) prev_offset = *offsetp; else { mail_index_lookup_ext_full(ctx->view->trans_view, recs[i].seq, ctx->cache->ext_id, &map, &data, NULL); prev_offsetp = data; if (prev_offsetp == NULL || *prev_offsetp == 0) prev_offset = 0; else if (mail_index_ext_get_reset_id(ctx->view->trans_view, map, ctx->cache->ext_id, &reset_id) && reset_id == ctx->cache_file_seq) prev_offset = *prev_offsetp; else prev_offset = 0; if (prev_offset >= write_offset) { mail_cache_set_corrupted(ctx->cache, "Cache record offset points outside existing file"); array_free(&seq_offsets); return -1; } } if (prev_offset != 0) { /* link this record to previous one */ rec->prev_offset = prev_offset; ctx->cache->hdr_copy.continued_record_count++; } else { ctx->cache->hdr_copy.record_count++; } *offsetp = write_offset; write_offset += rec->size; rec = PTR_OFFSET(rec, rec->size); } array_free(&seq_offsets); ctx->cache->hdr_modified = TRUE; return 0; } static bool mail_cache_transaction_set_used(struct mail_cache_transaction_ctx *ctx) { const uint8_t *cache_fields_used; unsigned int field_idx, count; bool missing_file_fields = FALSE; cache_fields_used = array_get(&ctx->cache_field_idx_used, &count); i_assert(count <= ctx->cache->fields_count); for (field_idx = 0; field_idx < count; field_idx++) { if (cache_fields_used[field_idx] != 0) { ctx->cache->fields[field_idx].used = TRUE; if (ctx->cache->field_file_map[field_idx] == (uint32_t)-1) missing_file_fields = TRUE; } } return missing_file_fields; } static int mail_cache_transaction_update_fields(struct mail_cache_transaction_ctx *ctx) { unsigned char *p; const unsigned char *end, *rec_end; uint32_t field_idx, data_size; if (mail_cache_transaction_set_used(ctx)) { /* add missing fields to cache */ if (mail_cache_header_rewrite_fields(ctx->cache) < 0) return -1; /* make sure they were actually added */ if (mail_cache_transaction_set_used(ctx)) { mail_index_set_error(ctx->cache->index, "Cache file %s: Unexpectedly lost newly added field", ctx->cache->filepath); return -1; } } /* Go through all the added cache records and replace the in-memory field_idx with the cache file-specific field index. Update only up to last_rec_pos, because that's how far flushing is done. The fields after that keep the in-memory field_idx until the next flush. */ p = buffer_get_modifiable_data(ctx->cache_data, NULL); end = CONST_PTR_OFFSET(ctx->cache_data->data, ctx->last_rec_pos); rec_end = p; while (p < end) { if (p >= rec_end) { /* next cache record */ i_assert(p == rec_end); const struct mail_cache_record *rec = (const struct mail_cache_record *)p; /* note that the last rec->size==0 */ rec_end = CONST_PTR_OFFSET(p, rec->size); p += sizeof(*rec); } /* replace field_idx */ uint32_t *file_fieldp = (uint32_t *)p; field_idx = *file_fieldp; *file_fieldp = ctx->cache->field_file_map[field_idx]; i_assert(*file_fieldp != (uint32_t)-1); p += sizeof(field_idx); /* Skip to next cache field. Next is if the field is not fixed size. */ data_size = ctx->cache->fields[field_idx].field.field_size; if (data_size == UINT_MAX) { memcpy(&data_size, p, sizeof(data_size)); p += sizeof(data_size); } /* data & 32bit padding */ p += data_size; if ((data_size & 3) != 0) p += 4 - (data_size & 3); } i_assert(p == end); return 0; } static void mail_cache_transaction_drop_last_flush(struct mail_cache_transaction_ctx *ctx) { buffer_copy(ctx->cache_data, 0, ctx->cache_data, ctx->last_rec_pos, SIZE_MAX); buffer_set_used_size(ctx->cache_data, ctx->cache_data->used - ctx->last_rec_pos); ctx->last_rec_pos = 0; ctx->min_seq = 0; array_clear(&ctx->cache_data_seq); array_clear(&ctx->cache_data_wanted_seqs); } static int mail_cache_transaction_flush(struct mail_cache_transaction_ctx *ctx, bool committing) { struct stat st; uint32_t write_offset = 0; int ret = 0; i_assert(!ctx->cache->locked); if (array_count(&ctx->cache_data_seq) == 0) { /* we had done some changes, but they were aborted. */ i_assert(ctx->last_rec_pos == 0); ctx->min_seq = 0; return 0; } /* If we're going to be committing a transaction, the log must be locked before we lock cache or we can deadlock. */ bool lock_log = !ctx->cache->index->log_sync_locked && !committing && !ctx->have_noncommited_mails; if (lock_log) { uint32_t file_seq; uoff_t file_offset; if (mail_transaction_log_sync_lock(ctx->cache->index->log, "mail cache transaction flush", &file_seq, &file_offset) < 0) return -1; } if (mail_cache_transaction_lock(ctx) <= 0) { if (lock_log) { mail_transaction_log_sync_unlock(ctx->cache->index->log, "mail cache transaction flush: cache lock failed"); } return -1; } i_assert(ctx->cache_data != NULL); i_assert(ctx->last_rec_pos <= ctx->cache_data->used); if (mail_cache_transaction_update_fields(ctx) < 0) { if (lock_log) { mail_transaction_log_sync_unlock(ctx->cache->index->log, "mail cache transaction flush: field update failed"); } mail_cache_unlock(ctx->cache); return -1; } /* we need to get the final write offset for linking records */ if (fstat(ctx->cache->fd, &st) < 0) { if (!ESTALE_FSTAT(errno)) mail_cache_set_syscall_error(ctx->cache, "fstat()"); ret = -1; } else if ((uoff_t)st.st_size + ctx->last_rec_pos > ctx->cache->index->optimization_set.cache.max_size) { mail_cache_set_corrupted(ctx->cache, "Cache file too large"); ret = -1; } else { write_offset = st.st_size; if (mail_cache_link_records(ctx, write_offset) < 0) ret = -1; } /* write to cache file */ if (ret < 0 || mail_cache_append(ctx->cache, ctx->cache_data->data, ctx->last_rec_pos, &write_offset) < 0) ret = -1; else { /* update records' cache offsets to index */ mail_cache_transaction_update_index(ctx, write_offset, committing); } if (mail_cache_flush_and_unlock(ctx->cache) < 0) ret = -1; if (lock_log) { mail_transaction_log_sync_unlock(ctx->cache->index->log, "mail cache transaction flush"); } return ret; } static void mail_cache_transaction_drop_unwanted(struct mail_cache_transaction_ctx *ctx, size_t space_needed) { struct mail_cache_transaction_rec *recs; unsigned int i, count; recs = array_get_modifiable(&ctx->cache_data_seq, &count); /* find out how many records to delete. delete all unwanted sequences, and if that's not enough delete some more. */ for (i = 0; i < count; i++) { if (seq_range_exists(&ctx->cache_data_wanted_seqs, recs[i].seq)) { if (recs[i].cache_data_pos >= space_needed) break; /* we're going to forcibly delete it - remove it also from the array since it's no longer useful there */ seq_range_array_remove(&ctx->cache_data_wanted_seqs, recs[i].seq); } } unsigned int deleted_count = i; size_t deleted_space = i < count ? recs[i].cache_data_pos : ctx->last_rec_pos; for (; i < count; i++) recs[i].cache_data_pos -= deleted_space; ctx->last_rec_pos -= deleted_space; array_delete(&ctx->cache_data_seq, 0, deleted_count); buffer_delete(ctx->cache_data, 0, deleted_space); } static bool mail_cache_transaction_update_last_rec_size(struct mail_cache_transaction_ctx *ctx, size_t *size_r) { struct mail_cache_record *rec; void *data; size_t size; data = buffer_get_modifiable_data(ctx->cache_data, &size); rec = PTR_OFFSET(data, ctx->last_rec_pos); rec->size = size - ctx->last_rec_pos; if (rec->size == sizeof(*rec)) return FALSE; i_assert(rec->size > sizeof(*rec)); *size_r = rec->size; return TRUE; } static void mail_cache_transaction_update_last_rec(struct mail_cache_transaction_ctx *ctx) { struct mail_cache_transaction_rec *trans_rec; size_t size; if (!mail_cache_transaction_update_last_rec_size(ctx, &size) || size > ctx->cache->index->optimization_set.cache.record_max_size) { buffer_set_used_size(ctx->cache_data, ctx->last_rec_pos); return; } if (ctx->min_seq > ctx->prev_seq || ctx->min_seq == 0) ctx->min_seq = ctx->prev_seq; trans_rec = array_append_space(&ctx->cache_data_seq); trans_rec->seq = ctx->prev_seq; trans_rec->cache_data_pos = ctx->last_rec_pos; ctx->last_rec_pos = ctx->cache_data->used; } static void mail_cache_transaction_switch_seq(struct mail_cache_transaction_ctx *ctx) { struct mail_cache_record new_rec; if (ctx->prev_seq != 0) { /* update previously added cache record's size */ mail_cache_transaction_update_last_rec(ctx); } else if (ctx->cache_data == NULL) { ctx->cache_data = buffer_create_dynamic(default_pool, MAIL_CACHE_INIT_WRITE_BUFFER); i_array_init(&ctx->cache_data_seq, 64); i_array_init(&ctx->cache_data_wanted_seqs, 32); i_array_init(&ctx->cache_field_idx_used, 64); } i_zero(&new_rec); buffer_append(ctx->cache_data, &new_rec, sizeof(new_rec)); ctx->prev_seq = 0; ctx->changes = TRUE; } int mail_cache_transaction_commit(struct mail_cache_transaction_ctx **_ctx) { struct mail_cache_transaction_ctx *ctx = *_ctx; int ret = 0; if (ctx->changes) { if (ctx->prev_seq != 0) mail_cache_transaction_update_last_rec(ctx); if (mail_cache_transaction_flush(ctx, TRUE) < 0) ret = -1; else { /* successfully wrote everything */ ctx->records_written = 0; } /* Here would be a good place to do fdatasync() to make sure everything is written before offsets are updated to index. However it slows down I/O needlessly and we're pretty good at catching and fixing cache corruption, so we no longer do it. */ } mail_cache_transaction_rollback(_ctx); return ret; } static int mail_cache_header_fields_write(struct mail_cache *cache, const buffer_t *buffer) { uint32_t offset, hdr_offset; i_assert(cache->locked); offset = 0; if (mail_cache_append(cache, buffer->data, buffer->used, &offset) < 0) return -1; if (cache->index->set.fsync_mode == FSYNC_MODE_ALWAYS) { if (fdatasync(cache->fd) < 0) { mail_cache_set_syscall_error(cache, "fdatasync()"); return -1; } } /* find offset to the previous header's "next_offset" field */ if (mail_cache_header_fields_get_next_offset(cache, &hdr_offset) < 0) return -1; /* update the next_offset offset, so our new header will be found */ offset = mail_index_uint32_to_offset(offset); if (mail_cache_write(cache, &offset, sizeof(offset), hdr_offset) < 0) return -1; if (hdr_offset == offsetof(struct mail_cache_header, field_header_offset)) { /* we're adding the first field. hdr_copy needs to be kept in sync so unlocking won't overwrite it. */ cache->hdr_copy.field_header_offset = hdr_offset; cache->hdr_ro_copy.field_header_offset = hdr_offset; } return 0; } static int mail_cache_header_rewrite_fields(struct mail_cache *cache) { int ret; /* re-read header to make sure we don't lose any fields. */ if (mail_cache_header_fields_read(cache) < 0) return -1; T_BEGIN { buffer_t *buffer; buffer = t_buffer_create(256); mail_cache_header_fields_get(cache, buffer); ret = mail_cache_header_fields_write(cache, buffer); } T_END; if (ret == 0) { /* we wrote all the headers, so there are no pending changes */ cache->field_header_write_pending = FALSE; ret = mail_cache_header_fields_read(cache); } return ret; } static void mail_cache_transaction_refresh_decisions(struct mail_cache_transaction_ctx *ctx) { if (ctx->decisions_refreshed) return; /* Read latest caching decisions from the cache file's header once per transaction. */ if (!ctx->cache->opened) (void)mail_cache_open_and_verify(ctx->cache); else (void)mail_cache_header_fields_read(ctx->cache); ctx->decisions_refreshed = TRUE; } void mail_cache_add(struct mail_cache_transaction_ctx *ctx, uint32_t seq, unsigned int field_idx, const void *data, size_t data_size) { uint32_t data_size32; unsigned int fixed_size; size_t full_size, record_size; i_assert(field_idx < ctx->cache->fields_count); i_assert(data_size < (uint32_t)-1); if (ctx->cache->fields[field_idx].field.decision == (MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED)) return; if (seq >= ctx->trans->first_new_seq) ctx->have_noncommited_mails = TRUE; /* If the cache file exists, make sure the caching decisions have been read. */ mail_cache_transaction_refresh_decisions(ctx); mail_cache_decision_add(ctx->view, seq, field_idx); fixed_size = ctx->cache->fields[field_idx].field.field_size; i_assert(fixed_size == UINT_MAX || fixed_size == data_size); data_size32 = (uint32_t)data_size; full_size = sizeof(field_idx) + ((data_size + 3) & ~3U); if (fixed_size == UINT_MAX) full_size += sizeof(data_size32); if (ctx->prev_seq != seq) { mail_cache_transaction_switch_seq(ctx); ctx->prev_seq = seq; seq_range_array_add(&ctx->cache_data_wanted_seqs, seq); /* remember roughly what we have modified, so cache lookups can look into transactions to see changes. */ if (seq < ctx->view->trans_seq1 || ctx->view->trans_seq1 == 0) ctx->view->trans_seq1 = seq; if (seq > ctx->view->trans_seq2) ctx->view->trans_seq2 = seq; } if (mail_cache_transaction_update_last_rec_size(ctx, &record_size) && record_size + full_size > ctx->cache->index->optimization_set.cache.record_max_size) { /* Adding this field would exceed the cache record's maximum size. If we don't add this, it's possible that other fields could still be added. */ return; } /* Remember that this field has been used within the transaction. Later on we fill mail_cache_field_private.used with it. We can't rely on setting it here, because cache purging may run and clear it. */ uint8_t field_idx_set = 1; array_idx_set(&ctx->cache_field_idx_used, field_idx, &field_idx_set); /* Remember that this value exists for the mail, in case we try to look it up. Note that this gets forgotten whenever changing the mail. */ buffer_write(ctx->view->cached_exists_buf, field_idx, &ctx->view->cached_exists_value, 1); if (ctx->cache_data->used + full_size > MAIL_CACHE_MAX_WRITE_BUFFER && ctx->last_rec_pos > 0) { /* time to flush our buffer. */ if (MAIL_INDEX_IS_IN_MEMORY(ctx->cache->index)) { /* just drop the old data to free up memory */ size_t space_needed = ctx->cache_data->used + full_size - MAIL_CACHE_MAX_WRITE_BUFFER; mail_cache_transaction_drop_unwanted(ctx, space_needed); } else { if (mail_cache_transaction_flush(ctx, FALSE) < 0) { /* If this is a syscall failure, the already flushed changes could still be finished by writing the offsets to .log file. If this is a corruption/lost cache, the offsets will point to a nonexistent file or be ignored. Either way, we don't really need to handle this failure in any special way. */ } /* Regardless of whether the flush succeeded, drop all data that it would have written. This way the flush is attempted only once, but it could still be possible to write new data later. Also don't reset the transaction entirely so that the last partially cached mail can still be accessed from memory. */ mail_cache_transaction_drop_last_flush(ctx); } } buffer_append(ctx->cache_data, &field_idx, sizeof(field_idx)); if (fixed_size == UINT_MAX) { buffer_append(ctx->cache_data, &data_size32, sizeof(data_size32)); } buffer_append(ctx->cache_data, data, data_size); if ((data_size & 3) != 0) buffer_append_zero(ctx->cache_data, 4 - (data_size & 3)); } bool mail_cache_field_want_add(struct mail_cache_transaction_ctx *ctx, uint32_t seq, unsigned int field_idx) { enum mail_cache_decision_type decision; mail_cache_transaction_refresh_decisions(ctx); decision = mail_cache_field_get_decision(ctx->view->cache, field_idx); decision &= ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED); switch (decision) { case MAIL_CACHE_DECISION_NO: return FALSE; case MAIL_CACHE_DECISION_TEMP: /* add it only if it's newer than what we would drop when purging */ if (ctx->first_new_seq == 0) { ctx->first_new_seq = mail_cache_get_first_new_seq(ctx->view->view); } if (seq < ctx->first_new_seq) return FALSE; break; default: break; } return mail_cache_field_exists(ctx->view, seq, field_idx) == 0; } bool mail_cache_field_can_add(struct mail_cache_transaction_ctx *ctx, uint32_t seq, unsigned int field_idx) { enum mail_cache_decision_type decision; mail_cache_transaction_refresh_decisions(ctx); decision = mail_cache_field_get_decision(ctx->view->cache, field_idx); if (decision == (MAIL_CACHE_DECISION_FORCED | MAIL_CACHE_DECISION_NO)) return FALSE; return mail_cache_field_exists(ctx->view, seq, field_idx) == 0; } void mail_cache_close_mail(struct mail_cache_transaction_ctx *ctx, uint32_t seq) { if (array_is_created(&ctx->cache_data_wanted_seqs)) seq_range_array_remove(&ctx->cache_data_wanted_seqs, seq); }