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

com.grahamedgecombe.db.DatabaseService Maven / Gradle / Ivy

Go to download

A thin layer on top of the JDBC API that takes care of awkward boilerplate code.

The newest version!
package com.grahamedgecombe.db;

import java.sql.SQLException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import javax.sql.DataSource;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.AbstractService;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.common.util.concurrent.Uninterruptibles;
import org.checkerframework.checker.lock.qual.GuardedBy;

/**
 * The core class of the database API, which manages the pool of transaction
 * execution threads and the queue of pending transactions.
 * @author Graham Edgecombe
 */
public final class DatabaseService extends AbstractService {
	private static final BackoffStrategy DEFAULT_BACKOFF_STRATEGY = new BinaryExponentialBackoffStrategy(8, 10);
	private static final DeadlockDetector DEFAULT_DEADLOCK_DETECTOR = ex -> true;
	private static final int DEFAULT_MAX_ATTEMPTS = 5;
	private static final int DEFAULT_THREADS = 1;
	private static final ThreadFactory DEFAULT_THREAD_FACTORY = Thread::new;

	/**
	 * Creates a new {@link Builder} object.
	 * @param dataSource The JDBC data source.
	 * @return The builder.
	 */
	public static Builder builder(DataSource dataSource) {
		return new Builder(dataSource);
	}

	/**
	 * Creates {@link DatabaseService} objects.
	 */
	public static final class Builder {
		private final DataSource dataSource;
		private BackoffStrategy backoffStrategy = DEFAULT_BACKOFF_STRATEGY;
		private DeadlockDetector deadlockDetector = DEFAULT_DEADLOCK_DETECTOR;
		private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
		private int threads = DEFAULT_THREADS;
		private ThreadFactory threadFactory = DEFAULT_THREAD_FACTORY;

		private Builder(DataSource dataSource) {
			this.dataSource = dataSource;
		}

		/**
		 * Sets the {@link BackoffStrategy} of the {@link DatabaseService}
		 * object currently being built.
		 *
		 * The default {@link BackoffStrategy} uses binary exponential backoff
		 * with a {@code cMax} value of {@code 8} and a {@code scale} value of
		 * {@code 10} milliseconds.
		 * @param backoffStrategy The {@link BackoffStrategy}.
		 * @return This {@link Builder} object, for method chaining.
		 */
		public Builder setBackoffStrategy(BackoffStrategy backoffStrategy) {
			this.backoffStrategy = backoffStrategy;
			return this;
		}

		/**
		 * Sets the {@link DeadlockDetector} of the {@link DatabaseService}
		 * object currently being built.
		 *
		 * The default {@link DeadlockDetector} simply returns {@code true} for
		 * all types of {@link SQLException}, for the broadest compatibility
		 * across database servers.
		 * @param deadlockDetector The {@link DeadlockDetector}.
		 * @return This {@link Builder} object, for method chaining.
		 */
		public Builder setDeadlockDetector(DeadlockDetector deadlockDetector) {
			this.deadlockDetector = deadlockDetector;
			return this;
		}

		/**
		 * Sets the maximum number of transaction attempts.
		 *
		 * The default is 5 attempts.
		 * @param maxAttempts The maximum number of attempts before marking a
		 *                    transaction as failed.
		 * @throws IllegalArgumentException if {@code maxAttempts} is zero or
		 *                                  negative.
		 * @return This {@link Builder} object, for method chaining.
		 */
		public Builder setMaxAttempts(int maxAttempts) {
			Preconditions.checkArgument(maxAttempts > 0, "maxAttempts must be positive");
			this.maxAttempts = maxAttempts;
			return this;
		}

		/**
		 * Sets the number of transaction execution threads. Each thread
		 * controls a single connection.
		 *
		 * The default is 1 thread.
		 * @param threads The number of transaction execution threads.
		 * @throws IllegalArgumentException if {@code threads} is zero or
		 *                                  negative.
		 * @return This {@link Builder} object, for method chaining.
		 */
		public Builder setThreads(int threads) {
			Preconditions.checkArgument(threads > 0, "threads must be positive");
			this.threads = threads;
			return this;
		}

		/**
		 * Sets the {@link ThreadFactory} used to create the transaction
		 * executor threads.
		 *
		 * The default thread factory simply returns a new thread created using
		 * the {@link Thread#Thread(Runnable)} constructor.
		 * @param threadFactory The {@link ThreadFactory}.
		 * @return This {@link Builder} object, for method chaining.
		 */
		public Builder setThreadFactory(ThreadFactory threadFactory) {
			this.threadFactory = threadFactory;
			return this;
		}

