blob: 2ded24a0e5fbb6547e96582d704367f366a2d576 [file] [log] [blame]
/*
*
* Derby - Class org.apache.derbyTesting.functionTests.tests.store.ClobReclamationTest
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package org.apache.derbyTesting.functionTests.tests.store;
import java.lang.Integer;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import junit.framework.Test;
import org.apache.derby.shared.common.sanity.SanityManager;
import org.apache.derbyTesting.functionTests.util.Formatters;
import org.apache.derbyTesting.junit.BaseJDBCTestCase;
import org.apache.derbyTesting.junit.CleanDatabaseTestSetup;
import org.apache.derbyTesting.junit.JDBC;
import org.apache.derbyTesting.junit.TestConfiguration;
/**
* Verify that space gets reclaimed for multi-threaded Clob updates
*
*/
public class ClobReclamationTest extends BaseJDBCTestCase {
// Need to adjust NUM_THREADS and expectedNumAllocated.
// For 2 threads expectedNumAllocated is 5
// For 100 threads expectedNumAllocated is 201
private static final int NUM_THREADS = 2;
private static int expectedNumAllocated = 5;
public ClobReclamationTest(String name) {
super(name);
}
/**
* Two threads simultaneously updating a table. Threads each
* update a separate row with a long value (>32K). NUMALLOCATED
* pages should not grow past expected value after 500 updates
* by each thread.
*
* @param lockTable true if we should get an exclusive lock on the table
* before update
* @param updateSingleRow true if we should try updating a single row
* instead of different rows
*
* @throws SQLException
* @throws InterruptedException
*/
public void testMultiThreadedUpdate(final boolean lockTable, boolean updateSingleRow) throws SQLException,
InterruptedException {
// need to do a getConnection or we get a
// junit assertion that driver is not registered.
Connection conn = getConnection();
final String updateString = Formatters.repeatChar("a", 33000);
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
final int key = updateSingleRow ? 1 : i + 1;
threads[i] = new Thread() {
public void run() {
try {
Connection conn = openDefaultConnection();
conn.setAutoCommit(false);
ClobReclamationTest.fiveHundredUpdates(conn,
updateString, key, lockTable);
} catch (SQLException e) {
fail(e.getMessage());
}
}
};
}
for (int i = 0; i < NUM_THREADS; i++) {
threads[i].start();
}
for (int i = 0; i < NUM_THREADS; i++) {
threads[i].join();
}
checkNumAllocatedPages("CLOBTAB",expectedNumAllocated, false);
}
/**
* Check that table has specified number of allocated pages.
*
* @param table
* @param expectedAlloc
* @throws SQLException
*/
private void checkNumAllocatedPages(
String table,
int expectedAlloc,
boolean retry_with_sleeps)
throws SQLException {
// Check the space table
// Should not have grown.
PreparedStatement ps = prepareStatement("SELECT NUMALLOCATEDPAGES FROM "
+ " new org.apache.derby.diag.SpaceTable('APP',?) t"
+ " WHERE CONGLOMERATENAME = ?");
ps.setString(1, table);
ps.setString(2, table);
// initialize previous_alloc_count such that we will always try
// at least one sleep/retry.
int previous_alloc_count = Integer.MAX_VALUE;
int num_retries = 0;
for(;;)
{
// loop until success, or until sleep/retry does not result
// in less allocated pages.
// get 1 row, it has the num allocated page count.
ResultSet rs = ps.executeQuery();
rs.next();
int num_allocated_pages = rs.getInt(1);
// first check if count is the expected value, if so done.
if (num_allocated_pages == expectedAlloc)
{
// expected result is met, success
break;
}
else if (retry_with_sleeps)
{
if (num_allocated_pages < previous_alloc_count)
{
num_retries++;
// background thread made progress in last sleep,
// try sleeping again, wait longer as this machine
// seems to take longer to process the post
// commit work. Most environments just need one
// short retry, while enviroments where this does
// not work need much more time. Make second retry
// much longer.
try
{
if (num_retries <= 1)
Thread.sleep(10000);
else
Thread.sleep(60000 * num_retries);
}
catch (Exception ex)
{
// just ignore interrupted sleep
}
}
else
{
// sleep has not found less alloc rows let the assert fail.
assertTrue(
"Fail with retries -- num_allocated_pages:" +
num_allocated_pages +
" == expectedAlloc: " + expectedAlloc +
" previous_alloc_count: " + previous_alloc_count +
" num_retries: " + num_retries,
num_allocated_pages == expectedAlloc);
}
// force at least 2 retries
if (num_retries > 1)
previous_alloc_count = num_allocated_pages;
}
else
{
// no retries allowed, let assert fail.
assertTrue(
"Fail, no retries -- num_allocated_pages:" +
num_allocated_pages +
" > expectedAlloc: " + expectedAlloc,
num_allocated_pages == expectedAlloc);
}
rs.close();
}
}
/**
* Check that table has specified number of free pages.
*
* @param table
* @param expectedFree expected number of free pages.
* @throws SQLException
*/
private void checkNumFreePages(
String table,
int expectedFree) throws SQLException {
// Check the space table
// Should not have grown.
PreparedStatement ps =
prepareStatement(
"SELECT NUMFREEPAGES FROM "
+ " new org.apache.derby.diag.SpaceTable('APP',?) t"
+ " WHERE CONGLOMERATENAME = ?");
ps.setString(1, table);
ps.setString(2, table);
ResultSet rs = ps.executeQuery();
JDBC.assertFullResultSet(rs, new String[][] { { "" + expectedFree } });
}
private void checkNumFreePagesMax(
String table,
int expectedFreeMax) throws SQLException {
// Check the space table
// Should not have grown.
PreparedStatement ps =
prepareStatement(
"SELECT NUMFREEPAGES FROM "
+ " new org.apache.derby.diag.SpaceTable('APP',?) t"
+ " WHERE CONGLOMERATENAME = ?");
ps.setString(1, table);
ps.setString(2, table);
ResultSet rs = ps.executeQuery();
// get 1 row, it has the num free page count and make sure it is
// lower than the passed in maximum.
rs.next();
int numfreerows = rs.getInt(1);
assertTrue(
"Fail -- numfreerows:" + numfreerows +
" > expectedFreeMax: " + expectedFreeMax,
numfreerows < expectedFreeMax);
rs.close();
ps.close();
}
private static void fiveHundredUpdates(Connection conn,
String updateString, int key, boolean lockTable) throws SQLException {
PreparedStatement ps = conn
.prepareStatement("UPDATE CLOBTAB SET C = ? WHERE I = ?");
for (int i = 0; i < 500; i++) {
if (lockTable) {
Statement s = conn.createStatement();
s.executeUpdate("LOCK TABLE CLOBTAB IN EXCLUSIVE MODE");
}
ps.setString(1, updateString);
ps.setInt(2, key);
ps.executeUpdate();
conn.commit();
}
}
/**
* Test multithreaded clob update using standard row locking
* @throws SQLException
* @throws InterruptedException
*/
public void testMultiThreadedUpdateRowLocking() throws SQLException, InterruptedException {
testMultiThreadedUpdate(false /* don't lock table */, false /*don't update single row*/);
}
/**
* Test multithreaded clob update but get an exclusive lock on the
* table for each update. We can't enable this teset until DERBY-4054
* is fixed.
*
* @throws SQLException
* @throws InterruptedException
*/
public void xtestMultiThreadedUpdateTableLocking() throws SQLException, InterruptedException {
testMultiThreadedUpdate(true /*lock table */, false /* don't update single row */ );
}
/**
* Test multiThreaded update of single row to cause lock contention
* This will trigger the row lock retry case of DERBY-4055
*
* @throws SQLException
* @throws InterruptedException
*/
public void xtestMultiThreadUpdateSingleRow() throws SQLException, InterruptedException {
testMultiThreadedUpdate(false /*don't lock table */, true /* update single row */ );
}
/**
* Make sure we reclaim space on rollback. Cannot enable this test
* until DERBY-4057 is fixed.
*
* @throws SQLException
*/
public void testReclamationOnRollback() throws SQLException {
setAutoCommit(false);
String insertString = Formatters.repeatChar("a", 33000);
PreparedStatement ps = prepareStatement("INSERT INTO CLOBTAB2 VALUES(?,?)");
for (int i = 0; i < 500; i++) {
ps.setInt(1, i);
ps.setString(2, insertString);
ps.executeUpdate();
rollback();
}
// sleep 5 seconds to give background space reclamation worker thread
// a chance to reclaim all the aborted insert space.
try
{
Thread.sleep(5000);
}
catch (Exception ex)
{
// just ignore interrupted sleep
}
checkNumAllocatedPages("CLOBTAB2",1,true);
}
/**
* Test for DERBY-4182.
*
* This test just exercises the abort specific part of DERBY-4182. After
* the fix abort of an insert containing a blob will leave the head row,
* plus the first page of the overflow chain. The rest of the chain
* will be moved to free pages.
*
* @throws SQLException
*/
public void testBlobLinkedListReclamationOnRollback() throws SQLException {
setAutoCommit(false);
int clob_length = 200000;
// pick a clob bigger than 2*max page size
String insertString = Formatters.repeatChar("a", 200000);
PreparedStatement ps =
prepareStatement("INSERT INTO CLOBTAB3 VALUES(?,?)");
int numrows = 500;
for (int i = 0; i < numrows; i++) {
ps.setInt(1, i);
ps.setString(2, insertString);
ps.executeUpdate();
rollback();
}
ps.close();
// until DERBY-4057 fixed expect space to be 2 pages per row plus
// 1 head page per container. DERBY-4057 has been fixed so allocated
// pages is showing up as 1 in my runs. Am a bit worried that because
// this is thread and post commit dependent that we may have to play
// with the "expected" allocated a little to allow for post commit
// on some machines not running fast enough. For now just setting
// to 1 as the head page will remain allocated, all the rest should
// be free after post commit reclaim.
checkNumAllocatedPages("CLOBTAB3", 1, true);
// expect most free pages to get used by subsequent inserts. Only
// free pages should be from the last remaining aborted insert of
// the last clob chain. It should include all of the clob except the
// head page of the chain: (sizeof(clob) / page size) - 1
// Derby should default to 32k page size for any table with a clob in
// it.
// after fix for DERBY-4057 work gets queued immediately after the
// rollback, but also the next insert happens at same time. On
// my machine I am getting 12 pages free which looks like space from
// the first abort gets used for the 3rd insert, space from second
// abort gets used for the 4th insert ... Running full test suite
// on my machine got 14 free pages which is still very good considering
// before change there were 1000 allocated pages and now there is 1
// allocated and 14 free. There is a timing
// issue with test and maybe a sleep of some sort is necessary after
// the rollback.
// declaring correct run if only max of free pages from 20 rows worth
// of free space remains.
// ((clob length / page size ) * 20) +
// 1 page for int divide round off - 1 for the head page.
// The above check that there is 1 allocated page shows that we
// are reclaiming space. The number of free pages left is very
// machine dependent as it depends on speed of background thread
// reclaiming space and the above user loop doing the inserts
// concurrent with the background threads reclaiming the aborted
// inserted space.
// See DERBY-6775 for better test
/*
checkNumFreePagesMax("CLOBTAB3", ((clob_length / 32000) * 20) + 1 - 1);
commit();
*/
// running inplace compress should reclaim all the remaining aborted
// insert space, previous to fix inplace compress would leave stranded
// allocated pages that were part of the clob overflow chains.
CallableStatement call_compress =
prepareCall(
"CALL SYSCS_UTIL.SYSCS_INPLACE_COMPRESS_TABLE(?, ?, 1, 1, 1)");
call_compress.setString(1, "APP");
call_compress.setString(2, "CLOBTAB3");
call_compress.executeUpdate();
// all space except for head page should be reclaimed.
checkNumAllocatedPages("CLOBTAB3", 1, false);
// should be no free pages after the inplace compress of a table with
// all deleted rows.
checkNumFreePages("CLOBTAB3", 0);
commit();
}
public static Test suite() {
Test suite = TestConfiguration.embeddedSuite(ClobReclamationTest.class);
return new CleanDatabaseTestSetup(suite) {
/**
* Creates the table used in the test cases.
*
*/
protected void decorateSQL(Statement s) throws SQLException {
if (SanityManager.DEBUG) {
SanityManager.DEBUG_SET("DaemonTrace");
SanityManager.DEBUG_SET("verbose_heap_post_commit");
}
Connection conn = s.getConnection();
s
.executeUpdate("CREATE TABLE CLOBTAB (I INT PRIMARY KEY NOT NULL, c CLOB)");
PreparedStatement ps = conn
.prepareStatement("INSERT INTO CLOBTAB VALUES(?,?)");
String insertString = "hello";
for (int i = 1; i <= NUM_THREADS; i++) {
ps.setInt(1, i);
ps.setString(2, insertString);
ps.executeUpdate();
}
s.executeUpdate("CREATE TABLE CLOBTAB2 (I INT, C CLOB)");
s.executeUpdate("CREATE TABLE CLOBTAB3 (I INT, C CLOB)");
}
protected void tearDown() throws Exception {
if (SanityManager.DEBUG) {
SanityManager.DEBUG_CLEAR("DaemonTrace");
}
super.tearDown();
}
};
}
}