/* * Copyright 2007-2008 Konrad-Zuse-Zentrum für Informationstechnik Berlin * * 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 de.zib.scalaris; import java.io.IOException; import com.ericsson.otp.erlang.OtpAuthException; import com.ericsson.otp.erlang.OtpConnection; import com.ericsson.otp.erlang.OtpErlangAtom; import com.ericsson.otp.erlang.OtpErlangExit; import com.ericsson.otp.erlang.OtpErlangList; import com.ericsson.otp.erlang.OtpErlangObject; import com.ericsson.otp.erlang.OtpErlangString; import com.ericsson.otp.erlang.OtpErlangTuple; /** * Provides means to realise a transaction with the scalaris ring using Java. * * <p> * Instances of this class can be generated using a given connection to a * scalaris node using {@link #Transaction(OtpConnection)} or without a * connection ({@link #Transaction()}) in which case a new connection is * created using {@link ConnectionFactory#createConnection()}. * </p> * * <p> * There are two paradigms for reading and writing values: * <ul> * <li> using Java {@link String}s: {@link #read(String)}, {@link #write(String, String)} * <p>This is the safe way of accessing scalaris where type conversions * are handled by the API and the user doesn't have to worry about anything else.</p> * <p>Be aware though that this is not the most efficient way of handling strings!</p> * <li> using custom {@link OtpErlangObject}s: {@link #readObject(OtpErlangString)}, * {@link #writeObject(OtpErlangString, OtpErlangObject)} * <p>Here the user can specify custom behaviour and increase performance. * Handling the stored types correctly is at the user's hand.</p> * <p>An example using erlang objects to improve performance for inserting strings is * provided by {@link de.zib.scalaris.examples.CustomOtpFastStringObject} and can be * tested by {@link de.zib.scalaris.examples.FastStringBenchmark}.</p> * </ul> * </p> * * <h3>Example:</h3> * <pre> * <code style="white-space:pre;"> * OtpErlangString otpKey; * OtpErlangString otpValue; * OtpErlangObject otpResult; * * String key; * String value; * String result; * * Transaction t1 = new Transaction(); // {@link #Transaction()} * t1.start(); // {@link #start()} * * t1.write(key, value); // {@link #write(String, String)} * t1.writeObject(otpKey, otpValue); // {@link #writeObject(OtpErlangString, OtpErlangObject)} * * result = t1.read(key); //{@link #read(String)} * otpResult = t1.readObject(otpKey); //{@link #readObject(OtpErlangString)} * * transaction.commit(); // {@link #commit()} * </code> * </pre> * * <p> * For more examples, have a look at {@link de.zib.scalaris.examples.TransactionReadExample}, * {@link de.zib.scalaris.examples.TransactionWriteExample} and * {@link de.zib.scalaris.examples.TransactionReadWriteExample}. * </p> * * <h3>Attention:</h3> * <p> * If a read or write operation fails within a transaction all subsequent * operations on that key will fail as well. This behaviour may particularly be * undesirable if a read operation just checks whether a value already exists or * not. To overcome this situation call {@link #revertLastOp()} immediately * after the failed operation which restores the state as it was before that * operation.<br /> * The {@link de.zib.scalaris.examples.TransactionReadWriteExample} example * shows such a use case. * </p> * * @author Nico Kruber, kruber@zib.de * @version 2.2 * @since 2.0 */ public class Transaction { /** * erlang transaction log */ private OtpErlangList transLog = null; /** * erlang transaction log before the last operation * * @see #revertLastOp() */ private OtpErlangList transLog_old = null; /** * connection to a scalaris node */ private OtpConnection connection; /** * Constructor, uses the default connection returned by * {@link ConnectionFactory#createConnection()}. * * @throws ConnectionException * if the connection fails */ public Transaction() throws ConnectionException { connection = ConnectionFactory.getInstance().createConnection(); } /** * Constructor, uses the given connection to an erlang node. * * @param conn * connection to use for the transaction * * @throws ConnectionException * if the connection fails */ public Transaction(OtpConnection conn) throws ConnectionException { connection = conn; } /** * Resets the transaction to its initial state. * * This action is not reversible. */ public void reset() { transLog = null; transLog_old = null; } /** * Starts a new transaction by generating a new transaction log. * * @throws ConnectionException * if the connection is not active or a communication error * occurs or an exit signal was received or the remote node * sends a message containing an invalid cookie * @throws TransactionNotFinishedException * if an old transaction is not finished (via {@link #commit()} * or {@link #abort()}) yet * @throws UnknownException * if the returned value from erlang does not have the expected * type/structure */ public void start() throws ConnectionException, TransactionNotFinishedException, UnknownException { if (transLog != null || transLog_old != null) { throw new TransactionNotFinishedException(); } OtpErlangObject received_raw = null; try { connection.sendRPC("transstore.transaction", "translog_new", new OtpErlangList()); // return value: [] received_raw = connection.receiveRPC(); OtpErlangList received = (OtpErlangList) received_raw; transLog = received; } catch (OtpErlangExit e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (OtpAuthException e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (IOException e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (ClassCastException e) { // e.printStackTrace(); // received_raw is not null since the first class cast is after the RPC! throw new UnknownException(e, received_raw); } } /** * Commits the current transaction. * * <p> * The transaction's log is reset if the commit was successful, otherwise it * still retains in the transaction which must be successfully committed, * aborted or reset in order to be restarted. * </p> * * @throws UnknownException * If the commit fails or the returned value from erlang is of * an unknown type/structure, this exception is thrown. Neither * the transaction log nor the local operations buffer is * emptied, so that the commit can be tried again. * @throws ConnectionException * if the connection is not active or a communication error * occurs or an exit signal was received or the remote node * sends a message containing an invalid cookie * * @see #abort() * @see #reset() */ public void commit() throws UnknownException, ConnectionException { if (transLog == null) { throw new TransactionNotStartedException(); } OtpErlangObject received_raw = null; try { connection.sendRPC("transstore.transaction_api", "commit", new OtpErlangList(transLog)); /* * possible return values: * - {ok} * - {fail, Reason} */ received_raw = connection.receiveRPC(); OtpErlangTuple received = (OtpErlangTuple) received_raw; if(received.elementAt(0).equals(new OtpErlangAtom("ok"))) { // transaction was successful: reset transaction log reset(); return; } else { // transaction failed // OtpErlangObject reason = received.elementAt(1); // throw new UnknownException(reason.toString()); throw new UnknownException(received_raw); } } catch (OtpErlangExit e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (OtpAuthException e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (IOException e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (ClassCastException e) { // e.printStackTrace(); // received_raw is not null since the first class cast is after the RPC! throw new UnknownException(e, received_raw); } } /** * Cancels the current transaction. * * <p> * For a transaction to be cancelled, only the {@link #transLog} needs to be * reset. Nothing else needs to be done since the data was not modified * until the transaction was committed. * </p> * * @see #commit() * @see #reset() */ public void abort() { reset(); } /** * Gets the value stored under the given <code>key</code>. * * @param key * the key to look up * * @return the value stored under the given <code>key</code> as a raw erlang type * * @throws ConnectionException * if the connection is not active or a communication error * occurs or an exit signal was received or the remote node * sends a message containing an invalid cookie * @throws TimeoutException * if a timeout occurred while trying to fetch the value * @throws NotFoundException * if the requested key does not exist * @throws UnknownException * if any other error occurs */ public OtpErlangObject readObject(OtpErlangString key) throws ConnectionException, TimeoutException, UnknownException, NotFoundException { if (transLog == null) { throw new TransactionNotStartedException(); } OtpErlangObject received_raw = null; try { connection.sendRPC("transstore.transaction_api", "jRead", new OtpErlangList(new OtpErlangObject[] {key, transLog})); /* * possible return values: * - {{fail, not_found}, TransLog} * - {{fail, timeout}, TransLog} * - {{fail, fail}, TransLog} * - {{value, Value}, NewTransLog} */ received_raw = connection.receiveRPC(); OtpErlangTuple received = (OtpErlangTuple) received_raw; transLog_old = transLog; transLog = (OtpErlangList) received.elementAt(1); OtpErlangTuple status = (OtpErlangTuple) received.elementAt(0); if (status.elementAt(0).equals(new OtpErlangAtom("value"))) { return status.elementAt(1); } else { if (status.elementAt(1).equals(new OtpErlangAtom("timeout"))) { throw new TimeoutException(received_raw); } else if (status.elementAt(1).equals( new OtpErlangAtom("not_found"))) { throw new NotFoundException(received_raw); } else { throw new UnknownException(received_raw); } } } catch (OtpErlangExit e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (OtpAuthException e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (IOException e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (ClassCastException e) { // e.printStackTrace(); // received_raw is not null since the first class cast is after the RPC! throw new UnknownException(e, received_raw); } } /** * Gets the value stored under the given <code>key</code>. * * @param key * the key to look up * * @return the (string) value stored under the given <code>key</code> * * @throws ConnectionException * if the connection is not active or a communication error * occurs or an exit signal was received or the remote node * sends a message containing an invalid cookie * @throws TimeoutException * if a timeout occurred while trying to fetch the value * @throws NotFoundException * if the requested key does not exist * @throws UnknownException * if any other error occurs * * @see #readObject(OtpErlangString) */ public String read(String key) throws ConnectionException, TimeoutException, UnknownException, NotFoundException { try { CustomOtpStringObject result = new CustomOtpStringObject(); readCustom(key, result); return result.getValue(); // return ((OtpErlangString) readObject(new OtpErlangString(key))) // .stringValue(); } catch (ClassCastException e) { // e.printStackTrace(); throw new UnknownException(e); } } /** * Gets the value stored under the given <code>key</code>. * * @param key * the key to look up * @param value * container that stores the value returned by scalaris * * @throws ConnectionException * if the connection is not active or a communication error * occurs or an exit signal was received or the remote node * sends a message containing an invalid cookie * @throws TimeoutException * if a timeout occurred while trying to fetch the value * @throws NotFoundException * if the requested key does not exist * @throws UnknownException * if any other error occurs * * @see #readObject(OtpErlangString) * @since 2.1 */ public void readCustom(String key, CustomOtpObject<?> value) throws ConnectionException, TimeoutException, UnknownException, NotFoundException { try { value.setOtpValue(readObject(new OtpErlangString(key))); } catch (ClassCastException e) { // e.printStackTrace(); throw new UnknownException(e); } } /** * Stores the given <code>key</code>/<code>value</code> pair. * * @param key * the key to store the value for * @param value * the value to store * * @throws ConnectionException * if the connection is not active or a communication error * occurs or an exit signal was received or the remote node * sends a message containing an invalid cookie * @throws TimeoutException * if a timeout occurred while trying to write the value * @throws UnknownException * if any other error occurs */ public void writeObject(OtpErlangString key, OtpErlangObject value) throws ConnectionException, TimeoutException, UnknownException { if (transLog == null) { throw new TransactionNotStartedException(); } OtpErlangObject received_raw = null; try { connection.sendRPC("transstore.transaction_api", "jWrite", new OtpErlangList(new OtpErlangObject[] {key, value, transLog})); /* * possible return values: * - {{fail, not_found}, TransLog} * - {{fail, timeout}, TransLog} * - {{fail, fail}, TransLog} * - {ok, NewTransLog} */ received_raw = connection.receiveRPC(); OtpErlangTuple received = (OtpErlangTuple) received_raw; transLog_old = transLog; transLog = (OtpErlangList) received.elementAt(1); if (received.elementAt(0).equals(new OtpErlangAtom("ok"))) { return; } else { OtpErlangTuple status = (OtpErlangTuple) received.elementAt(0); if (status.elementAt(1).equals(new OtpErlangAtom("timeout"))) { throw new TimeoutException(received_raw); // } else if (status.elementAt(1).equals(new OtpErlangAtom("fail"))) { //// throw new UnknownException(status.elementAt(1).toString()); // throw new UnknownException(received_raw); } else { throw new UnknownException(received_raw); } } } catch (OtpErlangExit e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (OtpAuthException e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (IOException e) { // e.printStackTrace(); throw new ConnectionException(e); } catch (ClassCastException e) { // e.printStackTrace(); // received_raw is not null since the first class cast is after the RPC! throw new UnknownException(e, received_raw); } } /** * Stores the given <code>key</code>/<code>value</code> pair. * * @param key * the key to store the value for * @param value * the value to store * * @throws ConnectionException * if the connection is not active or a communication error * occurs or an exit signal was received or the remote node * sends a message containing an invalid cookie * @throws TimeoutException * if a timeout occurred while trying to write the value * @throws UnknownException * if any other error occurs * * @see #writeObject(OtpErlangString, OtpErlangObject) */ public void write(String key, String value) throws ConnectionException, TimeoutException, UnknownException { writeCustom(key, new CustomOtpStringObject(value)); // writeObject(new OtpErlangString(key), new OtpErlangString(value)); } /** * Stores the given <code>key</code>/<code>value</code> pair. * * @param key * the key to store the value for * @param value * the value to store * * @throws ConnectionException * if the connection is not active or a communication error * occurs or an exit signal was received or the remote node * sends a message containing an invalid cookie * @throws TimeoutException * if a timeout occurred while trying to write the value * @throws UnknownException * if any other error occurs * * @see #writeObject(OtpErlangString, OtpErlangObject) * @since 2.1 */ public void writeCustom(String key, CustomOtpObject<?> value) throws ConnectionException, TimeoutException, UnknownException { writeObject(new OtpErlangString(key), value.getOtpValue()); } /** * Reverts the last (read, parallelRead or write) operation by restoring the * last state. * * If there was no operation or the last operation was already * reverted, this method does nothing. * * <p> * This method is especially useful if after an unsuccessful read a value * with the same key should be written which is not possible if the failed * read is still in the transaction's log. * </p> * <p> * NOTE: This method works only ONCE! Subsequent calls will do nothing. * </p> */ public void revertLastOp() { if (transLog_old != null) { transLog = transLog_old; transLog_old = null; } } /** * Closes the transaction's connection to a scalaris node. * * Note: Subsequent calls to the other methods will throw * {@link ConnectionException}s! */ public void closeConnection() { connection.close(); } }