org.hyperic.hq.agent.db.DiskList.java Source code

Java tutorial

Introduction

Here is the source code for org.hyperic.hq.agent.db.DiskList.java

Source

/*
 * NOTE: This copyright does *not* cover user programs that use HQ
 * program services by normal system calls through the application
 * program interfaces provided as part of the Hyperic Plug-in Development
 * Kit or the Hyperic Client Development Kit - this is merely considered
 * normal use of the program, and does *not* fall under the heading of
 * "derived work".
 * 
 * Copyright (C) [2004, 2005, 2006], Hyperic, Inc.
 * This file is part of HQ.
 * 
 * HQ is free software; you can redistribute it and/or modify
 * it under the terms version 2 of the GNU General Public License as
 * published by the Free Software Foundation. This program 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. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA.
 */

package org.hyperic.hq.agent.db;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperic.hq.agent.stats.AgentStatsCollector;

/**
 * A DiskList is a representation of a list on disk.  The basic
 * usage is to add members to the end of the list, and remove 
 * entries from it.  The iterators returned support the remove()
 * operation, but will throw a ConcurrentModificationException 
 * and fail-fast if an update is detected while iteration is occuring.
 *
 * The storage is contained in 2 files, one housing the data, and
 * the other housing the index.  
 *
 * The format of the data file is as follows:
 *
 * [Record]
 *      data     - recordSize bytes containing the raw data
 *
 * The format of the index file is as follows:
 *
 * [Idx]
 *      boolean  - Indicates whether the record is in use or not
 *      long int - Index of the previous record
 *      long int - Index of the next record
 */

public class DiskList {
    private static final int IDX_REC_LEN = 1 + 8 + 8;
    private static final Log log = LogFactory.getLog(DiskList.class.getName());

    private final String fileName;
    private final String idxFileName;
    private final RandomAccessFile indexFile;
    protected RandomAccessFile dataFile;
    private int recordSize; // Size of each record
    private long firstRec; // IDX of first record
    private long lastRec; // IDX of last record
    private final byte[] padBytes; // Utility array for padding
    protected SortedSet freeList; // Set(Long) of free rec idxs
    private int modNum; // Modification random number
    private final long checkSize; // Start to check for unused blocks
    // when the datafile reaches this 
    // size in bytes
    private final int checkPerc; // Max percent (0-100) of free space
    // allowed in the data file.  Only
    // significant when datafile size is
    // greated than checkSize
    private final long maxLength; // Max file size in bytes
    private final Random rand;
    private boolean closed;
    private static final AgentStatsCollector statsCollector = AgentStatsCollector.getInstance();
    private static final String DISK_LIST_DISK_ITERATOR_REMOVE_TIME = AgentStatsCollector.DISK_LIST_DISK_ITERATOR_REMOVE_TIME;
    private static final String DISK_LIST_READ_RECORD_TIME = AgentStatsCollector.DISK_LIST_READ_RECORD_TIME;
    private static final String DISK_LIST_ADD_TO_LIST_TIME = AgentStatsCollector.DISK_LIST_ADD_TO_LIST_TIME;
    private static final String DISK_LIST_DELETE_ALL_RECORDS_TIME = AgentStatsCollector.DISK_LIST_DELETE_ALL_RECORDS_TIME;
    static {
        statsCollector.register(DISK_LIST_ADD_TO_LIST_TIME);
        statsCollector.register(DISK_LIST_READ_RECORD_TIME);
        statsCollector.register(DISK_LIST_DISK_ITERATOR_REMOVE_TIME);
        statsCollector.register(DISK_LIST_DELETE_ALL_RECORDS_TIME);
    }

    /**
     * Construct a new DiskList
     */
    public DiskList(File dataFile, int recordSize, long checkSize, int checkPerc) throws IOException {
        this(dataFile, recordSize, checkSize, checkPerc, Long.MAX_VALUE);
    }

