aboutsummaryrefslogtreecommitdiff
path: root/mysql/mysys/mf_keycache.c
diff options
context:
space:
mode:
Diffstat (limited to 'mysql/mysys/mf_keycache.c')
-rw-r--r--mysql/mysys/mf_keycache.c4051
1 files changed, 4051 insertions, 0 deletions
diff --git a/mysql/mysys/mf_keycache.c b/mysql/mysys/mf_keycache.c
new file mode 100644
index 0000000..cb712ed
--- /dev/null
+++ b/mysql/mysys/mf_keycache.c
@@ -0,0 +1,4051 @@
+/* Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; version 2 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
+
+/**
+ @file
+ These functions handle keyblock cacheing for ISAM and MyISAM tables.
+
+ One cache can handle many files.
+ It must contain buffers of the same blocksize.
+ init_key_cache() should be used to init cache handler.
+
+ The free list (free_block_list) is a stack like structure.
+ When a block is freed by free_block(), it is pushed onto the stack.
+ When a new block is required it is first tried to pop one from the stack.
+ If the stack is empty, it is tried to get a never-used block from the pool.
+ If this is empty too, then a block is taken from the LRU ring, flushing it
+ to disk, if neccessary. This is handled in find_key_block().
+ With the new free list, the blocks can have three temperatures:
+ hot, warm and cold (which is free). This is remembered in the block header
+ by the enum BLOCK_TEMPERATURE temperature variable. Remembering the
+ temperature is neccessary to correctly count the number of warm blocks,
+ which is required to decide when blocks are allowed to become hot. Whenever
+ a block is inserted to another (sub-)chain, we take the old and new
+ temperature into account to decide if we got one more or less warm block.
+ blocks_unused is the sum of never used blocks in the pool and of currently
+ free blocks. blocks_used is the number of blocks fetched from the pool and
+ as such gives the maximum number of in-use blocks at any time.
+*/
+
+/*
+ Key Cache Locking
+ =================
+
+ All key cache locking is done with a single mutex per key cache:
+ keycache->cache_lock. This mutex is locked almost all the time
+ when executing code in this file (mf_keycache.c).
+ However it is released for I/O and some copy operations.
+
+ The cache_lock is also released when waiting for some event. Waiting
+ and signalling is done via condition variables. In most cases the
+ thread waits on its thread->suspend condition variable. Every thread
+ has a my_thread_var structure, which contains this variable and a
+ '*next' and '**prev' pointer. These pointers are used to insert the
+ thread into a wait queue.
+
+ A thread can wait for one block and thus be in one wait queue at a
+ time only.
+
+ Before starting to wait on its condition variable with
+ mysql_cond_wait(), the thread enters itself to a specific wait queue
+ with link_into_queue() (double linked with '*next' + '**prev') or
+ wait_on_queue() (single linked with '*next').
+
+ Another thread, when releasing a resource, looks up the waiting thread
+ in the related wait queue. It sends a signal with
+ mysql_cond_signal() to the waiting thread.
+
+ NOTE: Depending on the particular wait situation, either the sending
+ thread removes the waiting thread from the wait queue with
+ unlink_from_queue() or release_whole_queue() respectively, or the waiting
+ thread removes itself.
+
+ There is one exception from this locking scheme when one thread wants
+ to reuse a block for some other address. This works by first marking
+ the block reserved (status= BLOCK_IN_SWITCH) and then waiting for all
+ threads that are reading the block to finish. Each block has a
+ reference to a condition variable (condvar). It holds a reference to
+ the thread->suspend condition variable for the waiting thread (if such
+ a thread exists). When that thread is signaled, the reference is
+ cleared. The number of readers of a block is registered in
+ block->hash_link->requests. See wait_for_readers() / remove_reader()
+ for details. This is similar to the above, but it clearly means that
+ only one thread can wait for a particular block. There is no queue in
+ this case. Strangely enough block->convar is used for waiting for the
+ assigned hash_link only. More precisely it is used to wait for all
+ requests to be unregistered from the assigned hash_link.
+
+ The resize_queue serves two purposes:
+ 1. Threads that want to do a resize wait there if in_resize is set.
+ This is not used in the server. The server refuses a second resize
+ request if one is already active. keycache->in_init is used for the
+ synchronization. See set_var.cc.
+ 2. Threads that want to access blocks during resize wait here during
+ the re-initialization phase.
+ When the resize is done, all threads on the queue are signalled.
+ Hypothetical resizers can compete for resizing, and read/write
+ requests will restart to request blocks from the freshly resized
+ cache. If the cache has been resized too small, it is disabled and
+ 'can_be_used' is false. In this case read/write requests bypass the
+ cache. Since they increment and decrement 'cnt_for_resize_op', the
+ next resizer can wait on the queue 'waiting_for_resize_cnt' until all
+ I/O finished.
+*/
+
+#include "mysys_priv.h"
+#include "mysys_err.h"
+#include <keycache.h>
+#include "my_static.h"
+#include <m_string.h>
+#include <my_bit.h>
+#include <errno.h>
+#include <stdarg.h>
+#include "probes_mysql.h"
+#include "my_thread_local.h"
+
+#define STRUCT_PTR(TYPE, MEMBER, a) \
+ (TYPE *) ((char *) (a) - offsetof(TYPE, MEMBER))
+
+/* types of condition variables */
+#define COND_FOR_REQUESTED 0
+#define COND_FOR_SAVED 1
+#define COND_FOR_READERS 2
+
+typedef mysql_cond_t KEYCACHE_CONDVAR;
+
+/* descriptor of the page in the key cache block buffer */
+struct st_keycache_page
+{
+ int file; /* file to which the page belongs to */
+ my_off_t filepos; /* position of the page in the file */
+};
+typedef struct st_keycache_page KEYCACHE_PAGE;
+
+/* element in the chain of a hash table bucket */
+struct st_hash_link
+{
+ struct st_hash_link *next, **prev; /* to connect links in the same bucket */
+ struct st_block_link *block; /* reference to the block for the page: */
+ File file; /* from such a file */
+ my_off_t diskpos; /* with such an offset */
+ uint requests; /* number of requests for the page */
+};
+
+/* simple states of a block */
+#define BLOCK_ERROR 1 /* an error occured when performing file i/o */
+#define BLOCK_READ 2 /* file block is in the block buffer */
+#define BLOCK_IN_SWITCH 4 /* block is preparing to read new page */
+#define BLOCK_REASSIGNED 8 /* blk does not accept requests for old page */
+#define BLOCK_IN_FLUSH 16 /* block is selected for flush */
+#define BLOCK_CHANGED 32 /* block buffer contains a dirty page */
+#define BLOCK_IN_USE 64 /* block is not free */
+#define BLOCK_IN_EVICTION 128 /* block is selected for eviction */
+#define BLOCK_IN_FLUSHWRITE 256 /* block is in write to file */
+#define BLOCK_FOR_UPDATE 512 /* block is selected for buffer modification */
+
+/* page status, returned by find_key_block */
+#define PAGE_READ 0
+#define PAGE_TO_BE_READ 1
+#define PAGE_WAIT_TO_BE_READ 2
+
+/* block temperature determines in which (sub-)chain the block currently is */
+enum BLOCK_TEMPERATURE { BLOCK_COLD /*free*/ , BLOCK_WARM , BLOCK_HOT };
+
+/* key cache block */
+struct st_block_link
+{
+ struct st_block_link
+ *next_used, **prev_used; /* to connect links in the LRU chain (ring) */
+ struct st_block_link
+ *next_changed, **prev_changed; /* for lists of file dirty/clean blocks */
+ struct st_hash_link *hash_link; /* backward ptr to referring hash_link */
+ KEYCACHE_WQUEUE wqueue[2]; /* queues on waiting requests for new/old pages */
+ uint requests; /* number of requests for the block */
+ uchar *buffer; /* buffer for the block page */
+ uint offset; /* beginning of modified data in the buffer */
+ uint length; /* end of data in the buffer */
+ uint status; /* state of the block */
+ enum BLOCK_TEMPERATURE temperature; /* block temperature: cold, warm, hot */
+ uint hits_left; /* number of hits left until promotion */
+ ulonglong last_hit_time; /* timestamp of the last hit */
+ KEYCACHE_CONDVAR *condvar; /* condition variable for 'no readers' event */
+};
+
+KEY_CACHE dflt_key_cache_var;
+KEY_CACHE *dflt_key_cache= &dflt_key_cache_var;
+
+#define FLUSH_CACHE 2000 /* sort this many blocks at once */
+
+static void change_key_cache_param(KEY_CACHE *keycache,
+ ulonglong division_limit,
+ ulonglong age_threshold);
+static int flush_all_key_blocks(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var);
+
+static void wait_on_queue(KEYCACHE_WQUEUE *wqueue,
+ mysql_mutex_t *mutex,
+ st_keycache_thread_var *thread);
+static void release_whole_queue(KEYCACHE_WQUEUE *wqueue);
+
+static void free_block(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ BLOCK_LINK *block);
+
+#define KEYCACHE_HASH(f, pos) \
+(((ulong) ((pos) / keycache->key_cache_block_size) + \
+ (ulong) (f)) & (keycache->hash_entries-1))
+#define FILE_HASH(f) ((uint) (f) & (CHANGED_BLOCKS_HASH-1))
+
+#define BLOCK_NUMBER(b) \
+ ((uint) (((char*)(b)-(char *) keycache->block_root)/sizeof(BLOCK_LINK)))
+#define HASH_LINK_NUMBER(h) \
+ ((uint) (((char*)(h)-(char *) keycache->hash_link_root)/sizeof(HASH_LINK)))
+
+#if !defined(DBUG_OFF)
+static int fail_block(BLOCK_LINK *block);
+static int fail_hlink(HASH_LINK *hlink);
+static int cache_empty(KEY_CACHE *keycache);
+#endif
+
+static inline uint next_power(uint value)
+{
+ return (uint) my_round_up_to_next_power((uint32) value) << 1;
+}
+
+
+/*
+ Initialize a key cache
+
+ SYNOPSIS
+ init_key_cache()
+ keycache pointer to a key cache data structure
+ key_cache_block_size size of blocks to keep cached data
+ use_mem total memory to use for the key cache
+ division_limit division limit (may be zero)
+ age_threshold age threshold (may be zero)
+
+ RETURN VALUE
+ number of blocks in the key cache, if successful,
+ 0 - otherwise.
+
+ NOTES.
+ if keycache->key_cache_inited != 0 we assume that the key cache
+ is already initialized. This is for now used by myisamchk, but shouldn't
+ be something that a program should rely on!
+
+ It's assumed that no two threads call this function simultaneously
+ referring to the same key cache handle.
+
+*/
+
+int init_key_cache(KEY_CACHE *keycache, ulonglong key_cache_block_size,
+ size_t use_mem, ulonglong division_limit,
+ ulonglong age_threshold)
+{
+ ulong blocks, hash_links;
+ size_t length;
+ int error;
+ DBUG_ENTER("init_key_cache");
+ DBUG_ASSERT(key_cache_block_size >= 512);
+
+ if (keycache->key_cache_inited && keycache->disk_blocks > 0)
+ {
+ DBUG_PRINT("warning",("key cache already in use"));
+ DBUG_RETURN(0);
+ }
+
+ keycache->global_cache_w_requests= keycache->global_cache_r_requests= 0;
+ keycache->global_cache_read= keycache->global_cache_write= 0;
+ keycache->disk_blocks= -1;
+ if (! keycache->key_cache_inited)
+ {
+ keycache->key_cache_inited= 1;
+ /*
+ Initialize these variables once only.
+ Their value must survive re-initialization during resizing.
+ */
+ keycache->in_resize= 0;
+ keycache->resize_in_flush= 0;
+ keycache->cnt_for_resize_op= 0;
+ keycache->waiting_for_resize_cnt.last_thread= NULL;
+ keycache->in_init= 0;
+ mysql_mutex_init(key_KEY_CACHE_cache_lock,
+ &keycache->cache_lock, MY_MUTEX_INIT_FAST);
+ keycache->resize_queue.last_thread= NULL;
+ }
+
+ keycache->key_cache_mem_size= use_mem;
+ keycache->key_cache_block_size= (uint)key_cache_block_size;
+ DBUG_PRINT("info", ("key_cache_block_size: %llu",
+ key_cache_block_size));
+
+ blocks= (ulong) (use_mem / (sizeof(BLOCK_LINK) + 2 * sizeof(HASH_LINK) +
+ sizeof(HASH_LINK*) * 5/4 + key_cache_block_size));
+ /* It doesn't make sense to have too few blocks (less than 8) */
+ if (blocks >= 8)
+ {
+ for ( ; ; )
+ {
+ /* Set my_hash_entries to the next bigger 2 power */
+ if ((keycache->hash_entries= next_power(blocks)) < blocks * 5/4)
+ keycache->hash_entries<<= 1;
+ hash_links= 2 * blocks;
+ while ((length= (ALIGN_SIZE(blocks * sizeof(BLOCK_LINK)) +
+ ALIGN_SIZE(hash_links * sizeof(HASH_LINK)) +
+ ALIGN_SIZE(sizeof(HASH_LINK*) *
+ keycache->hash_entries))) +
+ ((size_t) blocks * keycache->key_cache_block_size) > use_mem)
+ blocks--;
+ /* Allocate memory for cache page buffers */
+ if ((keycache->block_mem=
+ my_large_malloc(key_memory_KEY_CACHE,
+ (size_t) blocks * keycache->key_cache_block_size,
+ MYF(0))))
+ {
+ /*
+ Allocate memory for blocks, hash_links and hash entries;
+ For each block 2 hash links are allocated
+ */
+ if ((keycache->block_root= (BLOCK_LINK*) my_malloc(key_memory_KEY_CACHE,
+ length,
+ MYF(0))))
+ break;
+ my_large_free(keycache->block_mem);
+ keycache->block_mem= 0;
+ }
+ if (blocks < 8)
+ {
+ set_my_errno(ENOMEM);
+ my_error(EE_OUTOFMEMORY, MYF(ME_FATALERROR),
+ blocks * keycache->key_cache_block_size);
+ goto err;
+ }
+ blocks= blocks / 4*3;
+ }
+ keycache->blocks_unused= blocks;
+ keycache->disk_blocks= (int) blocks;
+ keycache->hash_links= hash_links;
+ keycache->hash_root= (HASH_LINK**) ((char*) keycache->block_root +
+ ALIGN_SIZE(blocks*sizeof(BLOCK_LINK)));
+ keycache->hash_link_root= (HASH_LINK*) ((char*) keycache->hash_root +
+ ALIGN_SIZE((sizeof(HASH_LINK*) *
+ keycache->hash_entries)));
+ memset(keycache->block_root, 0,
+ keycache->disk_blocks * sizeof(BLOCK_LINK));
+ memset(keycache->hash_root, 0,
+ keycache->hash_entries * sizeof(HASH_LINK*));
+ memset(keycache->hash_link_root, 0,
+ keycache->hash_links * sizeof(HASH_LINK));
+ keycache->hash_links_used= 0;
+ keycache->free_hash_list= NULL;
+ keycache->blocks_used= keycache->blocks_changed= 0;
+
+ keycache->global_blocks_changed= 0;
+ keycache->blocks_available=0; /* For debugging */
+
+ /* The LRU chain is empty after initialization */
+ keycache->used_last= NULL;
+ keycache->used_ins= NULL;
+ keycache->free_block_list= NULL;
+ keycache->keycache_time= 0;
+ keycache->warm_blocks= 0;
+ keycache->min_warm_blocks= (division_limit ?
+ blocks * division_limit / 100 + 1 :
+ blocks);
+ keycache->age_threshold= (age_threshold ?
+ blocks * age_threshold / 100 :
+ blocks);
+
+ keycache->can_be_used= 1;
+
+ keycache->waiting_for_hash_link.last_thread= NULL;
+ keycache->waiting_for_block.last_thread= NULL;
+ DBUG_PRINT("exit",
+ ("disk_blocks: %d block_root: 0x%lx hash_entries: %d\
+ hash_root: 0x%lx hash_links: %d hash_link_root: 0x%lx",
+ keycache->disk_blocks, (long) keycache->block_root,
+ keycache->hash_entries, (long) keycache->hash_root,
+ keycache->hash_links, (long) keycache->hash_link_root));
+ memset(keycache->changed_blocks, 0,
+ sizeof(keycache->changed_blocks[0]) * CHANGED_BLOCKS_HASH);
+ memset(keycache->file_blocks, 0,
+ sizeof(keycache->file_blocks[0]) * CHANGED_BLOCKS_HASH);
+ }
+ else
+ {
+ /* key_buffer_size is specified too small. Disable the cache. */
+ keycache->can_be_used= 0;
+ }
+
+ keycache->blocks= keycache->disk_blocks > 0 ? keycache->disk_blocks : 0;
+ DBUG_RETURN((int) keycache->disk_blocks);
+
+err:
+ error= my_errno();
+ keycache->disk_blocks= 0;
+ keycache->blocks= 0;
+ if (keycache->block_mem)
+ {
+ my_large_free((uchar*) keycache->block_mem);
+ keycache->block_mem= NULL;
+ }
+ if (keycache->block_root)
+ {
+ my_free(keycache->block_root);
+ keycache->block_root= NULL;
+ }
+ set_my_errno(error);
+ keycache->can_be_used= 0;
+ DBUG_RETURN(0);
+}
+
+
+/*
+ Resize a key cache
+
+ SYNOPSIS
+ resize_key_cache()
+ keycache pointer to a key cache data structure
+ thread_var pointer to thread specific variables
+ key_cache_block_size size of blocks to keep cached data
+ use_mem total memory to use for the new key cache
+ division_limit new division limit (if not zero)
+ age_threshold new age threshold (if not zero)
+
+ RETURN VALUE
+ number of blocks in the key cache, if successful,
+ 0 - otherwise.
+
+ NOTES.
+ The function first compares the memory size and the block size parameters
+ with the key cache values.
+
+ If they differ the function free the the memory allocated for the
+ old key cache blocks by calling the end_key_cache function and
+ then rebuilds the key cache with new blocks by calling
+ init_key_cache.
+
+ The function starts the operation only when all other threads
+ performing operations with the key cache let her to proceed
+ (when cnt_for_resize=0).
+*/
+
+int resize_key_cache(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ ulonglong key_cache_block_size,
+ size_t use_mem, ulonglong division_limit,
+ ulonglong age_threshold)
+{
+ int blocks;
+ DBUG_ENTER("resize_key_cache");
+
+ if (!keycache->key_cache_inited)
+ DBUG_RETURN(keycache->disk_blocks);
+
+ if(key_cache_block_size == keycache->key_cache_block_size &&
+ use_mem == keycache->key_cache_mem_size)
+ {
+ change_key_cache_param(keycache, division_limit, age_threshold);
+ DBUG_RETURN(keycache->disk_blocks);
+ }
+
+ mysql_mutex_lock(&keycache->cache_lock);
+
+ /*
+ We may need to wait for another thread which is doing a resize
+ already. This cannot happen in the MySQL server though. It allows
+ one resizer only. In set_var.cc keycache->in_init is used to block
+ multiple attempts.
+ */
+ while (keycache->in_resize)
+ {
+ /* purecov: begin inspected */
+ wait_on_queue(&keycache->resize_queue, &keycache->cache_lock,
+ thread_var);
+ /* purecov: end */
+ }
+
+ /*
+ Mark the operation in progress. This blocks other threads from doing
+ a resize in parallel. It prohibits new blocks to enter the cache.
+ Read/write requests can bypass the cache during the flush phase.
+ */
+ keycache->in_resize= 1;
+
+ /* Need to flush only if keycache is enabled. */
+ if (keycache->can_be_used)
+ {
+ /* Start the flush phase. */
+ keycache->resize_in_flush= 1;
+
+ if (flush_all_key_blocks(keycache, thread_var))
+ {
+ /* TODO: if this happens, we should write a warning in the log file ! */
+ keycache->resize_in_flush= 0;
+ blocks= 0;
+ keycache->can_be_used= 0;
+ goto finish;
+ }
+ DBUG_ASSERT(cache_empty(keycache));
+
+ /* End the flush phase. */
+ keycache->resize_in_flush= 0;
+ }
+
+ /*
+ Some direct read/write operations (bypassing the cache) may still be
+ unfinished. Wait until they are done. If the key cache can be used,
+ direct I/O is done in increments of key_cache_block_size. That is,
+ every block is checked if it is in the cache. We need to wait for
+ pending I/O before re-initializing the cache, because we may change
+ the block size. Otherwise they could check for blocks at file
+ positions where the new block division has none. We do also want to
+ wait for I/O done when (if) the cache was disabled. It must not
+ run in parallel with normal cache operation.
+ */
+ while (keycache->cnt_for_resize_op)
+ wait_on_queue(&keycache->waiting_for_resize_cnt, &keycache->cache_lock,
+ thread_var);
+
+ /*
+ Free old cache structures, allocate new structures, and initialize
+ them. Note that the cache_lock mutex and the resize_queue are left
+ untouched. We do not lose the cache_lock and will release it only at
+ the end of this function.
+ */
+ end_key_cache(keycache, 0); /* Don't free mutex */
+ /* The following will work even if use_mem is 0 */
+ blocks= init_key_cache(keycache, key_cache_block_size, use_mem,
+ division_limit, age_threshold);
+
+finish:
+ /*
+ Mark the resize finished. This allows other threads to start a
+ resize or to request new cache blocks.
+ */
+ keycache->in_resize= 0;
+
+ /* Signal waiting threads. */
+ release_whole_queue(&keycache->resize_queue);
+
+ mysql_mutex_unlock(&keycache->cache_lock);
+ DBUG_RETURN(blocks);
+}
+
+
+/*
+ Increment counter blocking resize key cache operation
+*/
+static inline void inc_counter_for_resize_op(KEY_CACHE *keycache)
+{
+ keycache->cnt_for_resize_op++;
+}
+
+
+/*
+ Decrement counter blocking resize key cache operation;
+ Signal the operation to proceed when counter becomes equal zero
+*/
+static inline void dec_counter_for_resize_op(KEY_CACHE *keycache)
+{
+ if (!--keycache->cnt_for_resize_op)
+ release_whole_queue(&keycache->waiting_for_resize_cnt);
+}
+
+/*
+ Change the key cache parameters
+
+ SYNOPSIS
+ change_key_cache_param()
+ keycache pointer to a key cache data structure
+ division_limit new division limit (if not zero)
+ age_threshold new age threshold (if not zero)
+
+ RETURN VALUE
+ none
+
+ NOTES.
+ Presently the function resets the key cache parameters
+ concerning midpoint insertion strategy - division_limit and
+ age_threshold.
+*/
+
+static void change_key_cache_param(KEY_CACHE *keycache,
+ ulonglong division_limit,
+ ulonglong age_threshold)
+{
+ DBUG_ENTER("change_key_cache_param");
+
+ mysql_mutex_lock(&keycache->cache_lock);
+ if (division_limit)
+ keycache->min_warm_blocks= (keycache->disk_blocks *
+ division_limit / 100 + 1);
+ if (age_threshold)
+ keycache->age_threshold= (keycache->disk_blocks *
+ age_threshold / 100);
+ mysql_mutex_unlock(&keycache->cache_lock);
+ DBUG_VOID_RETURN;
+}
+
+
+/*
+ Remove key_cache from memory
+
+ SYNOPSIS
+ end_key_cache()
+ keycache key cache handle
+ cleanup Complete free (Free also mutex for key cache)
+
+ RETURN VALUE
+ none
+*/
+
+void end_key_cache(KEY_CACHE *keycache, my_bool cleanup)
+{
+ DBUG_ENTER("end_key_cache");
+ DBUG_PRINT("enter", ("key_cache: 0x%lx", (long) keycache));
+
+ if (!keycache->key_cache_inited)
+ DBUG_VOID_RETURN;
+
+ if (keycache->disk_blocks > 0)
+ {
+ if (keycache->block_mem)
+ {
+ my_large_free((uchar*) keycache->block_mem);
+ keycache->block_mem= NULL;
+ my_free(keycache->block_root);
+ keycache->block_root= NULL;
+ }
+ keycache->disk_blocks= -1;
+ /* Reset blocks_changed to be safe if flush_all_key_blocks is called */
+ keycache->blocks_changed= 0;
+ }
+
+ DBUG_PRINT("status", ("used: %lu changed: %lu w_requests: %lu "
+ "writes: %lu r_requests: %lu reads: %lu",
+ keycache->blocks_used, keycache->global_blocks_changed,
+ (ulong) keycache->global_cache_w_requests,
+ (ulong) keycache->global_cache_write,
+ (ulong) keycache->global_cache_r_requests,
+ (ulong) keycache->global_cache_read));
+
+ /*
+ Reset these values to be able to detect a disabled key cache.
+ See Bug#44068 (RESTORE can disable the MyISAM Key Cache).
+ */
+ keycache->blocks_used= 0;
+ keycache->blocks_unused= 0;
+
+ if (cleanup)
+ {
+ mysql_mutex_destroy(&keycache->cache_lock);
+ keycache->key_cache_inited= keycache->can_be_used= 0;
+ }
+ DBUG_VOID_RETURN;
+} /* end_key_cache */
+
+
+/**
+ Link a thread into double-linked queue of waiting threads.
+
+ @param wqueue pointer to the queue structure
+ @param thread pointer to the keycache variables for the
+ thread to be added to the queue
+
+ Queue is represented by a circular list of the keycache variable structures.
+ Since each thread has its own keycache variables, this is equal to a list
+ of threads. The list is double-linked of the type (**prev,*next), accessed by
+ a pointer to the last element.
+*/
+
+static void link_into_queue(KEYCACHE_WQUEUE *wqueue,
+ st_keycache_thread_var *thread)
+{
+ st_keycache_thread_var *last;
+
+ DBUG_ASSERT(!thread->next && !thread->prev);
+ if (! (last= wqueue->last_thread))
+ {
+ /* Queue is empty */
+ thread->next= thread;
+ thread->prev= &thread->next;
+ }
+ else
+ {
+ thread->prev= last->next->prev;
+ last->next->prev= &thread->next;
+ thread->next= last->next;
+ last->next= thread;
+ }
+ wqueue->last_thread= thread;
+}
+
+
+/**
+ Unlink a thread from double-linked queue of waiting threads
+
+ @param wqueue pointer to the queue structure
+ @param thread pointer to the keycache variables for the
+ thread to be removed to the queue
+
+ @note See link_into_queue
+*/
+
+static void unlink_from_queue(KEYCACHE_WQUEUE *wqueue,
+ st_keycache_thread_var *thread)
+{
+ DBUG_ASSERT(thread->next && thread->prev);
+ if (thread->next == thread)
+ /* The queue contains only one member */
+ wqueue->last_thread= NULL;
+ else
+ {
+ thread->next->prev= thread->prev;
+ *thread->prev=thread->next;
+ if (wqueue->last_thread == thread)
+ wqueue->last_thread= STRUCT_PTR(st_keycache_thread_var, next,
+ thread->prev);
+ }
+ thread->next= NULL;
+#if !defined(DBUG_OFF)
+ /*
+ This makes it easier to see it's not in a chain during debugging.
+ And some DBUG_ASSERT() rely on it.
+ */
+ thread->prev= NULL;
+#endif
+}
+
+
+/*
+ Add a thread to single-linked queue of waiting threads
+
+ SYNOPSIS
+ wait_on_queue()
+ wqueue Pointer to the queue structure.
+ mutex Cache_lock to acquire after awake.
+ thread Thread to be added
+
+ RETURN VALUE
+ none
+
+ NOTES.
+ Queue is represented by a circular list of the thread structures
+ The list is single-linked of the type (*next), accessed by a pointer
+ to the last element.
+
+ The function protects against stray signals by verifying that the
+ current thread is unlinked from the queue when awaking. However,
+ since several threads can wait for the same event, it might be
+ necessary for the caller of the function to check again if the
+ condition for awake is indeed matched.
+*/
+
+static void wait_on_queue(KEYCACHE_WQUEUE *wqueue,
+ mysql_mutex_t *mutex,
+ st_keycache_thread_var *thread)
+{
+ st_keycache_thread_var *last;
+
+ /* Add to queue. */
+ DBUG_ASSERT(!thread->next);
+ DBUG_ASSERT(!thread->prev); /* Not required, but must be true anyway. */
+ if (! (last= wqueue->last_thread))
+ thread->next= thread;
+ else
+ {
+ thread->next= last->next;
+ last->next= thread;
+ }
+ wqueue->last_thread= thread;
+
+ /*
+ Wait until thread is removed from queue by the signalling thread.
+ The loop protects against stray signals.
+ */
+ do
+ {
+ mysql_cond_wait(&thread->suspend, mutex);
+ }
+ while (thread->next);
+}
+
+
+/*
+ Remove all threads from queue signaling them to proceed
+
+ SYNOPSIS
+ release_whole_queue()
+ wqueue pointer to the queue structure
+
+ RETURN VALUE
+ none
+
+ NOTES.
+ See notes for wait_on_queue().
+ When removed from the queue each thread is signaled via condition
+ variable thread->suspend.
+*/
+
+static void release_whole_queue(KEYCACHE_WQUEUE *wqueue)
+{
+ st_keycache_thread_var *last;
+ st_keycache_thread_var *next;
+ st_keycache_thread_var *thread;
+
+ /* Queue may be empty. */
+ if (!(last= wqueue->last_thread))
+ return;
+
+ next= last->next;
+ do
+ {
+ thread=next;
+ /* Signal the thread. */
+ mysql_cond_signal(&thread->suspend);
+ /* Take thread from queue. */
+ next=thread->next;
+ thread->next= NULL;
+ }
+ while (thread != last);
+
+ /* Now queue is definitely empty. */
+ wqueue->last_thread= NULL;
+}
+
+
+/*
+ Unlink a block from the chain of dirty/clean blocks
+*/
+
+static inline void unlink_changed(BLOCK_LINK *block)
+{
+ DBUG_ASSERT(block->prev_changed && *block->prev_changed == block);
+ if (block->next_changed)
+ block->next_changed->prev_changed= block->prev_changed;
+ *block->prev_changed= block->next_changed;
+
+#if !defined(DBUG_OFF)
+ /*
+ This makes it easier to see it's not in a chain during debugging.
+ And some DBUG_ASSERT() rely on it.
+ */
+ block->next_changed= NULL;
+ block->prev_changed= NULL;
+#endif
+}
+
+
+/*
+ Link a block into the chain of dirty/clean blocks
+*/
+
+static inline void link_changed(BLOCK_LINK *block, BLOCK_LINK **phead)
+{
+ DBUG_ASSERT(!block->next_changed);
+ DBUG_ASSERT(!block->prev_changed);
+ block->prev_changed= phead;
+ if ((block->next_changed= *phead))
+ (*phead)->prev_changed= &block->next_changed;
+ *phead= block;
+}
+
+
+/*
+ Link a block in a chain of clean blocks of a file.
+
+ SYNOPSIS
+ link_to_file_list()
+ keycache Key cache handle
+ block Block to relink
+ file File to be linked to
+ unlink If to unlink first
+
+ DESCRIPTION
+ Unlink a block from whichever chain it is linked in, if it's
+ asked for, and link it to the chain of clean blocks of the
+ specified file.
+
+ NOTE
+ Please do never set/clear BLOCK_CHANGED outside of
+ link_to_file_list() or link_to_changed_list().
+ You would risk to damage correct counting of changed blocks
+ and to find blocks in the wrong hash.
+
+ RETURN
+ void
+*/
+
+static void link_to_file_list(KEY_CACHE *keycache,
+ BLOCK_LINK *block, int file,
+ my_bool unlink_block)
+{
+ DBUG_ASSERT(block->status & BLOCK_IN_USE);
+ DBUG_ASSERT(block->hash_link && block->hash_link->block == block);
+ DBUG_ASSERT(block->hash_link->file == file);
+ if (unlink_block)
+ unlink_changed(block);
+ link_changed(block, &keycache->file_blocks[FILE_HASH(file)]);
+ if (block->status & BLOCK_CHANGED)
+ {
+ block->status&= ~BLOCK_CHANGED;
+ keycache->blocks_changed--;
+ keycache->global_blocks_changed--;
+ }
+}
+
+
+/*
+ Re-link a block from the clean chain to the dirty chain of a file.
+
+ SYNOPSIS
+ link_to_changed_list()
+ keycache key cache handle
+ block block to relink
+
+ DESCRIPTION
+ Unlink a block from the chain of clean blocks of a file
+ and link it to the chain of dirty blocks of the same file.
+
+ NOTE
+ Please do never set/clear BLOCK_CHANGED outside of
+ link_to_file_list() or link_to_changed_list().
+ You would risk to damage correct counting of changed blocks
+ and to find blocks in the wrong hash.
+
+ RETURN
+ void
+*/
+
+static void link_to_changed_list(KEY_CACHE *keycache,
+ BLOCK_LINK *block)
+{
+ DBUG_ASSERT(block->status & BLOCK_IN_USE);
+ DBUG_ASSERT(!(block->status & BLOCK_CHANGED));
+ DBUG_ASSERT(block->hash_link && block->hash_link->block == block);
+
+ unlink_changed(block);
+ link_changed(block,
+ &keycache->changed_blocks[FILE_HASH(block->hash_link->file)]);
+ block->status|=BLOCK_CHANGED;
+ keycache->blocks_changed++;
+ keycache->global_blocks_changed++;
+}
+
+
+/*
+ Link a block to the LRU chain at the beginning or at the end of
+ one of two parts.
+
+ SYNOPSIS
+ link_block()
+ keycache pointer to a key cache data structure
+ block pointer to the block to link to the LRU chain
+ hot <-> to link the block into the hot subchain
+ at_end <-> to link the block at the end of the subchain
+
+ RETURN VALUE
+ none
+
+ NOTES.
+ The LRU ring is represented by a circular list of block structures.
+ The list is double-linked of the type (**prev,*next) type.
+ The LRU ring is divided into two parts - hot and warm.
+ There are two pointers to access the last blocks of these two
+ parts. The beginning of the warm part follows right after the
+ end of the hot part.
+ Only blocks of the warm part can be used for eviction.
+ The first block from the beginning of this subchain is always
+ taken for eviction (keycache->last_used->next)
+
+ LRU chain: +------+ H O T +------+
+ +----| end |----...<----| beg |----+
+ | +------+last +------+ |
+ v<-link in latest hot (new end) |
+ | link in latest warm (new end)->^
+ | +------+ W A R M +------+ |
+ +----| beg |---->...----| end |----+
+ +------+ +------+ins
+ first for eviction
+
+ It is also possible that the block is selected for eviction and thus
+ not linked in the LRU ring.
+*/
+
+static void link_block(KEY_CACHE *keycache, BLOCK_LINK *block, my_bool hot,
+ my_bool at_end)
+{
+ BLOCK_LINK *ins;
+ BLOCK_LINK **pins;
+
+ DBUG_ASSERT((block->status & ~BLOCK_CHANGED) == (BLOCK_READ | BLOCK_IN_USE));
+ DBUG_ASSERT(block->hash_link); /*backptr to block NULL from free_block()*/
+ DBUG_ASSERT(!block->requests);
+ DBUG_ASSERT(block->prev_changed && *block->prev_changed == block);
+ DBUG_ASSERT(!block->next_used);
+ DBUG_ASSERT(!block->prev_used);
+
+ if (!hot && keycache->waiting_for_block.last_thread)
+ {
+ /* Signal that in the LRU warm sub-chain an available block has appeared */
+ st_keycache_thread_var *last_thread=
+ keycache->waiting_for_block.last_thread;
+ st_keycache_thread_var *first_thread= last_thread->next;
+ st_keycache_thread_var *next_thread= first_thread;
+ HASH_LINK *hash_link= (HASH_LINK *) first_thread->opt_info;
+ st_keycache_thread_var *thread;
+ do
+ {
+ thread= next_thread;
+ next_thread= thread->next;
+ /*
+ We notify about the event all threads that ask
+ for the same page as the first thread in the queue
+ */
+ if ((HASH_LINK *) thread->opt_info == hash_link)
+ {
+ mysql_cond_signal(&thread->suspend);
+ unlink_from_queue(&keycache->waiting_for_block, thread);
+ block->requests++;
+ }
+ }
+ while (thread != last_thread);
+ hash_link->block= block;
+ /*
+ NOTE: We assigned the block to the hash_link and signalled the
+ requesting thread(s). But it is possible that other threads runs
+ first. These threads see the hash_link assigned to a block which
+ is assigned to another hash_link and not marked BLOCK_IN_SWITCH.
+ This can be a problem for functions that do not select the block
+ via its hash_link: flush and free. They do only see a block which
+ is in a "normal" state and don't know that it will be evicted soon.
+
+ We cannot set BLOCK_IN_SWITCH here because only one of the
+ requesting threads must handle the eviction. All others must wait
+ for it to complete. If we set the flag here, the threads would not
+ know who is in charge of the eviction. Without the flag, the first
+ thread takes the stick and sets the flag.
+
+ But we need to note in the block that is has been selected for
+ eviction. It must not be freed. The evicting thread will not
+ expect the block in the free list. Before freeing we could also
+ check if block->requests > 1. But I think including another flag
+ in the check of block->status is slightly more efficient and
+ probably easier to read.
+ */
+ block->status|= BLOCK_IN_EVICTION;
+ return;
+ }
+
+ pins= hot ? &keycache->used_ins : &keycache->used_last;
+ ins= *pins;
+ if (ins)
+ {
+ ins->next_used->prev_used= &block->next_used;
+ block->next_used= ins->next_used;
+ block->prev_used= &ins->next_used;
+ ins->next_used= block;
+ if (at_end)
+ *pins= block;
+ }
+ else
+ {
+ /* The LRU ring is empty. Let the block point to itself. */
+ keycache->used_last= keycache->used_ins= block->next_used= block;
+ block->prev_used= &block->next_used;
+ }
+ DBUG_ASSERT((ulong) keycache->blocks_available <=
+ keycache->blocks_used);
+}
+
+
+/*
+ Unlink a block from the LRU chain
+
+ SYNOPSIS
+ unlink_block()
+ keycache pointer to a key cache data structure
+ block pointer to the block to unlink from the LRU chain
+
+ RETURN VALUE
+ none
+
+ NOTES.
+ See NOTES for link_block
+*/
+
+static void unlink_block(KEY_CACHE *keycache, BLOCK_LINK *block)
+{
+ DBUG_ASSERT((block->status & ~BLOCK_CHANGED) == (BLOCK_READ | BLOCK_IN_USE));
+ DBUG_ASSERT(block->hash_link); /*backptr to block NULL from free_block()*/
+ DBUG_ASSERT(!block->requests);
+ DBUG_ASSERT(block->prev_changed && *block->prev_changed == block);
+ DBUG_ASSERT(block->next_used && block->prev_used &&
+ (block->next_used->prev_used == &block->next_used) &&
+ (*block->prev_used == block));
+ if (block->next_used == block)
+ /* The list contains only one member */
+ keycache->used_last= keycache->used_ins= NULL;
+ else
+ {
+ block->next_used->prev_used= block->prev_used;
+ *block->prev_used= block->next_used;
+ if (keycache->used_last == block)
+ keycache->used_last= STRUCT_PTR(BLOCK_LINK, next_used, block->prev_used);
+ if (keycache->used_ins == block)
+ keycache->used_ins=STRUCT_PTR(BLOCK_LINK, next_used, block->prev_used);
+ }
+ block->next_used= NULL;
+#if !defined(DBUG_OFF)
+ /*
+ This makes it easier to see it's not in a chain during debugging.
+ And some DBUG_ASSERT() rely on it.
+ */
+ block->prev_used= NULL;
+#endif
+}
+
+
+/*
+ Register requests for a block.
+
+ SYNOPSIS
+ reg_requests()
+ keycache Pointer to a key cache data structure.
+ block Pointer to the block to register a request on.
+ count Number of requests. Always 1.
+
+ NOTE
+ The first request unlinks the block from the LRU ring. This means
+ that it is protected against eveiction.
+
+ RETURN
+ void
+*/
+static void reg_requests(KEY_CACHE *keycache, BLOCK_LINK *block, int count)
+{
+ DBUG_ASSERT(block->status & BLOCK_IN_USE);
+ DBUG_ASSERT(block->hash_link);
+
+ if (!block->requests)
+ unlink_block(keycache, block);
+ block->requests+=count;
+}
+
+
+/*
+ Unregister request for a block
+ linking it to the LRU chain if it's the last request
+
+ SYNOPSIS
+ unreg_request()
+ keycache pointer to a key cache data structure
+ block pointer to the block to link to the LRU chain
+ at_end <-> to link the block at the end of the LRU chain
+
+ RETURN VALUE
+ none
+
+ NOTES.
+ Every linking to the LRU ring decrements by one a special block
+ counter (if it's positive). If the at_end parameter is TRUE the block is
+ added either at the end of warm sub-chain or at the end of hot sub-chain.
+ It is added to the hot subchain if its counter is zero and number of
+ blocks in warm sub-chain is not less than some low limit (determined by
+ the division_limit parameter). Otherwise the block is added to the warm
+ sub-chain. If the at_end parameter is FALSE the block is always added
+ at beginning of the warm sub-chain.
+ Thus a warm block can be promoted to the hot sub-chain when its counter
+ becomes zero for the first time.
+ At the same time the block at the very beginning of the hot subchain
+ might be moved to the beginning of the warm subchain if it stays untouched
+ for a too long time (this time is determined by parameter age_threshold).
+
+ It is also possible that the block is selected for eviction and thus
+ not linked in the LRU ring.
+*/
+
+static void unreg_request(KEY_CACHE *keycache,
+ BLOCK_LINK *block, int at_end)
+{
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ DBUG_ASSERT(block->hash_link); /*backptr to block NULL from free_block()*/
+ DBUG_ASSERT(block->requests);
+ DBUG_ASSERT(block->prev_changed && *block->prev_changed == block);
+ DBUG_ASSERT(!block->next_used);
+ DBUG_ASSERT(!block->prev_used);
+ /*
+ Unregister the request, but do not link erroneous blocks into the
+ LRU ring.
+ */
+ if (!--block->requests && !(block->status & BLOCK_ERROR))
+ {
+ my_bool hot;
+ if (block->hits_left)
+ block->hits_left--;
+ hot= !block->hits_left && at_end &&
+ keycache->warm_blocks > keycache->min_warm_blocks;
+ if (hot)
+ {
+ if (block->temperature == BLOCK_WARM)
+ keycache->warm_blocks--;
+ block->temperature= BLOCK_HOT;
+ }
+ link_block(keycache, block, hot, (my_bool)at_end);
+ block->last_hit_time= keycache->keycache_time;
+ keycache->keycache_time++;
+ /*
+ At this place, the block might be in the LRU ring or not. If an
+ evicter was waiting for a block, it was selected for eviction and
+ not linked in the LRU ring.
+ */
+
+ /*
+ Check if we should link a hot block to the warm block sub-chain.
+ It is possible that we select the same block as above. But it can
+ also be another block. In any case a block from the LRU ring is
+ selected. In other words it works even if the above block was
+ selected for eviction and not linked in the LRU ring. Since this
+ happens only if the LRU ring is empty, the block selected below
+ would be NULL and the rest of the function skipped.
+ */
+ block= keycache->used_ins;
+ if (block && keycache->keycache_time - block->last_hit_time >
+ keycache->age_threshold)
+ {
+ unlink_block(keycache, block);
+ link_block(keycache, block, 0, 0);
+ if (block->temperature != BLOCK_WARM)
+ {
+ keycache->warm_blocks++;
+ block->temperature= BLOCK_WARM;
+ }
+ }
+ }
+}
+
+/*
+ Remove a reader of the page in block
+*/
+
+static void remove_reader(BLOCK_LINK *block)
+{
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ DBUG_ASSERT(block->hash_link && block->hash_link->block == block);
+ DBUG_ASSERT(block->prev_changed && *block->prev_changed == block);
+ DBUG_ASSERT(!block->next_used);
+ DBUG_ASSERT(!block->prev_used);
+ DBUG_ASSERT(block->hash_link->requests);
+
+ if (! --block->hash_link->requests && block->condvar)
+ mysql_cond_signal(block->condvar);
+}
+
+
+/*
+ Wait until the last reader of the page in block
+ signals on its termination
+*/
+
+static void wait_for_readers(KEY_CACHE *keycache,
+ BLOCK_LINK *block,
+ st_keycache_thread_var *thread)
+{
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ DBUG_ASSERT(!(block->status & (BLOCK_IN_FLUSH | BLOCK_CHANGED)));
+ DBUG_ASSERT(block->hash_link);
+ DBUG_ASSERT(block->hash_link->block == block);
+ /* Linked in file_blocks or changed_blocks hash. */
+ DBUG_ASSERT(block->prev_changed && *block->prev_changed == block);
+ /* Not linked in LRU ring. */
+ DBUG_ASSERT(!block->next_used);
+ DBUG_ASSERT(!block->prev_used);
+ while (block->hash_link->requests)
+ {
+ /* There must be no other waiter. We have no queue here. */
+ DBUG_ASSERT(!block->condvar);
+ block->condvar= &thread->suspend;
+ mysql_cond_wait(&thread->suspend, &keycache->cache_lock);
+ block->condvar= NULL;
+ }
+}
+
+
+/*
+ Add a hash link to a bucket in the hash_table
+*/
+
+static inline void link_hash(HASH_LINK **start, HASH_LINK *hash_link)
+{
+ if (*start)
+ (*start)->prev= &hash_link->next;
+ hash_link->next= *start;
+ hash_link->prev= start;
+ *start= hash_link;
+}
+
+
+/*
+ Remove a hash link from the hash table
+*/
+
+static void unlink_hash(KEY_CACHE *keycache, HASH_LINK *hash_link)
+{
+ DBUG_ASSERT(hash_link->requests == 0);
+ if ((*hash_link->prev= hash_link->next))
+ hash_link->next->prev= hash_link->prev;
+ hash_link->block= NULL;
+
+ if (keycache->waiting_for_hash_link.last_thread)
+ {
+ /* Signal that a free hash link has appeared */
+ st_keycache_thread_var *last_thread=
+ keycache->waiting_for_hash_link.last_thread;
+ st_keycache_thread_var *first_thread= last_thread->next;
+ st_keycache_thread_var *next_thread= first_thread;
+ KEYCACHE_PAGE *first_page= (KEYCACHE_PAGE *) (first_thread->opt_info);
+ st_keycache_thread_var *thread;
+
+ hash_link->file= first_page->file;
+ hash_link->diskpos= first_page->filepos;
+ do
+ {
+ KEYCACHE_PAGE *page;
+ thread= next_thread;
+ page= (KEYCACHE_PAGE *) thread->opt_info;
+ next_thread= thread->next;
+ /*
+ We notify about the event all threads that ask
+ for the same page as the first thread in the queue
+ */
+ if (page->file == hash_link->file && page->filepos == hash_link->diskpos)
+ {
+ mysql_cond_signal(&thread->suspend);
+ unlink_from_queue(&keycache->waiting_for_hash_link, thread);
+ }
+ }
+ while (thread != last_thread);
+ link_hash(&keycache->hash_root[KEYCACHE_HASH(hash_link->file,
+ hash_link->diskpos)],
+ hash_link);
+ return;
+ }
+ hash_link->next= keycache->free_hash_list;
+ keycache->free_hash_list= hash_link;
+}
+
+
+/*
+ Get the hash link for a page
+*/
+
+static HASH_LINK *get_hash_link(KEY_CACHE *keycache,
+ int file, my_off_t filepos,
+ st_keycache_thread_var *thread)
+{
+ HASH_LINK *hash_link, **start;
+#ifndef DBUG_OFF
+ int cnt;
+#endif
+
+restart:
+ /*
+ Find the bucket in the hash table for the pair (file, filepos);
+ start contains the head of the bucket list,
+ hash_link points to the first member of the list
+ */
+ hash_link= *(start= &keycache->hash_root[KEYCACHE_HASH(file, filepos)]);
+#ifndef DBUG_OFF
+ cnt= 0;
+#endif
+ /* Look for an element for the pair (file, filepos) in the bucket chain */
+ while (hash_link &&
+ (hash_link->diskpos != filepos || hash_link->file != file))
+ {
+ hash_link= hash_link->next;
+#ifndef DBUG_OFF
+ cnt++;
+ DBUG_ASSERT(cnt <= keycache->hash_links_used);
+#endif
+ }
+ if (! hash_link)
+ {
+ /* There is no hash link in the hash table for the pair (file, filepos) */
+ if (keycache->free_hash_list)
+ {
+ hash_link= keycache->free_hash_list;
+ keycache->free_hash_list= hash_link->next;
+ }
+ else if (keycache->hash_links_used < keycache->hash_links)
+ {
+ hash_link= &keycache->hash_link_root[keycache->hash_links_used++];
+ }
+ else
+ {
+ /* Wait for a free hash link */
+ KEYCACHE_PAGE page;
+ page.file= file;
+ page.filepos= filepos;
+ thread->opt_info= (void *) &page;
+ link_into_queue(&keycache->waiting_for_hash_link, thread);
+ mysql_cond_wait(&thread->suspend,
+ &keycache->cache_lock);
+ thread->opt_info= NULL;
+ goto restart;
+ }
+ hash_link->file= file;
+ hash_link->diskpos= filepos;
+ link_hash(start, hash_link);
+ }
+ /* Register the request for the page */
+ hash_link->requests++;
+
+ return hash_link;
+}
+
+
+/*
+ Get a block for the file page requested by a keycache read/write operation;
+ If the page is not in the cache return a free block, if there is none
+ return the lru block after saving its buffer if the page is dirty.
+
+ SYNOPSIS
+
+ find_key_block()
+ keycache pointer to a key cache data structure
+ thread pointer to thread specific variables
+ file handler for the file to read page from
+ filepos position of the page in the file
+ init_hits_left how initialize the block counter for the page
+ wrmode <-> get for writing
+ page_st out {PAGE_READ,PAGE_TO_BE_READ,PAGE_WAIT_TO_BE_READ}
+
+ RETURN VALUE
+ Pointer to the found block if successful, 0 - otherwise
+
+ NOTES.
+ For the page from file positioned at filepos the function checks whether
+ the page is in the key cache specified by the first parameter.
+ If this is the case it immediately returns the block.
+ If not, the function first chooses a block for this page. If there is
+ no not used blocks in the key cache yet, the function takes the block
+ at the very beginning of the warm sub-chain. It saves the page in that
+ block if it's dirty before returning the pointer to it.
+ The function returns in the page_st parameter the following values:
+ PAGE_READ - if page already in the block,
+ PAGE_TO_BE_READ - if it is to be read yet by the current thread
+ WAIT_TO_BE_READ - if it is to be read by another thread
+ If an error occurs THE BLOCK_ERROR bit is set in the block status.
+ It might happen that there are no blocks in LRU chain (in warm part) -
+ all blocks are unlinked for some read/write operations. Then the function
+ waits until first of this operations links any block back.
+*/
+
+static BLOCK_LINK *find_key_block(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread,
+ File file, my_off_t filepos,
+ int init_hits_left,
+ int wrmode, int *page_st)
+{
+ HASH_LINK *hash_link;
+ BLOCK_LINK *block;
+ int error= 0;
+ int page_status;
+
+ DBUG_ENTER("find_key_block");
+ DBUG_PRINT("enter", ("fd: %d pos: %lu wrmode: %d",
+ file, (ulong) filepos, wrmode));
+
+restart:
+ /*
+ If the flush phase of a resize operation fails, the cache is left
+ unusable. This will be detected only after "goto restart".
+ */
+ if (!keycache->can_be_used)
+ DBUG_RETURN(0);
+
+ /*
+ Find the hash_link for the requested file block (file, filepos). We
+ do always get a hash_link here. It has registered our request so
+ that no other thread can use it for another file block until we
+ release the request (which is done by remove_reader() usually). The
+ hash_link can have a block assigned to it or not. If there is a
+ block, it may be assigned to this hash_link or not. In cases where a
+ block is evicted from the cache, it is taken from the LRU ring and
+ referenced by the new hash_link. But the block can still be assigned
+ to its old hash_link for some time if it needs to be flushed first,
+ or if there are other threads still reading it.
+
+ Summary:
+ hash_link is always returned.
+ hash_link->block can be:
+ - NULL or
+ - not assigned to this hash_link or
+ - assigned to this hash_link. If assigned, the block can have
+ - invalid data (when freshly assigned) or
+ - valid data. Valid data can be
+ - changed over the file contents (dirty) or
+ - not changed (clean).
+ */
+ hash_link= get_hash_link(keycache, file, filepos, thread);
+ DBUG_ASSERT((hash_link->file == file) && (hash_link->diskpos == filepos));
+
+ page_status= -1;
+ if ((block= hash_link->block) &&
+ block->hash_link == hash_link && (block->status & BLOCK_READ))
+ {
+ /* Assigned block with valid (changed or unchanged) contents. */
+ page_status= PAGE_READ;
+ }
+ /*
+ else (page_status == -1)
+ - block == NULL or
+ - block not assigned to this hash_link or
+ - block assigned but not yet read from file (invalid data).
+ */
+
+ if (keycache->in_resize)
+ {
+ /* This is a request during a resize operation */
+
+ if (!block)
+ {
+ /*
+ The file block is not in the cache. We don't need it in the
+ cache: we are going to read or write directly to file. Cancel
+ the request. We can simply decrement hash_link->requests because
+ we did not release cache_lock since increasing it. So no other
+ thread can wait for our request to become released.
+ */
+ if (hash_link->requests == 1)
+ {
+ /*
+ We are the only one to request this hash_link (this file/pos).
+ Free the hash_link.
+ */
+ hash_link->requests--;
+ unlink_hash(keycache, hash_link);
+ DBUG_RETURN(0);
+ }
+
+ /*
+ More requests on the hash_link. Someone tries to evict a block
+ for this hash_link (could have started before resizing started).
+ This means that the LRU ring is empty. Otherwise a block could
+ be assigned immediately. Behave like a thread that wants to
+ evict a block for this file/pos. Add to the queue of threads
+ waiting for a block. Wait until there is one assigned.
+
+ Refresh the request on the hash-link so that it cannot be reused
+ for another file/pos.
+ */
+ thread->opt_info= (void *) hash_link;
+ link_into_queue(&keycache->waiting_for_block, thread);
+ do
+ {
+ mysql_cond_wait(&thread->suspend,
+ &keycache->cache_lock);
+ } while (thread->next);
+ thread->opt_info= NULL;
+ /*
+ A block should now be assigned to the hash_link. But it may
+ still need to be evicted. Anyway, we should re-check the
+ situation. page_status must be set correctly.
+ */
+ hash_link->requests--;
+ goto restart;
+ } /* end of if (!block) */
+
+ /*
+ There is a block for this file/pos in the cache. Register a
+ request on it. This unlinks it from the LRU ring (if it is there)
+ and hence protects it against eviction (if not already in
+ eviction). We need this for returning the block to the caller, for
+ calling remove_reader() (for debugging purposes), and for calling
+ free_block(). The only case where we don't need the request is if
+ the block is in eviction. In that case we have to unregister the
+ request later.
+ */
+ reg_requests(keycache, block, 1);
+
+ if (page_status != PAGE_READ)
+ {
+ /*
+ - block not assigned to this hash_link or
+ - block assigned but not yet read from file (invalid data).
+
+ This must be a block in eviction. It will be read soon. We need
+ to wait here until this happened. Otherwise the caller could
+ access a wrong block or a block which is in read. While waiting
+ we cannot lose hash_link nor block. We have registered a request
+ on the hash_link. Everything can happen to the block but changes
+ in the hash_link -> block relationship. In other words:
+ everything can happen to the block but free or another completed
+ eviction.
+
+ Note that we bahave like a secondary requestor here. We just
+ cannot return with PAGE_WAIT_TO_BE_READ. This would work for
+ read requests and writes on dirty blocks that are not in flush
+ only. Waiting here on COND_FOR_REQUESTED works in all
+ situations.
+ */
+ DBUG_ASSERT(((block->hash_link != hash_link) &&
+ (block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH))) ||
+ ((block->hash_link == hash_link) &&
+ !(block->status & BLOCK_READ)));
+ wait_on_queue(&block->wqueue[COND_FOR_REQUESTED], &keycache->cache_lock,
+ thread);
+ /*
+ Here we can trust that the block has been assigned to this
+ hash_link (block->hash_link == hash_link) and read into the
+ buffer (BLOCK_READ). The worst things possible here are that the
+ block is in free (BLOCK_REASSIGNED). But the block is still
+ assigned to the hash_link. The freeing thread waits until we
+ release our request on the hash_link. The block must not be
+ again in eviction because we registered an request on it before
+ starting to wait.
+ */
+ DBUG_ASSERT(block->hash_link == hash_link);
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ DBUG_ASSERT(!(block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH)));
+ }
+ /*
+ The block is in the cache. Assigned to the hash_link. Valid data.
+ Note that in case of page_st == PAGE_READ, the block can be marked
+ for eviction. In any case it can be marked for freeing.
+ */
+
+ if (!wrmode)
+ {
+ /* A reader can just read the block. */
+ *page_st= PAGE_READ;
+ DBUG_ASSERT((hash_link->file == file) &&
+ (hash_link->diskpos == filepos) &&
+ (block->hash_link == hash_link));
+ DBUG_RETURN(block);
+ }
+
+ /*
+ This is a writer. No two writers for the same block can exist.
+ This must be assured by locks outside of the key cache.
+ */
+ DBUG_ASSERT(!(block->status & BLOCK_FOR_UPDATE) || fail_block(block));
+
+ while (block->status & BLOCK_IN_FLUSH)
+ {
+ /*
+ Wait until the block is flushed to file. Do not release the
+ request on the hash_link yet to prevent that the block is freed
+ or reassigned while we wait. While we wait, several things can
+ happen to the block, including another flush. But the block
+ cannot be reassigned to another hash_link until we release our
+ request on it. But it can be marked BLOCK_REASSIGNED from free
+ or eviction, while they wait for us to release the hash_link.
+ */
+ wait_on_queue(&block->wqueue[COND_FOR_SAVED], &keycache->cache_lock,
+ thread);
+ /*
+ If the flush phase failed, the resize could have finished while
+ we waited here.
+ */
+ if (!keycache->in_resize)
+ {
+ remove_reader(block);
+ unreg_request(keycache, block, 1);
+ goto restart;
+ }
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ DBUG_ASSERT(!(block->status & BLOCK_FOR_UPDATE) || fail_block(block));
+ DBUG_ASSERT(block->hash_link == hash_link);
+ }
+
+ if (block->status & BLOCK_CHANGED)
+ {
+ /*
+ We want to write a block with changed contents. If the cache
+ block size is bigger than the callers block size (e.g. MyISAM),
+ the caller may replace part of the block only. Changes of the
+ other part of the block must be preserved. Since the block has
+ not yet been selected for flush, we can still add our changes.
+ */
+ *page_st= PAGE_READ;
+ DBUG_ASSERT((hash_link->file == file) &&
+ (hash_link->diskpos == filepos) &&
+ (block->hash_link == hash_link));
+ DBUG_RETURN(block);
+ }
+
+ /*
+ This is a write request for a clean block. We do not want to have
+ new dirty blocks in the cache while resizing. We will free the
+ block and write directly to file. If the block is in eviction or
+ in free, we just let it go.
+
+ Unregister from the hash_link. This must be done before freeing
+ the block. And it must be done if not freeing the block. Because
+ we could have waited above, we need to call remove_reader(). Other
+ threads could wait for us to release our request on the hash_link.
+ */
+ remove_reader(block);
+
+ /* If the block is not in eviction and not in free, we can free it. */
+ if (!(block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH |
+ BLOCK_REASSIGNED)))
+ {
+ /*
+ Free block as we are going to write directly to file.
+ Although we have an exlusive lock for the updated key part,
+ the control can be yielded by the current thread as we might
+ have unfinished readers of other key parts in the block
+ buffer. Still we are guaranteed not to have any readers
+ of the key part we are writing into until the block is
+ removed from the cache as we set the BLOCK_REASSIGNED
+ flag (see the code below that handles reading requests).
+ */
+ free_block(keycache, thread, block);
+ }
+ else
+ {
+ /*
+ The block will be evicted/freed soon. Don't touch it in any way.
+ Unregister the request that we registered above.
+ */
+ unreg_request(keycache, block, 1);
+
+ /*
+ The block is still assigned to the hash_link (the file/pos that
+ we are going to write to). Wait until the eviction/free is
+ complete. Otherwise the direct write could complete before all
+ readers are done with the block. So they could read outdated
+ data.
+
+ Since we released our request on the hash_link, it can be reused
+ for another file/pos. Hence we cannot just check for
+ block->hash_link == hash_link. As long as the resize is
+ proceeding the block cannot be reassigned to the same file/pos
+ again. So we can terminate the loop when the block is no longer
+ assigned to this file/pos.
+ */
+ do
+ {
+ wait_on_queue(&block->wqueue[COND_FOR_SAVED],
+ &keycache->cache_lock, thread);
+ /*
+ If the flush phase failed, the resize could have finished
+ while we waited here.
+ */
+ if (!keycache->in_resize)
+ goto restart;
+ } while (block->hash_link &&
+ (block->hash_link->file == file) &&
+ (block->hash_link->diskpos == filepos));
+ }
+ DBUG_RETURN(0);
+ }
+
+ if (page_status == PAGE_READ &&
+ (block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH |
+ BLOCK_REASSIGNED)))
+ {
+ /*
+ This is a request for a block to be removed from cache. The block
+ is assigned to this hash_link and contains valid data, but is
+ marked for eviction or to be freed. Possible reasons why it has
+ not yet been evicted/freed can be a flush before reassignment
+ (BLOCK_IN_SWITCH), readers of the block have not finished yet
+ (BLOCK_REASSIGNED), or the evicting thread did not yet awake after
+ the block has been selected for it (BLOCK_IN_EVICTION).
+ */
+
+ /*
+ Only reading requests can proceed until the old dirty page is flushed,
+ all others are to be suspended, then resubmitted
+ */
+ if (!wrmode && !(block->status & BLOCK_REASSIGNED))
+ {
+ /*
+ This is a read request and the block not yet reassigned. We can
+ register our request and proceed. This unlinks the block from
+ the LRU ring and protects it against eviction.
+ */
+ reg_requests(keycache, block, 1);
+ }
+ else
+ {
+ /*
+ Either this is a write request for a block that is in eviction
+ or in free. We must not use it any more. Instead we must evict
+ another block. But we cannot do this before the eviction/free is
+ done. Otherwise we would find the same hash_link + block again
+ and again.
+
+ Or this is a read request for a block in eviction/free that does
+ not require a flush, but waits for readers to finish with the
+ block. We do not read this block to let the eviction/free happen
+ as soon as possible. Again we must wait so that we don't find
+ the same hash_link + block again and again.
+ */
+ DBUG_ASSERT(hash_link->requests);
+ hash_link->requests--;
+ wait_on_queue(&block->wqueue[COND_FOR_SAVED], &keycache->cache_lock,
+ thread);
+ /*
+ The block is no longer assigned to this hash_link.
+ Get another one.
+ */
+ goto restart;
+ }
+ }
+ else
+ {
+ /*
+ This is a request for a new block or for a block not to be removed.
+ Either
+ - block == NULL or
+ - block not assigned to this hash_link or
+ - block assigned but not yet read from file,
+ or
+ - block assigned with valid (changed or unchanged) data and
+ - it will not be reassigned/freed.
+ */
+ if (! block)
+ {
+ /* No block is assigned to the hash_link yet. */
+ if (keycache->blocks_unused)
+ {
+ if (keycache->free_block_list)
+ {
+ /* There is a block in the free list. */
+ block= keycache->free_block_list;
+ keycache->free_block_list= block->next_used;
+ block->next_used= NULL;
+ }
+ else
+ {
+ size_t block_mem_offset;
+ /* There are some never used blocks, take first of them */
+ DBUG_ASSERT(keycache->blocks_used <
+ (ulong) keycache->disk_blocks);
+ block= &keycache->block_root[keycache->blocks_used];
+ block_mem_offset=
+ ((size_t) keycache->blocks_used) * keycache->key_cache_block_size;
+ block->buffer= ADD_TO_PTR(keycache->block_mem,
+ block_mem_offset,
+ uchar*);
+ keycache->blocks_used++;
+ DBUG_ASSERT(!block->next_used);
+ }
+ DBUG_ASSERT(!block->prev_used);
+ DBUG_ASSERT(!block->next_changed);
+ DBUG_ASSERT(!block->prev_changed);
+ DBUG_ASSERT(!block->hash_link);
+ DBUG_ASSERT(!block->status);
+ DBUG_ASSERT(!block->requests);
+ keycache->blocks_unused--;
+ block->status= BLOCK_IN_USE;
+ block->length= 0;
+ block->offset= keycache->key_cache_block_size;
+ block->requests= 1;
+ block->temperature= BLOCK_COLD;
+ block->hits_left= init_hits_left;
+ block->last_hit_time= 0;
+ block->hash_link= hash_link;
+ hash_link->block= block;
+ link_to_file_list(keycache, block, file, 0);
+ page_status= PAGE_TO_BE_READ;
+ }
+ else
+ {
+ /*
+ There are no free blocks and no never used blocks, use a block
+ from the LRU ring.
+ */
+
+ if (! keycache->used_last)
+ {
+ /*
+ The LRU ring is empty. Wait until a new block is added to
+ it. Several threads might wait here for the same hash_link,
+ all of them must get the same block. While waiting for a
+ block, after a block is selected for this hash_link, other
+ threads can run first before this one awakes. During this
+ time interval other threads find this hash_link pointing to
+ the block, which is still assigned to another hash_link. In
+ this case the block is not marked BLOCK_IN_SWITCH yet, but
+ it is marked BLOCK_IN_EVICTION.
+ */
+
+ thread->opt_info= (void *) hash_link;
+ link_into_queue(&keycache->waiting_for_block, thread);
+ do
+ {
+ mysql_cond_wait(&thread->suspend,
+ &keycache->cache_lock);
+ }
+ while (thread->next);
+ thread->opt_info= NULL;
+ /* Assert that block has a request registered. */
+ DBUG_ASSERT(hash_link->block->requests);
+ /* Assert that block is not in LRU ring. */
+ DBUG_ASSERT(!hash_link->block->next_used);
+ DBUG_ASSERT(!hash_link->block->prev_used);
+ }
+
+ /*
+ If we waited above, hash_link->block has been assigned by
+ link_block(). Otherwise it is still NULL. In the latter case
+ we need to grab a block from the LRU ring ourselves.
+ */
+ block= hash_link->block;
+ if (! block)
+ {
+ /* Select the last block from the LRU ring. */
+ block= keycache->used_last->next_used;
+ block->hits_left= init_hits_left;
+ block->last_hit_time= 0;
+ hash_link->block= block;
+ /*
+ Register a request on the block. This unlinks it from the
+ LRU ring and protects it against eviction.
+ */
+ DBUG_ASSERT(!block->requests);
+ reg_requests(keycache, block,1);
+ /*
+ We do not need to set block->status|= BLOCK_IN_EVICTION here
+ because we will set block->status|= BLOCK_IN_SWITCH
+ immediately without releasing the lock in between. This does
+ also support debugging. When looking at the block, one can
+ see if the block has been selected by link_block() after the
+ LRU ring was empty, or if it was grabbed directly from the
+ LRU ring in this branch.
+ */
+ }
+
+ /*
+ If we had to wait above, there is a small chance that another
+ thread grabbed this block for the same file block already. But
+ in most cases the first condition is true.
+ */
+ if (block->hash_link != hash_link &&
+ ! (block->status & BLOCK_IN_SWITCH) )
+ {
+ /* this is a primary request for a new page */
+ block->status|= BLOCK_IN_SWITCH;
+
+ if (block->status & BLOCK_CHANGED)
+ {
+ /* The block contains a dirty page - push it out of the cache */
+
+ if (block->status & BLOCK_IN_FLUSH)
+ {
+ /*
+ The block is marked for flush. If we do not wait here,
+ it could happen that we write the block, reassign it to
+ another file block, then, before the new owner can read
+ the new file block, the flusher writes the cache block
+ (which still has the old contents) to the new file block!
+ */
+ wait_on_queue(&block->wqueue[COND_FOR_SAVED],
+ &keycache->cache_lock, thread);
+ /*
+ The block is marked BLOCK_IN_SWITCH. It should be left
+ alone except for reading. No free, no write.
+ */
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ DBUG_ASSERT(!(block->status & (BLOCK_REASSIGNED |
+ BLOCK_CHANGED |
+ BLOCK_FOR_UPDATE)));
+ }
+ else
+ {
+ block->status|= BLOCK_IN_FLUSH | BLOCK_IN_FLUSHWRITE;
+ /*
+ BLOCK_IN_EVICTION may be true or not. Other flags must
+ have a fixed value.
+ */
+ DBUG_ASSERT((block->status & ~BLOCK_IN_EVICTION) ==
+ (BLOCK_READ | BLOCK_IN_SWITCH |
+ BLOCK_IN_FLUSH | BLOCK_IN_FLUSHWRITE |
+ BLOCK_CHANGED | BLOCK_IN_USE));
+ DBUG_ASSERT(block->hash_link);
+
+ mysql_mutex_unlock(&keycache->cache_lock);
+ /*
+ The call is thread safe because only the current
+ thread might change the block->hash_link value
+ */
+ error= (int)my_pwrite(block->hash_link->file,
+ block->buffer + block->offset,
+ block->length - block->offset,
+ block->hash_link->diskpos + block->offset,
+ MYF(MY_NABP | MY_WAIT_IF_FULL));
+ mysql_mutex_lock(&keycache->cache_lock);
+
+ /* Block status must not have changed. */
+ DBUG_ASSERT((block->status & ~BLOCK_IN_EVICTION) ==
+ (BLOCK_READ | BLOCK_IN_SWITCH |
+ BLOCK_IN_FLUSH | BLOCK_IN_FLUSHWRITE |
+ BLOCK_CHANGED | BLOCK_IN_USE) || fail_block(block));
+ keycache->global_cache_write++;
+ }
+ }
+
+ block->status|= BLOCK_REASSIGNED;
+ /*
+ The block comes from the LRU ring. It must have a hash_link
+ assigned.
+ */
+ DBUG_ASSERT(block->hash_link);
+ if (block->hash_link)
+ {
+ /*
+ All pending requests for this page must be resubmitted.
+ This must be done before waiting for readers. They could
+ wait for the flush to complete. And we must also do it
+ after the wait. Flushers might try to free the block while
+ we wait. They would wait until the reassignment is
+ complete. Also the block status must reflect the correct
+ situation: The block is not changed nor in flush any more.
+ Note that we must not change the BLOCK_CHANGED flag
+ outside of link_to_file_list() so that it is always in the
+ correct queue and the *blocks_changed counters are
+ correct.
+ */
+ block->status&= ~(BLOCK_IN_FLUSH | BLOCK_IN_FLUSHWRITE);
+ link_to_file_list(keycache, block, block->hash_link->file, 1);
+ release_whole_queue(&block->wqueue[COND_FOR_SAVED]);
+ /*
+ The block is still assigned to its old hash_link.
+ Wait until all pending read requests
+ for this page are executed
+ (we could have avoided this waiting, if we had read
+ a page in the cache in a sweep, without yielding control)
+ */
+ wait_for_readers(keycache, block, thread);
+ DBUG_ASSERT(block->hash_link && block->hash_link->block == block &&
+ block->prev_changed);
+ /* The reader must not have been a writer. */
+ DBUG_ASSERT(!(block->status & BLOCK_CHANGED));
+
+ /* Wake flushers that might have found the block in between. */
+ release_whole_queue(&block->wqueue[COND_FOR_SAVED]);
+
+ /* Remove the hash link for the old file block from the hash. */
+ unlink_hash(keycache, block->hash_link);
+
+ /*
+ For sanity checks link_to_file_list() asserts that block
+ and hash_link refer to each other. Hence we need to assign
+ the hash_link first, but then we would not know if it was
+ linked before. Hence we would not know if to unlink it. So
+ unlink it here and call link_to_file_list(..., FALSE).
+ */
+ unlink_changed(block);
+ }
+ block->status= error ? BLOCK_ERROR : BLOCK_IN_USE ;
+ block->length= 0;
+ block->offset= keycache->key_cache_block_size;
+ block->hash_link= hash_link;
+ link_to_file_list(keycache, block, file, 0);
+ page_status= PAGE_TO_BE_READ;
+
+ DBUG_ASSERT(block->hash_link->block == block);
+ DBUG_ASSERT(hash_link->block->hash_link == hash_link);
+ }
+ else
+ {
+ /*
+ Either (block->hash_link == hash_link),
+ or (block->status & BLOCK_IN_SWITCH).
+
+ This is for secondary requests for a new file block only.
+ Either it is already assigned to the new hash_link meanwhile
+ (if we had to wait due to empty LRU), or it is already in
+ eviction by another thread. Since this block has been
+ grabbed from the LRU ring and attached to this hash_link,
+ another thread cannot grab the same block from the LRU ring
+ anymore. If the block is in eviction already, it must become
+ attached to the same hash_link and as such destined for the
+ same file block.
+ */
+ page_status= (((block->hash_link == hash_link) &&
+ (block->status & BLOCK_READ)) ?
+ PAGE_READ : PAGE_WAIT_TO_BE_READ);
+ }
+ }
+ }
+ else
+ {
+ /*
+ Block is not NULL. This hash_link points to a block.
+ Either
+ - block not assigned to this hash_link (yet) or
+ - block assigned but not yet read from file,
+ or
+ - block assigned with valid (changed or unchanged) data and
+ - it will not be reassigned/freed.
+
+ The first condition means hash_link points to a block in
+ eviction. This is not necessarily marked by BLOCK_IN_SWITCH yet.
+ But then it is marked BLOCK_IN_EVICTION. See the NOTE in
+ link_block(). In both cases it is destined for this hash_link
+ and its file block address. When this hash_link got its block
+ address, the block was removed from the LRU ring and cannot be
+ selected for eviction (for another hash_link) again.
+
+ Register a request on the block. This is another protection
+ against eviction.
+ */
+ DBUG_ASSERT(((block->hash_link != hash_link) &&
+ (block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH))) ||
+ ((block->hash_link == hash_link) &&
+ !(block->status & BLOCK_READ)) ||
+ ((block->status & BLOCK_READ) &&
+ !(block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH))));
+ reg_requests(keycache, block, 1);
+ page_status= (((block->hash_link == hash_link) &&
+ (block->status & BLOCK_READ)) ?
+ PAGE_READ : PAGE_WAIT_TO_BE_READ);
+ }
+ }
+
+ DBUG_ASSERT(page_status != -1);
+ /* Same assert basically, but be very sure. */
+ DBUG_ASSERT(block);
+ /* Assert that block has a request and is not in LRU ring. */
+ DBUG_ASSERT(block->requests);
+ DBUG_ASSERT(!block->next_used);
+ DBUG_ASSERT(!block->prev_used);
+ /* Assert that we return the correct block. */
+ DBUG_ASSERT((page_status == PAGE_WAIT_TO_BE_READ) ||
+ ((block->hash_link->file == file) &&
+ (block->hash_link->diskpos == filepos)));
+ *page_st=page_status;
+ DBUG_RETURN(block);
+}
+
+
+/*
+ Read into a key cache block buffer from disk.
+
+ SYNOPSIS
+
+ read_block()
+ keycache pointer to a key cache data structure
+ thread_var pointer to thread specific variables
+ block block to which buffer the data is to be read
+ read_length size of data to be read
+ min_length at least so much data must be read
+ primary <-> the current thread will read the data
+
+ RETURN VALUE
+ None
+
+ NOTES.
+ The function either reads a page data from file to the block buffer,
+ or waits until another thread reads it. What page to read is determined
+ by a block parameter - reference to a hash link for this page.
+ If an error occurs THE BLOCK_ERROR bit is set in the block status.
+ We do not report error when the size of successfully read
+ portion is less than read_length, but not less than min_length.
+*/
+
+static void read_block(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ BLOCK_LINK *block, uint read_length,
+ uint min_length, my_bool primary)
+{
+ size_t got_length;
+
+ /* On entry cache_lock is locked */
+
+ if (primary)
+ {
+ /*
+ This code is executed only by threads that submitted primary
+ requests. Until block->status contains BLOCK_READ, all other
+ request for the block become secondary requests. For a primary
+ request the block must be properly initialized.
+ */
+ DBUG_ASSERT(((block->status & ~BLOCK_FOR_UPDATE) == BLOCK_IN_USE) ||
+ fail_block(block));
+ DBUG_ASSERT((block->length == 0) || fail_block(block));
+ DBUG_ASSERT((block->offset == keycache->key_cache_block_size) ||
+ fail_block(block));
+ DBUG_ASSERT((block->requests > 0) || fail_block(block));
+
+ keycache->global_cache_read++;
+ /* Page is not in buffer yet, is to be read from disk */
+ mysql_mutex_unlock(&keycache->cache_lock);
+ /*
+ Here other threads may step in and register as secondary readers.
+ They will register in block->wqueue[COND_FOR_REQUESTED].
+ */
+ got_length= my_pread(block->hash_link->file, block->buffer,
+ read_length, block->hash_link->diskpos, MYF(0));
+ mysql_mutex_lock(&keycache->cache_lock);
+ /*
+ The block can now have been marked for free (in case of
+ FLUSH_RELEASE). Otherwise the state must be unchanged.
+ */
+ DBUG_ASSERT(((block->status & ~(BLOCK_REASSIGNED |
+ BLOCK_FOR_UPDATE)) == BLOCK_IN_USE) ||
+ fail_block(block));
+ DBUG_ASSERT((block->length == 0) || fail_block(block));
+ DBUG_ASSERT((block->offset == keycache->key_cache_block_size) ||
+ fail_block(block));
+ DBUG_ASSERT((block->requests > 0) || fail_block(block));
+
+ if (got_length < min_length)
+ block->status|= BLOCK_ERROR;
+ else
+ {
+ block->status|= BLOCK_READ;
+ block->length= (int)got_length;
+ /*
+ Do not set block->offset here. If this block is marked
+ BLOCK_CHANGED later, we want to flush only the modified part. So
+ only a writer may set block->offset down from
+ keycache->key_cache_block_size.
+ */
+ }
+ /* Signal that all pending requests for this page now can be processed */
+ release_whole_queue(&block->wqueue[COND_FOR_REQUESTED]);
+ }
+ else
+ {
+ /*
+ This code is executed only by threads that submitted secondary
+ requests. At this point it could happen that the cache block is
+ not yet assigned to the hash_link for the requested file block.
+ But at awake from the wait this should be the case. Unfortunately
+ we cannot assert this here because we do not know the hash_link
+ for the requested file block nor the file and position. So we have
+ to assert this in the caller.
+ */
+ wait_on_queue(&block->wqueue[COND_FOR_REQUESTED], &keycache->cache_lock,
+ thread_var);
+ }
+}
+
+
+/*
+ Read a block of data from a cached file into a buffer;
+
+ SYNOPSIS
+
+ key_cache_read()
+ keycache pointer to a key cache data structure
+ thread_var pointer to thread specific variables
+ file handler for the file for the block of data to be read
+ filepos position of the block of data in the file
+ level determines the weight of the data
+ buff buffer to where the data must be placed
+ length length of the buffer
+ block_length length of the block in the key cache buffer
+ return_buffer return pointer to the key cache buffer with the data
+
+ RETURN VALUE
+ Returns address from where the data is placed if sucessful, 0 - otherwise.
+
+ NOTES.
+ The function ensures that a block of data of size length from file
+ positioned at filepos is in the buffers for some key cache blocks.
+ Then the function either copies the data into the buffer buff, or,
+ if return_buffer is TRUE, it just returns the pointer to the key cache
+ buffer with the data.
+ Filepos must be a multiple of 'block_length', but it doesn't
+ have to be a multiple of key_cache_block_size;
+*/
+
+uchar *key_cache_read(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ File file, my_off_t filepos, int level,
+ uchar *buff, uint length,
+ uint block_length MY_ATTRIBUTE((unused)),
+ int return_buffer MY_ATTRIBUTE((unused)))
+{
+ my_bool locked_and_incremented= FALSE;
+ int error=0;
+ uchar *start= buff;
+ DBUG_ENTER("key_cache_read");
+ DBUG_PRINT("enter", ("fd: %u pos: %lu length: %u",
+ (uint) file, (ulong) filepos, length));
+
+ if (keycache->key_cache_inited)
+ {
+ /* Key cache is used */
+ BLOCK_LINK *block;
+ uint read_length;
+ uint offset;
+ int page_st;
+
+ if (MYSQL_KEYCACHE_READ_START_ENABLED())
+ {
+ MYSQL_KEYCACHE_READ_START(my_filename(file), length,
+ (ulong) (keycache->blocks_used *
+ keycache->key_cache_block_size),
+ (ulong) (keycache->blocks_unused *
+ keycache->key_cache_block_size));
+ }
+
+ /*
+ When the key cache is once initialized, we use the cache_lock to
+ reliably distinguish the cases of normal operation, resizing, and
+ disabled cache. We always increment and decrement
+ 'cnt_for_resize_op' so that a resizer can wait for pending I/O.
+ */
+ mysql_mutex_lock(&keycache->cache_lock);
+ /*
+ Cache resizing has two phases: Flushing and re-initializing. In
+ the flush phase read requests are allowed to bypass the cache for
+ blocks not in the cache. find_key_block() returns NULL in this
+ case.
+
+ After the flush phase new I/O requests must wait until the
+ re-initialization is done. The re-initialization can be done only
+ if no I/O request is in progress. The reason is that
+ key_cache_block_size can change. With enabled cache, I/O is done
+ in chunks of key_cache_block_size. Every chunk tries to use a
+ cache block first. If the block size changes in the middle, a
+ block could be missed and old data could be read.
+ */
+ while (keycache->in_resize && !keycache->resize_in_flush)
+ wait_on_queue(&keycache->resize_queue, &keycache->cache_lock,
+ thread_var);
+ /* Register the I/O for the next resize. */
+ inc_counter_for_resize_op(keycache);
+ locked_and_incremented= TRUE;
+ /* Requested data may not always be aligned to cache blocks. */
+ offset= (uint) (filepos % keycache->key_cache_block_size);
+ /* Read data in key_cache_block_size increments */
+ do
+ {
+ /* Cache could be disabled in a later iteration. */
+ if (!keycache->can_be_used)
+ {
+ goto no_key_cache;
+ }
+ /* Start reading at the beginning of the cache block. */
+ filepos-= offset;
+ /* Do not read beyond the end of the cache block. */
+ read_length= length;
+ set_if_smaller(read_length, keycache->key_cache_block_size-offset);
+ DBUG_ASSERT(read_length > 0);
+
+ if (block_length > keycache->key_cache_block_size || offset)
+ return_buffer=0;
+
+ /* Request the cache block that matches file/pos. */
+ keycache->global_cache_r_requests++;
+
+ MYSQL_KEYCACHE_READ_BLOCK(keycache->key_cache_block_size);
+
+ block= find_key_block(keycache, thread_var, file, filepos, level, 0,
+ &page_st);
+ if (!block)
+ {
+ /*
+ This happens only for requests submitted during key cache
+ resize. The block is not in the cache and shall not go in.
+ Read directly from file.
+ */
+ keycache->global_cache_read++;
+ mysql_mutex_unlock(&keycache->cache_lock);
+ error= (my_pread(file, (uchar*) buff, read_length,
+ filepos + offset, MYF(MY_NABP)) != 0);
+ mysql_mutex_lock(&keycache->cache_lock);
+ goto next_block;
+ }
+ if (!(block->status & BLOCK_ERROR))
+ {
+ if (page_st != PAGE_READ)
+ {
+ MYSQL_KEYCACHE_READ_MISS();
+ /* The requested page is to be read into the block buffer */
+ read_block(keycache, thread_var, block,
+ keycache->key_cache_block_size, read_length+offset,
+ (my_bool)(page_st == PAGE_TO_BE_READ));
+ /*
+ A secondary request must now have the block assigned to the
+ requested file block. It does not hurt to check it for
+ primary requests too.
+ */
+ DBUG_ASSERT(keycache->can_be_used);
+ DBUG_ASSERT(block->hash_link->file == file);
+ DBUG_ASSERT(block->hash_link->diskpos == filepos);
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ }
+ else if (block->length < read_length + offset)
+ {
+ /*
+ Impossible if nothing goes wrong:
+ this could only happen if we are using a file with
+ small key blocks and are trying to read outside the file
+ */
+ set_my_errno(-1);
+ block->status|= BLOCK_ERROR;
+ }
+ else
+ {
+ MYSQL_KEYCACHE_READ_HIT();
+ }
+ }
+
+ /* block status may have added BLOCK_ERROR in the above 'if'. */
+ if (!(block->status & BLOCK_ERROR))
+ {
+ {
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ mysql_mutex_unlock(&keycache->cache_lock);
+
+ /* Copy data from the cache buffer */
+ memcpy(buff, block->buffer+offset, (size_t) read_length);
+
+ mysql_mutex_lock(&keycache->cache_lock);
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ }
+ }
+
+ remove_reader(block);
+
+ /* Error injection for coverage testing. */
+ DBUG_EXECUTE_IF("key_cache_read_block_error",
+ block->status|= BLOCK_ERROR;);
+
+ /* Do not link erroneous blocks into the LRU ring, but free them. */
+ if (!(block->status & BLOCK_ERROR))
+ {
+ /*
+ Link the block into the LRU ring if it's the last submitted
+ request for the block. This enables eviction for the block.
+ */
+ unreg_request(keycache, block, 1);
+ }
+ else
+ {
+ free_block(keycache, thread_var, block);
+ error= 1;
+ break;
+ }
+
+ next_block:
+ buff+= read_length;
+ filepos+= read_length+offset;
+ offset= 0;
+
+ } while ((length-= read_length));
+ if (MYSQL_KEYCACHE_READ_DONE_ENABLED())
+ {
+ MYSQL_KEYCACHE_READ_DONE((ulong) (keycache->blocks_used *
+ keycache->key_cache_block_size),
+ (ulong) (keycache->blocks_unused *
+ keycache->key_cache_block_size));
+ }
+ goto end;
+ }
+
+no_key_cache:
+ /* Key cache is not used */
+
+ keycache->global_cache_r_requests++;
+ keycache->global_cache_read++;
+
+ if (locked_and_incremented)
+ mysql_mutex_unlock(&keycache->cache_lock);
+ if (my_pread(file, (uchar*) buff, length, filepos, MYF(MY_NABP)))
+ error= 1;
+ if (locked_and_incremented)
+ mysql_mutex_lock(&keycache->cache_lock);
+
+end:
+ if (locked_and_incremented)
+ {
+ dec_counter_for_resize_op(keycache);
+ mysql_mutex_unlock(&keycache->cache_lock);
+ }
+ DBUG_PRINT("exit", ("error: %d", error ));
+ DBUG_RETURN(error ? (uchar*) 0 : start);
+}
+
+
+/*
+ Insert a block of file data from a buffer into key cache
+
+ SYNOPSIS
+ key_cache_insert()
+ keycache pointer to a key cache data structure
+ thread_var pointer to thread specific variables
+ file handler for the file to insert data from
+ filepos position of the block of data in the file to insert
+ level determines the weight of the data
+ buff buffer to read data from
+ length length of the data in the buffer
+
+ NOTES
+ This is used by MyISAM to move all blocks from a index file to the key
+ cache
+
+ RETURN VALUE
+ 0 if a success, 1 - otherwise.
+*/
+
+int key_cache_insert(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ File file, my_off_t filepos, int level,
+ uchar *buff, uint length)
+{
+ int error= 0;
+ DBUG_ENTER("key_cache_insert");
+ DBUG_PRINT("enter", ("fd: %u pos: %lu length: %u",
+ (uint) file,(ulong) filepos, length));
+
+ if (keycache->key_cache_inited)
+ {
+ /* Key cache is used */
+ BLOCK_LINK *block;
+ uint read_length;
+ uint offset;
+ int page_st;
+ my_bool locked_and_incremented= FALSE;
+
+ /*
+ When the keycache is once initialized, we use the cache_lock to
+ reliably distinguish the cases of normal operation, resizing, and
+ disabled cache. We always increment and decrement
+ 'cnt_for_resize_op' so that a resizer can wait for pending I/O.
+ */
+ mysql_mutex_lock(&keycache->cache_lock);
+ /*
+ We do not load index data into a disabled cache nor into an
+ ongoing resize.
+ */
+ if (!keycache->can_be_used || keycache->in_resize)
+ goto no_key_cache;
+ /* Register the pseudo I/O for the next resize. */
+ inc_counter_for_resize_op(keycache);
+ locked_and_incremented= TRUE;
+ /* Loaded data may not always be aligned to cache blocks. */
+ offset= (uint) (filepos % keycache->key_cache_block_size);
+ /* Load data in key_cache_block_size increments. */
+ do
+ {
+ /* Cache could be disabled or resizing in a later iteration. */
+ if (!keycache->can_be_used || keycache->in_resize)
+ goto no_key_cache;
+ /* Start loading at the beginning of the cache block. */
+ filepos-= offset;
+ /* Do not load beyond the end of the cache block. */
+ read_length= length;
+ set_if_smaller(read_length, keycache->key_cache_block_size-offset);
+ DBUG_ASSERT(read_length > 0);
+
+ /* The block has been read by the caller already. */
+ keycache->global_cache_read++;
+ /* Request the cache block that matches file/pos. */
+ keycache->global_cache_r_requests++;
+ block= find_key_block(keycache, thread_var, file, filepos, level, 0,
+ &page_st);
+ if (!block)
+ {
+ /*
+ This happens only for requests submitted during key cache
+ resize. The block is not in the cache and shall not go in.
+ Stop loading index data.
+ */
+ goto no_key_cache;
+ }
+ if (!(block->status & BLOCK_ERROR))
+ {
+ if ((page_st == PAGE_WAIT_TO_BE_READ) ||
+ ((page_st == PAGE_TO_BE_READ) &&
+ (offset || (read_length < keycache->key_cache_block_size))))
+ {
+ /*
+ Either
+
+ this is a secondary request for a block to be read into the
+ cache. The block is in eviction. It is not yet assigned to
+ the requested file block (It does not point to the right
+ hash_link). So we cannot call remove_reader() on the block.
+ And we cannot access the hash_link directly here. We need to
+ wait until the assignment is complete. read_block() executes
+ the correct wait when called with primary == FALSE.
+
+ Or
+
+ this is a primary request for a block to be read into the
+ cache and the supplied data does not fill the whole block.
+
+ This function is called on behalf of a LOAD INDEX INTO CACHE
+ statement, which is a read-only task and allows other
+ readers. It is possible that a parallel running reader tries
+ to access this block. If it needs more data than has been
+ supplied here, it would report an error. To be sure that we
+ have all data in the block that is available in the file, we
+ read the block ourselves.
+
+ Though reading again what the caller did read already is an
+ expensive operation, we need to do this for correctness.
+ */
+ read_block(keycache, thread_var, block,
+ keycache->key_cache_block_size,
+ read_length + offset, (page_st == PAGE_TO_BE_READ));
+ /*
+ A secondary request must now have the block assigned to the
+ requested file block. It does not hurt to check it for
+ primary requests too.
+ */
+ DBUG_ASSERT(keycache->can_be_used);
+ DBUG_ASSERT(block->hash_link->file == file);
+ DBUG_ASSERT(block->hash_link->diskpos == filepos);
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ }
+ else if (page_st == PAGE_TO_BE_READ)
+ {
+ /*
+ This is a new block in the cache. If we come here, we have
+ data for the whole block.
+ */
+ DBUG_ASSERT(block->hash_link->requests);
+ DBUG_ASSERT(block->status & BLOCK_IN_USE);
+ DBUG_ASSERT((page_st == PAGE_TO_BE_READ) ||
+ (block->status & BLOCK_READ));
+
+ mysql_mutex_unlock(&keycache->cache_lock);
+ /*
+ Here other threads may step in and register as secondary readers.
+ They will register in block->wqueue[COND_FOR_REQUESTED].
+ */
+
+ /* Copy data from buff */
+ memcpy(block->buffer+offset, buff, (size_t) read_length);
+
+ mysql_mutex_lock(&keycache->cache_lock);
+ DBUG_ASSERT(block->status & BLOCK_IN_USE);
+ DBUG_ASSERT((page_st == PAGE_TO_BE_READ) ||
+ (block->status & BLOCK_READ));
+ /*
+ After the data is in the buffer, we can declare the block
+ valid. Now other threads do not need to register as
+ secondary readers any more. They can immediately access the
+ block.
+ */
+ block->status|= BLOCK_READ;
+ block->length= read_length+offset;
+ /*
+ Do not set block->offset here. If this block is marked
+ BLOCK_CHANGED later, we want to flush only the modified part. So
+ only a writer may set block->offset down from
+ keycache->key_cache_block_size.
+ */
+ /* Signal all pending requests. */
+ release_whole_queue(&block->wqueue[COND_FOR_REQUESTED]);
+ }
+ else
+ {
+ /*
+ page_st == PAGE_READ. The block is in the buffer. All data
+ must already be present. Blocks are always read with all
+ data available on file. Assert that the block does not have
+ less contents than the preloader supplies. If the caller has
+ data beyond block->length, it means that a file write has
+ been done while this block was in cache and not extended
+ with the new data. If the condition is met, we can simply
+ ignore the block.
+ */
+ DBUG_ASSERT((page_st == PAGE_READ) &&
+ (read_length + offset <= block->length));
+ }
+
+ /*
+ A secondary request must now have the block assigned to the
+ requested file block. It does not hurt to check it for primary
+ requests too.
+ */
+ DBUG_ASSERT(block->hash_link->file == file);
+ DBUG_ASSERT(block->hash_link->diskpos == filepos);
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ } /* end of if (!(block->status & BLOCK_ERROR)) */
+
+ remove_reader(block);
+
+ /* Error injection for coverage testing. */
+ DBUG_EXECUTE_IF("key_cache_insert_block_error",
+ block->status|= BLOCK_ERROR; errno=EIO;);
+
+ /* Do not link erroneous blocks into the LRU ring, but free them. */
+ if (!(block->status & BLOCK_ERROR))
+ {
+ /*
+ Link the block into the LRU ring if it's the last submitted
+ request for the block. This enables eviction for the block.
+ */
+ unreg_request(keycache, block, 1);
+ }
+ else
+ {
+ free_block(keycache, thread_var, block);
+ error= 1;
+ break;
+ }
+
+ buff+= read_length;
+ filepos+= read_length+offset;
+ offset= 0;
+
+ } while ((length-= read_length));
+
+ no_key_cache:
+ if (locked_and_incremented)
+ dec_counter_for_resize_op(keycache);
+ mysql_mutex_unlock(&keycache->cache_lock);
+ }
+ DBUG_RETURN(error);
+}
+
+
+/*
+ Write a buffer into a cached file.
+
+ SYNOPSIS
+
+ key_cache_write()
+ keycache pointer to a key cache data structure
+ thread_var pointer to thread specific variables
+ file handler for the file to write data to
+ filepos position in the file to write data to
+ level determines the weight of the data
+ buff buffer with the data
+ length length of the buffer
+ dont_write if is 0 then all dirty pages involved in writing
+ should have been flushed from key cache
+
+ RETURN VALUE
+ 0 if a success, 1 - otherwise.
+
+ NOTES.
+ The function copies the data of size length from buff into buffers
+ for key cache blocks that are assigned to contain the portion of
+ the file starting with position filepos.
+ It ensures that this data is flushed to the file if dont_write is FALSE.
+ Filepos must be a multiple of 'block_length', but it doesn't
+ have to be a multiple of key_cache_block_size;
+
+ dont_write is always TRUE in the server (info->lock_type is never F_UNLCK).
+*/
+
+int key_cache_write(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ File file, my_off_t filepos, int level,
+ uchar *buff, uint length,
+ uint block_length MY_ATTRIBUTE((unused)),
+ int dont_write)
+{
+ my_bool locked_and_incremented= FALSE;
+ int error=0;
+ DBUG_ENTER("key_cache_write");
+ DBUG_PRINT("enter",
+ ("fd: %u pos: %lu length: %u block_length: %u"
+ " key_block_length: %u",
+ (uint) file, (ulong) filepos, length, block_length,
+ keycache ? keycache->key_cache_block_size : 0));
+
+ if (!dont_write)
+ {
+ /* purecov: begin inspected */
+ /* Not used in the server. */
+ /* Force writing from buff into disk. */
+ keycache->global_cache_w_requests++;
+ keycache->global_cache_write++;
+ if (my_pwrite(file, buff, length, filepos, MYF(MY_NABP | MY_WAIT_IF_FULL)))
+ DBUG_RETURN(1);
+ /* purecov: end */
+ }
+
+ if (keycache->key_cache_inited)
+ {
+ /* Key cache is used */
+ BLOCK_LINK *block;
+ uint read_length;
+ uint offset;
+ int page_st;
+
+ if (MYSQL_KEYCACHE_WRITE_START_ENABLED())
+ {
+ MYSQL_KEYCACHE_WRITE_START(my_filename(file), length,
+ (ulong) (keycache->blocks_used *
+ keycache->key_cache_block_size),
+ (ulong) (keycache->blocks_unused *
+ keycache->key_cache_block_size));
+ }
+
+ /*
+ When the key cache is once initialized, we use the cache_lock to
+ reliably distinguish the cases of normal operation, resizing, and
+ disabled cache. We always increment and decrement
+ 'cnt_for_resize_op' so that a resizer can wait for pending I/O.
+ */
+ mysql_mutex_lock(&keycache->cache_lock);
+ /*
+ Cache resizing has two phases: Flushing and re-initializing. In
+ the flush phase write requests can modify dirty blocks that are
+ not yet in flush. Otherwise they are allowed to bypass the cache.
+ find_key_block() returns NULL in both cases (clean blocks and
+ non-cached blocks).
+
+ After the flush phase new I/O requests must wait until the
+ re-initialization is done. The re-initialization can be done only
+ if no I/O request is in progress. The reason is that
+ key_cache_block_size can change. With enabled cache I/O is done in
+ chunks of key_cache_block_size. Every chunk tries to use a cache
+ block first. If the block size changes in the middle, a block
+ could be missed and data could be written below a cached block.
+ */
+ while (keycache->in_resize && !keycache->resize_in_flush)
+ wait_on_queue(&keycache->resize_queue, &keycache->cache_lock,
+ thread_var);
+ /* Register the I/O for the next resize. */
+ inc_counter_for_resize_op(keycache);
+ locked_and_incremented= TRUE;
+ /* Requested data may not always be aligned to cache blocks. */
+ offset= (uint) (filepos % keycache->key_cache_block_size);
+ /* Write data in key_cache_block_size increments. */
+ do
+ {
+ /* Cache could be disabled in a later iteration. */
+ if (!keycache->can_be_used)
+ goto no_key_cache;
+
+ MYSQL_KEYCACHE_WRITE_BLOCK(keycache->key_cache_block_size);
+ /* Start writing at the beginning of the cache block. */
+ filepos-= offset;
+ /* Do not write beyond the end of the cache block. */
+ read_length= length;
+ set_if_smaller(read_length, keycache->key_cache_block_size-offset);
+ DBUG_ASSERT(read_length > 0);
+
+ /* Request the cache block that matches file/pos. */
+ keycache->global_cache_w_requests++;
+ block= find_key_block(keycache, thread_var, file, filepos, level, 1,
+ &page_st);
+ if (!block)
+ {
+ /*
+ This happens only for requests submitted during key cache
+ resize. The block is not in the cache and shall not go in.
+ Write directly to file.
+ */
+ if (dont_write)
+ {
+ /* Used in the server. */
+ keycache->global_cache_write++;
+ mysql_mutex_unlock(&keycache->cache_lock);
+ if (my_pwrite(file, (uchar*) buff, read_length, filepos + offset,
+ MYF(MY_NABP | MY_WAIT_IF_FULL)))
+ error=1;
+ mysql_mutex_lock(&keycache->cache_lock);
+ }
+ goto next_block;
+ }
+ /*
+ Prevent block from flushing and from being selected for to be
+ freed. This must be set when we release the cache_lock.
+ However, we must not set the status of the block before it is
+ assigned to this file/pos.
+ */
+ if (page_st != PAGE_WAIT_TO_BE_READ)
+ block->status|= BLOCK_FOR_UPDATE;
+ /*
+ We must read the file block first if it is not yet in the cache
+ and we do not replace all of its contents.
+
+ In cases where the cache block is big enough to contain (parts
+ of) index blocks of different indexes, our request can be
+ secondary (PAGE_WAIT_TO_BE_READ). In this case another thread is
+ reading the file block. If the read completes after us, it
+ overwrites our new contents with the old contents. So we have to
+ wait for the other thread to complete the read of this block.
+ read_block() takes care for the wait.
+ */
+ if (!(block->status & BLOCK_ERROR) &&
+ ((page_st == PAGE_TO_BE_READ &&
+ (offset || read_length < keycache->key_cache_block_size)) ||
+ (page_st == PAGE_WAIT_TO_BE_READ)))
+ {
+ read_block(keycache, thread_var, block,
+ offset + read_length >= keycache->key_cache_block_size?
+ offset : keycache->key_cache_block_size,
+ offset, (page_st == PAGE_TO_BE_READ));
+ DBUG_ASSERT(keycache->can_be_used);
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ /*
+ Prevent block from flushing and from being selected for to be
+ freed. This must be set when we release the cache_lock.
+ Here we set it in case we could not set it above.
+ */
+ block->status|= BLOCK_FOR_UPDATE;
+ }
+ /*
+ The block should always be assigned to the requested file block
+ here. It need not be BLOCK_READ when overwriting the whole block.
+ */
+ DBUG_ASSERT(block->hash_link->file == file);
+ DBUG_ASSERT(block->hash_link->diskpos == filepos);
+ DBUG_ASSERT(block->status & BLOCK_IN_USE);
+ DBUG_ASSERT((page_st == PAGE_TO_BE_READ) || (block->status & BLOCK_READ));
+ /*
+ The block to be written must not be marked BLOCK_REASSIGNED.
+ Otherwise it could be freed in dirty state or reused without
+ another flush during eviction. It must also not be in flush.
+ Otherwise the old contens may have been flushed already and
+ the flusher could clear BLOCK_CHANGED without flushing the
+ new changes again.
+ */
+ DBUG_ASSERT(!(block->status & BLOCK_REASSIGNED));
+
+ while (block->status & BLOCK_IN_FLUSHWRITE)
+ {
+ /*
+ Another thread is flushing the block. It was dirty already.
+ Wait until the block is flushed to file. Otherwise we could
+ modify the buffer contents just while it is written to file.
+ An unpredictable file block contents would be the result.
+ While we wait, several things can happen to the block,
+ including another flush. But the block cannot be reassigned to
+ another hash_link until we release our request on it.
+ */
+ wait_on_queue(&block->wqueue[COND_FOR_SAVED], &keycache->cache_lock,
+ thread_var);
+ DBUG_ASSERT(keycache->can_be_used);
+ DBUG_ASSERT(block->status & (BLOCK_READ | BLOCK_IN_USE));
+ /* Still must not be marked for free. */
+ DBUG_ASSERT(!(block->status & BLOCK_REASSIGNED));
+ DBUG_ASSERT(block->hash_link && (block->hash_link->block == block));
+ }
+
+ /*
+ We could perhaps release the cache_lock during access of the
+ data like in the other functions. Locks outside of the key cache
+ assure that readers and a writer do not access the same range of
+ data. Parallel accesses should happen only if the cache block
+ contains multiple index block(fragment)s. So different parts of
+ the buffer would be read/written. An attempt to flush during
+ memcpy() is prevented with BLOCK_FOR_UPDATE.
+ */
+ if (!(block->status & BLOCK_ERROR))
+ {
+ mysql_mutex_unlock(&keycache->cache_lock);
+ memcpy(block->buffer+offset, buff, (size_t) read_length);
+
+ mysql_mutex_lock(&keycache->cache_lock);
+ }
+
+ if (!dont_write)
+ {
+ /* Not used in the server. buff has been written to disk at start. */
+ if ((block->status & BLOCK_CHANGED) &&
+ (!offset && read_length >= keycache->key_cache_block_size))
+ link_to_file_list(keycache, block, block->hash_link->file, 1);
+ }
+ else if (! (block->status & BLOCK_CHANGED))
+ link_to_changed_list(keycache, block);
+ block->status|=BLOCK_READ;
+ /*
+ Allow block to be selected for to be freed. Since it is marked
+ BLOCK_CHANGED too, it won't be selected for to be freed without
+ a flush.
+ */
+ block->status&= ~BLOCK_FOR_UPDATE;
+ set_if_smaller(block->offset, offset);
+ set_if_bigger(block->length, read_length+offset);
+
+ /* Threads may be waiting for the changes to be complete. */
+ release_whole_queue(&block->wqueue[COND_FOR_REQUESTED]);
+
+ /*
+ If only a part of the cache block is to be replaced, and the
+ rest has been read from file, then the cache lock has been
+ released for I/O and it could be possible that another thread
+ wants to evict or free the block and waits for it to be
+ released. So we must not just decrement hash_link->requests, but
+ also wake a waiting thread.
+ */
+ remove_reader(block);
+
+ /* Error injection for coverage testing. */
+ DBUG_EXECUTE_IF("key_cache_write_block_error",
+ block->status|= BLOCK_ERROR;);
+
+ /* Do not link erroneous blocks into the LRU ring, but free them. */
+ if (!(block->status & BLOCK_ERROR))
+ {
+ /*
+ Link the block into the LRU ring if it's the last submitted
+ request for the block. This enables eviction for the block.
+ */
+ unreg_request(keycache, block, 1);
+ }
+ else
+ {
+ /* Pretend a "clean" block to avoid complications. */
+ block->status&= ~(BLOCK_CHANGED);
+ free_block(keycache, thread_var, block);
+ error= 1;
+ break;
+ }
+
+ next_block:
+ buff+= read_length;
+ filepos+= read_length+offset;
+ offset= 0;
+
+ } while ((length-= read_length));
+ goto end;
+ }
+
+no_key_cache:
+ /* Key cache is not used */
+ if (dont_write)
+ {
+ /* Used in the server. */
+ keycache->global_cache_w_requests++;
+ keycache->global_cache_write++;
+ if (locked_and_incremented)
+ mysql_mutex_unlock(&keycache->cache_lock);
+ if (my_pwrite(file, (uchar*) buff, length, filepos,
+ MYF(MY_NABP | MY_WAIT_IF_FULL)))
+ error=1;
+ if (locked_and_incremented)
+ mysql_mutex_lock(&keycache->cache_lock);
+ }
+
+end:
+ if (locked_and_incremented)
+ {
+ dec_counter_for_resize_op(keycache);
+ mysql_mutex_unlock(&keycache->cache_lock);
+ }
+
+ if (MYSQL_KEYCACHE_WRITE_DONE_ENABLED())
+ {
+ MYSQL_KEYCACHE_WRITE_DONE((ulong) (keycache->blocks_used *
+ keycache->key_cache_block_size),
+ (ulong) (keycache->blocks_unused *
+ keycache->key_cache_block_size));
+ }
+
+ DBUG_RETURN(error);
+}
+
+
+/*
+ Free block.
+
+ SYNOPSIS
+ free_block()
+ keycache Pointer to a key cache data structure
+ thread_var Pointer to thread specific variables
+ block Pointer to the block to free
+
+ DESCRIPTION
+ Remove reference to block from hash table.
+ Remove block from the chain of clean blocks.
+ Add block to the free list.
+
+ NOTE
+ Block must not be free (status == 0).
+ Block must not be in free_block_list.
+ Block must not be in the LRU ring.
+ Block must not be in eviction (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH).
+ Block must not be in free (BLOCK_REASSIGNED).
+ Block must not be in flush (BLOCK_IN_FLUSH).
+ Block must not be dirty (BLOCK_CHANGED).
+ Block must not be in changed_blocks (dirty) hash.
+ Block must be in file_blocks (clean) hash.
+ Block must refer to a hash_link.
+ Block must have a request registered on it.
+*/
+
+static void free_block(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ BLOCK_LINK *block)
+{
+ /*
+ Assert that the block is not free already. And that it is in a clean
+ state. Note that the block might just be assigned to a hash_link and
+ not yet read (BLOCK_READ may not be set here). In this case a reader
+ is registered in the hash_link and free_block() will wait for it
+ below.
+ */
+ DBUG_ASSERT((block->status & BLOCK_IN_USE) &&
+ !(block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH |
+ BLOCK_REASSIGNED | BLOCK_IN_FLUSH |
+ BLOCK_CHANGED | BLOCK_FOR_UPDATE)));
+ /* Assert that the block is in a file_blocks chain. */
+ DBUG_ASSERT(block->prev_changed && *block->prev_changed == block);
+ /* Assert that the block is not in the LRU ring. */
+ DBUG_ASSERT(!block->next_used && !block->prev_used);
+ /*
+ IMHO the below condition (if()) makes no sense. I can't see how it
+ could be possible that free_block() is entered with a NULL hash_link
+ pointer. The only place where it can become NULL is in free_block()
+ (or before its first use ever, but for those blocks free_block() is
+ not called). I don't remove the conditional as it cannot harm, but
+ place an DBUG_ASSERT to confirm my hypothesis. Eventually the
+ condition (if()) can be removed.
+ */
+ DBUG_ASSERT(block->hash_link && block->hash_link->block == block);
+ if (block->hash_link)
+ {
+ /*
+ While waiting for readers to finish, new readers might request the
+ block. But since we set block->status|= BLOCK_REASSIGNED, they
+ will wait on block->wqueue[COND_FOR_SAVED]. They must be signalled
+ later.
+ */
+ block->status|= BLOCK_REASSIGNED;
+ wait_for_readers(keycache, block, thread_var);
+ /*
+ The block must not have been freed by another thread. Repeat some
+ checks. An additional requirement is that it must be read now
+ (BLOCK_READ).
+ */
+ DBUG_ASSERT(block->hash_link && block->hash_link->block == block);
+ DBUG_ASSERT((block->status & (BLOCK_READ | BLOCK_IN_USE |
+ BLOCK_REASSIGNED)) &&
+ !(block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH |
+ BLOCK_IN_FLUSH | BLOCK_CHANGED |
+ BLOCK_FOR_UPDATE)));
+ DBUG_ASSERT(block->prev_changed && *block->prev_changed == block);
+ DBUG_ASSERT(!block->prev_used);
+ /*
+ Unset BLOCK_REASSIGNED again. If we hand the block to an evicting
+ thread (through unreg_request() below), other threads must not see
+ this flag. They could become confused.
+ */
+ block->status&= ~BLOCK_REASSIGNED;
+ /*
+ Do not release the hash_link until the block is off all lists.
+ At least not if we hand it over for eviction in unreg_request().
+ */
+ }
+
+ /*
+ Unregister the block request and link the block into the LRU ring.
+ This enables eviction for the block. If the LRU ring was empty and
+ threads are waiting for a block, then the block wil be handed over
+ for eviction immediately. Otherwise we will unlink it from the LRU
+ ring again, without releasing the lock in between. So decrementing
+ the request counter and updating statistics are the only relevant
+ operation in this case. Assert that there are no other requests
+ registered.
+ */
+ DBUG_ASSERT(block->requests == 1);
+ unreg_request(keycache, block, 0);
+ /*
+ Note that even without releasing the cache lock it is possible that
+ the block is immediately selected for eviction by link_block() and
+ thus not added to the LRU ring. In this case we must not touch the
+ block any more.
+ */
+ if (block->status & BLOCK_IN_EVICTION)
+ return;
+
+ /* Error blocks are not put into the LRU ring. */
+ if (!(block->status & BLOCK_ERROR))
+ {
+ /* Here the block must be in the LRU ring. Unlink it again. */
+ DBUG_ASSERT(block->next_used && block->prev_used &&
+ *block->prev_used == block);
+ unlink_block(keycache, block);
+ }
+ if (block->temperature == BLOCK_WARM)
+ keycache->warm_blocks--;
+ block->temperature= BLOCK_COLD;
+
+ /* Remove from file_blocks hash. */
+ unlink_changed(block);
+
+ /* Remove reference to block from hash table. */
+ unlink_hash(keycache, block->hash_link);
+ block->hash_link= NULL;
+
+ block->status= 0;
+ block->length= 0;
+ block->offset= keycache->key_cache_block_size;
+
+ /* Enforced by unlink_changed(), but just to be sure. */
+ DBUG_ASSERT(!block->next_changed && !block->prev_changed);
+ /* Enforced by unlink_block(): not in LRU ring nor in free_block_list. */
+ DBUG_ASSERT(!block->next_used && !block->prev_used);
+ /* Insert the free block in the free list. */
+ block->next_used= keycache->free_block_list;
+ keycache->free_block_list= block;
+ /* Keep track of the number of currently unused blocks. */
+ keycache->blocks_unused++;
+
+ /* All pending requests for this page must be resubmitted. */
+ release_whole_queue(&block->wqueue[COND_FOR_SAVED]);
+}
+
+
+static int cmp_sec_link(BLOCK_LINK **a, BLOCK_LINK **b)
+{
+ return (((*a)->hash_link->diskpos < (*b)->hash_link->diskpos) ? -1 :
+ ((*a)->hash_link->diskpos > (*b)->hash_link->diskpos) ? 1 : 0);
+}
+
+
+/*
+ Flush a portion of changed blocks to disk,
+ free used blocks if requested
+*/
+
+static int flush_cached_blocks(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ File file, BLOCK_LINK **cache,
+ BLOCK_LINK **end,
+ enum flush_type type)
+{
+ int error;
+ int last_errno= 0;
+ uint count= (uint) (end-cache);
+
+ /* Don't lock the cache during the flush */
+ mysql_mutex_unlock(&keycache->cache_lock);
+ /*
+ As all blocks referred in 'cache' are marked by BLOCK_IN_FLUSH
+ we are guarunteed no thread will change them
+ */
+ my_qsort((uchar*) cache, count, sizeof(*cache), (qsort_cmp) cmp_sec_link);
+
+ mysql_mutex_lock(&keycache->cache_lock);
+ /*
+ Note: Do not break the loop. We have registered a request on every
+ block in 'cache'. These must be unregistered by free_block() or
+ unreg_request().
+ */
+ for ( ; cache != end ; cache++)
+ {
+ BLOCK_LINK *block= *cache;
+
+ /*
+ If the block contents is going to be changed, we abandon the flush
+ for this block. flush_key_blocks_int() will restart its search and
+ handle the block properly.
+ */
+ if (!(block->status & BLOCK_FOR_UPDATE))
+ {
+ /* Blocks coming here must have a certain status. */
+ DBUG_ASSERT(block->hash_link);
+ DBUG_ASSERT(block->hash_link->block == block);
+ DBUG_ASSERT(block->hash_link->file == file);
+ DBUG_ASSERT((block->status & ~BLOCK_IN_EVICTION) ==
+ (BLOCK_READ | BLOCK_IN_FLUSH | BLOCK_CHANGED | BLOCK_IN_USE));
+ block->status|= BLOCK_IN_FLUSHWRITE;
+ mysql_mutex_unlock(&keycache->cache_lock);
+ error= (int)my_pwrite(file, block->buffer+block->offset,
+ block->length - block->offset,
+ block->hash_link->diskpos+ block->offset,
+ MYF(MY_NABP | MY_WAIT_IF_FULL));
+ mysql_mutex_lock(&keycache->cache_lock);
+ keycache->global_cache_write++;
+ if (error)
+ {
+ block->status|= BLOCK_ERROR;
+ if (!last_errno)
+ last_errno= errno ? errno : -1;
+ }
+ block->status&= ~BLOCK_IN_FLUSHWRITE;
+ /* Block must not have changed status except BLOCK_FOR_UPDATE. */
+ DBUG_ASSERT(block->hash_link);
+ DBUG_ASSERT(block->hash_link->block == block);
+ DBUG_ASSERT(block->hash_link->file == file);
+ DBUG_ASSERT((block->status & ~(BLOCK_FOR_UPDATE | BLOCK_IN_EVICTION)) ==
+ (BLOCK_READ | BLOCK_IN_FLUSH | BLOCK_CHANGED | BLOCK_IN_USE));
+ /*
+ Set correct status and link in right queue for free or later use.
+ free_block() must not see BLOCK_CHANGED and it may need to wait
+ for readers of the block. These should not see the block in the
+ wrong hash. If not freeing the block, we need to have it in the
+ right queue anyway.
+ */
+ link_to_file_list(keycache, block, file, 1);
+ }
+ block->status&= ~BLOCK_IN_FLUSH;
+ /*
+ Let to proceed for possible waiting requests to write to the block page.
+ It might happen only during an operation to resize the key cache.
+ */
+ release_whole_queue(&block->wqueue[COND_FOR_SAVED]);
+ /* type will never be FLUSH_IGNORE_CHANGED here */
+ if (!(type == FLUSH_KEEP || type == FLUSH_FORCE_WRITE) &&
+ !(block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH |
+ BLOCK_FOR_UPDATE)))
+ {
+ /*
+ Note that a request has been registered against the block in
+ flush_key_blocks_int().
+ */
+ free_block(keycache, thread_var, block);
+ }
+ else
+ {
+ /*
+ Link the block into the LRU ring if it's the last submitted
+ request for the block. This enables eviction for the block.
+ Note that a request has been registered against the block in
+ flush_key_blocks_int().
+ */
+ unreg_request(keycache, block, 1);
+ }
+
+ } /* end of for ( ; cache != end ; cache++) */
+ return last_errno;
+}
+
+
+/*
+ Flush all key blocks for a file to disk, but don't do any mutex locks.
+
+ SYNOPSIS
+ flush_key_blocks_int()
+ keycache pointer to a key cache data structure
+ thread_var pointer to thread specific variables
+ file handler for the file to flush to
+ flush_type type of the flush
+
+ NOTES
+ This function doesn't do any mutex locks because it needs to be called both
+ from flush_key_blocks and flush_all_key_blocks (the later one does the
+ mutex lock in the resize_key_cache() function).
+
+ We do only care about changed blocks that exist when the function is
+ entered. We do not guarantee that all changed blocks of the file are
+ flushed if more blocks change while this function is running.
+
+ RETURN
+ 0 ok
+ 1 error
+*/
+
+static int flush_key_blocks_int(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ File file, enum flush_type type)
+{
+ BLOCK_LINK *cache_buff[FLUSH_CACHE],**cache;
+ int last_errno= 0;
+ int last_errcnt= 0;
+ DBUG_ENTER("flush_key_blocks_int");
+ DBUG_PRINT("enter",("file: %d blocks_used: %lu blocks_changed: %lu",
+ file, keycache->blocks_used, keycache->blocks_changed));
+
+ cache= cache_buff;
+ if (keycache->disk_blocks > 0)
+ {
+ /* Key cache exists and flush is not disabled */
+ int error= 0;
+ uint count= FLUSH_CACHE;
+ BLOCK_LINK **pos,**end;
+ BLOCK_LINK *first_in_switch= NULL;
+ BLOCK_LINK *last_in_flush;
+ BLOCK_LINK *last_for_update;
+ BLOCK_LINK *block, *next;
+#ifndef DBUG_OFF
+ uint cnt=0;
+#endif
+
+ if (type != FLUSH_IGNORE_CHANGED)
+ {
+ /*
+ Count how many key blocks we have to cache to be able
+ to flush all dirty pages with minimum seek moves
+ */
+ count= 0;
+ for (block= keycache->changed_blocks[FILE_HASH(file)] ;
+ block ;
+ block= block->next_changed)
+ {
+ if ((block->hash_link->file == file) &&
+ !(block->status & BLOCK_IN_FLUSH))
+ {
+ count++;
+ DBUG_ASSERT(count<= keycache->blocks_used);
+ }
+ }
+ /*
+ Allocate a new buffer only if its bigger than the one we have.
+ Assure that we always have some entries for the case that new
+ changed blocks appear while we need to wait for something.
+ */
+ if ((count > FLUSH_CACHE) &&
+ !(cache= (BLOCK_LINK**) my_malloc(key_memory_KEY_CACHE,
+ sizeof(BLOCK_LINK*)*count,
+ MYF(0))))
+ cache= cache_buff;
+ /*
+ After a restart there could be more changed blocks than now.
+ So we should not let count become smaller than the fixed buffer.
+ */
+ if (cache == cache_buff)
+ count= FLUSH_CACHE;
+ }
+
+ /* Retrieve the blocks and write them to a buffer to be flushed */
+restart:
+ last_in_flush= NULL;
+ last_for_update= NULL;
+ end= (pos= cache)+count;
+ for (block= keycache->changed_blocks[FILE_HASH(file)] ;
+ block ;
+ block= next)
+ {
+#ifndef DBUG_OFF
+ cnt++;
+ DBUG_ASSERT(cnt <= keycache->blocks_used);
+#endif
+ next= block->next_changed;
+ if (block->hash_link->file == file)
+ {
+ if (!(block->status & (BLOCK_IN_FLUSH | BLOCK_FOR_UPDATE)))
+ {
+ /*
+ Note: The special handling of BLOCK_IN_SWITCH is obsolete
+ since we set BLOCK_IN_FLUSH if the eviction includes a
+ flush. It can be removed in a later version.
+ */
+ if (!(block->status & BLOCK_IN_SWITCH))
+ {
+ /*
+ We care only for the blocks for which flushing was not
+ initiated by another thread and which are not in eviction.
+ Registering a request on the block unlinks it from the LRU
+ ring and protects against eviction.
+ */
+ reg_requests(keycache, block, 1);
+ if (type != FLUSH_IGNORE_CHANGED)
+ {
+ /* It's not a temporary file */
+ if (pos == end)
+ {
+ /*
+ This should happen relatively seldom. Remove the
+ request because we won't do anything with the block
+ but restart and pick it again in the next iteration.
+ */
+ unreg_request(keycache, block, 0);
+ /*
+ This happens only if there is not enough
+ memory for the big block
+ */
+ if ((error= flush_cached_blocks(keycache, thread_var, file,
+ cache, end, type)))
+ {
+ /* Do not loop infinitely trying to flush in vain. */
+ if ((last_errno == error) && (++last_errcnt > 5))
+ goto err;
+ last_errno= error;
+ }
+ /*
+ Restart the scan as some other thread might have changed
+ the changed blocks chain: the blocks that were in switch
+ state before the flush started have to be excluded
+ */
+ goto restart;
+ }
+ /*
+ Mark the block with BLOCK_IN_FLUSH in order not to let
+ other threads to use it for new pages and interfere with
+ our sequence of flushing dirty file pages. We must not
+ set this flag before actually putting the block on the
+ write burst array called 'cache'.
+ */
+ block->status|= BLOCK_IN_FLUSH;
+ /* Add block to the array for a write burst. */
+ *pos++= block;
+ }
+ else
+ {
+ /* It's a temporary file */
+ DBUG_ASSERT(!(block->status & BLOCK_REASSIGNED));
+ /*
+ free_block() must not be called with BLOCK_CHANGED. Note
+ that we must not change the BLOCK_CHANGED flag outside of
+ link_to_file_list() so that it is always in the correct
+ queue and the *blocks_changed counters are correct.
+ */
+ link_to_file_list(keycache, block, file, 1);
+ if (!(block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH)))
+ {
+ /* A request has been registered against the block above. */
+ free_block(keycache, thread_var, block);
+ }
+ else
+ {
+ /*
+ Link the block into the LRU ring if it's the last
+ submitted request for the block. This enables eviction
+ for the block. A request has been registered against
+ the block above.
+ */
+ unreg_request(keycache, block, 1);
+ }
+ }
+ }
+ else
+ {
+ /*
+ Link the block into a list of blocks 'in switch'.
+
+ WARNING: Here we introduce a place where a changed block
+ is not in the changed_blocks hash! This is acceptable for
+ a BLOCK_IN_SWITCH. Never try this for another situation.
+ Other parts of the key cache code rely on changed blocks
+ being in the changed_blocks hash.
+ */
+ unlink_changed(block);
+ link_changed(block, &first_in_switch);
+ }
+ }
+ else if (type != FLUSH_KEEP)
+ {
+ /*
+ During the normal flush at end of statement (FLUSH_KEEP) we
+ do not need to ensure that blocks in flush or update by
+ other threads are flushed. They will be flushed by them
+ later. In all other cases we must assure that we do not have
+ any changed block of this file in the cache when this
+ function returns.
+ */
+ if (block->status & BLOCK_IN_FLUSH)
+ {
+ /* Remember the last block found to be in flush. */
+ last_in_flush= block;
+ }
+ else
+ {
+ /* Remember the last block found to be selected for update. */
+ last_for_update= block;
+ }
+ }
+ }
+ }
+ if (pos != cache)
+ {
+ if ((error=
+ flush_cached_blocks(keycache, thread_var, file, cache, pos, type)))
+ {
+ /* Do not loop inifnitely trying to flush in vain. */
+ if ((last_errno == error) && (++last_errcnt > 5))
+ goto err;
+ last_errno= error;
+ }
+ /*
+ Do not restart here during the normal flush at end of statement
+ (FLUSH_KEEP). We have now flushed at least all blocks that were
+ changed when entering this function. In all other cases we must
+ assure that we do not have any changed block of this file in the
+ cache when this function returns.
+ */
+ if (type != FLUSH_KEEP)
+ goto restart;
+ }
+ if (last_in_flush)
+ {
+ /*
+ There are no blocks to be flushed by this thread, but blocks in
+ flush by other threads. Wait until one of the blocks is flushed.
+ Re-check the condition for last_in_flush. We may have unlocked
+ the cache_lock in flush_cached_blocks(). The state of the block
+ could have changed.
+ */
+ if (last_in_flush->status & BLOCK_IN_FLUSH)
+ wait_on_queue(&last_in_flush->wqueue[COND_FOR_SAVED],
+ &keycache->cache_lock, thread_var);
+ /* Be sure not to lose a block. They may be flushed in random order. */
+ goto restart;
+ }
+ if (last_for_update)
+ {
+ /*
+ There are no blocks to be flushed by this thread, but blocks for
+ update by other threads. Wait until one of the blocks is updated.
+ Re-check the condition for last_for_update. We may have unlocked
+ the cache_lock in flush_cached_blocks(). The state of the block
+ could have changed.
+ */
+ if (last_for_update->status & BLOCK_FOR_UPDATE)
+ wait_on_queue(&last_for_update->wqueue[COND_FOR_REQUESTED],
+ &keycache->cache_lock, thread_var);
+ /* The block is now changed. Flush it. */
+ goto restart;
+ }
+
+ /*
+ Wait until the list of blocks in switch is empty. The threads that
+ are switching these blocks will relink them to clean file chains
+ while we wait and thus empty the 'first_in_switch' chain.
+ */
+ while (first_in_switch)
+ {
+#ifndef DBUG_OFF
+ cnt= 0;
+#endif
+ wait_on_queue(&first_in_switch->wqueue[COND_FOR_SAVED],
+ &keycache->cache_lock, thread_var);
+#ifndef DBUG_OFF
+ cnt++;
+ DBUG_ASSERT(cnt <= keycache->blocks_used);
+#endif
+ /*
+ Do not restart here. We have flushed all blocks that were
+ changed when entering this function and were not marked for
+ eviction. Other threads have now flushed all remaining blocks in
+ the course of their eviction.
+ */
+ }
+
+ if (! (type == FLUSH_KEEP || type == FLUSH_FORCE_WRITE))
+ {
+ BLOCK_LINK *last_for_update= NULL;
+ BLOCK_LINK *last_in_switch= NULL;
+ uint total_found= 0;
+ uint found;
+
+ /*
+ Finally free all clean blocks for this file.
+ During resize this may be run by two threads in parallel.
+ */
+ do
+ {
+ found= 0;
+ for (block= keycache->file_blocks[FILE_HASH(file)] ;
+ block ;
+ block= next)
+ {
+ /* Remember the next block. After freeing we cannot get at it. */
+ next= block->next_changed;
+
+ /* Changed blocks cannot appear in the file_blocks hash. */
+ DBUG_ASSERT(!(block->status & BLOCK_CHANGED));
+ if (block->hash_link->file == file)
+ {
+ /* We must skip blocks that will be changed. */
+ if (block->status & BLOCK_FOR_UPDATE)
+ {
+ last_for_update= block;
+ continue;
+ }
+
+ /*
+ We must not free blocks in eviction (BLOCK_IN_EVICTION |
+ BLOCK_IN_SWITCH) or blocks intended to be freed
+ (BLOCK_REASSIGNED).
+ */
+ if (!(block->status & (BLOCK_IN_EVICTION | BLOCK_IN_SWITCH |
+ BLOCK_REASSIGNED)))
+ {
+ struct st_hash_link *next_hash_link= NULL;
+ my_off_t next_diskpos= 0;
+ File next_file= 0;
+ uint next_status= 0;
+ uint hash_requests= 0;
+
+ total_found++;
+ found++;
+ DBUG_ASSERT(found <= keycache->blocks_used);
+
+ /*
+ Register a request. This unlinks the block from the LRU
+ ring and protects it against eviction. This is required
+ by free_block().
+ */
+ reg_requests(keycache, block, 1);
+
+ /*
+ free_block() may need to wait for readers of the block.
+ This is the moment where the other thread can move the
+ 'next' block from the chain. free_block() needs to wait
+ if there are requests for the block pending.
+ */
+ if (next && (hash_requests= block->hash_link->requests))
+ {
+ /* Copy values from the 'next' block and its hash_link. */
+ next_status= next->status;
+ next_hash_link= next->hash_link;
+ next_diskpos= next_hash_link->diskpos;
+ next_file= next_hash_link->file;
+ DBUG_ASSERT(next == next_hash_link->block);
+ }
+
+ free_block(keycache, thread_var, block);
+ /*
+ If we had to wait and the state of the 'next' block
+ changed, break the inner loop. 'next' may no longer be
+ part of the current chain.
+
+ We do not want to break the loop after every free_block(),
+ not even only after waits. The chain might be quite long
+ and contain blocks for many files. Traversing it again and
+ again to find more blocks for this file could become quite
+ inefficient.
+ */
+ if (next && hash_requests &&
+ ((next_status != next->status) ||
+ (next_hash_link != next->hash_link) ||
+ (next_file != next_hash_link->file) ||
+ (next_diskpos != next_hash_link->diskpos) ||
+ (next != next_hash_link->block)))
+ break;
+ }
+ else
+ {
+ last_in_switch= block;
+ }
+ }
+ } /* end for block in file_blocks */
+ } while (found);
+
+ /*
+ If any clean block has been found, we may have waited for it to
+ become free. In this case it could be possible that another clean
+ block became dirty. This is possible if the write request existed
+ before the flush started (BLOCK_FOR_UPDATE). Re-check the hashes.
+ */
+ if (total_found)
+ goto restart;
+
+ /*
+ To avoid an infinite loop, wait until one of the blocks marked
+ for update is updated.
+ */
+ if (last_for_update)
+ {
+ /* We did not wait. Block must not have changed status. */
+ DBUG_ASSERT(last_for_update->status & BLOCK_FOR_UPDATE);
+ wait_on_queue(&last_for_update->wqueue[COND_FOR_REQUESTED],
+ &keycache->cache_lock, thread_var);
+ goto restart;
+ }
+
+ /*
+ To avoid an infinite loop wait until one of the blocks marked
+ for eviction is switched.
+ */
+ if (last_in_switch)
+ {
+ /* We did not wait. Block must not have changed status. */
+ DBUG_ASSERT(last_in_switch->status & (BLOCK_IN_EVICTION |
+ BLOCK_IN_SWITCH |
+ BLOCK_REASSIGNED));
+ wait_on_queue(&last_in_switch->wqueue[COND_FOR_SAVED],
+ &keycache->cache_lock, thread_var);
+ goto restart;
+ }
+
+ } /* if (! (type == FLUSH_KEEP || type == FLUSH_FORCE_WRITE)) */
+
+ } /* if (keycache->disk_blocks > 0 */
+
+err:
+ if (cache != cache_buff)
+ my_free(cache);
+ if (last_errno)
+ errno=last_errno; /* Return first error */
+ DBUG_RETURN(last_errno != 0);
+}
+
+
+/*
+ Flush all blocks for a file to disk
+
+ SYNOPSIS
+
+ flush_key_blocks()
+ keycache pointer to a key cache data structure
+ thread_var pointer to thread specific variables
+ file handler for the file to flush to
+ flush_type type of the flush
+
+ RETURN
+ 0 ok
+ 1 error
+*/
+
+int flush_key_blocks(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var,
+ File file, enum flush_type type)
+{
+ int res= 0;
+ DBUG_ENTER("flush_key_blocks");
+ DBUG_PRINT("enter", ("keycache: 0x%lx", (long) keycache));
+
+ if (!keycache->key_cache_inited)
+ DBUG_RETURN(0);
+
+ mysql_mutex_lock(&keycache->cache_lock);
+ /* While waiting for lock, keycache could have been ended. */
+ if (keycache->disk_blocks > 0)
+ {
+ inc_counter_for_resize_op(keycache);
+ res= flush_key_blocks_int(keycache, thread_var, file, type);
+ dec_counter_for_resize_op(keycache);
+ }
+ mysql_mutex_unlock(&keycache->cache_lock);
+ DBUG_RETURN(res);
+}
+
+
+/*
+ Flush all blocks in the key cache to disk.
+
+ SYNOPSIS
+ flush_all_key_blocks()
+ keycache pointer to key cache root structure
+ thread_var pointer to thread specific variables
+
+ DESCRIPTION
+
+ Flushing of the whole key cache is done in two phases.
+
+ 1. Flush all changed blocks, waiting for them if necessary. Loop
+ until there is no changed block left in the cache.
+
+ 2. Free all clean blocks. Normally this means free all blocks. The
+ changed blocks were flushed in phase 1 and became clean. However we
+ may need to wait for blocks that are read by other threads. While we
+ wait, a clean block could become changed if that operation started
+ before the resize operation started. To be safe we must restart at
+ phase 1.
+
+ When we can run through the changed_blocks and file_blocks hashes
+ without finding a block any more, then we are done.
+
+ Note that we hold keycache->cache_lock all the time unless we need
+ to wait for something.
+
+ RETURN
+ 0 OK
+ != 0 Error
+*/
+
+static int flush_all_key_blocks(KEY_CACHE *keycache,
+ st_keycache_thread_var *thread_var)
+{
+ BLOCK_LINK *block;
+ uint total_found;
+ uint found;
+ uint idx;
+ DBUG_ENTER("flush_all_key_blocks");
+
+ do
+ {
+ mysql_mutex_assert_owner(&keycache->cache_lock);
+ total_found= 0;
+
+ /*
+ Phase1: Flush all changed blocks, waiting for them if necessary.
+ Loop until there is no changed block left in the cache.
+ */
+ do
+ {
+ found= 0;
+ /* Step over the whole changed_blocks hash array. */
+ for (idx= 0; idx < CHANGED_BLOCKS_HASH; idx++)
+ {
+ /*
+ If an array element is non-empty, use the first block from its
+ chain to find a file for flush. All changed blocks for this
+ file are flushed. So the same block will not appear at this
+ place again with the next iteration. New writes for blocks are
+ not accepted during the flush. If multiple files share the
+ same hash bucket, one of them will be flushed per iteration
+ of the outer loop of phase 1.
+ */
+ if ((block= keycache->changed_blocks[idx]))
+ {
+ found++;
+ /*
+ Flush dirty blocks but do not free them yet. They can be used
+ for reading until all other blocks are flushed too.
+ */
+ if (flush_key_blocks_int(keycache, thread_var,
+ block->hash_link->file,
+ FLUSH_FORCE_WRITE))
+ DBUG_RETURN(1);
+ }
+ }
+
+ } while (found);
+
+ /*
+ Phase 2: Free all clean blocks. Normally this means free all
+ blocks. The changed blocks were flushed in phase 1 and became
+ clean. However we may need to wait for blocks that are read by
+ other threads. While we wait, a clean block could become changed
+ if that operation started before the resize operation started. To
+ be safe we must restart at phase 1.
+ */
+ do
+ {
+ found= 0;
+ /* Step over the whole file_blocks hash array. */
+ for (idx= 0; idx < CHANGED_BLOCKS_HASH; idx++)
+ {
+ /*
+ If an array element is non-empty, use the first block from its
+ chain to find a file for flush. All blocks for this file are
+ freed. So the same block will not appear at this place again
+ with the next iteration. If multiple files share the
+ same hash bucket, one of them will be flushed per iteration
+ of the outer loop of phase 2.
+ */
+ if ((block= keycache->file_blocks[idx]))
+ {
+ total_found++;
+ found++;
+ if (flush_key_blocks_int(keycache, thread_var,
+ block->hash_link->file,
+ FLUSH_RELEASE))
+ DBUG_RETURN(1);
+ }
+ }
+
+ } while (found);
+
+ /*
+ If any clean block has been found, we may have waited for it to
+ become free. In this case it could be possible that another clean
+ block became dirty. This is possible if the write request existed
+ before the resize started (BLOCK_FOR_UPDATE). Re-check the hashes.
+ */
+ } while (total_found);
+
+#ifndef DBUG_OFF
+ /* Now there should not exist any block any more. */
+ for (idx= 0; idx < CHANGED_BLOCKS_HASH; idx++)
+ {
+ DBUG_ASSERT(!keycache->changed_blocks[idx]);
+ DBUG_ASSERT(!keycache->file_blocks[idx]);
+ }
+#endif
+
+ DBUG_RETURN(0);
+}
+
+
+/*
+ Reset the counters of a key cache.
+
+ SYNOPSIS
+ reset_key_cache_counters()
+ name the name of a key cache
+ key_cache pointer to the key kache to be reset
+
+ DESCRIPTION
+ This procedure is used by process_key_caches() to reset the counters of all
+ currently used key caches, both the default one and the named ones.
+
+ RETURN
+ 0 on success (always because it can't fail)
+*/
+
+int reset_key_cache_counters(const char *name MY_ATTRIBUTE((unused)),
+ KEY_CACHE *key_cache)
+{
+ DBUG_ENTER("reset_key_cache_counters");
+ if (!key_cache->key_cache_inited)
+ {
+ DBUG_PRINT("info", ("Key cache %s not initialized.", name));
+ DBUG_RETURN(0);
+ }
+ DBUG_PRINT("info", ("Resetting counters for key cache %s.", name));
+
+ key_cache->global_blocks_changed= 0; /* Key_blocks_not_flushed */
+ key_cache->global_cache_r_requests= 0; /* Key_read_requests */
+ key_cache->global_cache_read= 0; /* Key_reads */
+ key_cache->global_cache_w_requests= 0; /* Key_write_requests */
+ key_cache->global_cache_write= 0; /* Key_writes */
+ DBUG_RETURN(0);
+}
+
+
+#if !defined(DBUG_OFF)
+#define F_B_PRT(_f_, _v_) DBUG_PRINT("assert_fail", (_f_, _v_))
+
+static int fail_block(BLOCK_LINK *block)
+{
+ F_B_PRT("block->next_used: %lx\n", (ulong) block->next_used);
+ F_B_PRT("block->prev_used: %lx\n", (ulong) block->prev_used);
+ F_B_PRT("block->next_changed: %lx\n", (ulong) block->next_changed);
+ F_B_PRT("block->prev_changed: %lx\n", (ulong) block->prev_changed);
+ F_B_PRT("block->hash_link: %lx\n", (ulong) block->hash_link);
+ F_B_PRT("block->status: %u\n", block->status);
+ F_B_PRT("block->length: %u\n", block->length);
+ F_B_PRT("block->offset: %u\n", block->offset);
+ F_B_PRT("block->requests: %u\n", block->requests);
+ F_B_PRT("block->temperature: %u\n", block->temperature);
+ return 0; /* Let the assert fail. */
+}
+
+static int fail_hlink(HASH_LINK *hlink)
+{
+ F_B_PRT("hlink->next: %lx\n", (ulong) hlink->next);
+ F_B_PRT("hlink->prev: %lx\n", (ulong) hlink->prev);
+ F_B_PRT("hlink->block: %lx\n", (ulong) hlink->block);
+ F_B_PRT("hlink->diskpos: %lu\n", (ulong) hlink->diskpos);
+ F_B_PRT("hlink->file: %d\n", hlink->file);
+ return 0; /* Let the assert fail. */
+}
+
+static int cache_empty(KEY_CACHE *keycache)
+{
+ int errcnt= 0;
+ int idx;
+ if (keycache->disk_blocks <= 0)
+ return 1;
+ for (idx= 0; idx < keycache->disk_blocks; idx++)
+ {
+ BLOCK_LINK *block= keycache->block_root + idx;
+ if (block->status || block->requests || block->hash_link)
+ {
+ my_message_local(INFORMATION_LEVEL, "block index: %u", idx);
+ fail_block(block);
+ errcnt++;
+ }
+ }
+ for (idx= 0; idx < keycache->hash_links; idx++)
+ {
+ HASH_LINK *hash_link= keycache->hash_link_root + idx;
+ if (hash_link->requests || hash_link->block)
+ {
+ my_message_local(INFORMATION_LEVEL, "hash_link index: %u", idx);
+ fail_hlink(hash_link);
+ errcnt++;
+ }
+ }
+ if (errcnt)
+ {
+ my_message_local(INFORMATION_LEVEL, "blocks: %d used: %lu",
+ keycache->disk_blocks, keycache->blocks_used);
+ my_message_local(INFORMATION_LEVEL, "hash_links: %d used: %d",
+ keycache->hash_links, keycache->hash_links_used);
+ }
+ return !errcnt;
+}
+#endif
+