
org.atmosphere.cpr.DefaultBroadcaster Maven / Gradle / Ivy
/*
* Copyright 2013 Jeanfrancois Arcand
*
* 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.
*/
/*
*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2007-2008 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can obtain
* a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
* or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
* Sun designates this particular file as subject to the "Classpath" exception
* as provided by Sun in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the License
* Header, with the fields enclosed by brackets [] replaced by your own
* identifying information: "Portions Copyrighted [year]
* [name of copyright owner]"
*
* Contributor(s):
*
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*
*/
package org.atmosphere.cpr;
import org.atmosphere.cache.UUIDBroadcasterCache;
import org.atmosphere.cpr.BroadcastFilter.BroadcastAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import static org.atmosphere.cache.UUIDBroadcasterCache.CacheMessage;
import static org.atmosphere.cpr.ApplicationConfig.BROADCASTER_WAIT_TIME;
import static org.atmosphere.cpr.ApplicationConfig.MAX_INACTIVE;
import static org.atmosphere.cpr.ApplicationConfig.OUT_OF_ORDER_BROADCAST;
import static org.atmosphere.cpr.ApplicationConfig.SUSPENDED_ATMOSPHERE_RESOURCE_UUID;
import static org.atmosphere.cpr.BroadcasterLifeCyclePolicy.ATMOSPHERE_RESOURCE_POLICY.EMPTY;
import static org.atmosphere.cpr.BroadcasterLifeCyclePolicy.ATMOSPHERE_RESOURCE_POLICY.EMPTY_DESTROY;
import static org.atmosphere.cpr.BroadcasterLifeCyclePolicy.ATMOSPHERE_RESOURCE_POLICY.IDLE;
import static org.atmosphere.cpr.BroadcasterLifeCyclePolicy.ATMOSPHERE_RESOURCE_POLICY.IDLE_DESTROY;
import static org.atmosphere.cpr.BroadcasterLifeCyclePolicy.ATMOSPHERE_RESOURCE_POLICY.IDLE_RESUME;
import static org.atmosphere.cpr.BroadcasterLifeCyclePolicy.ATMOSPHERE_RESOURCE_POLICY.NEVER;
/**
* {@link Broadcaster} implementation.
*
* Broadcast messages to suspended response using the caller's Thread.
* This basic {@link Broadcaster} use an {@link java.util.concurrent.ExecutorService}
* to broadcast message, hence the broadcast operation is asynchronous. Make sure
* you block on {@link #broadcast(Object)}.get()} if you need synchronous operations.
*
* @author Jeanfrancois Arcand
*/
public class DefaultBroadcaster implements Broadcaster {
public static final String CACHED = DefaultBroadcaster.class.getName() + ".messagesCached";
public static final String ASYNC_TOKEN = DefaultBroadcaster.class.getName() + ".token";
private static final Logger logger = LoggerFactory.getLogger(DefaultBroadcaster.class);
private static final String DESTROYED = "This Broadcaster has been destroyed and cannot be used {} by invoking {}";
protected final ConcurrentLinkedQueue resources =
new ConcurrentLinkedQueue();
protected BroadcasterConfig bc;
protected final BlockingQueue messages = new LinkedBlockingQueue();
protected final ConcurrentLinkedQueue broadcasterListeners = new ConcurrentLinkedQueue();
protected final AtomicBoolean started = new AtomicBoolean(false);
protected final AtomicBoolean destroyed = new AtomicBoolean(false);
protected SCOPE scope = SCOPE.APPLICATION;
protected String name = DefaultBroadcaster.class.getSimpleName();
protected final ConcurrentLinkedQueue delayedBroadcast = new ConcurrentLinkedQueue();
protected final ConcurrentLinkedQueue broadcastOnResume = new ConcurrentLinkedQueue();
protected final ConcurrentLinkedQueue lifeCycleListeners = new ConcurrentLinkedQueue();
protected final ConcurrentHashMap writeQueues = new ConcurrentHashMap();
protected final WriteQueue uniqueWriteQueue = new WriteQueue("-1");
protected final AtomicInteger dispatchThread = new AtomicInteger();
protected Future>[] notifierFuture;
protected Future>[] asyncWriteFuture;
private POLICY policy = POLICY.FIFO;
private final AtomicLong maxSuspendResource = new AtomicLong(-1);
private final AtomicBoolean requestScoped = new AtomicBoolean(false);
private final AtomicBoolean recentActivity = new AtomicBoolean(false);
private BroadcasterLifeCyclePolicy lifeCyclePolicy = new BroadcasterLifeCyclePolicy.Builder()
.policy(NEVER).build();
private Future> currentLifecycleTask;
protected URI uri;
protected AtmosphereConfig config;
protected BroadcasterCache.STRATEGY cacheStrategy = BroadcasterCache.STRATEGY.AFTER_FILTER;
private final Object[] awaitBarrier = new Object[0];
protected final AtomicBoolean outOfOrderBroadcastSupported = new AtomicBoolean(false);
protected int writeTimeoutInSecond = -1;
protected final AtmosphereResource noOpsResource;
protected int waitTime = 100;
public DefaultBroadcaster(String name, URI uri, AtmosphereConfig config) {
this.name = name;
this.uri = uri;
this.config = config;
bc = createBroadcasterConfig(config);
String s = config.getInitParameter(ApplicationConfig.BROADCASTER_CACHE_STRATEGY);
if (s != null) {
if (s.equalsIgnoreCase("afterFilter")) {
cacheStrategy = BroadcasterCache.STRATEGY.AFTER_FILTER;
} else if (s.equalsIgnoreCase("beforeFilter")) {
cacheStrategy = BroadcasterCache.STRATEGY.BEFORE_FILTER;
}
}
s = config.getInitParameter(OUT_OF_ORDER_BROADCAST);
if (s != null) {
outOfOrderBroadcastSupported.set(Boolean.valueOf(s));
}
s = config.getInitParameter(BROADCASTER_WAIT_TIME);
if (s != null) {
waitTime = Integer.valueOf(s);
}
s = config.getInitParameter(ApplicationConfig.WRITE_TIMEOUT);
if (s != null) {
writeTimeoutInSecond = Integer.valueOf(s);
}
noOpsResource = AtmosphereResourceFactory.getDefault().create(config, "-1");
logger.trace("{} support Out Of Order Broadcast: {}", name, outOfOrderBroadcastSupported.get());
}
public DefaultBroadcaster(String name, AtmosphereConfig config) {
this(name, URI.create("http://localhost"), config);
}
/**
* Create {@link BroadcasterConfig}
*
* @param config the {@link AtmosphereConfig}
* @return an instance of {@link BroadcasterConfig}
*/
protected BroadcasterConfig createBroadcasterConfig(AtmosphereConfig config) {
return new BroadcasterConfig(config.framework().broadcasterFilters, config, getID());
}
/**
* Set the {@link BroadcasterCache.STRATEGY}
*
* @param cacheStrategy the {@link BroadcasterCache.STRATEGY}
* @return this
*/
public Broadcaster setBroadcasterCacheStrategy(BroadcasterCache.STRATEGY cacheStrategy) {
this.cacheStrategy = cacheStrategy;
return this;
}
/**
* {@inheritDoc}
*/
public void destroy() {
if (destroyed.getAndSet(true)) return;
notifyOnPreDestroy();
notifyDestroyListener();
try {
logger.trace("Broadcaster {} is being destroyed and cannot be re-used. Policy was {}", getID(), policy);
logger.trace("Broadcaster {} is being destroyed and cannot be re-used. Resources are {}", getID(), resources);
if (config.getBroadcasterFactory() != null) {
config.getBroadcasterFactory().remove(this, this.getID());
}
if (currentLifecycleTask != null) {
currentLifecycleTask.cancel(true);
}
started.set(false);
releaseExternalResources();
killReactiveThreads();
if (bc != null) {
bc.destroy();
}
resources.clear();
broadcastOnResume.clear();
messages.clear();
delayedBroadcast.clear();
broadcasterListeners.clear();
writeQueues.clear();
} catch (Throwable t) {
logger.error("Unexpected exception during Broadcaster destroy {}", getID(), t);
}
}
/**
* {@inheritDoc}
*/
@Override
public Collection getAtmosphereResources() {
return Collections.unmodifiableCollection(resources);
}
/**
* {@inheritDoc}
*/
@Override
public void setScope(SCOPE scope) {
if (destroyed.get()) {
logger.debug(DESTROYED, getID(), "setScope");
return;
}
this.scope = scope;
if (scope != SCOPE.REQUEST) {
return;
}
logger.debug("Changing broadcaster scope for {}. This broadcaster will be destroyed.", getID());
synchronized (resources) {
try {
// Next, we need to create a new broadcaster per resource.
for (AtmosphereResource resource : resources) {
Broadcaster b = config.getBroadcasterFactory()
.get(getClass(), getClass().getSimpleName() + "/" + UUID.randomUUID());
/*
if (DefaultBroadcaster.class.isAssignableFrom(this.getClass())) {
BroadcasterCache cache = bc.getBroadcasterCache().getClass().newInstance();
InjectorProvider.getInjector().inject(cache);
DefaultBroadcaster.class.cast(b).broadcasterCache = cache;
DefaultBroadcaster.class.cast(b).getBroadcasterConfig().setBroadcasterCache(cache);
} */
resource.setBroadcaster(b);
b.setScope(SCOPE.REQUEST);
if (resource.getAtmosphereResourceEvent().isSuspended()) {
b.addAtmosphereResource(resource);
}
logger.debug("Resource {} not using broadcaster {}", resource, b.getID());
}
// Do not destroy because this is a new Broadcaster
if (resources.isEmpty()) {
return;
}
destroy();
} catch (Exception e) {
logger.error("Failed to set request scope for current resources", e);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public SCOPE getScope() {
return scope;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized void setID(String id) {
if (id == null) {
id = getClass().getSimpleName() + "/" + UUID.randomUUID();
}
if (config.getBroadcasterFactory() == null)
return; // we are shutdown or destroyed, but someone still reference
Broadcaster b = config.getBroadcasterFactory().lookup(this.getClass(), id);
if (b != null && b.getScope() == SCOPE.REQUEST) {
throw new IllegalStateException("Broadcaster ID already assigned to SCOPE.REQUEST. Cannot change the id");
} else if (b != null) {
return;
}
config.getBroadcasterFactory().remove(this, name);
this.name = id;
config.getBroadcasterFactory().add(this, name);
bc.broadcasterID(name);
}
/**
* {@inheritDoc}
*/
@Override
public String getID() {
return name;
}
/**
* {@inheritDoc}
*/
@Override
public void resumeAll() {
synchronized (resources) {
for (AtmosphereResource r : resources) {
try {
r.resume();
} catch (Throwable t) {
logger.trace("resumeAll", t);
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void releaseExternalResources() {
}
/**
* {@inheritDoc}
*/
@Override
public void setBroadcasterLifeCyclePolicy(final BroadcasterLifeCyclePolicy lifeCyclePolicy) {
this.lifeCyclePolicy = lifeCyclePolicy;
if (currentLifecycleTask != null) {
currentLifecycleTask.cancel(false);
}
if (bc != null && bc.getScheduledExecutorService() == null) {
logger.error("No Broadcaster's SchedulerExecutorService has been configured on {}. BroadcasterLifeCyclePolicy won't work.", getID());
return;
}
if (lifeCyclePolicy.getLifeCyclePolicy() == IDLE
|| lifeCyclePolicy.getLifeCyclePolicy() == IDLE_RESUME
|| lifeCyclePolicy.getLifeCyclePolicy() == IDLE_DESTROY) {
recentActivity.set(false);
int time = lifeCyclePolicy.getTimeout();
if (time == -1) {
throw new IllegalStateException("BroadcasterLifeCyclePolicy time is not set");
}
final AtomicReference> ref = new AtomicReference>();
currentLifecycleTask = bc.getScheduledExecutorService().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
// Check for activity since the last execution.
if (recentActivity.getAndSet(false)) {
return;
} else if (resources.isEmpty()) {
if (lifeCyclePolicy.getLifeCyclePolicy() == IDLE) {
notifyEmptyListener();
notifyIdleListener();
releaseExternalResources();
logger.debug("Applying BroadcasterLifeCyclePolicy IDLE policy to Broadcaster {}", getID());
} else if (lifeCyclePolicy.getLifeCyclePolicy() == IDLE_DESTROY) {
notifyEmptyListener();
notifyIdleListener();
destroy(false);
logger.debug("Applying BroadcasterLifeCyclePolicy IDLE_DESTROY policy to Broadcaster {}", getID());
}
} else if (lifeCyclePolicy.getLifeCyclePolicy() == IDLE_RESUME) {
notifyIdleListener();
destroy(true);
logger.debug("Applying BroadcasterLifeCyclePolicy IDLE_RESUME policy to Broadcaster {}", getID());
}
} catch (Throwable t) {
if (destroyed.get()) {
logger.trace("Scheduled BroadcasterLifeCyclePolicy exception", t);
} else {
logger.warn("Scheduled BroadcasterLifeCyclePolicy exception", t);
}
}
}
void destroy(boolean resume) {
if (resume) {
logger.info("All AtmosphereResource will now be resumed from Broadcaster {}", getID());
resumeAll();
}
DefaultBroadcaster.this.destroy();
/**
* The value may be null if the timeout is too low. Hopefully next execution will
* cancel the task properly.
*/
if (ref.get() != null) {
currentLifecycleTask.cancel(true);
}
}
}, time, time, lifeCyclePolicy.getTimeUnit());
ref.set(currentLifecycleTask);
}
}
/**
* {@inheritDoc}
*/
@Override
public void addBroadcasterLifeCyclePolicyListener(BroadcasterLifeCyclePolicyListener b) {
lifeCycleListeners.add(b);
}
/**
* {@inheritDoc}
*/
@Override
public void removeBroadcasterLifeCyclePolicyListener(BroadcasterLifeCyclePolicyListener b) {
lifeCycleListeners.remove(b);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDestroyed() {
return destroyed.get();
}
/**
* {@inheritDoc}
*/
@Override
public Future awaitAndBroadcast(T t, long time, TimeUnit timeUnit) {
if (resources.isEmpty()) {
synchronized (awaitBarrier) {
try {
logger.trace("Awaiting for AtmosphereResource for {} {}", time, timeUnit);
awaitBarrier.wait(translateTimeUnit(time, timeUnit));
} catch (Throwable e) {
logger.warn("awaitAndBroadcast", e);
return null;
}
}
}
return broadcast(t);
}
/**
* {@inheritDoc}
*/
@Override
public Broadcaster addBroadcasterListener(BroadcasterListener b) {
if (!broadcasterListeners.contains(b)) {
broadcasterListeners.add(b);
}
return this;
}
/**
* {@inheritDoc}
*/
@Override
public Broadcaster removeBroadcasterListener(BroadcasterListener b) {
broadcasterListeners.remove(b);
return this;
}
public static final class Entry {
public Object message;
public Object multipleAtmoResources;
public BroadcasterFuture> future;
public boolean writeLocally;
public Object originalMessage;
// https://github.com/Atmosphere/atmosphere/issues/864
public CacheMessage cache;
public Entry(Object message, Object multipleAtmoResources, BroadcasterFuture> future, Object originalMessage) {
this.message = message;
this.multipleAtmoResources = multipleAtmoResources;
this.future = future;
this.writeLocally = true;
this.originalMessage = originalMessage;
}
public Entry(Object message, Object multipleAtmoResources, BroadcasterFuture> future, boolean writeLocally) {
this.message = message;
this.multipleAtmoResources = multipleAtmoResources;
this.future = future;
this.writeLocally = writeLocally;
this.originalMessage = message;
}
@Override
public String toString() {
return "Entry{" +
"message=" + message +
", multipleAtmoResources=" + multipleAtmoResources +
", future=" + future +
'}';
}
}
protected Runnable getBroadcastHandler() {
return new Runnable() {
public void run() {
while (!isDestroyed()) {
Entry msg = null;
try {
msg = messages.poll(waitTime, TimeUnit.MILLISECONDS);
if (msg == null) {
dispatchThread.decrementAndGet();
return;
}
} catch (InterruptedException ex) {
logger.trace("{} got interrupted for Broadcaster {}", Thread.currentThread().getName(), getID());
logger.trace("", ex);
return;
} finally {
if (outOfOrderBroadcastSupported.get()) {
bc.getExecutorService().submit(this);
}
}
try {
logger.trace("{} is about to broadcast {} to " + resources, getID(), msg);
push(msg);
} catch (Throwable ex) {
if (!started.get() || destroyed.get()) {
logger.trace("Failed to submit broadcast handler runnable on shutdown for Broadcaster {}", getID(), ex);
return;
} else {
logger.warn("This message {} will be lost", msg);
logger.debug("Failed to submit broadcast handler runnable to for Broadcaster {}", getID(), ex);
}
} finally {
if (outOfOrderBroadcastSupported.get()) {
return;
}
}
}
}
};
}
protected Runnable getAsyncWriteHandler(final WriteQueue writeQueue) {
return new Runnable() {
public void run() {
while (!isDestroyed()) {
AsyncWriteToken token = null;
try {
token = writeQueue.queue.poll(waitTime, TimeUnit.MILLISECONDS);
if (token == null && !outOfOrderBroadcastSupported.get()) {
synchronized (writeQueue) {
if (writeQueue.queue.size() == 0) {
writeQueue.monitored.set(false);
writeQueues.remove(writeQueue.uuid);
return;
}
}
} else if (token == null) {
return;
}
} catch (InterruptedException ex) {
logger.trace("{} got interrupted for Broadcaster {}", Thread.currentThread().getName(), getID());
logger.trace("", ex);
return;
} finally {
if (!bc.getAsyncWriteService().isShutdown() && outOfOrderBroadcastSupported.get()) {
bc.getAsyncWriteService().submit(this);
}
}
// Shield us from https://github.com/Atmosphere/atmosphere/issues/1187
if (token != null) {
synchronized (token.resource) {
try {
logger.trace("About to write to {}", token.resource);
executeAsyncWrite(token);
} catch (Throwable ex) {
if (!started.get() || destroyed.get()) {
logger.trace("Failed to execute a write operation. Broadcaster is destroyed or not yet started for Broadcaster {}", getID(), ex);
return;
} else {
if (token != null) {
logger.warn("This message {} will be lost for AtmosphereResource {}, adding it to the BroadcasterCache",
token.originalMessage, token.resource != null ? token.resource.uuid() : "null");
cacheLostMessage(token.resource, token, true);
}
logger.debug("Failed to execute a write operation for Broadcaster {}", getID(), ex);
}
} finally {
if (!bc.getAsyncWriteService().isShutdown() && outOfOrderBroadcastSupported.get()) {
return;
}
}
}
}
}
}
};
}
protected void start() {
if (!started.getAndSet(true)) {
bc.getBroadcasterCache().start();
setID(name);
// Only start if we know a child haven't started them.
if (notifierFuture == null && asyncWriteFuture == null) {
spawnReactor();
}
}
}
protected void spawnReactor() {
killReactiveThreads();
int threads = outOfOrderBroadcastSupported.get() ? reactiveThreadsCount() : 1;
notifierFuture = new Future>[threads];
if (outOfOrderBroadcastSupported.get()) {
asyncWriteFuture = new Future>[threads];
for (int i = 0; i < threads; i++) {
notifierFuture[i] = bc.getExecutorService().submit(getBroadcastHandler());
asyncWriteFuture[i] = bc.getExecutorService().submit(getAsyncWriteHandler(uniqueWriteQueue));
}
} else {
notifierFuture[0] = bc.getExecutorService().submit(getBroadcastHandler());
}
dispatchThread.set(threads);
}
protected void killReactiveThreads() {
if (notifierFuture != null) {
for (Future> f : notifierFuture) {
if (f != null)
f.cancel(false);
}
}
if (asyncWriteFuture != null) {
for (Future> f : asyncWriteFuture) {
if (f != null)
f.cancel(false);
}
}
}
/**
* Return the default number of reactive threads that will be waiting for work when a broadcast operation
* is executed. Setting this value larger than 1 will produce out of order broadcast delivery.
*
* @return the default number of reactive threads
*/
protected int reactiveThreadsCount() {
return Runtime.getRuntime().availableProcessors();
}
/**
* {@inheritDoc}
*/
@Override
public void finalize() throws Throwable {
super.finalize();
try {
killReactiveThreads();
} catch (Throwable t) {
logger.trace("", t);
}
}
public void push(Entry entry) {
if (destroyed.get()) {
return;
}
deliverPush(entry, true);
}
protected void deliverPush(Entry entry, boolean rec) {
if (destroyed.get()) {
return;
}
recentActivity.set(true);
String prevMessage = entry.message.toString();
if (rec && !delayedBroadcast.isEmpty()) {
Iterator i = delayedBroadcast.iterator();
StringBuilder b = new StringBuilder();
while (i.hasNext()) {
Entry e = i.next();
e.future.cancel(true);
try {
// Append so we do a single flush
if (e.message instanceof String
&& entry.message instanceof String) {
b.append(e.message);
} else {
deliverPush(e, false);
}
} finally {
i.remove();
}
}
if (b.length() > 0) {
entry.message = b.append(entry.message).toString();
}
}
Object finalMsg = callable(entry.message);
if (finalMsg == null) {
logger.error("Callable exception. Please catch all exception from you callable. Message {} will be lost and all AtmosphereResource " +
"associated with this Broadcaster resumed.", entry.message);
entryDone(entry.future);
synchronized (resources) {
for (AtmosphereResource r : resources) {
if (r.transport().equals(AtmosphereResource.TRANSPORT.JSONP) || r.transport().equals(AtmosphereResource.TRANSPORT.LONG_POLLING))
try {
r.resume();
} catch (Throwable t) {
logger.trace("resumeAll", t);
}
}
}
return;
}
Object prevM = entry.originalMessage;
entry.originalMessage = (entry.originalMessage != entry.message ? callable(entry.originalMessage) : finalMsg);
if (entry.originalMessage == null) {
logger.trace("Broadcast message was null {}", prevM);
entryDone(entry.future);
return;
}
entry.message = finalMsg;
// We cache first, and if the broadcast succeed, we will remove it.
if (resources.isEmpty()) {
boolean continueProcessing;
if (!bc.getBroadcasterCache().getClass().equals(BroadcasterConfig.DefaultBroadcasterCache.class.getName())) {
synchronized (resources) {
continueProcessing = invokeFiltersAndCache(entry);
}
} else {
continueProcessing = invokeFiltersAndCache(entry);
}
if (!continueProcessing) return;
}
// https://github.com/Atmosphere/atmosphere/issues/864
// Cache the message before executing any operation.
// TODO: This won't work with AFTER_FILTER, but anyway the message will be processed when retrieved from the cache
BroadcasterCache broadcasterCache = bc.getBroadcasterCache();
if (bc.uuidCache()) {
Object m = cacheStrategy == BroadcasterCache.STRATEGY.AFTER_FILTER ? perRequestFilter(noOpsResource, entry, false) : entry.originalMessage;
entry.cache = UUIDBroadcasterCache.class.cast(broadcasterCache).addCacheCandidate(getID(), null, m);
}
try {
if (logger.isTraceEnabled()) {
for (AtmosphereResource r : resources) {
logger.trace("AtmosphereResources {} available for message {}", r.uuid(), entry);
}
}
if (entry.multipleAtmoResources == null) {
for (AtmosphereResource r : resources) {
finalMsg = perRequestFilter(r, entry, true);
if (finalMsg == null) {
logger.debug("Skipping broadcast delivery for resource {} ", r.uuid());
clearUUIDCache(r, entry.cache);
continue;
}
if (entry.writeLocally) {
queueWriteIO(r, finalMsg, entry);
}
}
} else if (entry.multipleAtmoResources instanceof AtmosphereResource) {
AtmosphereResource r = AtmosphereResource.class.cast(entry.multipleAtmoResources);
finalMsg = perRequestFilter(r, entry, true);
if (finalMsg == null) {
logger.debug("Skipping broadcast delivery resource {} ", entry.multipleAtmoResources);
clearUUIDCache(r, entry.cache);
return;
}
if (entry.writeLocally) {
queueWriteIO(r, finalMsg, entry);
}
} else if (entry.multipleAtmoResources instanceof Set) {
Set sub = (Set) entry.multipleAtmoResources;
if (sub.size() != 0) {
for (AtmosphereResource r : sub) {
finalMsg = perRequestFilter(r, entry, true);
if (finalMsg == null) {
logger.debug("Skipping broadcast delivery resource {} ", r.uuid());
clearUUIDCache(r, entry.cache);
continue;
}
if (entry.writeLocally) {
queueWriteIO(r, finalMsg, entry);
}
}
}
}
entry.message = prevMessage;
} catch (InterruptedException ex) {
logger.debug(ex.getMessage(), ex);
}
}
protected void afterFilterCacheEntry(AtmosphereResource r, Object finalMsg, CacheMessage cacheMessage) {
if (cacheStrategy == BroadcasterCache.STRATEGY.AFTER_FILTER && bc.uuidCache()) {
clearUUIDCache(r, cacheMessage);
UUIDBroadcasterCache.class.cast(bc.getBroadcasterCache()).addCacheCandidate(getID(), r, finalMsg);
}
}
protected boolean invokeFiltersAndCache(Entry entry) {
// While waiting, a new resource may have been added.
if (resources.isEmpty()) {
logger.trace("Broadcaster {} doesn't have any associated resource. " +
"Message will be cached in the configured BroadcasterCache {}", getID(), entry.message);
AtmosphereResource r = null;
if (entry.multipleAtmoResources != null && AtmosphereResource.class.isAssignableFrom(entry.multipleAtmoResources.getClass())) {
r = AtmosphereResource.class.cast(entry.multipleAtmoResources);
}
// Make sure we execute the filter
if (r == null) {
r = noOpsResource;
}
if (cacheStrategy == BroadcasterCache.STRATEGY.AFTER_FILTER) {
if (bc.hasPerRequestFilters()) {
logger.debug("Invoking BroadcastFilter with dummy AtmosphereResource {}", r.uuid());
}
entry.message = perRequestFilter(r, entry, true);
if (bc.uuidCache()) {
entry.cache = UUIDBroadcasterCache.class.cast(bc.getBroadcasterCache()).addCacheCandidate(getID(), null, entry.message);
}
} else if (!bc.uuidCache()) {
trackBroadcastMessage(r != null ? (r.uuid().equals("-1") ? null : r) : r, entry.originalMessage);
}
entryDone(entry.future);
return false;
}
return true;
}
protected void queueWriteIO(AtmosphereResource r, Object finalMsg, Entry entry) throws InterruptedException {
// The onStateChange/onRequest may change the isResumed value, hence we need to make sure only one thread flip
// the switch to guarantee the Entry will be cached in the order it was broadcasted.
// Without synchronizing we may end up with a out of order BroadcasterCache queue.
if (!bc.getBroadcasterCache().getClass().equals(BroadcasterConfig.DefaultBroadcasterCache.class.getName())) {
if (r.isResumed() || r.isCancelled()) {
logger.trace("AtmosphereResource {} has been resumed or cancelled, unable to Broadcast message {}", r.uuid(), finalMsg);
// https://github.com/Atmosphere/atmosphere/issues/864
// FIX ME IN 1.1 -- For legacy, we need to leave the logic here
if (!bc.uuidCache()) {
trackBroadcastMessage(r, cacheStrategy == BroadcasterCache.STRATEGY.AFTER_FILTER ? finalMsg : entry.originalMessage);
} else {
afterFilterCacheEntry(r, finalMsg, entry.cache);
}
return;
}
}
AsyncWriteToken w = new AsyncWriteToken(r, finalMsg, entry.future, entry.originalMessage, entry.cache);
if (!outOfOrderBroadcastSupported.get()) {
WriteQueue writeQueue = writeQueues.get(r.uuid());
if (writeQueue == null) {
writeQueue = new WriteQueue(r.uuid());
writeQueues.put(r.uuid(), writeQueue);
}
writeQueue.queue.put(w);
synchronized (writeQueue) {
if (!writeQueue.monitored.getAndSet(true)) {
logger.trace("Broadcaster {} is about to queueWriteIO for AtmosphereResource {}", name, r.uuid());
bc.getAsyncWriteService().submit(getAsyncWriteHandler(writeQueue));
}
}
} else {
uniqueWriteQueue.queue.offer(w);
}
}
private final static class WriteQueue {
final BlockingQueue queue = new LinkedBlockingQueue();
final AtomicBoolean monitored = new AtomicBoolean();
final String uuid;
private WriteQueue(String uuid) {
this.uuid = uuid;
}
}
protected Object perRequestFilter(AtmosphereResource r, Entry msg, boolean cache) {
// A broadcaster#broadcast(msg,Set) may contains null value.
if (r == null) {
logger.trace("Null AtmosphereResource passed inside a Set");
return msg.message;
}
Object finalMsg = msg.message;
if (isAtmosphereResourceValid(r)) {
if (bc.hasPerRequestFilters()) {
BroadcastAction a = bc.filter(r, msg.message, msg.originalMessage);
if (a.action() == BroadcastAction.ACTION.ABORT) {
return null;
}
if (a.message() != msg.originalMessage) {
finalMsg = a.message();
}
}
}
// For legacy BroadcasterCache
boolean after = cacheStrategy == BroadcasterCache.STRATEGY.AFTER_FILTER;
if (cache && after && !bc.uuidCache()) {
trackBroadcastMessage(r != null ? (r.uuid().equals("-1") ? null : r) : r, after ? finalMsg : msg.originalMessage);
return finalMsg;
}
return finalMsg;
}
private Object callable(Object msg) {
if (Callable.class.isAssignableFrom(msg.getClass())) {
try {
return Callable.class.cast(msg).call();
} catch (Throwable e) {
logger.warn("Callable exception", e);
return null;
}
}
return msg;
}
protected void executeAsyncWrite(final AsyncWriteToken token) {
boolean notifyListeners = true;
boolean lostCandidate = false;
final AtmosphereResourceEventImpl event = (AtmosphereResourceEventImpl) token.resource.getAtmosphereResourceEvent();
final AtmosphereResourceImpl r = AtmosphereResourceImpl.class.cast(token.resource);
try {
event.setMessage(token.msg);
// Make sure we cache the message in case the AtmosphereResource has been cancelled, resumed or the client disconnected.
if (!isAtmosphereResourceValid(r)) {
logger.trace("Unable to sent {}. AtmosphereResource {} state is invalid for Broadcaster " + name, token.msg, r.uuid());
afterFilterCacheEntry(r, token.msg, token.cache);
removeAtmosphereResource(r);
return;
}
clearUUIDCache(r, token.cache);
try {
r.getRequest().setAttribute(getID(), token.future);
r.getRequest().setAttribute(MAX_INACTIVE, System.currentTimeMillis());
r.getRequest().setAttribute(ASYNC_TOKEN, token);
} catch (Throwable t) {
logger.warn("Invalid AtmosphereResource state {}. The connection has been remotely" +
" closed and message {} will be added to the configured BroadcasterCache for later retrieval", r.uuid(), event.getMessage());
logger.trace("If you are using Tomcat 7.0.22 and lower, your most probably hitting http://is.gd/NqicFT");
logger.trace("{}", r.uuid(), t);
// The Request/Response associated with the AtmosphereResource has already been written and commited
removeAtmosphereResource(r, false);
config.getBroadcasterFactory().removeAllAtmosphereResource(r);
event.setCancelled(true);
event.setThrowable(t);
r.setIsInScope(false);
lostCandidate = true;
return;
}
prepareInvokeOnStateChange(r, event);
} finally {
if (notifyListeners) {
r.notifyListeners();
}
entryDone(token.future);
if (lostCandidate) {
cacheLostMessage(r, token, true);
}
token.destroy();
}
}
private void clearUUIDCache(AtmosphereResource r, CacheMessage cacheMessage) {
// https://github.com/Atmosphere/atmosphere/issues/864
// No exception so far, so remove the message from the cache. It will be re-added if something bad happened
BroadcasterCache broadcasterCache = bc.getBroadcasterCache();
if (bc.uuidCache()) {
UUIDBroadcasterCache.class.cast(broadcasterCache).clearCache(getID(), AtmosphereResourceImpl.class.cast(r), cacheMessage);
}
}
/**
* Return false if no cached messages has been found, true if cached.
*
* @param r
* @param e
* @return false if no cached messages has been found, true if cached.
*/
protected boolean checkCachedAndPush(final AtmosphereResource r, final AtmosphereResourceEvent e) {
boolean cached = retrieveTrackedBroadcast(r, e);
if (!cached) return false;
if (!((List) e.getMessage()).isEmpty()) {
logger.debug("Sending cached message {} to {}", e.getMessage(), r.uuid());
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy