// Copyright 2004-2014 Jim Voris
//
// 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 com.qumasoft.server;
import com.qumasoft.qvcslib.QVCSException;
import com.qumasoft.qvcslib.ServerResponseFactoryInterface;
import com.qumasoft.qvcslib.response.ServerResponseTransactionBegin;
import com.qumasoft.qvcslib.response.ServerResponseTransactionEnd;
import com.qumasoft.qvcslib.Utility;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A class to manage the outstanding transactions for the server. It is a singleton.
*
* @author Jim Voris
*/
public final class ServerTransactionManager {
// Create our logger object
private static final Logger LOGGER = Logger.getLogger("com.qumasoft.server");
private static final int STARTING_SERVER_TRANSACTION_NUMBER = 100000000;
private static final ServerTransactionManager SERVER_TRANSACTION_MANAGER = new ServerTransactionManager();
private final AtomicReference<Integer> nextTransactionId = new AtomicReference<>(STARTING_SERVER_TRANSACTION_NUMBER);
/**
* Our transaction counter collection
*/
private final Map<String, Integer> openTransactionCounterMap = Collections.synchronizedMap(new HashMap<String, Integer>());
/**
* The list of participants in an active transaction
*/
private final Map<String, Map<Integer, TransactionParticipantInterface>> pendingWorkToCompleteMap = Collections.synchronizedMap(new HashMap<String, Map<Integer,
TransactionParticipantInterface>>());
/**
* A map of timestamps to associate with pending transactions.
*/
private final Map<String, Date> pendingWorkTimestampMap = Collections.synchronizedMap(new HashMap<String, Date>());
/**
* Creates a new instance of ServerTransactionManager
*/
private ServerTransactionManager() {
}
/**
* Get the server transaction manager singleton.
* @return the server transaction manager singleton.
*/
public static ServerTransactionManager getInstance() {
return SERVER_TRANSACTION_MANAGER;
}
/**
* Set a begin transaction to the client.
* @param response link to the client.
* @return the transaction id.
*/
public int sendBeginTransaction(ServerResponseFactoryInterface response) {
String serverName = response.getServerName();
int transactionID = getNextTransactionID();
ServerResponseTransactionBegin beginTransaction = new ServerResponseTransactionBegin();
beginTransaction.setTransactionID(transactionID);
beginTransaction.setServerName(serverName);
clientBeginTransaction(response);
response.createServerResponse(beginTransaction);
return transactionID;
}
/**
* Send an end transaction to the client.
* @param response link to the client.
* @param transactionID the transaction id.
*/
public void sendEndTransaction(ServerResponseFactoryInterface response, int transactionID) {
String serverName = response.getServerName();
ServerResponseTransactionEnd endTransaction = new ServerResponseTransactionEnd();
endTransaction.setTransactionID(transactionID);
endTransaction.setServerName(serverName);
response.createServerResponse(endTransaction);
clientEndTransaction(response);
}
private int getNextTransactionID() {
return nextTransactionId.getAndSet(nextTransactionId.get() + 1);
}
/**
* Mark the beginning of a client 'transaction'.
* @param response the link to the client.
*/
public synchronized void clientBeginTransaction(ServerResponseFactoryInterface response) {
String key = getMapKey(response);
Integer useCount = openTransactionCounterMap.get(key);
if (useCount == null) {
useCount = Integer.valueOf(1);
pendingWorkTimestampMap.put(key, new Date());
} else {
useCount = Integer.valueOf(1 + useCount.intValue());
}
openTransactionCounterMap.put(key, useCount);
LOGGER.log(Level.INFO, "transaction count becomes: [" + useCount.toString() + "]");
}
/**
* Mark the end of a client 'transaction'. Commit any pending work by invoking the commit pending work call back on any registered transaction participants.
* @param response the link to the client.
*/
public synchronized void clientEndTransaction(ServerResponseFactoryInterface response) {
String key = getMapKey(response);
Integer useCount = openTransactionCounterMap.get(key);
if (useCount != null) {
useCount = Integer.valueOf(useCount.intValue() - 1);
if (useCount.intValue() == 0) {
openTransactionCounterMap.remove(key);
Date date = pendingWorkTimestampMap.remove(key);
commitPendingWork(key, date, response);
} else {
openTransactionCounterMap.put(key, useCount);
}
LOGGER.log(Level.INFO, "transaction count becomes: [" + useCount.toString() + "]");
} else {
LOGGER.log(Level.WARNING, "Unexpected client end transaction");
}
}
/**
* Does the given client have a transaction in progress.
* @param response the link to the client.
* @return true if there is a transaction in progress for the given client; false otherwise.
*/
public synchronized boolean transactionIsInProgress(ServerResponseFactoryInterface response) {
boolean flag = false;
String key = getMapKey(response);
Integer useCount = openTransactionCounterMap.get(key);
if (useCount != null && useCount.intValue() > 0) {
flag = true;
}
return flag;
}
/**
* Get the timestamp of the active transaction for the given client.
* @param response link to the client.
* @return the timestamp of the active transaction for the given client, or null if there is no active transaction.
*/
public synchronized Date getTransactionTimeStamp(ServerResponseFactoryInterface response) {
String key = getMapKey(response);
return pendingWorkTimestampMap.get(key);
}
/**
* Used when the client connection goes away to throw away any client references that we may have.
*
* @param response link to the client.
*/
public synchronized void flushClientTransaction(ServerResponseFactoryInterface response) {
String key = getMapKey(response);
openTransactionCounterMap.remove(key);
Date date = pendingWorkTimestampMap.remove(key);
commitPendingWork(key, date, response);
}
/**
* Join as a participant in the in-progress client transaction.
* @param response link to the client.
* @param participant the transaction participant. The participant's commitPendingChanges method will be called when the transaction completes.
*/
public synchronized void enlistPendingWork(ServerResponseFactoryInterface response, TransactionParticipantInterface participant) {
String key = getMapKey(response);
if (pendingWorkToCompleteMap.containsKey(key)) {
pendingWorkToCompleteMap.get(key).put(Integer.valueOf(participant.getPriority()), participant);
} else {
Map<Integer, TransactionParticipantInterface> workMap = new HashMap<>();
workMap.put(Integer.valueOf(participant.getPriority()), participant);
pendingWorkToCompleteMap.put(key, workMap);
}
}
private String getMapKey(ServerResponseFactoryInterface response) {
return response.getClientIPAddress() + ":" + Integer.toString(response.getClientPort());
}
private synchronized void commitPendingWork(final String key, final Date date, ServerResponseFactoryInterface response) {
if (pendingWorkToCompleteMap.containsKey(key)) {
Map<Integer, TransactionParticipantInterface> workMap = pendingWorkToCompleteMap.get(key);
Iterator<TransactionParticipantInterface> it = workMap.values().iterator();
while (it.hasNext()) {
TransactionParticipantInterface participant = it.next();
try {
participant.commitPendingChanges(response, date);
} catch (QVCSException e) {
LOGGER.log(Level.WARNING, Utility.expandStackTraceToString(e));
}
}
pendingWorkToCompleteMap.remove(key);
}
}
}