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

kvd.client.KvdClient Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 Andre Gebers
 *
 * Licensed 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 kvd.client;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import kvd.common.HostAndPort;
import kvd.common.KvdException;
import kvd.common.Utils;
import kvd.common.packet.Packets;
import kvd.common.packet.proto.PacketType;

/**
 * {@code KvdClient} is the public API that clients should use to interact with the server.
 *
 * 

Example usage:

 *  try(KvdClient client = new KvdClient("kvd.example.com:3030")) {
 *    try(DataOutputStream out = new DataOutputStream(client.put("test"))) {
 *      out.writeLong(42);
 *    }
 *  }
 * 
* * All {@link KvdOperations} methods implemented in this class execute a single operation transaction on the server that * is automatically committed when the operation completes. They partake in resource locking the same way as * {@link KvdTransaction} operations. The main difference between {@link KvdTransaction} operations is that operations * in {@code KvdClient} are auto committed. For manual commit/rollback start a new Transaction with {@link #beginTransaction()} * *

Note: {@link KvdClient#KvdClient(java.lang.String)} establishes a single socket connection to the server * that it keeps alive until {@link KvdClient#close} is called. * *

Note: {@code KvdClient} is thread-safe. */ public class KvdClient implements KvdOperations, AutoCloseable { private static final Logger log = LoggerFactory.getLogger(KvdClient.class); private static final int NO_TX = 0; private ClientBackend backend; private Set abortables = new HashSet<>(); private AtomicBoolean closed = new AtomicBoolean(false); private long transactionDefaultTimeoutMs; private int serverTimeoutSeconds; private ThreadLocal transactions = new ThreadLocal<>(); /** * Create a new {@code KvdClient} instance connecting to the server. Use {@link KvdClientBuilder} to create a * {@code KvdClient} with non standard options. * @param serverAddress in the form * {@code host:port}. * Port can be omitted and 3030 is used in this case. */ public KvdClient(String serverAddress) { this(new KvdClientBuilder(serverAddress)); } KvdClient(KvdClientBuilder builder) { try { this.transactionDefaultTimeoutMs = builder.getTransactionDefaultTimeoutMs(); this.serverTimeoutSeconds = builder.getServerTimeoutSeconds(); HostAndPort hp = HostAndPort.fromString(builder.getServerAddress()).withDefaultPort(3030); log.trace("connecting to '{}'", hp); Socket socket = new Socket(InetAddress.getByName(hp.getHost()), hp.getPort()); socket.setSoTimeout(builder.getSocketSoTimeoutMs()); backend = new ClientBackend(socket, serverTimeoutSeconds, () -> { try { log.debug("client backend close notification"); close(); } catch(Exception e) { // ignore } }); backend.start(); } catch(Exception e) { throw new KvdException(String.format("failed to connect to '%s'", builder.getServerAddress()), e); } } private synchronized void removeAbortable(Abortable a) { this.abortables.remove(a); } private void checkClosed() { if(isClosed()) { throw new KvdException("closed"); } } @Override public synchronized Future putAsync(byte[] key) { checkClosed(); Utils.checkKey(key); KvdPut put = new KvdPut(backend, NO_TX, key, this::removeAbortable); abortables.add(put); put.start(); return put.getFuture(); } @Override public synchronized Future getAsync(byte[] key) { checkClosed(); Utils.checkKey(key); KvdGet get = new KvdGet(backend, NO_TX, key, this::removeAbortable); abortables.add(get); get.start(); return get.getFuture(); } @Override public synchronized Future containsAsync(byte[] key) { checkClosed(); Utils.checkKey(key); KvdContains contains = new KvdContains(backend, NO_TX, key, this::removeAbortable); abortables.add(contains); contains.start(); return contains.getFuture(); } @Override public synchronized Future removeAsync(byte[] key) { checkClosed(); Utils.checkKey(key); KvdRemove remove = new KvdRemove(backend, NO_TX, key, this::removeAbortable); abortables.add(remove); remove.start(); return remove.getFuture(); } /** * Waits for pending requests to finish and closes the connection to the server. Once closed this instance * can't be reused and must be discarded. */ @Override public synchronized void close() { if(!closed.getAndSet(true)) { new HashSet<>(abortables).forEach(a -> { try { log.warn("aborting '{}'", a); a.abort(); } catch(Exception e) { // ignore } }); abortables.clear(); try { backend.sendAsync(Packets.packet(PacketType.BYE)); } catch(Exception e) { // ignore } backend.warnOnOpenChannels(); backend.closeGracefully(); } } /** * Check if the KvdClient can still be used. * @return {@code true} if the instance is closed, {@code false} otherwise. */ public boolean isClosed() { return closed.get(); } /** * Begin a new transaction with the specified timeout. Also see {{@link #beginTransaction(long)} * @param timeoutMs The transaction timeout in milliseconds or 0 for no timeout. If the timeout is exceeded * the transaction is aborted (rollback). * @return {@code Future} that evaluates to a {@link KvdTransaction} when the server has created the transaction */ public synchronized Future beginTransactionAsync(long timeoutMs) { checkClosed(); KvdBeginTransaction txBegin = new KvdBeginTransaction(backend, this::removeAbortable, timeoutMs); abortables.add(txBegin); txBegin.start(); return txBegin.getFuture(); } /** * Begin a new transaction with the specified timeout and waits until the transaction has been created on the server. * Normally this method should be used in a try-with-resource block to make * sure the transaction is closed. Note that you have to commit the transaction manually to make changes permanent * before the try-with-resource block closes the transaction. Also consider using {@link #withTransaction(KvdWork)} * which automatically commits transactions and also allows outer/inner units of work to share the same transaction * to improve code reusability * @param timeoutMs The transaction timeout in milliseconds or 0 for no timeout. If the timeout is exceeded * the transaction is aborted (rollback). * @return {@link KvdTransaction} */ public KvdTransaction beginTransaction(long timeoutMs) { try { return beginTransactionAsync(timeoutMs).get(); } catch (Exception e) { throw new KvdException("begin transaction failed", e); } } /** * See {@link #beginTransaction(long)} except this method uses the default transaction timeout * @return {@link KvdTransaction} */ public KvdTransaction beginTransaction() { return beginTransaction(transactionDefaultTimeoutMs); } private T withNewTransaction(KvdWork work) { // try-with-resource closes KvdTransaction (rollback), no need to handle exceptions here try(KvdTransaction tx = beginTransaction()) { transactions.set(tx); T result = work.execute(tx); tx.commit(); return result; } finally { transactions.remove(); } } /** * Execute a new or join an existing {@link KvdTransaction} that has been started either with * {@link #withTransaction(KvdWork)} or {@link #withTransactionVoid(KvdVoidWork)}. A new transaction is started * with the default timeout. The transaction is automatically committed when the most outer {@link KvdWork} finishes * or aborted (rollback) when the {@link KvdWork} throws an exceptions. * @param Result type of the {@link KvdWork} * @param work the unit of work to be executed within the transaction * @return The result of the unit of work */ public T withTransaction(KvdWork work) { KvdTransaction tx = transactions.get(); if(tx != null) { return work.execute(tx); } else { return withNewTransaction(work); } } /** * Same as {@link #withTransaction(KvdWork)} except this does not return a result * @param work the unit of work to be executed within the transaction */ public void withTransactionVoid(KvdVoidWork work) { withTransaction(tx -> { work.execute(tx); return null; }); } /** * Retrieve the default transaction timeout in milliseconds, 0 means no timeout. This value can only be set when * the {@code KvdClient} is constructed through the {@link KvdClientBuilder} * @return The default transaction timeout in milliseconds */ public long getTransactionDefaultTimeoutMs() { return transactionDefaultTimeoutMs; } /** * Removes all keys and values from the database. * @return {@code Future} which evaluates to {@code true} if when all key/values have been removed from the server, * {@code false} otherwise. */ public Future removeAllAsync() { checkClosed(); KvdRemoveAll removeAll = new KvdRemoveAll(backend, this::removeAbortable); abortables.add(removeAll); removeAll.start(); return removeAll.getFuture(); } /** * Remove all keys and values from the database. */ public Boolean removeAll() { try { return removeAllAsync().get(); } catch(Exception e) { throw new KvdException("failed to remove all key/values", e); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy