package simpledb; import java.io.*; import java.util.ArrayList; import java.util.Iterator; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; /** * BufferPool manages the reading and writing of pages into memory from * disk. Access methods call into it to retrieve pages, and it fetches * pages from the appropriate location. *

* The BufferPool is also responsible for locking; when a transaction fetches * a page, BufferPool checks that the transaction has the appropriate * locks to read/write the page. * * @Threadsafe, all fields are final */ public class BufferPool { /** Bytes per page, including header. */ public static final int PAGE_SIZE = 4096; private static int pageSize = PAGE_SIZE; /** Default number of pages passed to the constructor. This is used by other classes. BufferPool should use the numPages argument to the constructor instead. */ public static final int DEFAULT_PAGES = 50; final int numPages; final ConcurrentHashMap pages; // hash table storing current pages in memory private final Random random = new Random(); // for choosing random pages for eviction /* For Lab 4: instance of a private Lock Manager class. Should be instantiated in the constructor for BufferPool. */ private final LockManager lockmgr; // Added for Lab 4 /** * Creates a BufferPool that caches up to numPages pages. * * @param numPages maximum number of pages in this buffer pool. */ public BufferPool(int numPages) { this.numPages = numPages; this.pages = new ConcurrentHashMap(); lockmgr = new LockManager(); // Added for Lab 4 } public static int getPageSize() { return pageSize; } /** * Helper: this should be used for testing only!!! */ public static void setPageSize(int pageSize) { BufferPool.pageSize = pageSize; } /** * Retrieve the specified page with the associated permissions. * Will acquire a lock and may block if that lock is held by another * transaction. *

* The retrieved page should be looked up in the buffer pool. If it * is present, it should be returned. If it is not present, it should * be added to the buffer pool and returned. If there is insufficient * space in the buffer pool, an page should be evicted and the new page * should be added in its place. * * @param tid the ID of the transaction requesting the page * @param pid the ID of the requested page * @param perm the requested permissions on the page */ public Page getPage(TransactionId tid, PageId pid, Permissions perm) throws TransactionAbortedException, DbException { try { // Added for Lab 4: acquire the lock on the page first lockmgr.acquireLock(tid, pid, perm); } catch (DeadlockException e) { throw new TransactionAbortedException(); // caught by callee, who calls transactionComplete() } Page p; synchronized(this) { p = pages.get(pid); if(p == null) { if(pages.size() >= numPages) { evictPage();// added for lab 2 // throw new DbException("Out of buffer pages"); } p = Database.getCatalog().getDatabaseFile(pid.getTableId()).readPage(pid); pages.put(pid, p); } } return p; } /** * Releases the lock on a page. Mainly used for testing. * * @param tid the ID of the transaction requesting the unlock * @param pid the ID of the page to unlock */ public void releasePage(TransactionId tid, PageId pid) { // not necessary for lab1|lab2 lockmgr.releaseLock(tid,pid); // Added for Lab 4 } /** * Release all locks associated with a given transaction. * Calls other version of transactionComplete() with commit set to true. * * @param tid the ID of the transaction requesting the unlock */ public void transactionComplete(TransactionId tid) throws IOException { // not necessary for lab1|lab2 transactionComplete(tid,true); // Added for Lab 4 } /** Return true if the specified transaction has a lock on the specified page */ public boolean holdsLock(TransactionId tid, PageId p) { // not necessary for lab1|lab2 return lockmgr.holdsLock(tid, p); // Added for Lab 4 } /** * Commit or abort a given transaction; then release all locks associated to * the transaction. * If commit == true, be sure to flush any dirty pages from this xact * If commit == false, this signifies an abort, so any changes to dirty * pages should be thrown out of the buffer pool. * * @param tid the ID of the transaction requesting the unlock * @param commit a flag indicating whether we should commit or abort */ public void transactionComplete(TransactionId tid, boolean commit) throws IOException { // some code goes here // not necessary for lab1|lab2 // after dealing with commit vs. abort actions, ask lock manager to release locks lockmgr.releaseAllLocks(tid); // Added for Lab 4 } /** * Add a tuple to the specified table on behalf of transaction tid. Will * acquire a write lock on the page the tuple is added to and any other * pages that are updated (Lock acquisition is not needed for lab2). * May block if the lock(s) cannot be acquired. * * Marks any pages that were dirtied by the operation as dirty by calling * their markDirty bit, and updates cached versions of any pages that have * been dirtied so that future requests see up-to-date pages. * * @param tid the transaction adding the tuple * @param tableId the table to add the tuple to * @param t the tuple to add */ public void insertTuple(TransactionId tid, int tableId, Tuple t) throws DbException, IOException, TransactionAbortedException { // some code goes here // not necessary for lab1 DbFile file = Database.getCatalog().getDatabaseFile(tableId); // let the specific implementation of the file decide which page to add it to ArrayList dirtypages = file.insertTuple(tid, t); synchronized(this) { for (Page p : dirtypages){ p.markDirty(true, tid); // if page in pool already, done. if(pages.get(p.getId()) != null) { //replace old page with new one in case insertTuple returns a new copy of the page pages.put(p.getId(), p); } else { // put page in pool if(pages.size() >= numPages) evictPage(); pages.put(p.getId(), p); } } } } /** * Remove the specified tuple from the buffer pool. * Will acquire a write lock on the page the tuple is removed from and any * other pages that are updated. May block if the lock(s) cannot be acquired. * * Marks any pages that were dirtied by the operation as dirty by calling * their markDirty bit, and updates cached versions of any pages that have * been dirtied so that future requests see up-to-date pages. * * @param tid the transaction deleting the tuple. * @param t the tuple to delete */ public void deleteTuple(TransactionId tid, Tuple t) throws DbException, IOException, TransactionAbortedException { // some code goes here // not necessary for lab1 DbFile file = Database.getCatalog().getDatabaseFile(t.getRecordId().getPageId().getTableId()); ArrayList dirtypages = file.deleteTuple(tid, t); synchronized(this) { for (Page p : dirtypages){ p.markDirty(true, tid); } } } /** * Flush all dirty pages to disk. * Be careful using this routine -- it writes dirty data to disk so will * break simpledb if running in NO STEAL mode. */ public synchronized void flushAllPages() throws IOException { // some code goes here // not necessary for lab1 Iterator i = pages.keySet().iterator(); while(i.hasNext()) flushPage(i.next()); } /** Remove the specific page id from the buffer pool. Needed by the recovery manager to ensure that the buffer pool doesn't keep a rolled back page in its cache. */ public synchronized void discardPage(PageId pid) { // some code goes here // not necessary for labs 1--4 } /** * Flushes a certain page to disk * @param pid an ID indicating the page to flush */ private synchronized void flushPage(PageId pid) throws IOException { // some code goes here // not necessary for lab1 Page p = pages.get(pid); if (p == null) return; //not in buffer pool -- doesn't need to be flushed DbFile file = Database.getCatalog().getDatabaseFile(pid.getTableId()); file.writePage(p); p.markDirty(false, null); } /** Write all pages of the specified transaction to disk. */ public synchronized void flushPages(TransactionId tid) throws IOException { // some code goes here // not necessary for labs 1--4 } /** * Discards a page from the buffer pool. * Flushes the page to disk to ensure dirty pages are updated on disk. */ private synchronized void evictPage() throws DbException { // some code goes here // not necessary for lab1 // try to evict a random page, focusing first on finding one that is not dirty // currently does not check for pages with uncommitted xacts, which could impact future labs Object pids[] = pages.keySet().toArray(); PageId pid = (PageId) pids[random.nextInt(pids.length)]; try { Page p = pages.get(pid); if (p.isDirty() != null) { // this one is dirty, try to find first non-dirty for (PageId pg : pages.keySet()) { if (pages.get(pg).isDirty() == null) { pid = pg; break; } } } flushPage(pid); } catch (IOException e) { throw new DbException("could not evict page"); } pages.remove(pid); } /** * Manages locks on PageIds held by TransactionIds. * S-locks and X-locks are represented as Permissions.READ_ONLY and Permisions.READ_WRITE, respectively * * All the field read/write operations are protected by this * @Threadsafe */ private class LockManager { final int LOCK_WAIT = 10; // milliseconds /** * Sets up the lock manager to keep track of page-level locks for transactions * Should initialize state required for the lock table data structure(s) */ private LockManager() { // some code here } /** * Tries to acquire a lock on page pid for transaction tid, with permissions perm. * If cannot acquire the lock, waits for a timeout period, then tries again. * This method does not return until the lock is granted, or an exception is thrown * * In Exercise 5, checking for deadlock will be added in this method * Note that a transaction should throw a DeadlockException in this method to * signal that it should be aborted. * * @throws DeadlockException after on cycle-based deadlock */ @SuppressWarnings("unchecked") public boolean acquireLock(TransactionId tid, PageId pid, Permissions perm) throws DeadlockException { while(!lock(tid, pid, perm)) { // keep trying to get the lock synchronized(this) { // you don't have the lock yet // possibly some code here for Exercise 5, deadlock detection } try { // couldn't get lock, wait for some time, then try again Thread.sleep(LOCK_WAIT); } catch (InterruptedException e) { // do nothing } } synchronized(this) { // for Exercise 5, might need some cleanup on deadlock detection data structure } return true; } /** * Release all locks corresponding to TransactionId tid. * This method is used by BufferPool.transactionComplete() */ public synchronized void releaseAllLocks(TransactionId tid) { // some code here } /** Return true if the specified transaction has a lock on the specified page */ public synchronized boolean holdsLock(TransactionId tid, PageId p) { // some code here return false; } /** * Answers the question: is this transaction "locked out" of acquiring lock on this page with this perm? * Returns false if this tid/pid/perm lock combo can be achieved (i.e., not locked out), true otherwise. * * Logic: * * if perm == READ_ONLY * if tid is holding any sort of lock on pid, then the tid can acquire the lock (return false). * * if another tid is holding a READ lock on pid, then the tid can acquire the lock (return false). * if another tid is holding a WRITE lock on pid, then tid can not currently * acquire the lock (return true). * * else * if tid is THE ONLY ONE holding a READ lock on pid, then tid can acquire the lock (return false). * if tid is holding a WRITE lock on pid, then the tid already has the lock (return false). * * if another tid is holding any sort of lock on pid, then the tid cannot currenty acquire the lock (return true). */ private synchronized boolean locked(TransactionId tid, PageId pid, Permissions perm) { // some code here return true; } /** * Releases whatever lock this transaction has on this page * Should update lock table data structure(s) * * Note that you do not need to "wake up" another transaction that is waiting for a lock on this page, * since that transaction will be "sleeping" and will wake up and check if the page is available on its own * However, if you decide to change the fact that a thread is sleeping in acquireLock(), you would have to wake it up here */ public synchronized void releaseLock(TransactionId tid, PageId pid) { // some code here } /** * Attempt to lock the given PageId with the given Permissions for this TransactionId * Should update the lock table data structure(s) if successful * * Returns true if the lock attempt was successful, false otherwise */ private synchronized boolean lock(TransactionId tid, PageId pid, Permissions perm) { if(locked(tid, pid, perm)) return false; // this transaction cannot get the lock on this page; it is "locked out" // Else, this transaction is able to get the lock, update lock table // some code here return true; } } }