The current implementation uses a single page for the log tail and locks the entire shared memory structure while performing any operation. This considerably reduces concurrency. Since log record writes are in the critical path of every update (i.e., every update operation has to log its update), maximizing concurrency is crucial.
This section outlines an alternate, more complex design for the log manager that considerably increases the concurrency.
Concurrency can be enhanced if we reduce the size of the critical section to encompass just the assigning of LSNs. Writing on the log tail page can proceed concurrently because the writes are to non-overlapping portions of the page (the mutual exclusion of assignment of LSNs guarantees this). The scheme is more complicated because we no longer know when a page can be flushed. Also, since LSN assignments proceed at a faster rate, we have to ensure that the page for a log write is in the log tail before allowing the write to proceed. This raises the question of what should be done if the page is not in the log tail.
We propose the following solution to tackle these problems. We assume double buffering, i.e., two log tail pages such that when one is being flushed to disk, the other can be filled by log writes. Associated with each log tail page is the page ID in the log file it corresponds to. Each page ID is protected by a semaphore. We denote the page ID corresponding to log tail page p as ID and the semaphore as sp. Figure 3 illustrates the configuration.
Figure 3: The design for enhancing concurrency. Each page in the log
tail has the page ID of the log file that it corresponds to. Each
page ID is protected by a semaphore. wait is the list of
processes (transactions) waiting for the page ID, ID, to
change. write is the list of transactions that have been
assigned LSNs for page p but not yet written their records.
Let us walk through a typical set of log writes. Initially, both buffers are empty. ID is 0, ID is 1. Every log write goes through a critical section for assignment of LSN. Let the first five log records get LSNs assigned in page 0. All five of these can now concurrently write to page 0. Every time an LSN is assigned for a log write to page ID, an entry is made in write. When the corresponding memcpy to the log tail succeeds, the entry is removed.
When the sixth log write request arrives, the LSN assignment code detects that a page change has occurred. Thus, no more log writes to page ID occurs. This is made note of. Now, when write becomes empty, the flusher can be invoked.
The flusher will check whether another flusher is active for page q. If so, it waits until that flush completes. It will then start writing the page to disk. Since write is empty, it is guaranteed that no further updates to page ID will be made. (Flushers can check whether other flushers are active by having semaphores per log tail page that are downed and uped by the flushers only.)
Meanwhile, LSN assignments and log writes will continue in the page ID. Suppose that log writes to page ID are completed. No further log writes can occur because the page ID is still being flushed. This is detected by writers because LSN.pageID ID. When a transaction detects this, it adds itself to wait and goes to sleep. (Because of double buffering, each transaction will know which buffer it will write to eventually.)
When the flusher is done with page ID, it will down(sp), update ID and up(sp). Finally, it will wake up all processes in wait. These transactions will again check LSN.pageID against ID before proceeding with the log write.
Not much can be gained by increasing the number of buffers, because, finally, a new buffer can be written to only after it has been flushed. Double buffering with the above strategy is likely to give good concurrency.