org.wicketstuff.push.timer.TimerPushService Maven / Gradle / Ivy
Show all versions of wicketstuff-push-timer Show documentation
/*
* 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.wicketstuff.push.timer;
import static java.util.Collections.EMPTY_LIST;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.util.lang.Args;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wicketstuff.push.AbstractPushService;
import org.wicketstuff.push.AbstractPushServiceRef;
import org.wicketstuff.push.IPushChannel;
import org.wicketstuff.push.IPushEventHandler;
import org.wicketstuff.push.IPushNode;
import org.wicketstuff.push.IPushNodeDisconnectedListener;
import org.wicketstuff.push.IPushService;
import org.wicketstuff.push.IPushServiceRef;
/**
* AJAX timer based implementation of {@link IPushService}.
*
*
* @author Sebastian Thomschke
*/
public class TimerPushService extends AbstractPushService
{
private final class PushNodeState
{
final TimerPushNode node;
Instant lastPolledAt = Instant.now();
List> queuedEvents = new ArrayList>(
2);
PushNodeState(final TimerPushNode node)
{
this.node = node;
}
boolean isTimedOut()
{
return Duration.between(lastPolledAt, Instant.now()).compareTo(_maxTimeLag) > 0;
}
}
private static final Logger LOG = LoggerFactory.getLogger(TimerPushService.class);
private static final ConcurrentHashMap INSTANCES = new ConcurrentHashMap(
2);
private static final IPushServiceRef PUSH_SERVICE_REF = new AbstractPushServiceRef()
{
private static final long serialVersionUID = 1L;
@Override
protected TimerPushService lookupService()
{
return TimerPushService.get();
}
};
public static TimerPushService get()
{
return get(Application.get());
}
public static TimerPushService get(final Application application)
{
Args.notNull(application, "application");
TimerPushService service = INSTANCES.get(application);
if (service == null)
{
service = new TimerPushService();
final TimerPushService existingInstance = INSTANCES.putIfAbsent(application, service);
if (existingInstance == null)
/*
* If this is the first instance of this service for the given application, then
* schedule the cleanup task.
*/
service.setCleanupInterval(Duration.ofSeconds(60));
else
// If it is not the first instance, throw it away.
service = existingInstance;
}
return service;
}
/**
* @return a serializable service reference
*/
public static IPushServiceRef getRef()
{
return PUSH_SERVICE_REF;
}
static void onApplicationShutdown(final Application application)
{
Args.notNull(application, "application");
final TimerPushService srv = INSTANCES.remove(application);
if (srv != null)
{
LOG.info("Shutting down {}...", srv);
synchronized (srv._cleanupExecutor)
{
srv._cleanupFuture.cancel(false);
srv._cleanupFuture = null;
srv._cleanupExecutor.shutdownNow();
}
}
}
private Duration _defaultPollingInterval = Duration.ofSeconds(2);
private Duration _maxTimeLag = Duration.ofSeconds(10);
private final ConcurrentMap, PushNodeState>> _nodeStates = new ConcurrentHashMap, PushNodeState>>();
private final ScheduledThreadPoolExecutor _cleanupExecutor = new ScheduledThreadPoolExecutor(1);
private ScheduledFuture> _cleanupFuture = null;
private final Runnable _cleanupTask = new Runnable()
{
public void run()
{
LOG.debug("Running timer push node cleanup task...");
int count = 0;
for (final PushNodeState> state : _nodeStates.values())
synchronized (state)
{
if (state.isTimedOut())
{
onDisconnect(state.node);
count++;
}
}
LOG.debug("Cleaned up {} timer push nodes.", count);
}
};
private TimerPushService()
{
super();
}
private TimerPushBehavior _findPushBehaviour(final Component component)
{
for (final Behavior behavior : component.getBehaviors())
if (behavior instanceof TimerPushBehavior)
return (TimerPushBehavior)behavior;
return null;
}
private void _onConnect(final TimerPushNode node)
{
_nodeStates.put(node, new PushNodeState(node));
}
public Duration getDefaultPollingInterval()
{
return _defaultPollingInterval;
}
public Duration getMaxTimeLag()
{
return _maxTimeLag;
}
/**
* {@inheritDoc}
*/
public TimerPushNode installNode(final Component component,
final IPushEventHandler handler)
{
return installNode(component, handler, _defaultPollingInterval);
}
public TimerPushNode installNode(final Component component,
final IPushEventHandler handler, final Duration pollingInterval)
{
Args.notNull(component, "component");
Args.notNull(handler, "handler");
Args.notNull(pollingInterval, "pollingInterval");
TimerPushBehavior behavior = _findPushBehaviour(component);
if (behavior != null && behavior.isStopped())
{
component.remove(behavior);
behavior = null;
}
if (behavior == null)
{
behavior = new TimerPushBehavior(pollingInterval);
component.add(behavior);
}
final TimerPushNode node = behavior.addNode(handler, pollingInterval);
_onConnect(node);
return node;
}
/**
* {@inheritDoc}
*/
public boolean isConnected(final IPushNode> node)
{
Args.notNull(node, "node");
if (node instanceof TimerPushNode)
{
final PushNodeState> state = _nodeStates.get(node);
if (state == null)
return false;
synchronized (state)
{
if (state.isTimedOut())
{
onDisconnect(state.node);
return false;
}
}
return true;
}
LOG.warn("Unsupported push node type {}", node);
return false;
}
void onDisconnect(final TimerPushNode> node)
{
if (_nodeStates.remove(node) != null)
{
LOG.debug("Timer push node {} disconnected.", node);
disconnectFromAllChannels(node);
for (final IPushNodeDisconnectedListener listener : disconnectListeners)
try
{
listener.onDisconnect(node);
}
catch (final RuntimeException ex)
{
LOG.error("Failed to notify " + listener, ex);
}
}
}
@SuppressWarnings("unchecked")
List> pollEvents(
final TimerPushNode node)
{
final PushNodeState state = (PushNodeState)_nodeStates.get(node);
if (state == null)
{
LOG.debug("Reconnecting push node {}...", node);
_onConnect(node);
return EMPTY_LIST;
}
synchronized (state)
{
state.lastPolledAt = Instant.now();
if (state.queuedEvents.size() == 0)
return EMPTY_LIST;
final List> events = state.queuedEvents;
state.queuedEvents = new ArrayList>(2);
return events;
}
}
/**
* {@inheritDoc}
*/
public void publish(final IPushChannel channel, final EventType event)
{
Args.notNull(channel, "channel");
final Set> pnodes = nodesByChannels.get(channel);
if (pnodes == null)
throw new IllegalArgumentException("Unknown channel " + channel);
final TimerPushEventContext ctx = new TimerPushEventContext(event,
channel, this);
// publish the event to all registered nodes
for (final IPushNode> pnode : pnodes)
{
@SuppressWarnings("unchecked")
final TimerPushNode node = (TimerPushNode)pnode;
if (isConnected(node))
{
@SuppressWarnings("unchecked")
final PushNodeState state = (PushNodeState)_nodeStates.get(node);
if (state != null)
synchronized (state)
{
state.queuedEvents.add(ctx);
}
}
}
}
/**
* {@inheritDoc}
*/
public void publish(final IPushNode node, final EventType event)
{
Args.notNull(node, "node");
if (node instanceof TimerPushNode)
{
if (isConnected(node))
{
@SuppressWarnings("unchecked")
final PushNodeState state = (PushNodeState)_nodeStates.get(node);
if (state != null)
synchronized (state)
{
state.queuedEvents.add(new TimerPushEventContext(event, null,
this));
}
}
}
else
LOG.warn("Unsupported push node type {}", node);
}
/**
* Sets the interval in which the clean up task will be executed that removes information about
* disconnected push nodes. Default is 60 seconds.
*/
public void setCleanupInterval(final Duration interval)
{
Args.notNull(interval, "interval");
synchronized (_cleanupExecutor)
{
if (_cleanupFuture != null)
_cleanupFuture.cancel(false);
if (!_cleanupExecutor.isShutdown())
_cleanupFuture = _cleanupExecutor.scheduleAtFixedRate(_cleanupTask,
interval.toMillis(), interval.toMillis(), TimeUnit.MILLISECONDS);
}
}
public void setDefaultPollingInterval(final Duration defaultPollingInterval)
{
Args.notNull(defaultPollingInterval, "defaultPollingInterval");
_defaultPollingInterval = defaultPollingInterval;
}
public void setMaxTimeLag(final Duration maxTimeLag)
{
Args.notNull(maxTimeLag, "maxTimeLag");
_maxTimeLag = maxTimeLag;
}
/**
* {@inheritDoc}
*/
public void uninstallNode(final Component component, final IPushNode> node)
{
Args.notNull(component, "component");
Args.notNull(node, "node");
if (node instanceof TimerPushNode)
{
final TimerPushBehavior behavior = _findPushBehaviour(component);
if (behavior == null)
return;
if (behavior.removeNode(node) == 0)
behavior.stop(RequestCycle.get().find(AjaxRequestTarget.class).get());
}
else
LOG.warn("Unsupported push node type {}", node);
}
}