    /**
     * Construct a new DiskList
     *
     * @param dataFile   the base location for the datafile.  The index
     *                   file will be the same, with .idx appended
     * @param recordSize The maximum size for any record within the 
     *                   data file.  All records in the list will
     *                   occupy this many bytes worth of data
     * @param checkSize  Size in to bytes to start checking for
     *                   unused blocks
     * @param checkPerc  Maximum percentage of free blocks allowed when
     *                   data file is greater than checkSize.
     */
    public DiskList(File dataFile, int recordSize, long checkSize, int checkPerc, long maxLength)
            throws IOException {
        File idxFile;
        idxFile = new File(dataFile + ".idx");
        this.fileName = dataFile.getName();
        this.idxFileName = idxFile.getName();
        this.rand = new Random();
        this.dataFile = new RandomAccessFile(dataFile, "rw");
        this.recordSize = recordSize;
        this.padBytes = new byte[Math.max(recordSize, IDX_REC_LEN)];
        this.modNum = this.rand.nextInt();
        this.checkSize = checkSize;
        this.checkPerc = checkPerc;
        if (log.isDebugEnabled()) {
            log.debug("Setting max length for " + this.fileName + " to " + maxLength + " bytes");
        }
        this.maxLength = maxLength;
        this.indexFile = new RandomAccessFile(idxFile, "rw");
        this.genFreeList(idxFile);
        this.closed = false;
    }

    /**
     * Get the precentage of free space available in the datafile rounded
     * to the nearest whole number. (0-100)
     */
    private long getDataFileFreePercentage() throws IOException {
        double dataBytes = this.dataFile.length();
        double freeBytes = (this.freeList.size() * this.recordSize);

        return Math.round((freeBytes * 100) / dataBytes);
    }

    /**
     * Do maintenance on the data and index files.  If the datafile size and 
     * the free block percentage exceed the defined thresholds, the extra
     * free blocks will be removed by truncating the data and index files.
     *
     * Since truncation is used, some times it will be possible that even
     * though the criteria are met, we won't be able to delete the free space.
     * This is a recoverable situation though, since new blocks will be
     * inserted at the beginning of the data file.
     */
    private void doMaintenence() throws IOException {
        long lastData = this.dataFile.length() / this.recordSize;
        long lastFree = ((Long) this.freeList.last()).longValue();

        // Nothing we can do if the last block in the 
        // file is not free
        if (lastData != (lastFree + 1)) {
            return;
        }

        // Simple iteration of the list.  May be faster to do a
        // binary search using freeList.size() to determine if all
        // blocks are free, but this is more readable, and we are
        // dealing with small numbers (< a few million)
        long firstFree = lastFree;
        while (this.freeList.contains(new Long(firstFree - 1))) {
            firstFree--;
        }

        synchronized (this.dataFile) {
            // Truncate the data file
            this.dataFile.setLength(firstFree * this.recordSize);

            // Truncate the index file.
            this.indexFile.setLength(firstFree * IDX_REC_LEN);

            // Remove the free blocks deleted from the freelist.
            SortedSet subset = this.freeList.headSet(new Long(firstFree - 1));
            // Must create a new TreeSet, since the sorted set imposes
            // restrictions on maximum and minimum key values.
            this.freeList = new TreeSet(subset);
        }

        long num = (lastFree - firstFree) + 1;
        this.log.info(
                "Deleted " + (num * this.recordSize) + " bytes from " + this.fileName + " (" + num + " blocks)");
    }

    /**
     * A quick routine, which simply zips through the index file,
     * pulling out information about which records are free.
     *
     * We open up the file seperately here, so we can use the
     * buffered input stream, which makes our initial startup much
     * faster, if there is a lot of data sitting in the list.
     */
    private void genFreeList(File idxFile) throws IOException {
        BufferedInputStream bIs;
        FileInputStream fIs = null;
        DataInputStream dIs;

        this.firstRec = -1;
        this.lastRec = -1;

        // TreeSet is used here to ensure a natural ordering of
        // the elements.
        this.freeList = new TreeSet();

        try {
            fIs = new FileInputStream(idxFile);

            bIs = new BufferedInputStream(fIs);
            dIs = new DataInputStream(bIs);

            for (long idx = 0;; idx++) {
                boolean used;
                long prev, next;

                try {
                    used = dIs.readBoolean();
                } catch (EOFException exc) {
                    break;
                }

                prev = dIs.readLong();
                next = dIs.readLong();

                if (used == false) {
                    this.freeList.add(new Long(idx));
                } else {
                    if (prev == -1) {
                        this.firstRec = idx;
                    }

                    if (next == -1) {
                        this.lastRec = idx;
                    }
                }
            }
        } catch (FileNotFoundException exc) {
            return;
        } finally {
            try {
                if (fIs != null) {
                    fIs.close();
                }
            } catch (IOException exc) {
            }
        }
    }

    /**
     * Add the string to the list of data being stored in the DiskList.
     *
     * @param data Data to add to the end of the list
     */
    public void addToList(String data) throws IOException {
        if (this.closed) {
            throw new IOException("Datafile already closed");
        }
        ByteArrayOutputStream bOs = new ByteArrayOutputStream(this.recordSize);
        DataOutputStream dOs = new DataOutputStream(bOs);
        dOs.writeUTF(data);
        if (bOs.size() > this.recordSize) {
            throw new IOException(
                    "Data length(" + bOs.size() + ") exceeds " + "maximum record length(" + this.recordSize + ")");
        }
        final long start = now();
        bOs.write(this.padBytes, 0, this.recordSize - bOs.size());
        byte[] bytes = bOs.toByteArray();

        synchronized (this.dataFile) {
            Long firstFreeL;
            long firstFree;

            this.modNum = this.rand.nextInt();

            try {
                firstFreeL = (Long) this.freeList.first();
                firstFree = firstFreeL.longValue();
                this.freeList.remove(firstFreeL);
            } catch (NoSuchElementException exc) {
                // Else we're adding to the end
                firstFree = this.indexFile.length() / IDX_REC_LEN;
            }

            // Write the record to the data file
            this.dataFile.seek(firstFree * this.recordSize);
            this.dataFile.write(bytes);

            bOs.reset();
            dOs.writeBoolean(true); // Is Used
            dOs.writeLong(this.lastRec); // Previous record idx
            dOs.writeLong(-1); // Next record idx

            // Write the index for the record we just made
            this.indexFile.seek(firstFree * IDX_REC_LEN);
            bytes = bOs.toByteArray();
            this.indexFile.write(bytes, 0, bytes.length);

            // Update the previous 'last' record to point to us
            if (this.lastRec != -1) {
                this.indexFile.seek((this.lastRec * IDX_REC_LEN) + 1 + 8);
                this.indexFile.writeLong(firstFree);
            }

            this.lastRec = firstFree;
            if (this.firstRec == -1) {
                this.firstRec = firstFree;
            }
        }

        if (this.dataFile.length() > this.maxLength) {
            this.log.error("Maximum file size for data file: " + this.fileName + " reached (" + this.maxLength
                    + " bytes), truncating.");
            deleteAllRecords();
        }
        long duration = now() - start;
        statsCollector.addStat(duration, DISK_LIST_ADD_TO_LIST_TIME);
    }

    private static long now() {
        return System.currentTimeMillis();
    }

    private static class Record {
        private boolean isUsed;
        private long prevIdx;
        private long nextIdx;
        private String data;
    }

    private Record readRecord(long recNo) throws IOException {
        Record res = new Record();

        if (recNo < 0) {
            throw new IllegalArgumentException("IDX must be positive");
        }

        final long start = now();
        synchronized (this.dataFile) {
            this.dataFile.seek(recNo * this.recordSize);
            res.data = this.dataFile.readUTF();

            this.indexFile.seek(recNo * IDX_REC_LEN);
            res.isUsed = this.indexFile.readBoolean();
            res.prevIdx = this.indexFile.readLong();
            res.nextIdx = this.indexFile.readLong();
        }
        final long duration = now() - start;
        statsCollector.addStat(duration, DISK_LIST_READ_RECORD_TIME);

        return res;
    }

    /**
     * Delete all the records from storage. 
     */
    public void deleteAllRecords() throws IOException {
        IOException sExc = null;

        if (this.closed) {
            throw new IOException("Datafile already closed");
        }
        final long start = now();

        synchronized (this.dataFile) {
            this.modNum = this.rand.nextInt();
            this.firstRec = -1;
            this.lastRec = -1;

            try {
                this.indexFile.setLength(0);
            } catch (IOException exc) {
                this.log.error("IOException while truncating file " + idxFileName);
                if (this.log.isDebugEnabled()) {
                    this.log.debug(exc);
                }
                sExc = exc;
            }

            try {
                this.dataFile.setLength(0);
            } catch (IOException exc) {
                this.log.error("IOException while truncating file " + fileName);
                if (this.log.isDebugEnabled()) {
                    this.log.debug(exc);
                }
                if (sExc != null) {
                    sExc = exc;
                }
            }
            this.freeList.clear();
        }
        final long duration = now() - start;
        statsCollector.addStat(duration, DISK_LIST_DELETE_ALL_RECORDS_TIME);

        if (sExc != null) {
            throw sExc;
        }
    }

