/* * Copyright 1999-2008 University of Chicago * * 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 org.nimbustools.ctxbroker.blackboard; import org.nimbustools.ctxbroker.Identity; import org.nimbustools.ctxbroker.ContextBrokerException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.util.*; /** * Each resource gets one blackboard. Setting this up to keep its own * Hashtable of all instances to leave room for persistence in the future. */ public class Blackboard { // ------------------------------------------------------------------------- // STATIC VARIABLES // ------------------------------------------------------------------------- private static final Log logger = LogFactory.getLog(Blackboard.class.getName()); // The "database" of Blackboard objects // String ID --> Blackboard object private static final Hashtable<String,Blackboard> all = new Hashtable<String, Blackboard>(8); // ------------------------------------------------------------------------- // INSTANCE VARIABLES // ------------------------------------------------------------------------- private Identity[] allIdentityCache = null; // All nodes this blackboard knows about. // Integer workspaceID --> Node object private final Hashtable<Integer, Node> allNodes = new Hashtable<Integer, Node>(64); // All provided roles this blackboard knows about. // String roleName --> ProvidedRole object private final Hashtable<String, ProvidedRole> allProvidedRoles = new Hashtable<String, ProvidedRole>(16); // All data this blackboard knows about. // String roleName --> RequiredData object private final Hashtable<String, RequiredData> allRequiredDatas = new Hashtable<String, RequiredData>(16); // All RequiredRole objects this blackboard knows about. // No key to use for a hashtable because each is not only the // name but host and key requirements also. See RequiredRole // equals/hashCode method. No need for a set lock because set is // always accessed and mutated under this.dbLock. private final Set<RequiredRole> allRequiredRoles = new HashSet<RequiredRole>(16); private boolean needsRefresh = true; // One lock per blackboard. Locking "up high" for now, optimize later private final Object dbLock = new Object(); // Mainly for the future DB implementation (and logging) private final String id; // flips once and only once to true when all report OK private boolean allOK = false; // flips once and only once to true when a node reports an error private boolean oneErrorOccured = false; // stopping condition + for resource prop private int numNodes = 0; private int totalNodes = 0; // ------------------------------------------------------------------------- // CONSTRUCTOR // ------------------------------------------------------------------------- Blackboard(String id) { if (id == null) { throw new IllegalArgumentException("id cannot be null"); } this.id = id; } // ------------------------------------------------------------------------- // FACTORY // ------------------------------------------------------------------------- /** * Get or create a blackboard for a contextualization context. * * @param id may not be null * @return Blackboard */ public synchronized static Blackboard createOrGetBlackboard(String id) { if (id == null) { throw new IllegalArgumentException("id cannot be null"); } Blackboard bb = all.get(id); if (bb == null) { bb = new Blackboard(id); all.put(id, bb); return bb; } else { return bb; } } public CtxStatus getStatus() { synchronized (this.dbLock) { CtxStatus status = new CtxStatus(); status.setAllOk(this.allOK); status.setComplete(isComplete()); status.setErrorOccurred(this.oneErrorOccured); status.setPresentNodeCount(this.numNodes); status.setTotalNodeCount(this.totalNodes); return status; } } boolean isComplete() { synchronized (this.dbLock) { boolean complete; if (this.needsRefresh()) { this.refresh(); complete = !this.needsRefresh(); } else { complete = true; } // maybe we haven't heard from all the agents yet if (this.totalNodes <= 0 || this.totalNodes != this.numNodes) { complete = false; } return complete; } } // ------------------------------------------------------------------------- // MATCHING // ------------------------------------------------------------------------- // assumed under lock private void refreshNowNeeded() { this.needsRefresh = true; } // assumed under lock private void refreshNotNeeded() { this.needsRefresh = false; } private boolean needsRefresh() { return this.needsRefresh; } // assumed under lock private void refresh() { if (!this.needsRefresh()) { return; } logger.debug("Beginning refresh of blackboard '" + this.id + "'"); StringBuffer tracebuf = null; if (logger.isDebugEnabled()) { tracebuf = new StringBuffer(); tracebuf.append("\n==========================================\n"); } boolean stillNeedsRefresh = false; // look for provided roles not matched yet with required roles for (RequiredRole requiredRole : this.allRequiredRoles) { if (tracebuf != null) { tracebuf.append("Required role: ") .append(requiredRole) .append("\n"); } // delete old list and start over: requiredRole.clearProviders(); final ProvidedRole providedRole = this.allProvidedRoles.get(requiredRole.getName()); if (providedRole == null) { if (tracebuf != null) { tracebuf.append(" - Found no providers\n"); } stillNeedsRefresh = true; continue; } int count = 0; final Iterator<Identity> iter2 = providedRole.getProviders(); while (iter2.hasNext()) { requiredRole.addProvider(iter2.next()); count += 1; } if (tracebuf != null) { tracebuf.append(" - Added ") .append(count) .append(" providers\n"); } } // check the current node count against the expected node count if (this.totalNodes > 0 && this.totalNodes == this.numNodes) { ArrayList<Identity> allIdentities = new ArrayList<Identity>(); Enumeration<Node> nodes = this.allNodes.elements(); while (nodes.hasMoreElements()) { Node node = nodes.nextElement(); Enumeration<Identity> ids = node.getIdentities(); while (ids.hasMoreElements()) { allIdentities.add(ids.nextElement()); } } this.allIdentityCache = allIdentities.toArray( new Identity[allIdentities.size()]); if (tracebuf != null) { tracebuf.append(" - Instantiated, number of identities: ") .append(this.allIdentityCache.length) .append("\n"); } } else { if (this.allIdentityCache != null) { this.allIdentityCache = null; if (tracebuf != null) { tracebuf.append(" - Invalidated because of missing ") .append("IP or hostname\n"); } } else { if (tracebuf != null) { tracebuf.append(" - Remains invalid because of ") .append("missing nodes\n"); } } stillNeedsRefresh = true; } String tail = "\nFurther refresh"; if (stillNeedsRefresh) { this.refreshNowNeeded(); } else { this.refreshNotNeeded(); tail += " not"; } tail += " needed currently.\n"; if (tracebuf != null) { logger.trace("\n\nRefreshed blackboard '" + this.id + "'" + tracebuf.toString() + tail); } else { logger.debug("Refreshed blackboard '" + this.id + "'" + tail); } } // ------------------------------------------------------------------------- // DATA ADDITIONS // ------------------------------------------------------------------------- public void injectData(String dataName, String value) throws ContextBrokerException { if (dataName == null || dataName.trim().length() == 0) { // does not happen when object is created via XML (which is usual) throw new ContextBrokerException("Empty data element name (?)"); } synchronized (this.dbLock) { this._newData(dataName, value); } } // ------------------------------------------------------------------------- // NODE ADDITIONS // ------------------------------------------------------------------------- /** * Creates new Node, treats provides and requires documents in the * empty, provided interpretation. * * @param workspaceID workspace ID * @param nodeIdentities identity objects filled by factory/service. * What 'is' already based on creation request or initialization. * Once passed to this method, caller must discard pointers * (avoids needing to clone it). * @param allIdentitiesRequired This node expects identity info for all * nodes, not just those that fill a required role * @param requiredRoles The roles required by this node * @param requiredData The data required by this node * @param providedRoles the Roles provided by this node * @param totalNodesFromAgent total number of nodes reported by ctx agent * @throws ContextBrokerException illegalities */ public void addWorkspace(Integer workspaceID, Identity[] nodeIdentities, boolean allIdentitiesRequired, RequiredRole[] requiredRoles, DataPair[] requiredData, ProvidedRoleDescription[] providedRoles, int totalNodesFromAgent) throws ContextBrokerException { if (workspaceID == null) { throw new IllegalArgumentException("workspaceID cannot be null"); } if (nodeIdentities == null || nodeIdentities.length == 0) { throw new IllegalArgumentException("'real' nodeIdentities cannot be " + "null or empty, contextualization is not possible. " + "If a workspace has no NICs, requires and provides " + "documents should not have been given. If they " + "existed, the factory/service should have rejected " + "the request. " + "However this happened it is a programming error."); } synchronized (this.dbLock) { if (this.totalNodes > 0) { if (this.totalNodes != totalNodesFromAgent) { throw new ContextBrokerException("Context '" + this.id + "' has received a conflicting " + "total node count. Was previously " + this.totalNodes + "but has received a cluster definition with a total " + "of " + totalNodesFromAgent); } } else { this.totalNodes = totalNodesFromAgent; } this.numNodes += 1; if (this.numNodes > this.totalNodes) { throw new ContextBrokerException("Context '" + this.id + "' has heard from a new agent which " + "makes the total node count exceed the theoretical" + "maximum from the cluster definitions (" + this.totalNodes + ")."); } // invalidate cache if it existed this.allIdentityCache = null; Node node = this.allNodes.get(workspaceID); if (node != null) { throw new ContextBrokerException("Blackboard has " + "already added node with ID #" + workspaceID); } String[] requiredDataNames = null; if (requiredData != null && requiredData.length > 0) { // set up names of data this node needs requiredDataNames = new String[requiredData.length]; for (int i = 0; i < requiredData.length; i++) { requiredDataNames[i] = requiredData[i].getName(); } // If the contextualization definition included a value for // the data already, register it into the known-data store. // _intakeData also creates new RequiredData objects for any // newly seen data name (no matter if the value is present // or not). _intakeData(requiredData); } node = new Node(workspaceID, requiredDataNames); for (Identity identity : nodeIdentities) { node.addIdentity(identity); } if (providedRoles != null && providedRoles.length > 0) { this.handleProvidesRoles(node, providedRoles); } handleNewRequires(node, allIdentitiesRequired, requiredRoles); this.allNodes.put(workspaceID, node); this.refreshNowNeeded(); } } // no args are null and roles.length > 0 private void handleProvidesRoles(Node node, ProvidedRoleDescription[] roles) throws ContextBrokerException { for (ProvidedRoleDescription roleDesc : roles) { final String roleName = roleDesc.getRoleName(); // we are still under this.dbLock lock, check then act here is ok ProvidedRole role = this.allProvidedRoles.get(roleName); if (role == null) { role = new ProvidedRole(roleName); this.allProvidedRoles.put(roleName, role); } final String iface = roleDesc.getIface(); if (iface != null) { // only this specific interface provides the role final Identity identity = node.getParticularIdentity(iface); if (identity == null) { throw new ContextBrokerException("There is an " + "interface specification ('" + iface + "') in " + "the provides section for role '" + roleName + "' that does not have matching identity element " + "with that interface name. Cannot " + "contextualize #" + node.getId()); } role.addProvider(identity); } else { // each identity provides this role final Enumeration<Identity> identities = node.getIdentities(); while (identities.hasMoreElements()) { role.addProvider(identities.nextElement()); } } } } // no args are null private void handleNewRequires(Node node, boolean allIdentitiesRequired, RequiredRole[] requiredRoles) throws ContextBrokerException { final int workspaceID = node.getId(); node.setAllIdentitiesRequired(allIdentitiesRequired); if (requiredRoles != null && requiredRoles.length > 0) { this.handleRequiredRoles(node, requiredRoles); } else if (logger.isTraceEnabled()) { logger.trace("Requires section for #" + workspaceID + " has " + "no role-required elements." + " Allowing, perhaps the only thing required by " + "this node is the contextualization context's " + "all-identity list and/or just data elements."); } } // no args are null and datas.length > 0, always call under this.dbLock private void _intakeData(DataPair[] datas) { for (DataPair data : datas) { final String dataName = data.getName(); this._newData(dataName, data.getValue()); } } private void _newData(String name, String value) { if (name == null) { throw new IllegalArgumentException("name may not be null"); } // we are under this.dbLock lock, check then act here is ok RequiredData data = this.allRequiredDatas.get(name); if (data == null) { data = new RequiredData(name); this.allRequiredDatas.put(name, data); } if (value != null) { data.addNewValue(value); } } private boolean isOneDataValuePresent(String dataname) { final RequiredData data = this.allRequiredDatas.get(dataname); return data != null && data.numValues() > 0; } private List<DataPair> getDataValues(String dataname) { final RequiredData data = this.allRequiredDatas.get(dataname); if (data != null) { return data.getDataList(); } else { return null; } } // no args are null and roles.length > 0 private void handleRequiredRoles(Node node, RequiredRole[] roles) throws ContextBrokerException { for (RequiredRole role : roles) { // We expect a lot of duplicates, HashSet does not add if exists // in list already. boolean didNotExist = this.allRequiredRoles.add(role); if (didNotExist && logger.isTraceEnabled()) { logger.trace("Found new RequiredRole for blackboard '" + this.id + "': " + role + " -- cardinality now " + this.allRequiredRoles.size()); } // Copy reference to node specific list. // There is a possibility client provided requires schema with // multiple duplicate elements for some reason. Just silently // allowing that (i.e. with server side log message and not // exception client will see). if (!didNotExist) { // It existed already. We need to add the exact object to the // node's required role's, not the duplicate we created above. boolean found = false; for (RequiredRole aRole : this.allRequiredRoles) { if (aRole.equals(role)) { role = aRole; found = true; break; } } if (!found) { throw new ContextBrokerException("Set contained a " + "required role already but we cannot find the " + "object in the set's iterator (?). Role: " + role); } } didNotExist = node.addRequiredRole(role); if (!didNotExist) { logger.warn("Client provided requires document with " + "duplicate required role elements. That is not " + "expected but ignoring and handling it. Required " + "role that trigged this: " + role + ". Node #" + node.getId()); } } } // ------------------------------------------------------------------------- // NODE RETRIEVAL/UPDATES // ------------------------------------------------------------------------- // (no service or client updates, VM assertions allowed) public NodeManifest retrieve(Integer workspaceID) throws ContextBrokerException { if (workspaceID == null) { throw new IllegalArgumentException("workspaceID cannot be null"); } final Node node = this.allNodes.get(workspaceID); if (node == null) { throw new ContextBrokerException("Blackboard is not aware " + "of node with ID #" + workspaceID); } synchronized(this.dbLock) { this.refresh(); // Check if identities are available final List<Identity> identities; final boolean allIdentities = node.isAllIdentitiesRequired(); if (allIdentities) { if (this.allIdentityCache == null) { return null; } else { identities = Arrays.asList(this.allIdentityCache); } } else { identities = new ArrayList<Identity>(); } // Check if all data is available. At least one value of each // data requirement constitutes "available" final ArrayList<DataPair> data = new ArrayList<DataPair>(); for (String reqData : node.getRequiredDataNames()) { if (this.isOneDataValuePresent(reqData)) { data.addAll(this.getDataValues(reqData)); } else { if (logger.isTraceEnabled()) { logger.trace("Not constructing node manifest because " + "suppressIncomplete is true, and a required " + "data item for this node (#" + node.getId() + ") is not present: "); } return null; // *** EARLY RETURN *** } } final ArrayList<RoleIdentityPair> roles = new ArrayList<RoleIdentityPair>(); final Iterator<RequiredRole> iter = node.getRequiredRoles(); while (iter.hasNext()) { final RequiredRole aRole = iter.next(); for (Identity provider : aRole.getProviders()) { roles.add(new RoleIdentityPair(aRole.getName(), provider)); if (!allIdentities) { identities.add(provider); } } } return new NodeManifest(identities, data, roles); } } // ------------------------------------------------------------------------- // NODE STATUS // ------------------------------------------------------------------------- public void okExit(Integer workspaceID) throws ContextBrokerException { final Node node = this.allNodes.get(workspaceID); if (node == null) { throw new ContextBrokerException("unknown workspace #" + workspaceID); } synchronized (this.dbLock) { CtxResult result = node.getCtxResult(); if (result.hasOkOccurred() || result.hasErrorOccurred()) { throw new ContextBrokerException("already received " + "exiting report from workspace #" + workspaceID); } result.setOkOccurred(true); // check if all are now OK for (Enumeration<Node> e = this.allNodes.elements(); e.hasMoreElements();) { final Node aNode = e.nextElement(); if (!aNode.getCtxResult().hasOkOccurred()) { return; } } this.allOK = true; } } public void errorExit(Integer workspaceID, short exitCode, String errorMessage) throws ContextBrokerException { final Node node = this.allNodes.get(workspaceID); if (node == null) { throw new ContextBrokerException("unknown workspace #" + workspaceID); } synchronized (this.dbLock) { CtxResult result = node.getCtxResult(); if (result.hasOkOccurred() || result.hasErrorOccurred()) { throw new ContextBrokerException("already received " + "exiting report from workspace #" + workspaceID); } result.setErrorOccurred(true); result.setErrorCode(exitCode); result.setErrorMessage(errorMessage); this.oneErrorOccured = true; } } public List<NodeStatus> identities(boolean allNodes, String host, String ip) { synchronized (this.dbLock) { if (allNodes) { final List<NodeStatus> list = new ArrayList<NodeStatus>(); for (Enumeration<Node> e = this.allNodes.elements(); e.hasMoreElements();) { list.add(new NodeStatus(e.nextElement())); } return list; } else { final Node node = this.findNode(host, ip); if (node != null) { return Collections.singletonList(new NodeStatus(node)); } return Collections.emptyList(); } } } private Node findNode(String host, String ip) { if (host == null && ip == null) { return null; } for (Enumeration<Node> e = this.allNodes.elements(); e.hasMoreElements();) { final Node node = e.nextElement(); for (Enumeration e2 = node.getIdentities(); e2.hasMoreElements();) { final Identity ident = (Identity) e2.nextElement(); if (host != null) { if (host.equals(ident.getHostname())) { return node; } } else { if (ip.equals(ident.getIp())) { return node; } } } } return null; } }