		/**
		 * Builds a new {@link DatabaseService} object.
		 * @return The {@link DatabaseService} object.
		 */
		public DatabaseService build() {
			return new DatabaseService(dataSource, backoffStrategy, deadlockDetector, maxAttempts, threads, threadFactory);
		}
	}

	private static TransactionExecutor[] createExecutors(DataSource dataSource, BackoffStrategy backoffStrategy, DeadlockDetector deadlockDetector, int maxAttempts, int threads, BlockingQueue> jobs) {
		TransactionExecutor[] executors = new TransactionExecutor[threads];

		for (int i = 0; i < executors.length; i++) {
			executors[i] = new TransactionExecutor(dataSource, backoffStrategy, deadlockDetector, maxAttempts, jobs);
		}

		return executors;
	}

	private static Thread[] createThreads(TransactionExecutor[] executors, ThreadFactory threadFactory) {
		Thread[] threads = new Thread[executors.length];

		for (int i = 0; i < threads.length; i++) {
			threads[i] = threadFactory.newThread(executors[i]);
		}

		return threads;
	}

	private final Object lock = new Object();
	private final BlockingQueue> jobs = new LinkedBlockingQueue<>();
	private final TransactionExecutor[] executors;
	private final Thread[] threads;
	private @GuardedBy("lock") boolean running;

	private DatabaseService(DataSource dataSource, BackoffStrategy backoffStrategy, DeadlockDetector deadlockDetector, int maxAttempts, int threads, ThreadFactory threadFactory) {
		this.executors = createExecutors(dataSource, backoffStrategy, deadlockDetector, maxAttempts, threads, jobs);
		this.threads = createThreads(executors, threadFactory);
	}

	/**
	 * Starts the {@link DatabaseService}. This method blocks until the
	 * {@link DatabaseService} is running.
	 * @return This {@link DatabaseService} object, for method chaining.
	 */
	public DatabaseService start() {
		startAsync().awaitRunning();
		return this;
	}

	/**
	 * Stops the {@link DatabaseService}. Any transactions submitted before the
	 * {@link #stop()} method is called will be executed. This method blocks
	 * until the {@link DatabaseService} has stopped.
	 */
	public void stop() {
		stopAsync().awaitTerminated();
	}

	/**
	 * Executes a {@link Transaction} asynchronously.
	 * @param transaction The transaction.
	 * @param  The type of result returned by the transaction.
	 * @throws IllegalStateException if this {@link DatabaseService} is not
	 *                               in the running state.
	 * @return A {@link ListenableFuture} representing the result of the
	 *         transaction.
	 */
	public  ListenableFuture executeAsync(Transaction transaction) {
		SettableFuture future = SettableFuture.create();

		synchronized (lock) {
			Preconditions.checkState(running);
			jobs.add(new TransactionJob<>(future, transaction));
		}

		return future;
	}

	/**
	 * Executes a {@link Transaction} synchronously.
	 * @param transaction The transaction.
	 * @param  The type of result returned by the transaction.
	 * @return The result of the transaction.
	 * @throws IllegalStateException if this {@link DatabaseService} is not
	 *                               in the running state.
	 * @throws ExecutionException if an exception was thrown during execution
	 *                            of the transaction.
	 */
	public  T execute(Transaction transaction) throws ExecutionException {
		return Uninterruptibles.getUninterruptibly(executeAsync(transaction));
	}

	/**
	 * Executes a {@link VoidTransaction} asynchronously.
	 * @param transaction The transaction.
	 * @throws IllegalStateException if this {@link DatabaseService} is not
	 *                               in the running state.
	 * @return A {@link ListenableFuture} representing the result of the
	 *         transaction.
	 */
	public ListenableFuture executeVoidAsync(VoidTransaction transaction) {
		return executeAsync(connection -> {
			transaction.execute(connection);
			return null;
		});
	}

	/**
	 * Executes a {@link VoidTransaction} synchronously.
	 * @param transaction The transaction.
	 * @throws IllegalStateException if this {@link DatabaseService} is not
	 *                               in the running state.
	 * @throws ExecutionException if an exception was thrown during execution
	 *                            of the transaction.
	 */
	public void executeVoid(VoidTransaction transaction) throws ExecutionException {
		Uninterruptibles.getUninterruptibly(executeVoidAsync(transaction));
	}

	@Override
	protected void doStart() {
		for (Thread thread : threads) {
			thread.start();
		}

		synchronized (lock) {
			running = true;
		}

		notifyStarted();
	}

	@Override
	protected void doStop() {
		synchronized (lock) {
			running = false;
		}

		for (TransactionExecutor executor : executors) {
			executor.stop();
		}

		for (Thread thread : threads) {
			thread.interrupt();
		}

		for (Thread thread : threads) {
			Uninterruptibles.joinUninterruptibly(thread);
		}

		notifyStopped();
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy