All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tinystruct.valve.Watcher Maven / Gradle / Ivy

package org.tinystruct.valve;

import org.tinystruct.ApplicationException;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

/**
 * Create a watcher to monitor the lock.
 *
 * @author James Zhou
 */
public final class Watcher implements Runnable {
    private static final Logger logger = Logger.getLogger(Watcher.class.getName());
    /**
     * Empty bytes.
     */
    private final static byte[] EMPTY_BYTES = new byte[36];
    /**
     * Lock size.
     */
    private static final int FIXED_LOCK_DATA_SIZE = 44;
    /**
     * Lock file name.
     */
    private final String file = ".lock";
    /**
     * Lock collection.
     */
    private final ConcurrentHashMap locks = new ConcurrentHashMap(8);
    /**
     * Lock event listeners.
     */
    private final ConcurrentHashMap listeners = new ConcurrentHashMap(8);
    private int[] interspace;
    private int size;
    private volatile boolean started = false;
    private volatile boolean stopped = false;

    private Watcher() {
        try (RandomAccessFile lockFile = new RandomAccessFile(file, "r")) {
            this.size = (int) (lockFile.length() / FIXED_LOCK_DATA_SIZE);
            this.interspace = new int[this.size + 1];
        } catch (FileNotFoundException e) {
            logger.warning(e.getMessage());
        } catch (IOException e) {
            logger.severe(e.getMessage());
        }
    }

    public static Watcher getInstance() {
        return SingletonHolder.manager;
    }

    public void waitFor(String lockId) throws InterruptedException {
        this.listeners.get(lockId).waitFor();
    }

    public void waitFor(String lockId, long timeout, TimeUnit unit) throws InterruptedException {
        this.listeners.get(lockId).waitFor(timeout, unit);
    }

    /**
     * Read the .lock file and transform the information to the hash map in memory.
     */
    @Override
    public void run() {
        synchronized (Lock.class) {
            this.started = true;
        }

        FileLock fileLock;
        while (!this.stopped) {
            synchronized (Watcher.class) {
                try {
                    Watcher.class.wait();
                } catch (InterruptedException e) {
                    logger.severe(e.getMessage());
                }
                // Synchronize the locks map with Lock file.
                try (RandomAccessFile lockFile = new RandomAccessFile(file, "rw")) {
                    // If the length of the lockFile is bigger than the default length: 44.
                    // then the size of the locks map would be easy to be calculated.
                    // Lock the file.
                    if (lockFile.length() >= FIXED_LOCK_DATA_SIZE) {
                        this.size = (int) lockFile.length() / FIXED_LOCK_DATA_SIZE;
                        fileLock = lockFile.getChannel().tryLock(0L, Long.MAX_VALUE, false);

                        if (fileLock != null) {
                            // If the size more than zero
                            if (this.size > 0) {
                                // Seek the file from 0 position.
                                lockFile.seek(0);

                                String lockId;
                                EventListener listener;
                                // Assume all of the locks are in use.
                                int availableLockSize = this.size;
                                // Read all locks into the map.
                                for (int i = 0; i < this.size && lockFile.length() > 0; i++) { // Cautious!!!
                                    byte[] id = EMPTY_BYTES;
                                    // Read lock id.
                                    if (lockFile.read(id) != -1) {
                                        lockId = new String(id, Charset.defaultCharset());
                                        listener = this.listeners.get(lockId);
                                        // Read lock status.
                                        // If the lock is expired, then it should not be in the locks map
                                        if (lockFile.readLong() == 0L) {
                                            if (this.locks.containsKey(lockId)) {
                                                this.locks.remove(lockId);
                                                if (listener != null)
                                                    listener.onDelete(lockId);
                                            }
                                            // If all locks are not available, then remove them all and set the lockFile to be empty.
                                            if (--availableLockSize == 0) {
                                                if (!this.locks.isEmpty())
                                                    this.locks.clear();

                                                lockFile.setLength(0);
                                            }
                                        }
                                        // Otherwise, the lock should be in the locks map
                                        else {
                                            // Check if the lock exists.
                                            if (!this.locks.containsKey(lockId)) {
                                                // Add a new lock with id.
                                                this.locks.putIfAbsent(lockId, new DistributedLock(id));
                                                if (listener != null)
                                                    listener.onCreate(lockId);
                                            }
                                        }
                                    }
                                }
                            }

                            fileLock.release();
                            // Notify the other thread to work on.
                            Watcher.class.notifyAll();
                        }
                    } else {
                        this.size = 0;
                        lockFile.setLength(0);
                        Watcher.class.notifyAll();
                    }
                } catch (IOException e) {
                    // If there is IO Exception, then the Watcher should stop to synchronize.
                    this.stop();
                    logger.severe(e.getMessage());
                }
            }
        }
    }

    public void addListener(EventListener listener) {
        this.listeners.put(listener.id(), listener);
    }

    private void start() {
        Thread monitor = new Thread(this);
        monitor.setDaemon(true);
        monitor.start();
    }

    public boolean watch(final Lock lock) throws ApplicationException {
        synchronized (Lock.class) {
            // If the Watcher has not been started, then should be started to synchronize the locks.
            if (!this.started) {
                this.start();
            }

            // Check if the lock is in the container.
            return locks.contains(lock);
        }
    }

    public void register(Lock lock) throws ApplicationException {
        this.register(lock, 0L, TimeUnit.SECONDS);
    }

