/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source 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; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source 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, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.db.table; import com.caucho.db.Database; import com.caucho.db.block.Block; import com.caucho.db.block.BlockStore; import com.caucho.db.index.BTree; import com.caucho.db.index.KeyCompare; // import com.caucho.db.lock.Lock; import com.caucho.db.sql.CreateQuery; import com.caucho.db.sql.Expr; import com.caucho.db.sql.Parser; import com.caucho.db.sql.QueryContext; import com.caucho.db.xa.DbTransaction; import com.caucho.env.thread.TaskWorker; import com.caucho.inject.Module; import com.caucho.sql.SQLExceptionWrapper; import com.caucho.util.Alarm; import com.caucho.util.L10N; import com.caucho.vfs.Path; import com.caucho.vfs.ReadStream; import com.caucho.vfs.TempBuffer; import com.caucho.vfs.TempStream; import com.caucho.vfs.WriteStream; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.locks.Lock; import java.util.logging.Level; import java.util.logging.Logger; /** * Table format: * * <pre> * Block 0: allocation table * Block 1: fragment table * Block 2: table definition * 0 - store data * 1024 - table data * 1024 - index pointers * 2048 - CREATE text * Block 3: first data * </pre> */ @Module public class Table extends BlockStore { private final static Logger log = Logger.getLogger(Table.class.getName()); private final static L10N L = new L10N(Table.class); private final static int ROOT_DATA_OFFSET = STORE_CREATE_END; private final static int INDEX_ROOT_OFFSET = ROOT_DATA_OFFSET + 32; private final static int ROOT_DATA_END = ROOT_DATA_OFFSET + 1024; public final static int INLINE_BLOB_SIZE = 120; public final static long ROW_CLOCK_MIN = 1024; public final static byte ROW_VALID = 0x1; public final static byte ROW_ALLOC = 0x2; public final static byte ROW_MASK = 0x3; private final static String DB_VERSION = "Resin-DB 4.0.6"; private final static String MIN_VERSION = "Resin-DB 4.0.6"; private final Row _row; private final int _rowLength; private final int _rowsPerBlock; private final int _rowEnd; private final Constraint[]_constraints; private final Column _autoIncrementColumn; private long _entries; private static final int FREE_ROW_BLOCK_SIZE = 256; private final AtomicLongArray _insertFreeRowBlockArray = new AtomicLongArray(FREE_ROW_BLOCK_SIZE); private final AtomicInteger _insertFreeRowBlockHead = new AtomicInteger(); private final AtomicInteger _insertFreeRowBlockTail = new AtomicInteger(); private long _rowTailTop = BlockStore.BLOCK_SIZE * FREE_ROW_BLOCK_SIZE; private final AtomicLong _rowTailOffset = new AtomicLong(); private final RowAllocator _rowAllocator = new RowAllocator(); private final AtomicLong _rowDeleteCount = new AtomicLong(); // clock counters for row insert allocation private long _rowClockTop; private long _rowClockOffset; private long _clockRowFree; private long _clockRowUsed; private long _clockBlockFree; private long _clockRowDeleteCount; private long _autoIncrementValue = -1; Table(Database database, String name, Row row, Constraint constraints[]) { super(database, name, null); _row = row; _constraints = constraints; _rowLength = _row.getLength(); _rowsPerBlock = BLOCK_SIZE / _rowLength; _rowEnd = _rowLength * _rowsPerBlock; Column []columns = _row.getColumns(); Column autoIncrementColumn = null; for (int i = 0; i < columns.length; i++) { columns[i].setTable(this); if (columns[i].getAutoIncrement() >= 0) autoIncrementColumn = columns[i]; } _autoIncrementColumn = autoIncrementColumn; //new Lock("table-insert:" + name); //new Lock("table-alloc:" + name); } Row getRow() { return _row; } /** * Returns the length of a row. */ public int getRowLength() { return _rowLength; } /** * Returns the end of the row */ int getRowEnd() { return _rowEnd; } public final Column []getColumns() { return _row.getColumns(); } /** * Returns the table's constraints. */ public final Constraint []getConstraints() { return _constraints; } /** * Returns the auto-increment column. */ public Column getAutoIncrementColumn() { return _autoIncrementColumn; } /** * Returns the column for the given column name. * * @param name the column name * * @return the column */ public Column getColumn(String name) { Column []columns = getColumns(); for (int i = 0; i < columns.length; i++) { if (columns[i].getName().equals(name)) return columns[i]; } return null; } /** * Returns the column index for the given column name. * * @param name the column name * * @return the column index. */ public int getColumnIndex(String name) throws SQLException { Column []columns = getColumns(); for (int i = 0; i < columns.length; i++) { if (columns[i].getName().equals(name)) return i; } return -1; } // // initialization // /** * Loads the table from the file. */ public static Table loadFromFile(Database db, String name) throws IOException, SQLException { Path path = db.getPath().lookup(name + ".db"); if (! path.exists()) { if (log.isLoggable(Level.FINE)) log.fine(db + " '" + path.getNativePath() + "' is an unknown table"); return null; //throw new SQLException(L.l("table {0} does not exist", name)); } String version = null; ReadStream is = path.openRead(); try { // skip allocation table and fragment table is.skip(DATA_START + ROOT_DATA_OFFSET); StringBuilder sb = new StringBuilder(); int ch; while ((ch = is.read()) > 0) { sb.append((char) ch); } version = sb.toString(); if (! version.startsWith("Resin-DB")) { throw new SQLException(L.l("table {0} is not a Resin DB. Version '{1}'", name, version)); } else if (version.compareTo(MIN_VERSION) < 0 || DB_VERSION.compareTo(version) < 0) { throw new SQLException(L.l("table {0} is out of date. Old version {1}.", name, version)); } } finally { is.close(); } is = path.openRead(); try { // skip allocation table and fragment table is.skip(DATA_START + ROOT_DATA_END); StringBuilder cb = new StringBuilder(); int ch; while ((ch = is.read()) > 0) { cb.append((char) ch); } String sql = cb.toString(); if (log.isLoggable(Level.FINER)) log.finer("Table[" + name + "] " + version + " loading\n" + sql); try { CreateQuery query = (CreateQuery) Parser.parse(db, sql); TableFactory factory = query.getFactory(); if (! factory.getName().equalsIgnoreCase(name)) throw new IOException(L.l("factory {0} does not match", name)); Table table = new Table(db, factory.getName(), factory.getRow(), factory.getConstraints()); table.init(); //if (! table.validateIndexesSafe()) { table.clearIndexes(); table.initIndexes(); table.rebuildIndexes(); //} return table; } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); throw new SQLException(L.l("can't load table {0} in {1}.\n{2}", name, path.getNativePath(), e.toString())); } } finally { is.close(); } } /** * Creates the table. */ @Override public void create() throws IOException, SQLException { super.create(); initIndexes(); byte []tempBuffer = new byte[BLOCK_SIZE]; getReadWrite().readBlock(BLOCK_SIZE, tempBuffer, 0, BLOCK_SIZE); TempStream ts = new TempStream(); WriteStream os = new WriteStream(ts); try { for (int i = 0; i < ROOT_DATA_OFFSET; i++) os.write(tempBuffer[i]); writeTableHeader(os); } finally { os.close(); } TempBuffer head = ts.getHead(); int offset = 0; for (; head != null; head = head.getNext()) { byte []buffer = head.getBuffer(); int length = head.getLength(); System.arraycopy(buffer, 0, tempBuffer, offset, length); for (; length < buffer.length; length++) { tempBuffer[offset + length] = 0; } offset += buffer.length; } for (; offset < BLOCK_SIZE; offset++) tempBuffer[offset] = 0; boolean isPriority = false; getReadWrite().writeBlock(BLOCK_SIZE, tempBuffer, 0, BLOCK_SIZE, isPriority); _database.addTable(this); } /** * Initialize the indexes */ private void initIndexes() throws IOException, SQLException { Column []columns = _row.getColumns(); for (int i = 0; i < columns.length; i++) { Column column = columns[i]; if (! column.isUnique()) continue; KeyCompare keyCompare = column.getIndexKeyCompare(); if (keyCompare == null) continue; Block rootBlock = allocateIndexBlock(); long rootBlockId = rootBlock.getBlockId(); rootBlock.free(); BTree btree = new BTree(this, rootBlockId, column.getLength(), keyCompare); column.setIndex(btree); } } /** * Clears the indexes */ private void clearIndexes() throws IOException { Column []columns = _row.getColumns(); for (int i = 0; i < columns.length; i++) { BTree index = columns[i].getIndex(); if (index == null) continue; long rootAddr = index.getIndexRoot(); Block block = readBlock(addressToBlockId(rootAddr)); try { byte []blockBuffer = block.getBuffer(); synchronized (blockBuffer) { for (int j = 0; j < blockBuffer.length; j++) { blockBuffer[j] = 0; } block.setDirty(0, BLOCK_SIZE); } } finally { block.free(); } } long blockAddr = 0; while ((blockAddr = firstBlock(blockAddr + BLOCK_SIZE, ALLOC_INDEX)) > 0) { deallocateBlock(blockAddr); } } /** * Rebuilds the indexes */ private void rebuildIndexes() throws IOException, SQLException { DbTransaction xa = DbTransaction.create(); xa.setAutoCommit(true); try { TableIterator iter = createTableIterator(); iter.init(xa); Column []columns = _row.getColumns(); while (iter.nextBlock()) { iter.initRow(); byte []blockBuffer = iter.getBuffer(); while (iter.nextRow()) { try { long rowAddress = iter.getRowAddress(); int rowOffset = iter.getRowOffset(); for (int i = 0; i < columns.length; i++) { Column column = columns[i]; /* if (column.getIndex() != null) System.out.println(Long.toHexString(iter.getBlock().getBlockId()) + ":" + Long.toHexString(rowAddress) + ":" + Long.toHexString(rowOffset) + ": " + column.getIndexKeyCompare().toString(blockBuffer, rowOffset + column.getColumnOffset(), column.getLength())); */ column.setIndex(xa, blockBuffer, rowOffset, rowAddress, null); } } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } } } } finally { xa.commit(); } } /** * Rebuilds the indexes */ public void validate() throws SQLException { try { validateIndexes(); } catch (IOException e) { throw new SQLExceptionWrapper(e); } } private boolean validateIndexesSafe() { try { validateIndexes(); return true; } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } return false; } /** * Rebuilds the indexes */ public void validateIndexes() throws IOException, SQLException { DbTransaction xa = DbTransaction.create(); xa.setAutoCommit(true); TableIterator iter = null; try { iter = createTableIterator(); iter.init(xa); Column []columns = _row.getColumns(); while (iter.nextBlock()) { iter.initRow(); byte []blockBuffer = iter.getBuffer(); while (iter.nextRow()) { try { long rowAddress = iter.getRowAddress(); int rowOffset = iter.getRowOffset(); for (int i = 0; i < columns.length; i++) { Column column = columns[i]; column.validateIndex(xa, blockBuffer, rowOffset, rowAddress); } } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } } } } finally { if (iter != null) iter.free(); xa.commit(); } } private void writeTableHeader(WriteStream os) throws IOException { os.print(DB_VERSION); os.write(0); while (os.getPosition() < INDEX_ROOT_OFFSET) { os.write(0); } Column []columns = _row.getColumns(); for (int i = 0; i < columns.length; i++) { if (! columns[i].isUnique()) continue; BTree index = columns[i].getIndex(); if (index != null) { writeLong(os, index.getIndexRoot()); } else { writeLong(os, 0); } } while (os.getPosition() < ROOT_DATA_END) { os.write(0); } os.print("CREATE TABLE " + getName() + "("); for (int i = 0; i < _row.getColumns().length; i++) { Column column = _row.getColumns()[i]; if (i != 0) os.print(","); os.print(column.getName()); os.print(" "); switch (column.getTypeCode()) { case IDENTITY: os.print("IDENTITY"); break; case VARCHAR: os.print("VARCHAR(" + column.getDeclarationSize() + ")"); break; case VARBINARY: os.print("VARBINARY(" + column.getDeclarationSize() + ")"); break; case BINARY: os.print("BINARY(" + column.getDeclarationSize() + ")"); break; case SHORT: os.print("SMALLINT"); break; case INT: os.print("INTEGER"); break; case LONG: os.print("BIGINT"); break; case DOUBLE: os.print("DOUBLE"); break; case DATE: os.print("TIMESTAMP"); break; case BLOB: os.print("BLOB"); break; case NUMERIC: { NumericColumn numeric = (NumericColumn) column; os.print("NUMERIC(" + numeric.getPrecision() + "," + numeric.getScale() + ")"); break; } default: throw new UnsupportedOperationException(String.valueOf(column)); } if (column.isPrimaryKey()) os.print(" PRIMARY KEY"); else if (column.isUnique()) os.print(" UNIQUE"); if (column.isNotNull()) os.print(" NOT NULL"); Expr defaultExpr = column.getDefault(); if (defaultExpr != null) { os.print(" DEFAULT ("); os.print(defaultExpr); os.print(")"); } if (column.getAutoIncrement() >= 0) os.print(" auto_increment"); } os.print(")"); /* writeLong(os, _blockMax); writeLong(os, _entries); writeLong(os, _clockAddr); */ } public TableIterator createTableIterator() { assertStoreActive(); return new TableIterator(this); } /** * Returns the next auto-increment value. */ public long nextAutoIncrement(QueryContext context) throws SQLException { synchronized (this) { if (_autoIncrementValue >= 0) return ++_autoIncrementValue; } long max = 0; try { TableIterator iter = createTableIterator(); iter.init(context); while (iter.next()) { byte []buffer = iter.getBuffer(); long blockId = iter.getBlockId(); long value = _autoIncrementColumn.getLong(blockId, buffer, iter.getRowOffset()); if (max < value) max = value; } } catch (IOException e) { throw new SQLExceptionWrapper(e); } synchronized (this) { if (_autoIncrementValue < max) _autoIncrementValue = max; return ++_autoIncrementValue; } } // // insert code // /** * Inserts a new row, returning the row address. */ public long insert(QueryContext queryContext, DbTransaction xa, ArrayList<Column> columns, ArrayList<Expr> values) throws IOException, SQLException { if (log.isLoggable(Level.ALL)) log.log(Level.ALL, "db table " + getName() + " insert row xa:" + xa); Block block = null; try { while (true) { long blockId = allocateInsertRowBlock(); block = xa.loadBlock(this, blockId); int rowOffset = allocateRow(block, xa); if (rowOffset >= 0) { insertRow(queryContext, xa, columns, values, block, rowOffset); block.saveAllocation(); freeRowBlockId(blockId); return blockIdToAddress(blockId, rowOffset); } Block freeBlock = block; block = null; freeBlock.free(); } } catch (InterruptedException e) { throw new IllegalStateException(e); } finally { if (block != null) block.free(); } } private int allocateRow(Block block, DbTransaction xa) throws IOException, SQLException, InterruptedException { Lock blockLock = block.getWriteLock(); blockLock.tryLock(xa.getTimeout(), TimeUnit.MILLISECONDS); try { block.read(); byte []buffer = block.getBuffer(); int rowOffset = 0; for (; rowOffset < _rowEnd; rowOffset += _rowLength) { if (buffer[rowOffset] == 0) { buffer[rowOffset] = ROW_ALLOC; block.setDirty(rowOffset, rowOffset + 1); return rowOffset; } } } finally { blockLock.unlock(); } return -1; } public void insertRow(QueryContext queryContext, DbTransaction xa, ArrayList<Column> columns, ArrayList<Expr> values, Block block, int rowOffset) throws SQLException { byte []buffer = block.getBuffer(); long rowAddr = blockIdToAddress(block.getBlockId(), rowOffset); //System.out.println("ADDR:" + rowAddr + " " + rowOffset + " " + block); TableIterator iter = createTableIterator(); TableIterator []iterSet = new TableIterator[] { iter }; // QueryContext context = QueryContext.allocate(); boolean isReadOnly = false; queryContext.init(xa, iterSet, isReadOnly); iter.init(queryContext); boolean isOkay = false; queryContext.lock(); try { iter.setRow(block, rowOffset); if (buffer[rowOffset] != ROW_ALLOC) throw new IllegalStateException(L.l("Expected ROW_ALLOC at '{0}'", buffer[rowOffset])); for (int i = rowOffset + _rowLength - 1; rowOffset < i; i--) buffer[i] = 0; for (int i = 0; i < columns.size(); i++) { Column column = columns.get(i); Expr value = values.get(i); column.setExpr(xa, buffer, rowOffset, value, queryContext); } // lock for insert, i.e. entries, indices, and validation // XXX: the set index needs to handle the validation //xa.lockWrite(_insertLock); try { validate(block, rowOffset, queryContext, xa); for (int i = 0; i < columns.size(); i++) { Column column = columns.get(i); column.setIndex(xa, buffer, rowOffset, rowAddr, queryContext); } buffer[rowOffset] = (byte) ((buffer[rowOffset] & ~ROW_MASK) | ROW_VALID); xa.addUpdateBlock(block); if (_autoIncrementColumn != null) { long blockId = iter.getBlockId(); long value = _autoIncrementColumn.getLong(blockId, buffer, rowOffset); synchronized (this) { if (_autoIncrementValue < value) _autoIncrementValue = value; } } block.setDirty(rowOffset, rowOffset + _rowLength); _entries++; isOkay = true; } catch (SQLException e) { // e.printStackTrace(); throw e; } finally { // xa.unlockWrite(_insertLock); if (! isOkay) { delete(xa, block, buffer, rowOffset, false); block.setDirty(rowOffset, rowOffset + _rowLength); } } } finally { queryContext.unlock(); } } // // row allocation // private long allocateInsertRowBlock() throws IOException { long blockId = allocateRowBlockId(); if (blockId != 0) { return blockId; } long rowTailOffset = _rowTailOffset.get(); blockId = firstRowBlock(rowTailOffset); if (blockId <= 0) { Block block = allocateRow(); blockId = block.getBlockId(); // System.out.println("ALLOC: " + blockId + " " + _rowTailOffset.get() + " " + _rowTailTop); block.free(); } _rowTailOffset.compareAndSet(rowTailOffset, blockId + BLOCK_SIZE); return blockId; } // // allocator // private void fillFreeRows() { if (_rowTailOffset.get() < _rowTailTop && isClosed()) return; while (scanClock()) { if (! resetClock()) { return; } } } private boolean scanClock() { while (! isClosed () && isFreeRowBlockIdAvailable()) { long clockBlockId = _rowClockOffset; try { clockBlockId = firstRowBlock(clockBlockId); if (clockBlockId < 0) { _rowClockOffset = _rowClockTop; return true; } if (isRowBlockFree(clockBlockId)) { _clockBlockFree++; freeRowBlockId(clockBlockId); } } catch (IOException e) { log.log(Level.FINE, e.toString(), e); clockBlockId = _rowClockTop; } finally { _rowClockOffset = clockBlockId + BlockStore.BLOCK_SIZE; } } return false; } private boolean resetClock() { // force 50% free rows before clock starts again long newRowCount = (_clockRowUsed - _clockRowFree) / _rowsPerBlock; long deleteRowCount = (_rowDeleteCount.get() - _clockRowDeleteCount) / _rowsPerBlock; if (_clockRowFree < ROW_CLOCK_MIN && _rowClockOffset > 0) { // minimum 256 blocks of free rows newRowCount = ROW_CLOCK_MIN; } else if (deleteRowCount < ROW_CLOCK_MIN) { // if none deleted, don't clock newRowCount = ROW_CLOCK_MIN; } if (newRowCount > 0) { _rowTailTop = _rowTailOffset.get() + newRowCount * _rowLength; _rowClockOffset = _rowTailTop; return false; } else { // System.out.println("RESET: used=" + _clockRowUsed + " free=" + _clockRowFree + " top=" + _rowTailTop); _rowClockOffset = 0; _rowClockTop = _rowTailOffset.get(); _clockRowUsed = 0; _clockRowFree = 0; _clockRowDeleteCount = _rowDeleteCount.get(); return true; } } /** * Test if any row in the block is free */ private boolean isRowBlockFree(long blockId) throws IOException { if (isClosed()) return false; Block block = readBlock(blockId); try { int rowOffset = 0; byte []buffer = block.getBuffer(); boolean isFree = false; for (; rowOffset < _rowEnd; rowOffset += _rowLength) { if (buffer[rowOffset] == 0) { isFree = true; _clockRowFree++; } else _clockRowUsed++; } return isFree; } finally { block.free(); } } private boolean isFreeRowBlockIdAvailable() { int head = _insertFreeRowBlockHead.get(); int tail = _insertFreeRowBlockTail.get(); return (head + 1) % FREE_ROW_BLOCK_SIZE != tail; } private long allocateRowBlockId() { while (true) { int tail = _insertFreeRowBlockTail.get(); int head = _insertFreeRowBlockHead.get(); if (head == tail) { _rowAllocator.wake(); return 0; } long blockId = _insertFreeRowBlockArray.getAndSet(tail, 0); int nextTail = (tail + 1) % FREE_ROW_BLOCK_SIZE; _insertFreeRowBlockTail.compareAndSet(tail, nextTail); if (blockId > 0) { int size = (head - tail + FREE_ROW_BLOCK_SIZE) % FREE_ROW_BLOCK_SIZE; if (2 * size < FREE_ROW_BLOCK_SIZE) { _rowAllocator.wake(); } return blockId; } } } private void freeRowBlockId(long blockId) { while (true) { int head = _insertFreeRowBlockHead.get(); int tail = _insertFreeRowBlockTail.get(); int nextHead = (head + 1) % FREE_ROW_BLOCK_SIZE; if (nextHead == tail) return; _insertFreeRowBlockHead.compareAndSet(head, nextHead); if (_insertFreeRowBlockArray.compareAndSet(head, 0, blockId)) return; } } // // insert // /** * Validates the given row. */ private void validate(Block block, int rowOffset, QueryContext queryContext, DbTransaction xa) throws SQLException { TableIterator row = createTableIterator(); TableIterator []rows = new TableIterator[] { row }; row.setRow(block, rowOffset); for (int i = 0; i < _constraints.length; i++) { _constraints[i].validate(rows, queryContext, xa); } } void delete(DbTransaction xa, Block block, byte []buffer, int rowOffset, boolean isDeleteIndex) throws SQLException { byte rowState = buffer[rowOffset]; /* if ((rowState & ROW_MASK) == 0) return; */ buffer[rowOffset] = (byte) ((rowState & ~ROW_MASK) | ROW_ALLOC); Column []columns = _row.getColumns(); for (int i = 0; i < columns.length; i++) { columns[i].deleteData(xa, buffer, rowOffset); } if (isDeleteIndex) { for (int i = 0; i < columns.length; i++) { try { columns[i].deleteIndex(xa, buffer, rowOffset); } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } } } buffer[rowOffset] = 0; _rowDeleteCount.incrementAndGet(); } @Override public void close() { _row.close(); super.close(); _rowAllocator.destroy(); } private void writeLong(WriteStream os, long value) throws IOException { os.write((int) (value >> 56)); os.write((int) (value >> 48)); os.write((int) (value >> 40)); os.write((int) (value >> 32)); os.write((int) (value >> 24)); os.write((int) (value >> 16)); os.write((int) (value >> 8)); os.write((int) value); } @Override public String toString() { int id = Alarm.isTest() ? 1 : getId(); return getClass().getSimpleName() + "[" + getName() + ":" + id + "]"; } class RowAllocator extends TaskWorker { @Override public long runTask() { fillFreeRows(); return -1; } } }