    public void removeRecord(long recNo) throws IOException {
        if (recNo < 0) {
            throw new IllegalArgumentException("IDX must be positive");
        }

        synchronized (this.dataFile) {
            long prevIdx, nextIdx;

            this.modNum = this.rand.nextInt();

            // Handle all the individual cases, to improve disk I/O performance
            // while maintaining data integrity if someone kills us during
            // the operation
            if (recNo == this.firstRec) {
                if (recNo == this.lastRec) {
                    // It's the only record -- it's unused, and add to freeList
                    this.firstRec = -1;
                    this.lastRec = -1;
                } else {
                    // It's the first in the list, but not the last
                    this.indexFile.seek((recNo * IDX_REC_LEN) + 1 + 8);
                    nextIdx = this.indexFile.readLong();

                    // Set next->prev to -1
                    this.indexFile.seek((nextIdx * IDX_REC_LEN) + 1);
                    this.indexFile.writeLong(-1);

                    this.firstRec = nextIdx;
                }
            } else if (recNo == this.lastRec) {
                // It's the last in the list, but not the first
                this.indexFile.seek((recNo * IDX_REC_LEN) + 1);
                prevIdx = this.indexFile.readLong();

                // Set prev->next to -1
                this.indexFile.seek((prevIdx * IDX_REC_LEN) + 1 + 8);
                this.indexFile.writeLong(-1);

                this.lastRec = prevIdx;
            } else {
                // Otherwise, it's somewhere in the middle, so we have to
                // update both the previous and next
                this.indexFile.seek((recNo * IDX_REC_LEN) + 1);
                prevIdx = this.indexFile.readLong();
                nextIdx = this.indexFile.readLong();

                // Set prev->next = next
                this.indexFile.seek((prevIdx * IDX_REC_LEN) + 1 + 8);
                this.indexFile.writeLong(nextIdx);

                // Set next->prev = prev
                this.indexFile.seek((nextIdx * IDX_REC_LEN) + 1);
                this.indexFile.writeLong(prevIdx);
            }

            this.indexFile.seek(recNo * IDX_REC_LEN);
            this.indexFile.writeBoolean(false);

            this.freeList.add(new Long(recNo));
        }

        long length = this.dataFile.length();
        long percFree = this.getDataFileFreePercentage();
        if ((length > this.checkSize) && (percFree > this.checkPerc)) {
            this.doMaintenence();
        }
    }

    /**
     * Close the DiskList.  All subsequent methods will
     * result in an IOException being thrown.
     */
    public void close() throws IOException {
        IOException sExc = null;

        if (this.closed) {
            throw new IOException("Datafile already closed");
        }

        this.closed = true;

        try {
            this.dataFile.close();
        } catch (IOException exc) {
            this.log.error("IOException while closing file " + fileName);
            if (this.log.isDebugEnabled()) {
                this.log.debug(exc);
            }
            sExc = exc;
        }

        try {
            this.indexFile.close();
        } catch (IOException exc) {
            this.log.error("IOException while closing file " + idxFileName);
            if (this.log.isDebugEnabled()) {
                this.log.debug(exc);
            }
            if (sExc == null) {
                sExc = exc;
            }
        }

        if (sExc != null) {
            throw sExc;
        }
    }

    /**
     * This method converts lists from the old record size to the current one -
     * it reads all the records from the list using the old size, deletes the list
     * and than saves all the records using the current record size. 
     * should be used when starting the first time after an upgrade. In version
     * 4.6.5 the default record size was changed from 1024 to 4000 and when we
     * will try to read the records with size 4000 we will get an exception because the
     * records size is 1024. This is a fix for Jira bug [HHQ-5387].
     * @param oldSize - the old size of the record
     * @throws IOException 
     */
    public void convertListToCurrentRecordSize(int oldSize) throws IOException {
        log.info("Converting list on file '" + this.fileName + "' from size " + oldSize + " to size "
                + this.recordSize);
        int realRecSize = this.recordSize;
        this.recordSize = oldSize;
        Collection<String> records = new ArrayList<String>();
        Iterator<String> iter = getListIterator();
        for (; (iter != null) && iter.hasNext();) {
            String data = iter.next();
            records.add(data);
        }
        log.info("Read " + records.size() + " records from file '" + this.fileName + "'");
        deleteAllRecords();

        this.recordSize = realRecSize;
        for (String rec : records) {
            addToList(rec);
        }
    }

    public static class DiskListIterator implements Iterator<String> {
        private final DiskList diskList; // Pointer back to the creating DiskList
        private long nextIdx; // Next index to read (or -1)
        private long curIdx;
        private boolean calledNext;
        private int modNum;