    public void register(Lock lock, long expiration, TimeUnit tu) throws ApplicationException {
        synchronized (Watcher.class) {
            if (!locks.contains(lock)) {
                FileLock fileLock;
                try (RandomAccessFile lockFile = new RandomAccessFile(file, "rw")) {
                    String id = lock.id();
                    fileLock = lockFile.getChannel().tryLock();

                    long length = lockFile.length();
                    if (null != fileLock) {
                        byte[] empty = EMPTY_BYTES;

                        if (length >= FIXED_LOCK_DATA_SIZE) {
                            this.size = (int) (length / FIXED_LOCK_DATA_SIZE);
                            this.interspace = new int[this.size];
                        } else
                            this.size = 0;
                        // If it's required to occupy for new space.
                        boolean required = true, registerred = false;
                        if (this.size > 0) {
                            int position = 0;
                            // Check if the lock id does exist, if so then start to
                            for (int i = 0; i < this.size; i++) {
                                position = i * FIXED_LOCK_DATA_SIZE;
                                lockFile.seek(position);
                                if (lockFile.read(empty) != -1) {
                                    if (Arrays.equals(lock.id().getBytes(), empty)) {
                                        if (lockFile.readLong() == 0L) {
                                            // If the Lock does exist, then just update the status for the Lock
                                            lockFile.seek(position + EMPTY_BYTES.length);
                                            lockFile.writeLong(1L);
                                        }
                                        // Not been used in logic currently.
                                        this.interspace[i] = 0;
                                        required = false;
                                        registerred = true;
                                        // Once get a space, then it's enough to be used for the current Lock.
                                        break;
                                    }

                                    if (lockFile.readLong() == 0L) {
                                        this.interspace[i] = 1;
                                        required = false;
                                    }
                                }
                            }

                            if (!registerred) {
                                for (int i = 0; i < this.size; i++) {
                                    if (this.interspace[i] == 1) {
                                        // If the Lock does exist, then just update the status for the Lock
                                        lockFile.seek(i * FIXED_LOCK_DATA_SIZE);
                                        lockFile.writeBytes(id);
                                        lockFile.writeLong(1L);

                                        this.interspace[i] = 0;
                                        required = false;
                                        // Once get a space, then it's enough to be used for the current Lock.
                                        break;
                                    }
                                }
                            }
                        }

                        if (required) {
                            lockFile.seek(this.size * FIXED_LOCK_DATA_SIZE);
                            lockFile.writeBytes(id);
                            lockFile.writeLong(1L);
                        }

                        this.locks.put(id, lock);
                        if (this.listeners.get(id) != null)
                            this.listeners.get(id).onCreate(id);

                        fileLock.release();

                        // Notify the other thread to work on.
                        Watcher.class.notifyAll();
                    }
                } catch (FileNotFoundException e) {
                    throw new ApplicationException(e.getMessage(), e.getCause());
                } catch (IOException e) {
                    throw new ApplicationException(e.getMessage(), e.getCause());
                }
            }
        }
    }

    public void unregister(Lock lock) throws ApplicationException {
        synchronized (Watcher.class) {
            try (RandomAccessFile lockFile = new RandomAccessFile(file, "rw")) {
                long length = lockFile.length();
                if (length < FIXED_LOCK_DATA_SIZE)
                    return;
                FileLock fileLock = lockFile.getChannel().tryLock();
                if (null != fileLock) {
                    byte[] empty = EMPTY_BYTES;

                    this.size = (int) (length / FIXED_LOCK_DATA_SIZE);
                    this.interspace = new int[this.size];

                    int position = 0;
                    String lockId = lock.id();
                    for (int i = 0; i < this.size; i++) {
                        position = i * FIXED_LOCK_DATA_SIZE;
                        lockFile.seek(position);
                        if (lockFile.read(empty) != -1) {
                            if (Arrays.equals(lockId.getBytes(), empty)) {
                                lockFile.seek(position + EMPTY_BYTES.length); // The pointer should be resumed.
                                lockFile.writeLong(0L);
                                if (this.locks.containsKey(lockId)) {
                                    this.locks.remove(lockId);

                                    if (this.listeners.get(lockId) != null)
                                        this.listeners.get(lockId).onDelete(lockId);
                                }
                                break;
                            }
                        }
                    }

                    fileLock.release();
                    // Notify the other thread to work on.
                    Watcher.class.notifyAll();
                } else {
                    this.unregister(lock);
                }
            } catch (FileNotFoundException e) {
                throw new ApplicationException(e.getMessage(), e.getCause());
            } catch (IOException e) {
                throw new ApplicationException(e.getMessage(), e.getCause());
            }
        }
    }

    public Lock acquire() {
        synchronized (Watcher.class) {
            Lock lock;
            if (null != this.locks && this.locks.size() > 0 && null != (lock = this.locks.elements().nextElement())) {
                return lock;
            }

            return new DistributedLock();
        }
    }

    public void stop() {
        this.stopped = this.locks.size() == 0;
    }

    public interface EventListener {
        /**
         * To be triggered when a lock created.
         *
         * @param lockId lock id
         */
        void onCreate(String lockId);

        /**
         * To be triggered when a lock updated.
         */
        void onUpdate();

        /**
         * To be triggered when a lock deleted.
         *
         * @param lockId lock id
         */
        void onDelete(String lockId);

        /**
         * Listener Id.
         *
         * @return unique identifier
         */
        String id();

        void waitFor() throws InterruptedException;

        boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException;
    }

    /**
     * Single instance holder for {@link Watcher}
     */
    private static final class SingletonHolder {
        static final Watcher manager = new Watcher();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy