@Test(timeout = 1000)
  public void loading_with_exclusive_lock_stops_all_others() throws Exception {
    // Given
    ClockSweepPageTable table = new ClockSweepPageTable(1, TEST_PAGE_SIZE, PageCacheMonitor.NULL);
    final BufferPageSwapper io = new BufferPageSwapper(bytesA);

    // When
    final PinnablePage page = table.load(io, 12, PageLock.EXCLUSIVE);

    // Then we should have to wait for the page to be unpinned if we want an
    // exclusive lock on it.
    final AtomicBoolean acquiredPage = new AtomicBoolean(false);
    Thread otherThread =
        fork(
            new Runnable() {
              @Override
              public void run() {
                page.pin(io, 12, PageLock.SHARED);
                acquiredPage.set(true);
              }
            });
    awaitThreadState(otherThread, Thread.State.WAITING);

    assertFalse(acquiredPage.get());

    // And when I unpin mine, the other thread should get it
    page.unpin(PageLock.EXCLUSIVE);

    otherThread.join();

    assertTrue(acquiredPage.get());
  }
  @Test
  public void evicting_must_flush_file() throws Exception {
    // Given a table with 1 entry, which I've modified
    ByteBuffer storageBuffer = ByteBuffer.allocate(TEST_PAGE_SIZE);
    BufferPageSwapper io = new BufferPageSwapper(storageBuffer);

    PinnablePage page = table.load(io, 12, PageLock.EXCLUSIVE);
    page.putBytes(bytesA, 0);
    page.unpin(PageLock.EXCLUSIVE);

    // When I perform an operation that will force eviction
    fork(new Runnable() {
          @Override
          public void run() {
            // This thread will cause the single page in the cache to be replaced
            BufferPageSwapper io = new BufferPageSwapper(ByteBuffer.allocate(1));
            try {
              table.load(io, 3, PageLock.SHARED);
            } catch (IOException e) {
              e.printStackTrace();
            }
          }
        })
        .join();

    // Then my changes should've been forced to disk
    byte[] actual = new byte[bytesA.length];
    storageBuffer.position(0);
    storageBuffer.get(actual);
    assertThat(actual, equalTo(bytesA));
  }
  @Test(timeout = 1000)
  public void readers_and_writers_must_block_on_evicting_page() throws Exception {
    // If we have a loaded page ...
    PageSwapper io = new BufferPageSwapper(ByteBuffer.allocate(TEST_PAGE_SIZE));
    long pageId = 12;
    PinnablePage page = table.load(io, pageId, PageLock.EXCLUSIVE);
    monitor.observe(Fault.class);

    // ... a page that will take a long time to evict
    CountDownLatch latch = monitor.trap(is(new Evict(io, pageId)));

    // ... and a page that is soon up for eviction
    page.unpin(PageLock.EXCLUSIVE);

    // ... then when we observe the eviction taking place
    monitor.observe(Evict.class);

    // ... other threads should not be able to pin that page
    Thread pinForShared = fork($pinUnpin(page, io, pageId, PageLock.SHARED));
    Thread pinForExclusive = fork($pinUnpin(page, io, pageId, PageLock.EXCLUSIVE));
    awaitThreadState(pinForShared, Thread.State.WAITING);
    awaitThreadState(pinForExclusive, Thread.State.WAITING);

    // ... until the eviction finishes
    latch.countDown();
    pinForShared.join();
    pinForExclusive.join();
  }
  @Test
  public void must_notify_io_object_on_eviction() throws Exception {
    // Given
    BufferPageSwapper io = spy(new BufferPageSwapper(bytesA));

    PinnablePage page = table.load(io, 12, PageLock.SHARED);
    page.unpin(PageLock.SHARED);

    // When
    Thread thread =
        fork(
            new Runnable() {
              @Override
              public void run() {
                // This thread will cause the single page in the cache to be replaced
                BufferPageSwapper io = new BufferPageSwapper(ByteBuffer.allocate(TEST_PAGE_SIZE));
                try {
                  table.load(io, 3, PageLock.SHARED).unpin(PageLock.SHARED);
                } catch (IOException e) {
                  e.printStackTrace();
                }
              }
            });
    thread.join();

    // Then
    verify(io).evicted(12);
  }
  @Test(timeout = 1000)
  public void pinning_replaced_page_must_fail() throws Exception {
    // Given
    BufferPageSwapper io = new BufferPageSwapper(bytesA);

    PinnablePage page = table.load(io, 12, PageLock.SHARED);
    page.unpin(PageLock.SHARED);

    // When
    fork(new Runnable() {
          @Override
          public void run() {
            // This thread will cause the single page in the cache to be replaced
            BufferPageSwapper io = new BufferPageSwapper(ByteBuffer.wrap(bytesB));
            try {
              table.load(io, 3, PageLock.SHARED).unpin(PageLock.SHARED);
            } catch (IOException e) {
              e.printStackTrace();
            }
          }
        })
        .join();

    // Then
    assertFalse(page.pin(io, 12, PageLock.SHARED));
  }
  @Test
  public void flushing_pages_with_specific_pageio_must_not_race_with_eviction() throws Exception {
    // The idea is that we repeatedly load a page with an EXCLUSIVE lock, and unpin it so it can
    // be evicted. As soon as we have unpinned, we repeatedly try to flush it with our given
    // PageIO. If this causes an exception to be thrown, then we've raced with the eviction
    // where we shouldn't.
    PageSwapper io = new BufferPageSwapper(ByteBuffer.allocate(TEST_PAGE_SIZE));
    long pageId = 12;

    PinnablePage page = table.load(io, pageId, PageLock.EXCLUSIVE);
    monitor.observe(Fault.class);
    page.unpin(PageLock.EXCLUSIVE); // eviction is now possible
    LockSupport.unpark(sweeperThread);

    while (monitor.tryObserve(Evict.class) == null) {
      table.flush(io);
    }
  }
  @Test
  public void loading_with_shared_lock_allows_other_shared() throws Exception {
    // Given
    BufferPageSwapper io = new BufferPageSwapper(bytesA);

    // When
    PinnablePage page = table.load(io, 12, PageLock.SHARED);

    // Then we should be able to grab another shared lock on it
    assertTrue(page.pin(io, 12, PageLock.SHARED));
  }
  @Test
  public void loading_must_read_file() throws Exception {
    // Given
    BufferPageSwapper io = new BufferPageSwapper(bytesA);

    // When
    PinnablePage page = table.load(io, 1, PageLock.EXCLUSIVE);

    // Then
    byte[] actual = new byte[bytesA.length];
    page.getBytes(actual, 0);

    assertThat(actual, equalTo(bytesA));
  }
  @Test
  public void must_notify_monitor_of_evicted_pages() throws Exception {
    // If we load a page ...
    PageSwapper io = new BufferPageSwapper(ByteBuffer.allocate(TEST_PAGE_SIZE));
    long pageId = 12;
    PinnablePage page = table.load(io, pageId, PageLock.EXCLUSIVE);
    page.unpin(PageLock.EXCLUSIVE);

    // ... then we should observe its page fault
    assertThat(monitor.observe(Fault.class), is(new Fault(io, pageId)));

    // ... and when it sits idle for long enough, we should observe its eviction
    assertThat(monitor.observe(Evict.class), is(new Evict(io, pageId)));
  }