package freenet.clients.http.updateableelements; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import freenet.support.Logger; import freenet.support.Ticker; /** A manager class that manages all the pushing. All it's public method must be synchronized to maintain consistency. */ public class PushDataManager { private static volatile boolean logMINOR; static { Logger.registerClass(PushDataManager.class); } /** What notifications are waiting for the leader */ private Map<String, List<UpdateEvent>> awaitingNotifications = new HashMap<String, List<UpdateEvent>>(); /** What elements are on the page */ private Map<String, List<BaseUpdateableElement>> pages = new HashMap<String, List<BaseUpdateableElement>>(); /** What pages are on the element. It is redundant with the pages map. */ private Map<String, List<String>> elements = new HashMap<String, List<String>>(); /** Stores whether a keepalive was received for a request since the Cleaner last run */ private Map<String, Boolean> isKeepaliveReceived = new HashMap<String, Boolean>(); private Map<String, Boolean> isFirstKeepaliveReceived = new HashMap<String, Boolean>(); /** The Cleaner that runs periodically and cleanes the failing requests */ private Ticker cleaner; /** A task for the Cleaner that the Cleaner invokes */ private CleanerTimerTask cleanerTask = new CleanerTimerTask(); /** The Cleaner only runs when needed. If this field is true, then the Cleaner is scheduled to run */ private boolean isScheduled = false; public PushDataManager(Ticker ticker) { cleaner = ticker; } /** * An element is updated and needs to be pushed to all requests. * * @param id * - The id of the element that changed */ public synchronized void updateElement(String id) { if (logMINOR) { Logger.minor(this, "Element updated id:" + id); } boolean needsUpdate = false; if(elements.containsKey(id)==false){ if(logMINOR){ Logger.minor(this, "Element is updating, but not present on elements! elements:"+elements+" pages:"+pages+" awaitingNotifications:"+awaitingNotifications); } } if (elements.containsKey(id)) for (String reqId : elements.get(id)) { if(logMINOR){ Logger.minor(this, "Element is present on page:"+reqId+". Adding an UpdateEvent for all notification list."); } for(Map.Entry<String, List<UpdateEvent>> entry : awaitingNotifications.entrySet()) { // for (List<UpdateEvent> notificationList : awaitingNotifications.values()) { List<UpdateEvent> notificationList = entry.getValue(); UpdateEvent updateEvent = new UpdateEvent(reqId, id); if (notificationList.contains(updateEvent) == false) { notificationList.add(updateEvent); if (logMINOR) { Logger.minor(this, "Notification("+updateEvent+") added to a notification list for "+entry.getKey()); } } else { if (logMINOR) Logger.minor(this, "Not notifying "+entry.getKey()+" because already on list"); } } needsUpdate = true; } if (needsUpdate) { if(logMINOR){ Logger.minor(this, "Waking up notification polls"); } notifyAll(); } } /** * A pushed element is rendered and needs to be tracked. * * @param requestUniqueId * - The requestId that rendered the element * @param element * - The element that is rendered */ public synchronized void elementRendered(String requestUniqueId, BaseUpdateableElement element) { if(logMINOR){ Logger.minor(this, "Element is rendered in page:"+requestUniqueId+" element:"+element); } // Add to the pages if (pages.containsKey(requestUniqueId) == false) { pages.put(requestUniqueId, new ArrayList<BaseUpdateableElement>()); } pages.get(requestUniqueId).add(element); // Add to the elements String id = element.getUpdaterId(requestUniqueId); if (elements.containsKey(id) == false) { elements.put(id, new ArrayList<String>()); } elements.get(id).add(requestUniqueId); // The request needs to be tracked isKeepaliveReceived.put(requestUniqueId, true); if (awaitingNotifications.containsKey(requestUniqueId) == false) { awaitingNotifications.put(requestUniqueId, new ArrayList<UpdateEvent>()); } // If the Cleaner isn't running, then we schedule it to clear this request if failing if (isScheduled == false) { if (logMINOR) { Logger.minor(this, "Cleaner is queued(1) time:" + System.currentTimeMillis()); } cleaner.queueTimedJob(cleanerTask, "cleanerTask", getDelayInMs(), false, true); isScheduled = true; } } /** * Returns the element's current state. * * @param requestId * - The requestId that needs the element. * @param id * - The element's id */ public synchronized BaseUpdateableElement getRenderedElement(String requestId, String id) { if(logMINOR){ Logger.minor(this, "Getting element data for element:"+id+" in page:"+requestId); } if (pages.get(requestId) != null) for (BaseUpdateableElement element : pages.get(requestId)) { if (element.getUpdaterId(requestId).compareTo(id) == 0) { element.updateState(false); return element; } } Logger.error(this, "Could not find data for the element requested. requestId:"+requestId+" id:"+id+" pages:"+pages+" keepaliveReceived:"+isKeepaliveReceived); return null; } /** * Fails a request and copies all notifications directed to it to another request. It is invoked when a leadership change occurs. * * @param originalRequestId * - The failing leader's id * @param newRequestId * - The new leader's id * @return Was the failover successful? */ public synchronized boolean failover(String originalRequestId, String newRequestId) { if (logMINOR) { Logger.minor(this, "Failover, original:" + originalRequestId + " new:" + newRequestId); } if (awaitingNotifications.containsKey(originalRequestId)) { awaitingNotifications.put(newRequestId, awaitingNotifications.remove(originalRequestId)); if (logMINOR) { Logger.minor(this, "copied " + awaitingNotifications.get(newRequestId).size() + " notification:" + awaitingNotifications.get(newRequestId)); } notifyAll(); return true; } else { if (logMINOR) { Logger.minor(this, "Does not contains key"); } return false; } } /** * The request leaves, so it needs to be deleted * * @param requestId * - The id of the request that is leaving * @return Was a request deleted? */ public synchronized boolean leaving(String requestId) { return deleteRequest(requestId); } /** * A keepalive received. * * @param requestId * - The id of the request that sent the keepalive * @return Was it successful? */ public synchronized boolean keepAliveReceived(String requestId) { if(logMINOR){ Logger.minor(this, "Keepalive is received for page:"+requestId); } // If the request is already deleted, then fail if (isKeepaliveReceived.containsKey(requestId) == false) { if(logMINOR){ Logger.minor(this, "Keepalive failed"); } return false; } isKeepaliveReceived.put(requestId, true); isFirstKeepaliveReceived.put(requestId, true); notifyAll(); return true; } /** * Waits and return the next notification. Calling this method setup the notification list. * * @param requestId * - The id of the request * @return The next notification when present */ public synchronized UpdateEvent getNextNotification(String requestId) { if (logMINOR) { Logger.minor(this, "Polling for notification:" + requestId); } while (awaitingNotifications.get(requestId) != null && awaitingNotifications.get(requestId).size() == 0 || // No notifications (awaitingNotifications.get(requestId) != null && awaitingNotifications.get(requestId).size() != 0 && isFirstKeepaliveReceived.containsKey(awaitingNotifications.get(requestId).get(0).requestId)==false)) { // Not asked us yet try { wait(); } catch (InterruptedException ie) { return null; } } if (awaitingNotifications.get(requestId) == null) { return null; } if (logMINOR) { Logger.minor(this, "Getting notification, notification:" + awaitingNotifications.get(requestId).get(0) + ",remaining:" + (awaitingNotifications.get(requestId).size() - 1)); } return awaitingNotifications.get(requestId).remove(0); } /** Returns the cleaner's delay in ms */ private int getDelayInMs() { return (int) (UpdaterConstants.KEEPALIVE_INTERVAL_SECONDS * 1000 * 2.1); } /** * Deletes a request either because of failing or leaving * * @param requestId * - The id of the request * @return Was a request deleted? */ private synchronized boolean deleteRequest(String requestId) { if (logMINOR) { Logger.minor(this, "DeleteRequest with requestId:" + requestId); } if (isKeepaliveReceived.containsKey(requestId) == false) { if (logMINOR) { Logger.minor(this, "Request already cleaned, doing nothing"); } return false; } isKeepaliveReceived.remove(requestId); isFirstKeepaliveReceived.remove(requestId); // Iterate over all the pushed elements present on the page for (BaseUpdateableElement element : new ArrayList<BaseUpdateableElement>(pages.get(requestId))) { pages.get(requestId).remove(element); // FIXME why can't we just unconditionally remove(requestId) at the end? if (pages.get(requestId).size() == 0) { pages.remove(requestId); } String id = element.getUpdaterId(requestId); elements.get(id).remove(requestId); if (elements.get(id).size() == 0) { elements.remove(id); } element.dispose(); // Delete all notification originated from the deleted element for (String events : awaitingNotifications.keySet()) { for (UpdateEvent updateEvent : new ArrayList<UpdateEvent>(awaitingNotifications.get(events))) { if (updateEvent.requestId.compareTo(requestId) == 0) { awaitingNotifications.get(events).remove(updateEvent); } } } } awaitingNotifications.remove(requestId); return true; } /** An event that tells the client what and how it should be updated */ public class UpdateEvent { private String requestId; private String elementId; private UpdateEvent(String requestId, String elementId) { this.requestId = requestId; this.elementId = elementId; } public String getRequestId() { return requestId; } public String getElementId() { return elementId; } @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj instanceof UpdateEvent) { UpdateEvent o = (UpdateEvent) obj; if (o.getRequestId().compareTo(requestId) == 0 && o.getElementId().compareTo(elementId) == 0) { return true; } } return false; } @Override public int hashCode() { return requestId.hashCode() + elementId.hashCode(); } @Override public String toString() { return "UpdateEvent[requestId=" + requestId + ",elementId=" + elementId + "]"; } } /** A task for the Cleaner, that periodically checks for failed requests. */ private class CleanerTimerTask implements Runnable { @Override public void run() { synchronized (PushDataManager.this) { if (logMINOR) { Logger.minor(this, "Cleaner running:" + isKeepaliveReceived); } isScheduled = false; for (Entry<String, Boolean> entry : new HashMap<String, Boolean>(isKeepaliveReceived).entrySet()) { if (entry.getValue() == false) { if (logMINOR) { Logger.minor(this, "Cleaner cleaned request:" + entry.getKey()); } deleteRequest(entry.getKey()); } else { if (logMINOR) { Logger.minor(this, "Cleaner reseted request:" + entry.getKey()); } isKeepaliveReceived.put(entry.getKey(), false); } } if (isKeepaliveReceived.size() != 0) { if (logMINOR) { Logger.minor(this, "Cleaner is queued(2) time:" + System.currentTimeMillis()); } cleaner.queueTimedJob(cleanerTask, "cleanerTask", getDelayInMs(), false, true); isScheduled = true; } } } } }