mirror of
https://codeberg.org/Mercury-IM/Smack
synced 2025-09-10 18:59:41 +02:00
Inroduce AsyncButOrdered
This commit is contained in:
parent
1acfd872a7
commit
476fdf99a1
11 changed files with 414 additions and 272 deletions
|
@ -29,13 +29,12 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
@ -86,7 +85,6 @@ import org.jivesoftware.smack.util.DNSUtil;
|
|||
import org.jivesoftware.smack.util.Objects;
|
||||
import org.jivesoftware.smack.util.PacketParserUtils;
|
||||
import org.jivesoftware.smack.util.ParserUtils;
|
||||
import org.jivesoftware.smack.util.SmackExecutorThreadFactory;
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
import org.jivesoftware.smack.util.dns.HostAddress;
|
||||
|
||||
|
@ -266,14 +264,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A executor service used to invoke the callbacks of synchronous stanza listeners. We use a executor service to
|
||||
* decouple incoming stanza processing from callback invocation. It is important that order of callback invocation
|
||||
* is the same as the order of the incoming stanzas. Therefore we use a <i>single</i> threaded executor service.
|
||||
*/
|
||||
private final ExecutorService singleThreadedExecutorService = new ThreadPoolExecutor(0, 1, 30L,
|
||||
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
|
||||
new SmackExecutorThreadFactory(this, "Single Threaded Executor"));
|
||||
private static final AsyncButOrdered<AbstractXMPPConnection> ASYNC_BUT_ORDERED = new AsyncButOrdered<>();
|
||||
|
||||
/**
|
||||
* The used host to establish the connection to
|
||||
|
@ -1112,10 +1103,10 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
|
|||
LOGGER.log(Level.WARNING, "Exception while sending error IQ to unkown IQ request", e);
|
||||
}
|
||||
} else {
|
||||
ExecutorService executorService = null;
|
||||
Executor executorService = null;
|
||||
switch (iqRequestHandler.getMode()) {
|
||||
case sync:
|
||||
executorService = singleThreadedExecutorService;
|
||||
executorService = ASYNC_BUT_ORDERED.asExecutorFor(this);
|
||||
break;
|
||||
case async:
|
||||
executorService = CACHED_EXECUTOR_SERVICE;
|
||||
|
@ -1192,7 +1183,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
|
|||
|
||||
// Decouple incoming stanza processing from listener invocation. Unlike async listeners, this uses a single
|
||||
// threaded executor service and therefore keeps the order.
|
||||
singleThreadedExecutorService.execute(new Runnable() {
|
||||
ASYNC_BUT_ORDERED.performAsyncButOrdered(this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (StanzaListener listener : listenersToNotify) {
|
||||
|
@ -1207,7 +1198,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1350,24 +1340,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
|
|||
return this.fromMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
LOGGER.fine("finalizing " + this + ": Shutting down executor services");
|
||||
try {
|
||||
// It's usually not a good idea to rely on finalize. But this is the easiest way to
|
||||
// avoid the "Smack Listener Processor" leaking. The thread(s) of the executor have a
|
||||
// reference to their ExecutorService which prevents the ExecutorService from being
|
||||
// gc'ed. It is possible that the XMPPConnection instance is gc'ed while the
|
||||
// listenerExecutor ExecutorService call not be gc'ed until it got shut down.
|
||||
singleThreadedExecutorService.shutdownNow();
|
||||
} catch (Throwable t) {
|
||||
LOGGER.log(Level.WARNING, "finalize() threw throwable", t);
|
||||
}
|
||||
finally {
|
||||
super.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
protected final void parseFeatures(XmlPullParser parser) throws Exception {
|
||||
streamFeatures.clear();
|
||||
final int initialDepth = parser.getDepth();
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2018 Florian Schmaus
|
||||
*
|
||||
* 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.jivesoftware.smack;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Helper class to perform an operation asynchronous but keeping the order in respect to a given key.
|
||||
* <p>
|
||||
* A typical use pattern for this helper class consists of callbacks for an abstract entity where the order of callbacks
|
||||
* matters, which eventually call user code in form of listeners. Since the order the callbacks matters, you need to use
|
||||
* synchronous connection listeners. But if those listeners would invoke the user provided listeners, and if those user
|
||||
* provided listeners would take a long time to complete, or even worse, block, then Smack's total progress is stalled,
|
||||
* since synchronous connection listeners are invoked from the main event loop.
|
||||
* </p>
|
||||
* <p>
|
||||
* It is common for those situations that the order of callbacks is not globally important, but only important in
|
||||
* respect to an particular entity. Take chat state notifications (CSN) for example: Assume there are two contacts which
|
||||
* send you CSNs. If a contact sends you first 'active' and then 'inactive, it is crucial that first the listener is
|
||||
* called with 'active' and afterwards with 'inactive'. But if there is another contact is sending 'composing' followed
|
||||
* by 'paused', then it is also important that the listeners are invoked in the correct order, but the order in which
|
||||
* the listeners for those two contacts are invoked does not matter.
|
||||
* </p>
|
||||
* <p>
|
||||
* Using this helper class, one would call {@link #performAsyncButOrdered(Object, Runnable)} which the remote contacts
|
||||
* JID as first argument and a {@link Runnable} invoking the user listeners as second. This class guarantees that
|
||||
* runnables of subsequent invocations are always executed after the runnables of previous invocations using the same
|
||||
* key.
|
||||
* </p>
|
||||
*
|
||||
* @param <K> the type of the key
|
||||
* @since 4.3
|
||||
*/
|
||||
public class AsyncButOrdered<K> {
|
||||
|
||||
private final Map<K, Queue<Runnable>> pendingRunnables = new WeakHashMap<>();
|
||||
|
||||
private final Map<K, Boolean> threadActiveMap = new WeakHashMap<>();
|
||||
|
||||
/**
|
||||
* Invoke the given {@link Runnable} asynchronous but ordered in respect to the given key.
|
||||
*
|
||||
* @param key the key deriving the order
|
||||
* @param runnable the {@link Runnable} to run
|
||||
* @return true if a new thread was created
|
||||
*/
|
||||
public boolean performAsyncButOrdered(K key, Runnable runnable) {
|
||||
Queue<Runnable> keyQueue;
|
||||
synchronized (pendingRunnables) {
|
||||
keyQueue = pendingRunnables.get(key);
|
||||
if (keyQueue == null) {
|
||||
keyQueue = new ConcurrentLinkedQueue<>();
|
||||
pendingRunnables.put(key, keyQueue);
|
||||
}
|
||||
}
|
||||
|
||||
keyQueue.add(runnable);
|
||||
|
||||
boolean newHandler;
|
||||
synchronized (threadActiveMap) {
|
||||
Boolean threadActive = threadActiveMap.get(key);
|
||||
if (threadActive == null) {
|
||||
threadActive = false;
|
||||
threadActiveMap.put(key, threadActive);
|
||||
}
|
||||
|
||||
newHandler = !threadActive;
|
||||
if (newHandler) {
|
||||
Handler handler = new Handler(keyQueue, key);
|
||||
threadActiveMap.put(key, true);
|
||||
AbstractXMPPConnection.asyncGo(handler);
|
||||
}
|
||||
}
|
||||
|
||||
return newHandler;
|
||||
}
|
||||
|
||||
public Executor asExecutorFor(final K key) {
|
||||
return new Executor() {
|
||||
@Override
|
||||
public void execute(Runnable runnable) {
|
||||
performAsyncButOrdered(key, runnable);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private class Handler implements Runnable {
|
||||
private final Queue<Runnable> keyQueue;
|
||||
private final K key;
|
||||
|
||||
Handler(Queue<Runnable> keyQueue, K key) {
|
||||
this.keyQueue = keyQueue;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mainloop:
|
||||
while (true) {
|
||||
Runnable runnable = null;
|
||||
while ((runnable = keyQueue.poll()) != null) {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Throwable t) {
|
||||
// The run() method threw, this handler thread is going to terminate because of that. Ensure we note
|
||||
// that in the map.
|
||||
synchronized (threadActiveMap) {
|
||||
threadActiveMap.put(key, false);
|
||||
}
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (threadActiveMap) {
|
||||
// If the queue is empty, stop this handler, otherwise continue looping.
|
||||
if (keyQueue.isEmpty()) {
|
||||
threadActiveMap.put(key, false);
|
||||
break mainloop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2014-2015 Florian Schmaus
|
||||
*
|
||||
* 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.jivesoftware.smack.util;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
import org.jivesoftware.smack.XMPPConnection;
|
||||
|
||||
/**
|
||||
* SmackExecutorThreadFactory creates daemon threads with a particular name. Note that you should
|
||||
* not use anonymous inner classes for thread factories in order to prevent threads from leaking.
|
||||
*/
|
||||
public final class SmackExecutorThreadFactory implements ThreadFactory {
|
||||
private final int connectionCounterValue;
|
||||
private final String name;
|
||||
private int count = 0;
|
||||
|
||||
public SmackExecutorThreadFactory(XMPPConnection connection, String name) {
|
||||
this.connectionCounterValue = connection.getConnectionCounter();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.setName("Smack-" + name + ' ' + count++ + " (" + connectionCounterValue + ")");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue