/** * Licensed to JumpMind Inc under one or more contributor * license agreements. See the NOTICE file distributed * with this work for additional information regarding * copyright ownership. JumpMind Inc licenses this file * to you under the GNU General Public License, version 3.0 (GPLv3) * (the "License"); you may not use this file except in compliance * with the License. * * You should have received a copy of the GNU General Public License, * version 3.0 (GPLv3) along with this library; if not, see * <http://www.gnu.org/licenses/>. * * 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.jumpmind.symmetric.route; import static org.apache.commons.lang.StringUtils.isNotBlank; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.jumpmind.extension.IBuiltInExtensionPoint; import org.jumpmind.symmetric.ISymmetricEngine; import org.jumpmind.symmetric.common.Constants; import org.jumpmind.symmetric.common.ParameterConstants; import org.jumpmind.symmetric.common.TableConstants; import org.jumpmind.symmetric.io.data.DataEventType; import org.jumpmind.symmetric.job.IJobManager; import org.jumpmind.symmetric.load.ConfigurationChangedDatabaseWriterFilter; import org.jumpmind.symmetric.model.DataMetaData; import org.jumpmind.symmetric.model.NetworkedNode; import org.jumpmind.symmetric.model.Node; import org.jumpmind.symmetric.model.NodeGroupLink; import org.jumpmind.symmetric.model.TableReloadRequest; import org.jumpmind.symmetric.model.TableReloadRequestKey; import org.jumpmind.symmetric.model.Trigger; import org.jumpmind.symmetric.model.TriggerRouter; import org.jumpmind.symmetric.service.IConfigurationService; import org.jumpmind.symmetric.service.ITriggerRouterService; public class ConfigurationChangedDataRouter extends AbstractDataRouter implements IDataRouter, IBuiltInExtensionPoint { public static final String ROUTER_TYPE = "configurationChanged"; final String CTX_KEY_TABLE_RELOAD_NEEDED = "Reload.Table." + ConfigurationChangedDataRouter.class.getSimpleName() + hashCode(); final String CTX_KEY_RESYNC_NEEDED = "Resync." + ConfigurationChangedDataRouter.class.getSimpleName() + hashCode(); final String CTX_KEY_FLUSH_CHANNELS_NEEDED = "FlushChannels." + ConfigurationChangedDataRouter.class.getSimpleName() + hashCode(); final String CTX_KEY_FLUSH_LOADFILTERS_NEEDED = "FlushLoadFilters." + ConfigurationChangedDatabaseWriterFilter.class.getSimpleName() + hashCode(); final String CTX_KEY_FLUSH_TRANSFORMS_NEEDED = "FlushTransforms." + ConfigurationChangedDataRouter.class.getSimpleName() + hashCode(); final String CTX_KEY_FLUSH_PARAMETERS_NEEDED = "FlushParameters." + ConfigurationChangedDataRouter.class.getSimpleName() + hashCode(); final String CTX_KEY_FLUSH_CONFLICTS_NEEDED = "FlushConflicts." + ConfigurationChangedDataRouter.class.getSimpleName() + hashCode(); final String CTX_KEY_RESTART_JOBMANAGER_NEEDED = "RestartJobManager." + ConfigurationChangedDataRouter.class.getSimpleName() + hashCode(); final String CTX_KEY_REFRESH_EXTENSIONS_NEEDED = "RefreshExtensions." + ConfigurationChangedDataRouter.class.getSimpleName() + hashCode(); public final static String KEY = "symconfig"; protected ISymmetricEngine engine; public ConfigurationChangedDataRouter() { } public ConfigurationChangedDataRouter(ISymmetricEngine engine) { this.engine = engine; } @SuppressWarnings("unchecked") public Set<String> routeToNodes(SimpleRouterContext routingContext, DataMetaData dataMetaData, Set<Node> possibleTargetNodes, boolean initialLoad, boolean initialLoadSelectUsed, TriggerRouter triggerRouter) { // the list of nodeIds that we will return Set<String> nodeIds = new HashSet<String>(); // the inbound data Map<String, String> columnValues = getDataMap(dataMetaData, engine != null ? engine.getSymmetricDialect() : null); Node me = findIdentity(); if (me != null) { NetworkedNode rootNetworkedNode = getRootNetworkNodeFromContext(routingContext); if (tableMatches(dataMetaData, TableConstants.SYM_NODE) || tableMatches(dataMetaData, TableConstants.SYM_NODE_SECURITY) || tableMatches(dataMetaData, TableConstants.SYM_NODE_HOST)) { /* * If this is sym_node or sym_node_security determine which * nodes it goes to. */ routeNodeTables(nodeIds, columnValues, rootNetworkedNode, me, routingContext, dataMetaData, possibleTargetNodes, initialLoad); } else if (tableMatches(dataMetaData, TableConstants.SYM_TABLE_RELOAD_REQUEST)) { String sourceNodeId = columnValues.get("SOURCE_NODE_ID"); String reloadEnabled = columnValues.get("RELOAD_ENABLED"); if (me.getNodeId().equals(sourceNodeId)) { if ("1".equals(reloadEnabled)) { List<TableReloadRequestKey> list = (List<TableReloadRequestKey>) routingContext .get(CTX_KEY_TABLE_RELOAD_NEEDED); if (list == null) { list = new ArrayList<TableReloadRequestKey>(); routingContext.put(CTX_KEY_TABLE_RELOAD_NEEDED, list); } String targetNodeId = columnValues.get("TARGET_NODE_ID"); String routerId = columnValues.get("ROUTER_ID"); String triggerId = columnValues.get("TRIGGER_ID"); list.add(new TableReloadRequestKey(targetNodeId, sourceNodeId, triggerId, routerId, dataMetaData.getData().getSourceNodeId())); } } else { for (Node nodeThatMayBeRoutedTo : possibleTargetNodes) { if (!Constants.DEPLOYMENT_TYPE_REST.equals(nodeThatMayBeRoutedTo .getDeploymentType()) && !nodeThatMayBeRoutedTo.requires13Compatiblity() && nodeThatMayBeRoutedTo.getNodeId().equals(sourceNodeId)) { nodeIds.add(sourceNodeId); } } } } else { IConfigurationService configurationService = engine.getConfigurationService(); for (Node nodeThatMayBeRoutedTo : possibleTargetNodes) { if (!Constants.DEPLOYMENT_TYPE_REST.equals(nodeThatMayBeRoutedTo .getDeploymentType()) && !nodeThatMayBeRoutedTo.requires13Compatiblity() && (initialLoad || !isSameNumberOfLinksAwayFromRoot(nodeThatMayBeRoutedTo, rootNetworkedNode, me))) { NodeGroupLink link = configurationService.getNodeGroupLinkFor( me.getNodeGroupId(), nodeThatMayBeRoutedTo.getNodeGroupId(), false); if (initialLoad || (link != null && link.isSyncConfigEnabled())) { nodeIds.add(nodeThatMayBeRoutedTo.getNodeId()); } } } if (StringUtils.isBlank(dataMetaData.getData().getSourceNodeId())) { queueSyncTriggers(routingContext, dataMetaData, columnValues); } if (tableMatches(dataMetaData, TableConstants.SYM_CHANNEL)) { routingContext.put(CTX_KEY_FLUSH_CHANNELS_NEEDED, Boolean.TRUE); } if (tableMatches(dataMetaData, TableConstants.SYM_CONFLICT)) { routingContext.put(CTX_KEY_FLUSH_CONFLICTS_NEEDED, Boolean.TRUE); } if (tableMatches(dataMetaData, TableConstants.SYM_LOAD_FILTER)) { routingContext.put(CTX_KEY_FLUSH_LOADFILTERS_NEEDED, Boolean.TRUE); } if (tableMatches(dataMetaData, TableConstants.SYM_PARAMETER)) { routingContext.put(CTX_KEY_FLUSH_PARAMETERS_NEEDED, Boolean.TRUE); if (me.getExternalId().equals(columnValues.get("EXTERNAL_ID")) && me.getNodeGroupId().equals(columnValues.get("NODE_GROUP_ID"))) { nodeIds.clear(); } if (StringUtils.isBlank(dataMetaData.getData().getSourceNodeId()) && (dataMetaData.getData().getRowData() != null && dataMetaData .getData().getRowData().contains("job."))) { routingContext.put(CTX_KEY_RESTART_JOBMANAGER_NEEDED, Boolean.TRUE); } } if (tableMatches(dataMetaData, TableConstants.SYM_TRANSFORM_COLUMN) || tableMatches(dataMetaData, TableConstants.SYM_TRANSFORM_TABLE)) { routingContext.put(CTX_KEY_FLUSH_TRANSFORMS_NEEDED, Boolean.TRUE); } if (tableMatches(dataMetaData, TableConstants.SYM_EXTENSION)) { routingContext.put(CTX_KEY_REFRESH_EXTENSIONS_NEEDED, Boolean.TRUE); } } } return nodeIds; } protected void routeNodeTables(Set<String> nodeIds, Map<String, String> columnValues, NetworkedNode rootNetworkedNode, Node me, SimpleRouterContext routingContext, DataMetaData dataMetaData, Set<Node> possibleTargetNodes, boolean initialLoad) { String nodeIdForRecordBeingRouted = columnValues.get("NODE_ID"); if (dataMetaData.getData().getDataEventType() == DataEventType.DELETE) { String createAtNodeId = columnValues.get("CREATED_AT_NODE_ID"); for (Node nodeThatMayBeRoutedTo : possibleTargetNodes) { if (!Constants.DEPLOYMENT_TYPE_REST.equals(nodeThatMayBeRoutedTo .getDeploymentType()) && !nodeIdForRecordBeingRouted.equals(nodeThatMayBeRoutedTo.getNodeId()) && !nodeThatMayBeRoutedTo.getNodeId().equals(createAtNodeId) && (nodeThatMayBeRoutedTo.getCreatedAtNodeId() == null || !nodeThatMayBeRoutedTo .getCreatedAtNodeId().equals(nodeIdForRecordBeingRouted))) { nodeIds.add(nodeThatMayBeRoutedTo.getNodeId()); } } } else { List<NodeGroupLink> nodeGroupLinks = getNodeGroupLinksFromContext(routingContext); for (Node nodeThatMayBeRoutedTo : possibleTargetNodes) { if (!Constants.DEPLOYMENT_TYPE_REST.equals(nodeThatMayBeRoutedTo.getDeploymentType()) && !nodeThatMayBeRoutedTo.requires13Compatiblity() && isLinked(nodeIdForRecordBeingRouted, nodeThatMayBeRoutedTo, rootNetworkedNode, me, nodeGroupLinks) && !isSameNumberOfLinksAwayFromRoot(nodeThatMayBeRoutedTo, rootNetworkedNode, me) || (nodeThatMayBeRoutedTo.getNodeId().equals(me.getNodeId()) && initialLoad)) { nodeIds.add(nodeThatMayBeRoutedTo.getNodeId()); } } if (!initialLoad && nodeIds != null) { if (tableMatches(dataMetaData, TableConstants.SYM_NODE_SECURITY)) { routeSymNodeSecurity(me, nodeIdForRecordBeingRouted, dataMetaData, nodeIds, columnValues); } /* * Don't route insert events for a node to itself. They will be * loaded during registration. If we route them, then an old * state can override the correct state * * Don't send deletes to a node. A node should be responsible * for deleting itself. */ if (dataMetaData.getData().getDataEventType() == DataEventType.INSERT) { nodeIds.remove(nodeIdForRecordBeingRouted); } } } } protected void routeSymNodeSecurity (Node me, String nodeIdForRecordBeingRouted, DataMetaData dataMetaData, Set<String> nodeIds, Map<String, String> columnValues) { DataEventType eventType = dataMetaData.getData().getDataEventType(); boolean fromAnotherNode = isNotBlank(dataMetaData.getData().getSourceNodeId()); if (nodeIds.contains(nodeIdForRecordBeingRouted)) { /* * Don't route node security to it's own node. That node will * get node security via registration and it will be updated by * initial load. Otherwise, updates can be unpredictable in the * order they will be applied at the node because updates are on * a different channel than reloads */ boolean remove = true; if (eventType == DataEventType.UPDATE) { if ("1".equals(columnValues.get("REV_INITIAL_LOAD_ENABLED"))) { boolean reverseLoadQueued = engine.getParameterService().is( ParameterConstants.INITIAL_LOAD_REVERSE_FIRST) || "0".equals(columnValues.get("INITIAL_LOAD_ENABLED")); /* * Only send the update if the client is going * to be expected to queue up a reverse load. * The trigger to do this is the arrival of * sym_node_security with * REV_INITIAL_LOAD_ENABLED set to 1. */ if (reverseLoadQueued) { remove = false; } } } if (remove) { nodeIds.remove(nodeIdForRecordBeingRouted); } } boolean removeParentNode = true; if (eventType == DataEventType.UPDATE) { if ("1".equals(columnValues.get("INITIAL_LOAD_ENABLED")) && me.getNodeId().equals(nodeIdForRecordBeingRouted) ) { removeParentNode = false; } } if (removeParentNode) { nodeIds.remove(columnValues.get("CREATED_AT_NODE_ID")); } if (engine.getConfigurationService().isMasterToMaster() || fromAnotherNode) { /* * Don't send updates where the initial load flags are enabled to other * nodes in the cluster */ if ("1".equals(columnValues.get("INITIAL_LOAD_ENABLED"))) { nodeIds.clear(); } } } @SuppressWarnings("unchecked") protected void queueSyncTriggers(SimpleRouterContext routingContext, DataMetaData dataMetaData, Map<String, String> columnValues) { if ((tableMatches(dataMetaData, TableConstants.SYM_TRIGGER) || tableMatches(dataMetaData, TableConstants.SYM_TRIGGER_ROUTER))) { Object needResync = routingContext.get(CTX_KEY_RESYNC_NEEDED); if (needResync == null || needResync instanceof Set) { if (needResync == null) { needResync = new HashSet<Trigger>(); routingContext.put(CTX_KEY_RESYNC_NEEDED, needResync); } ITriggerRouterService triggerRouterService = engine.getTriggerRouterService(); String triggerId = columnValues.get("TRIGGER_ID"); Trigger trigger = triggerRouterService.getTriggerById(true, triggerId); if (trigger != null) { ((Set<Trigger>) needResync).add(trigger); } else { routingContext.put(CTX_KEY_RESYNC_NEEDED, Boolean.TRUE); } } } else if (tableMatches(dataMetaData, TableConstants.SYM_ROUTER) || tableMatches(dataMetaData, TableConstants.SYM_NODE_GROUP_LINK)) { routingContext.put(CTX_KEY_RESYNC_NEEDED, Boolean.TRUE); } } protected Node findIdentity() { return engine.getNodeService().findIdentity(); } @SuppressWarnings("unchecked") protected List<NodeGroupLink> getNodeGroupLinksFromContext(SimpleRouterContext routingContext) { List<NodeGroupLink> list = (List<NodeGroupLink>) routingContext.get(NodeGroupLink.class .getName()); if (list == null) { list = engine.getConfigurationService().getNodeGroupLinks(false); routingContext.put(NodeGroupLink.class.getName(), list); } return list; } protected NetworkedNode getRootNetworkNodeFromContext(SimpleRouterContext routingContext) { NetworkedNode root = (NetworkedNode) routingContext.get(NetworkedNode.class.getName()); if (root == null) { root = engine.getNodeService().getRootNetworkedNode(); routingContext.put(NetworkedNode.class.getName(), root); } return root; } private boolean isSameNumberOfLinksAwayFromRoot(Node nodeThatCouldBeRoutedTo, NetworkedNode root, Node me) { return me != null && root != null && root.getNumberOfLinksAwayFromRoot(nodeThatCouldBeRoutedTo.getNodeId()) == root .getNumberOfLinksAwayFromRoot(me.getNodeId()); } private boolean isLinked(String nodeIdInQuestion, Node nodeThatCouldBeRoutedTo, NetworkedNode root, Node me, List<NodeGroupLink> allLinks) { if (root != null) { if (nodeIdInQuestion != null && nodeThatCouldBeRoutedTo != null && !nodeIdInQuestion.equals(nodeThatCouldBeRoutedTo.getNodeId())) { NetworkedNode networkedNodeInQuestion = root.findNetworkedNode(nodeIdInQuestion); NetworkedNode networkedNodeThatCouldBeRoutedTo = root .findNetworkedNode(nodeThatCouldBeRoutedTo.getNodeId()); if (networkedNodeInQuestion != null) { if (networkedNodeInQuestion.isInParentHierarchy(nodeThatCouldBeRoutedTo .getNodeId())) { // always route changes to parent nodes return true; } String createdAtNodeId = networkedNodeInQuestion.getNode().getCreatedAtNodeId(); if (createdAtNodeId != null && !createdAtNodeId.equals(me.getNodeId()) && !networkedNodeInQuestion.getNode().getNodeId() .equals(me.getNodeId())) { if (createdAtNodeId.equals(nodeThatCouldBeRoutedTo.getNodeId())) { return true; } else if (networkedNodeThatCouldBeRoutedTo != null) { // the node was created at some other node. lets // attempt // to get that update back to that node return networkedNodeThatCouldBeRoutedTo .isInChildHierarchy(createdAtNodeId); } } // if we haven't found a place to route by now, then we need // to // send the row to all nodes that have links to the node's // group String groupId = networkedNodeInQuestion.getNode().getNodeGroupId(); Set<String> groupsThatWillBeInterested = new HashSet<String>(); for (NodeGroupLink nodeGroupLink : allLinks) { if (nodeGroupLink.getTargetNodeGroupId().equals(groupId)) { groupsThatWillBeInterested.add(nodeGroupLink.getSourceNodeGroupId()); } else if (nodeGroupLink.getSourceNodeGroupId().equals(groupId)) { groupsThatWillBeInterested.add(nodeGroupLink.getTargetNodeGroupId()); } } if (groupsThatWillBeInterested.contains(nodeThatCouldBeRoutedTo .getNodeGroupId())) { return true; } else { return false; } } else { return false; } } else { return true; } } else { return false; } } @Override public void contextCommitted(SimpleRouterContext routingContext) { if (engine.getParameterService().is(ParameterConstants.AUTO_REFRESH_AFTER_CONFIG_CHANGED, true)) { if (routingContext.get(CTX_KEY_FLUSH_PARAMETERS_NEEDED) != null && engine.getParameterService().is(ParameterConstants.AUTO_SYNC_CONFIGURATION)) { log.info("About to refresh the cache of parameters because new configuration came through the data router"); engine.getParameterService().rereadParameters(); } if (routingContext.get(CTX_KEY_FLUSH_CHANNELS_NEEDED) != null) { log.info("Channels flushed because new channels came through the data router"); engine.getConfigurationService().clearCache(); } Object needsSynced = routingContext.get(CTX_KEY_RESYNC_NEEDED); if (needsSynced != null && engine.getParameterService().is(ParameterConstants.AUTO_SYNC_TRIGGERS) && engine.getParameterService().is( ParameterConstants.AUTO_SYNC_TRIGGERS_AFTER_CONFIG_CHANGED)) { if (Boolean.TRUE.equals(needsSynced)) { log.info("About to syncTriggers because new configuration came through the data router"); engine.getTriggerRouterService().syncTriggers(); } else if (needsSynced instanceof Set) { @SuppressWarnings("unchecked") Set<Trigger> triggers = (Set<Trigger>) needsSynced; for (Trigger trigger : triggers) { log.info("About to sync the " + trigger.getTriggerId() + " trigger because a change was detected by the config data router"); engine.getTriggerRouterService().syncTrigger(trigger.getTriggerId(), null); } } } if (routingContext.get(CTX_KEY_FLUSH_TRANSFORMS_NEEDED) != null) { log.info("About to refresh the cache of transformation because new configuration came through the data router"); engine.getTransformService().clearCache(); log.info("About to clear the staging area because new transform configuration came through the data router"); engine.getStagingManager().clean(0); } if (routingContext.get(CTX_KEY_FLUSH_CONFLICTS_NEEDED) != null) { log.info("About to refresh the cache of conflict settings because new configuration came through the data router"); engine.getDataLoaderService().clearCache(); } if (routingContext.get(CTX_KEY_FLUSH_LOADFILTERS_NEEDED) != null) { log.info("About to refresh the cache of load filters because new configuration came through the data router"); engine.getLoadFilterService().clearCache(); } insertReloadEvents(routingContext); if (routingContext.get(CTX_KEY_RESTART_JOBMANAGER_NEEDED) != null) { IJobManager jobManager = engine.getJobManager(); if (jobManager != null) { log.info("About to restart jobs because new configuration come through the data router"); jobManager.stopJobs(); jobManager.startJobs(); } } if (routingContext.get(CTX_KEY_REFRESH_EXTENSIONS_NEEDED) != null) { log.info("About to refresh the cache of extensions because new configuration came through the data router"); engine.getExtensionService().refresh(); } } } protected void insertReloadEvents(SimpleRouterContext routingContext) { @SuppressWarnings("unchecked") List<TableReloadRequestKey> reloadRequestKeys = (List<TableReloadRequestKey>) routingContext .get(CTX_KEY_TABLE_RELOAD_NEEDED); if (reloadRequestKeys != null) { for (TableReloadRequestKey reloadRequestKey : reloadRequestKeys) { TableReloadRequest request = engine.getDataService().getTableReloadRequest( reloadRequestKey); if (engine.getDataService().insertReloadEvent(request, reloadRequestKey.getReceivedFromNodeId() != null)) { log.info( "Inserted table reload request from config data router for node {} and trigger {}", reloadRequestKey.getTargetNodeId(), reloadRequestKey.getTriggerId()); } } routingContext.setRequestGapDetection(true); } } private String tableName(String tableName) { return TableConstants.getTableName(engine != null ? engine.getTablePrefix() : "sym", tableName); } private boolean tableMatches(DataMetaData dataMetaData, String tableName) { boolean matches = false; if (dataMetaData.getTable().getName().equalsIgnoreCase(tableName(tableName))) { matches = true; } return matches; } @Override public boolean isConfigurable() { return false; } }