        private DiskListIterator(DiskList diskList, long nextIdx, int modNum) {
            this.diskList = diskList;
            this.nextIdx = nextIdx;
            this.curIdx = -1;
            this.calledNext = false;
            this.modNum = modNum;
        }

        public boolean hasNext() {
            return this.nextIdx != -1;
        }

        public String next() throws NoSuchElementException {
            Record rec;

            if (this.nextIdx == -1) {
                throw new NoSuchElementException();
            }

            this.curIdx = this.nextIdx;
            synchronized (this.diskList.dataFile) {
                if (this.diskList.modNum != this.modNum) {
                    throw new ConcurrentModificationException();
                }

                try {
                    rec = this.diskList.readRecord(this.curIdx);
                } catch (IOException e) {
                    log.error("IOException while reading record");
                    if (log.isDebugEnabled()) {
                        log.debug("IOException while trying to read record number " + this.curIdx, e);
                    }
                    NoSuchElementException ex = new NoSuchElementException("Error getting next element: " + e);
                    ex.initCause(e);
                    throw ex;
                }
            }

            this.nextIdx = rec.nextIdx;
            this.calledNext = true;
            return rec.data;
        }

        public void remove() {
            if (!this.calledNext) {
                throw new IllegalStateException("remove() called without first calling next()");
            }

            this.calledNext = false;
            final long start = now();

            synchronized (this.diskList.dataFile) {
                if (this.diskList.modNum != this.modNum) {
                    throw new ConcurrentModificationException();
                }

                try {
                    this.diskList.removeRecord(this.curIdx);
                } catch (IOException exc) {
                    log.error("IOException while removing record");
                    if (log.isDebugEnabled()) {
                        log.debug(exc, exc);
                    }
                    throw new IllegalStateException("Error removing record: " + exc, exc);
                }

                this.modNum = this.diskList.modNum;
            }
            final long duration = now() - start;
            statsCollector.addStat(duration, DISK_LIST_DISK_ITERATOR_REMOVE_TIME);
        }
    }

    public Iterator<String> getListIterator() {
        synchronized (this.dataFile) {
            // XXX -- This is broken, and is used to satisfy a lame 
            // requirement I made on the AgentStorageProvider interface.. :-(
            if (this.firstRec == -1) {
                if (log.isDebugEnabled()) {
                    log.debug("getListIterator() - list '" + this.fileName + "' has no elements");
                }
                return null;
            }

            return new DiskListIterator(this, this.firstRec, this.modNum);
        }
    }

    public static void main(String[] args) throws Exception {
        DiskList d;
        long NUM = 1024 * 128;
        long count;

        System.out.println("Creating DiskList..");
        d = new DiskList(new File("mydb"), 1024, 2 * 1024 * 1024, 10);

        // Fill the entire file with data
        System.out.println("Adding " + NUM + " records..");
        for (int i = 0; i < NUM; i++) {
            d.addToList("one " + i);
        }

        // Remove 1/2 of the data
        count = NUM / 2;
        System.out.println("Removing " + (NUM / 2) + " records..");
        for (Iterator i = d.getListIterator(); i.hasNext() && (count > 0); count--) {
            String val = (String) i.next();
            i.remove();
        }

        // Add back 1/4 of the data
        System.out.println("Adding " + (NUM / 4) + " records..");
        for (int i = 0; i < (NUM / 4); i++) {
            d.addToList("two " + i);
        }

        // Remove 1/2 of the data
        count = NUM / 2;
        System.out.println("Removing " + (NUM / 2) + " records..");
        for (Iterator i = d.getListIterator(); i.hasNext() && (count > 0); count--) {
            String val = (String) i.next();
            i.remove();
        }

        // Add back 1/4 of the data
        System.out.println("Adding " + (NUM / 4) + " records..");
        for (int i = 0; i < (NUM / 4); i++) {
            d.addToList("three " + i);
        }

        // Remove all data
        System.out.println("Removing all data..");
        for (Iterator i = d.getListIterator(); i.hasNext();) {
            String val = (String) i.next();
            i.remove();
        }

        // Add back 1/16 of the data
        System.out.println("Adding " + (NUM / 16) + " records..");
        for (int i = 0; i < (NUM / 4); i++) {
            d.addToList("three " + i);
        }

        // Remove all data
        System.out.println("Removing all data..");
        for (Iterator i = d.getListIterator(); i.hasNext();) {
            String val = (String) i.next();
            i.remove();
        }

        d.close();
    }
}