/* * Copyright 2013-2016 the original author or authors. * * 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.springframework.integration.channel; import java.util.Date; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicLong; import org.springframework.context.Lifecycle; import org.springframework.integration.context.IntegrationObjectSupport; import org.springframework.integration.support.channel.BeanFactoryChannelResolver; import org.springframework.integration.support.channel.HeaderChannelRegistry; import org.springframework.messaging.MessageChannel; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; /** * Converts a channel to a name, retaining a reference to the channel keyed by the name. * Allows a downstream {@link BeanFactoryChannelResolver} to find the channel by name * in the event that the flow serialized the message at some point. * Channels are expired after a configurable delay (60 seconds by default). * The actual average expiry time will be 1.5x the delay. * * @author Gary Russell * @author Artem Bilan * @since 3.0 * */ public class DefaultHeaderChannelRegistry extends IntegrationObjectSupport implements HeaderChannelRegistry, Lifecycle, Runnable { private static final int DEFAULT_REAPER_DELAY = 60000; protected final Map<String, MessageChannelWrapper> channels = new ConcurrentHashMap<String, DefaultHeaderChannelRegistry.MessageChannelWrapper>(); protected static final AtomicLong id = new AtomicLong(); protected final String uuid = UUID.randomUUID().toString() + ":"; private volatile boolean removeOnGet; private volatile long reaperDelay; private volatile ScheduledFuture<?> reaperScheduledFuture; private volatile boolean running; private volatile boolean explicitlyStopped; /** * Constructs a registry with the default delay for channel expiry. */ public DefaultHeaderChannelRegistry() { this(DEFAULT_REAPER_DELAY); } /** * Constructs a registry with the provided delay (milliseconds) for * channel expiry. * @param reaperDelay the delay in milliseconds. */ public DefaultHeaderChannelRegistry(long reaperDelay) { this.setReaperDelay(reaperDelay); } /** * Set the reaper delay. * @param reaperDelay the delay in milliseconds. */ public final void setReaperDelay(long reaperDelay) { Assert.isTrue(reaperDelay > 0, "'reaperDelay' must be > 0"); this.reaperDelay = reaperDelay; } public final long getReaperDelay() { return this.reaperDelay; } /** * Set to true to immediately remove the channel mapping when * {@link #channelNameToChannel(String)} is invoked. * @param removeOnGet true to remove immediately, default false. * @since 4.1 */ public void setRemoveOnGet(boolean removeOnGet) { this.removeOnGet = removeOnGet; } @Override public void setTaskScheduler(TaskScheduler taskScheduler) { super.setTaskScheduler(taskScheduler); } @Override public final int size() { return this.channels.size(); } @Override protected void onInit() throws Exception { super.onInit(); Assert.notNull(this.getTaskScheduler(), "a task scheduler is required"); } @Override public synchronized void start() { if (!this.running) { Assert.notNull(this.getTaskScheduler(), "a task scheduler is required"); this.reaperScheduledFuture = this.getTaskScheduler().schedule(this, new Date(System.currentTimeMillis() + this.reaperDelay)); this.running = true; } } @Override public synchronized void stop() { this.running = false; if (this.reaperScheduledFuture != null) { this.reaperScheduledFuture.cancel(true); this.reaperScheduledFuture = null; } this.explicitlyStopped = true; } public void stop(Runnable callback) { this.stop(); callback.run(); } @Override public boolean isRunning() { return this.running; } @Override public Object channelToChannelName(Object channel) { return channelToChannelName(channel, this.reaperDelay); } @Override public Object channelToChannelName(Object channel, long timeToLive) { if (!this.running && !this.explicitlyStopped && this.getTaskScheduler() != null) { start(); } if (channel != null && channel instanceof MessageChannel) { String name = this.uuid + DefaultHeaderChannelRegistry.id.incrementAndGet(); this.channels.put(name, new MessageChannelWrapper((MessageChannel) channel, System.currentTimeMillis() + timeToLive)); if (logger.isDebugEnabled()) { logger.debug("Registered " + channel + " as " + name); } return name; } else { return channel; } } @Override public MessageChannel channelNameToChannel(String name) { if (name != null) { MessageChannelWrapper messageChannelWrapper; if (this.removeOnGet) { messageChannelWrapper = this.channels.remove(name); } else { messageChannelWrapper = this.channels.get(name); } if (logger.isDebugEnabled() && messageChannelWrapper != null) { logger.debug("Retrieved " + messageChannelWrapper.getChannel() + " with " + name); } return messageChannelWrapper == null ? null : messageChannelWrapper.getChannel(); } return null; } /** * Cancel the scheduled reap task and run immediately; then reschedule. */ @Override public synchronized void runReaper() { if (this.reaperScheduledFuture != null) { this.reaperScheduledFuture.cancel(true); this.reaperScheduledFuture = null; } this.run(); } @Override public synchronized void run() { if (logger.isTraceEnabled()) { logger.trace("Reaper started; channels size=" + this.channels.size()); } Iterator<Entry<String, MessageChannelWrapper>> iterator = this.channels.entrySet().iterator(); long now = System.currentTimeMillis(); while (iterator.hasNext()) { Entry<String, MessageChannelWrapper> entry = iterator.next(); if (entry.getValue().getExpireAt() < now) { if (logger.isDebugEnabled()) { logger.debug("Expiring " + entry.getKey() + " (" + entry.getValue().getChannel() + ")"); } iterator.remove(); } } this.reaperScheduledFuture = this.getTaskScheduler().schedule(this, new Date(System.currentTimeMillis() + this.reaperDelay)); if (logger.isTraceEnabled()) { logger.trace("Reaper completed; channels size=" + this.channels.size()); } } private static final class MessageChannelWrapper { private final MessageChannel channel; private final long expireAt; MessageChannelWrapper(MessageChannel channel, long expireAt) { this.channel = channel; this.expireAt = expireAt; } public long getExpireAt() { return this.expireAt; } public MessageChannel getChannel() { return this.channel; } } }