/* * CDDL HEADER START * * The contents of this file are subject to the terms of the Common Development * and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at * src/com/vodafone360/people/VODAFONE.LICENSE.txt or * http://github.com/360/360-Engine-for-Android * See the License for the specific language governing permissions and * limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each file and * include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the fields * enclosed by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved. * Use is subject to license terms. */ package com.vodafone360.people.service.utils; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import com.vodafone360.people.datatypes.BaseDataType; import com.vodafone360.people.datatypes.ServerError; import com.vodafone360.people.service.io.QueueManager; import com.vodafone360.people.service.io.Request; import com.vodafone360.people.service.io.ResponseQueue.DecodedResponse; import com.vodafone360.people.utils.LogUtils; /** * TimeOutWatcher utility... This is a utility class that is intended to * dispatch time-outs for each sent request individually. The thread should be * managing only the requests which can expire, i.e. don't add request with * timeout "-1" TODO: Consider using the WorkerThread to check for timeouts, * that would save us one thread! !!! Also ResponseQueue, RequestQueue and * TimeOutWatcher should be managed together and used via a common mutex !!! */ public class TimeOutWatcher implements Runnable { /** * Flag to determine whether or not the watcher is running. */ private boolean mIsRunning = false; /** * The list of watched requests sorted by their expiry dates in ascending * order. */ private LinkedList<Request> mRequests; /** * The thread watching for timed out requests. */ private Thread mThread; /** * Constructor. */ public TimeOutWatcher() { LogUtils.logV("TimeOutWatcher.TimeOutWatcher() => TimeOutWatcher constructor called."); } /** * TimeOutWatcher thread that checks for the next time to run, sleeps when * not busy and sends timeouts. */ @Override public void run() { long nextRuntime, currentTime; // need to synchronize it all but calling wait() will release the lock synchronized (QueueManager.getInstance().lock) { while (mIsRunning) { nextRuntime = getNextRuntime(); LogUtils.logV("TimeOutWatcher.run(): nextRuntime=" + nextRuntime); if (nextRuntime < 0) { // nothing to watch at the moment, let's wait for a // notification for "synchronized(this)" try { LogUtils.logV("TimeOutWatcher.run(): nothing to watch, calling wait()"); QueueManager.getInstance().lock.wait(); } catch (InterruptedException e) { LogUtils.logW("TimeOutWatcher.run(): " + "InterruptedException whithin this.wait() => " + e); } } else { // one or more requests need to be watched currentTime = System.currentTimeMillis(); nextRuntime = nextRuntime - currentTime; if (nextRuntime > 0) { // no request timed out yet, let's wait for the next // timeout try { LogUtils.logV("TimeOutWatcher.run(): no time out yet, calling wait(" + nextRuntime + ")"); QueueManager.getInstance().lock.wait(nextRuntime); } catch (InterruptedException e) { LogUtils.logW("TimeOutWatcher.run(): " + "InterruptedException within this.wait(nextRuntime) => " + e); } } else { // one or more request have timed out, send them a time // out event sendTimeoutEvent(currentTime); } } } mThread = null; } } /** * Starts the TimeOutWatcher thread and performs initialization. Note: this * method shall be called within synchronized(this) block. */ private void startThread() { mIsRunning = true; mRequests = new LinkedList<Request>(); mThread = new Thread(this); mThread.start(); } /** * Stops the TimeOutWatcher thread and releases the memory. Note: this * method shall be called within synchronized(this) block. */ private void stopThread() { if (mIsRunning) { mRequests.clear(); mRequests = null; // let the thread die mIsRunning = false; QueueManager.getInstance().lock.notify(); } } /** * Finds the closest time to perform a new check on timeouts. * * @return the next time when a timeout check is needed, -1 if no nothing to * perform */ private long getNextRuntime() { // just get the first one as the list of requests is sorted final Request request = mRequests.peek(); if (request == null) { return -1; } return request.getExpiryDate(); } /** * Sends a timeout event for all the expired requests. * * @param currentTime the current time until when a timeout event needs to * be sent */ private void sendTimeoutEvent(long currentTime) { LogUtils.logV("TimeOutWatcher.sendTimeoutEvent(" + currentTime + ")"); while (mRequests.size() > 0) { final Request request = mRequests.get(0); if (request.getExpiryDate() <= currentTime) { LogUtils.logW("TimeOutWatcher.sendTimeoutEvent(): " + "Expired request found with reqId=[" + request.getRequestId() + "], type=["+request.mType+"] and timeout=" + request.getTimeout() + " milliseconds"); fireRequestExpired(request); // no need to remove the request, this happened during previous // method call... (removeRequest is called when adding a // response) } else { // the list is ordered by expiry date, no need to check the rest // of it break; } } } /** * Inserts a request in the requests list while maintaining it sorted by * ascending order of expiry date. * * @param request the request to insert */ private int insertRequestByExpiryDate(Request request) { for (int i = 0; i < mRequests.size(); i++) { final Request currentRequest = mRequests.get(i); if (currentRequest.getExpiryDate() > request.getExpiryDate()) { // add the request before the current one mRequests.add(i, request); return i; } } // the request is either the first one or the last one mRequests.add(request); if (mRequests.size() == 1) { // was the only one return 0; } // was added at the end return mRequests.size() - 1; } /** * Creates a TimeOut event and adds it to the response queue. FIXME: this * assumes that adding a request to the response queue will trigger a * synchronous removeRequest() call with the same thread. * * @param request the request that has timed out */ private void fireRequestExpired(Request request) { // create a list with a server error containing a timeout final List<BaseDataType> data = new ArrayList<BaseDataType>(1); final ServerError timeoutError = new ServerError(ServerError.ErrorType.REQUEST_TIMEOUT, request.getRequestId()); timeoutError.errorDescription = "TimeOutWatcher detected that the request id=[" + request.getRequestId() + "] has timed out."; data.add(timeoutError); // set the request as expired request.expired = true; // add the timeout error to the response queue LogUtils.logW("TimeOutWatcher.fireRequestExpired(): " + "adding a timeout error to the response queue for reqId=[" + request.getRequestId() + "]"); QueueManager.getInstance().addResponse( new DecodedResponse(request.getRequestId(), data, request.mEngineId, DecodedResponse.ResponseType.SERVER_ERROR.ordinal())); } /** * Adds a request to be watched for timeouts. Note: it is assumed that the * request expiry date is calculated. * * @param request the request to add */ public void addRequest(Request request) { synchronized (QueueManager.getInstance().lock) { // make sure to add requests with a valid timeout if ((request != null) && (request.getExpiryDate() >= 0)) { if (!mIsRunning) { // start the thread if not already existing startThread(); } // insert the request in the sorted requests list final int index = insertRequestByExpiryDate(request); // check if request is added in front of the others. If not, no // need to notify, we can still sleep! if (index == 0) { LogUtils.logV("TimeOutWatcher.addRequest(): wake up the thread"); QueueManager.getInstance().lock.notify(); } } } } /** * Removes a request from being watched for timeouts. * * @param request the request to remove */ public void removeRequest(Request request) { // The TimeOutWatcher is not initialized or has been stopped already, // just ignore the request if (!mIsRunning) return; synchronized (QueueManager.getInstance().lock) { if ((request != null) && (request.getExpiryDate() >= 0) && mRequests != null) { int index = -1; for (int i = 0; i < mRequests.size(); i++) { final Request currentRequest = mRequests.get(i); if (currentRequest == request) { index = i; mRequests.remove(i); break; } } // check if the first request was removed. If not, no need to // notify, we can still sleep! if (index == 0) { LogUtils.logV("TimeOutWatcher.removeRequest(): wake up the thread"); QueueManager.getInstance().lock.notify(); } } } } /** * Kills the TimeOutWatcher (releases memory and running thread). */ public void kill() { synchronized (QueueManager.getInstance().lock) { stopThread(); } } /** * Sends a timeout event for all the requests. */ public void invalidateAllRequests() { synchronized (QueueManager.getInstance().lock) { LogUtils.logV("TimeOutWatcher.invalidateAllRequests()"); if (mRequests == null) return; while (mRequests.size() > 0) { final Request request = mRequests.get(0); LogUtils.logV("TimeOutWatcher.invalidateAllRequests(): " + "forcing a timeout for reqId=[" + request.getRequestId() + "] and timeout=" + request.getTimeout() + " milliseconds"); fireRequestExpired(request); // no need to remove the request, this happened during previous // method call... (removeRequest is called when adding a // response) } } } // //////////////// // TEST METHODS // // //////////////// // TODO: should have package access only but requires to modify the test // package to have the same package name /** * Gets the current number of requests being watched for timeouts. */ public int getRequestsCount() { synchronized (QueueManager.getInstance().lock) { if (mRequests != null) { return mRequests.size(); } return 0; } } /** * Gets an array containing all the requests being watched for timeouts. * * @return array containing all requests, NULL if list of requests * maintained internally is NLL or empty. */ public Request[] getRequestsArray() { synchronized (QueueManager.getInstance().lock) { if (mRequests != null && mRequests.size() > 0) { final Request[] requests = new Request[mRequests.size()]; return mRequests.toArray(requests); } return null; } } }