/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.camel.component.infinispan.policy; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.camel.CamelContext; import org.apache.camel.CamelContextAware; import org.apache.camel.Route; import org.apache.camel.RuntimeCamelException; import org.apache.camel.Service; import org.apache.camel.api.management.ManagedAttribute; import org.apache.camel.api.management.ManagedResource; import org.apache.camel.component.infinispan.InfinispanConfiguration; import org.apache.camel.component.infinispan.InfinispanManager; import org.apache.camel.component.infinispan.InfinispanUtil; import org.apache.camel.support.RoutePolicySupport; import org.apache.camel.support.ServiceSupport; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.StringHelper; import org.infinispan.Cache; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.annotation.ClientCacheEntryExpired; import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved; import org.infinispan.client.hotrod.annotation.ClientListener; import org.infinispan.client.hotrod.event.ClientCacheEntryExpiredEvent; import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent; import org.infinispan.commons.api.BasicCache; import org.infinispan.commons.api.BasicCacheContainer; import org.infinispan.notifications.Listener; import org.infinispan.notifications.cachelistener.annotation.CacheEntryExpired; import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved; import org.infinispan.notifications.cachelistener.event.CacheEntryEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @ManagedResource(description = "Route policy using Infinispan as clustered lock") public class InfinispanRoutePolicy extends RoutePolicySupport implements CamelContextAware { private static final Logger LOGGER = LoggerFactory.getLogger(InfinispanRoutePolicy.class); private final AtomicBoolean leader; private final Set<Route> suspendedRoutes; private final InfinispanManager manager; private Route route; private CamelContext camelContext; private ScheduledExecutorService executorService; private boolean shouldStopConsumer; private String lockMapName; private String lockKey; private String lockValue; private long lifespan; private TimeUnit lifespanTimeUnit; private ScheduledFuture<?> future; private Service service; public InfinispanRoutePolicy(InfinispanConfiguration configuration) { this(new InfinispanManager(configuration), null, null); } public InfinispanRoutePolicy(InfinispanManager manager) { this(manager, null, null); } public InfinispanRoutePolicy(InfinispanManager manager, String lockKey, String lockValue) { this.manager = manager; this.suspendedRoutes = new HashSet<>(); this.leader = new AtomicBoolean(false); this.shouldStopConsumer = true; this.lockKey = lockKey; this.lockValue = lockValue; this.lifespan = 30; this.lifespanTimeUnit = TimeUnit.SECONDS; this.service = null; } @Override public CamelContext getCamelContext() { return camelContext; } @Override public void setCamelContext(CamelContext camelContext) { this.camelContext = camelContext; } @Override public void onInit(Route route) { super.onInit(route); this.route = route; } @Override public void onStart(Route route) { try { startService(); } catch (Exception e) { throw new RuntimeCamelException(e); } if (!leader.get() && shouldStopConsumer) { stopConsumer(route); } } @Override public synchronized void onStop(Route route) { try { stopService(); } catch (Exception e) { throw new RuntimeCamelException(e); } suspendedRoutes.remove(route); } @Override public synchronized void onSuspend(Route route) { try { stopService(); } catch (Exception e) { throw new RuntimeCamelException(e); } suspendedRoutes.remove(route); } @Override protected void doStart() throws Exception { // validate StringHelper.notEmpty(lockMapName, "lockMapName", this); StringHelper.notEmpty(lockKey, "lockKey", this); StringHelper.notEmpty(lockValue, "lockValue", this); ObjectHelper.notNull(camelContext, "camelContext", this); if (this.lockValue == null) { this.lockValue = camelContext.getUuidGenerator().generateUuid(); } this.manager.start(); this.executorService = getCamelContext().getExecutorServiceManager().newSingleThreadScheduledExecutor(this, "InfinispanRoutePolicy"); if (lifespanTimeUnit.convert(lifespan, TimeUnit.SECONDS) < 2) { throw new IllegalArgumentException("Lock lifespan can not be less that 2 seconds"); } BasicCache<String, String> cache = manager.getCache(lockMapName); if (manager.isCacheContainerEmbedded()) { this.service = new EmbeddedCacheService(InfinispanUtil.asEmbedded(cache)); } else { this.service = new RemoteCacheService(InfinispanUtil.asRemote(cache)); } super.doStart(); } @Override protected void doStop() throws Exception { if (future != null) { future.cancel(true); future = null; } if (this.service != null) { this.service.stop(); } getCamelContext().getExecutorServiceManager().shutdownGraceful(executorService); leader.set(false); manager.stop(); super.doStop(); } private void startService() throws Exception { if (service == null) { throw new IllegalStateException("An Infinispan CacheService should be configured"); } service.start(); } private void stopService() throws Exception { leader.set(false); if (this.service != null) { this.service.stop(); } } // ************************************************************************* // // ************************************************************************* protected void setLeader(boolean isLeader) { if (isLeader && leader.compareAndSet(false, isLeader)) { LOGGER.info("Leadership taken (map={}, key={}, val={})", lockMapName, lockKey, lockValue); startAllStoppedConsumers(); } else if (!isLeader && leader.getAndSet(isLeader)) { LOGGER.info("Leadership lost (map={}, key={} val={})", lockMapName, lockKey, lockValue); } if (!isLeader && this.route != null) { stopConsumer(route); } } private synchronized void startConsumer(Route route) { try { if (suspendedRoutes.contains(route)) { startConsumer(route.getConsumer()); suspendedRoutes.remove(route); } } catch (Exception e) { handleException(e); } } private synchronized void stopConsumer(Route route) { try { if (!suspendedRoutes.contains(route)) { LOGGER.debug("Stopping consumer for {} ({})", route.getId(), route.getConsumer()); stopConsumer(route.getConsumer()); suspendedRoutes.add(route); } } catch (Exception e) { handleException(e); } } private synchronized void startAllStoppedConsumers() { try { for (Route route : suspendedRoutes) { LOGGER.debug("Starting consumer for {} ({})", route.getId(), route.getConsumer()); startConsumer(route.getConsumer()); } suspendedRoutes.clear(); } catch (Exception e) { handleException(e); } } // ************************************************************************* // Getter/Setters // ************************************************************************* @ManagedAttribute(description = "The route id") public String getRouteId() { if (route != null) { return route.getId(); } return null; } @ManagedAttribute(description = "The consumer endpoint", mask = true) public String getEndpointUrl() { if (route != null && route.getConsumer() != null && route.getConsumer().getEndpoint() != null) { return route.getConsumer().getEndpoint().toString(); } return null; } @ManagedAttribute(description = "Whether to stop consumer when starting up and failed to become master") public boolean isShouldStopConsumer() { return shouldStopConsumer; } public void setShouldStopConsumer(boolean shouldStopConsumer) { this.shouldStopConsumer = shouldStopConsumer; } @ManagedAttribute(description = "The lock map name") public String getLockMapName() { return lockMapName; } public void setLockMapName(String lockMapName) { this.lockMapName = lockMapName; } @ManagedAttribute(description = "The lock key") public String getLockKey() { return lockKey; } public void setLockKey(String lockKey) { this.lockKey = lockKey; } @ManagedAttribute(description = "The lock value") public String getLockValue() { return lockValue; } public void setLockValue(String lockValue) { this.lockValue = lockValue; } @ManagedAttribute(description = "The key lifespan for the lock") public long getLifespan() { return lifespan; } public void setLifespan(long lifespan) { this.lifespan = lifespan; } public void setLifespan(long lifespan, TimeUnit lifespanTimeUnit) { this.lifespan = lifespan; this.lifespanTimeUnit = lifespanTimeUnit; } @ManagedAttribute(description = "The key lifespan time unit for the lock") public TimeUnit getLifespanTimeUnit() { return lifespanTimeUnit; } public void setLifespanTimeUnit(TimeUnit lifespanTimeUnit) { this.lifespanTimeUnit = lifespanTimeUnit; } @ManagedAttribute(description = "Is this route the master or a slave") public boolean isLeader() { return leader.get(); } // ************************************************************************* // // ************************************************************************* @Listener(clustered = true, sync = false) private final class EmbeddedCacheService extends ServiceSupport implements Runnable { private Cache<String, String> cache; private ScheduledFuture<?> future; EmbeddedCacheService(Cache<String, String> cache) { this.cache = cache; this.future = null; } @Override protected void doStart() throws Exception { this.future = executorService.scheduleAtFixedRate(this::run, 0, lifespan / 2, lifespanTimeUnit); this.cache.addListener(this); } @Override protected void doStop() throws Exception { this.cache.removeListener(this); this.cache.remove(lockKey, lockValue); if (future != null) { future.cancel(true); future = null; } } @Override public void run() { if (!isRunAllowed() || !InfinispanRoutePolicy.this.isRunAllowed()) { return; } if (isLeader()) { // I'm still the leader, so refresh the key so it does not expire. if (!cache.replace(lockKey, lockValue, lockValue, lifespan, lifespanTimeUnit)) { // Looks like I've lost the leadership. setLeader(false); } } if (!isLeader()) { Object result = cache.putIfAbsent(lockKey, lockValue, lifespan, lifespanTimeUnit); if (result == null) { // Acquired the key so I'm the leader. setLeader(true); } else if (ObjectHelper.equal(lockValue, result) && !isLeader()) { // Hey, I may have recovered from failure (or reboot was really // fast) and my key was still there so yeah, I'm the leader again! setLeader(true); } else { setLeader(false); } } } @CacheEntryRemoved public void onCacheEntryRemoved(CacheEntryEvent<Object, Object> event) { if (ObjectHelper.equal(lockKey, event.getKey())) { run(); } } @CacheEntryExpired public void onCacheEntryExpired(CacheEntryEvent<Object, Object> event) { if (ObjectHelper.equal(lockKey, event.getKey())) { run(); } } } @ClientListener private final class RemoteCacheService extends ServiceSupport implements Runnable { private RemoteCache<String, String> cache; private ScheduledFuture<?> future; private Long version; RemoteCacheService(RemoteCache<String, String> cache) { this.cache = cache; this.future = null; this.version = null; } @Override protected void doStart() throws Exception { this.future = executorService.scheduleAtFixedRate(this::run, 0, lifespan / 2, lifespanTimeUnit); this.cache.addClientListener(this); } @Override protected void doStop() throws Exception { this.cache.removeClientListener(this); if (this.version != null) { this.cache.removeWithVersion(lockKey, this.version); } if (future != null) { future.cancel(true); future = null; } } @Override public void run() { if (!isRunAllowed() || !InfinispanRoutePolicy.this.isRunAllowed()) { return; } if (isLeader() && version != null) { LOGGER.debug("Lock refresh key={} with version={}", lockKey, version); // I'm still the leader, so refresh the key so it does not expire. if (!cache.replaceWithVersion(lockKey, lockValue, version, (int)lifespanTimeUnit.toSeconds(lifespan))) { // Looks like I've lost the leadership. setLeader(false); } } if (!isLeader()) { Object result = cache.putIfAbsent(lockKey, lockValue, lifespan, lifespanTimeUnit); if (result == null) { // Acquired the key so I'm the leader. setLeader(true); // Get the version version = cache.getWithMetadata(lockKey).getVersion(); LOGGER.debug("Lock acquired key={} with version={}", lockKey, version); } else if (ObjectHelper.equal(lockValue, result) && !isLeader()) { // Hey, I may have recovered from failure (or reboot was really // fast) and my key was still there so yeah, I'm the leader again! setLeader(true); // Get the version version = cache.getWithMetadata(lockKey).getVersion(); LOGGER.debug("Lock resumed key={} with version={}", lockKey, version); } else { setLeader(false); } } } @ClientCacheEntryRemoved public void onCacheEntryRemoved(ClientCacheEntryRemovedEvent<Object> event) { if (ObjectHelper.equal(lockKey, event.getKey())) { run(); } } @ClientCacheEntryExpired public void onCacheEntryExpired(ClientCacheEntryExpiredEvent<Object> event) { if (ObjectHelper.equal(lockKey, event.getKey())) { run(); } } } // ************************************************************************* // Helpers // ************************************************************************* public static InfinispanRoutePolicy withManager(BasicCacheContainer cacheContainer) { InfinispanConfiguration conf = new InfinispanConfiguration(); conf.setCacheContainer(cacheContainer); return new InfinispanRoutePolicy(conf); } }