
com.threerings.crowd.chat.server.ChatChannelManager Maven / Gradle / Ivy
//
// $Id: ChatChannelManager.java 6661 2011-06-18 22:47:23Z charlie $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.crowd.chat.server;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Longs;
import com.google.inject.Inject;
import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.ResultListener;
import com.threerings.util.Name;
import com.threerings.presents.annotation.AnyThread;
import com.threerings.presents.client.InvocationService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationException;
import com.threerings.presents.server.InvocationManager;
import com.threerings.presents.server.PresentsDObjectMgr;
import com.threerings.presents.peer.data.ClientInfo;
import com.threerings.presents.peer.data.NodeObject;
import com.threerings.presents.peer.server.PeerManager;
import com.threerings.presents.peer.server.PeerManager.NodeRequest;
import com.threerings.presents.peer.server.NodeRequestsListener;
import com.threerings.crowd.chat.data.ChannelSpeakMarshaller;
import com.threerings.crowd.chat.data.ChatChannel;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.CrowdCodes;
import com.threerings.crowd.peer.data.CrowdClientInfo;
import com.threerings.crowd.peer.data.CrowdNodeObject;
import com.threerings.crowd.peer.server.CrowdPeerManager;
import com.threerings.crowd.server.BodyLocator;
import static com.threerings.crowd.Log.log;
/**
* Handles chat channel services.
*/
public abstract class ChatChannelManager
implements ChannelSpeakProvider
{
/**
* Value asynchronously returned by {@link #collectChatHistory} after polling all peer nodes.
*/
public static class ChatHistoryResult
{
/** The set of nodes that either did not reply within the timeout, or had a failure. */
public Set failedNodes;
/** The things in the user's chat history, aggregated from all nodes and sorted by
* timestamp. */
public List history;
}
/**
* When a body becomes a member of a channel, this method should be called so that any server
* that happens to be hosting that channel can be told that the body in question is now a
* participant.
*/
@AnyThread
public void bodyAddedToChannel (ChatChannel channel, final int bodyId)
{
_peerMan.invokeNodeAction(new ChannelAction(channel) {
@Override protected void execute () {
ChannelInfo info = _channelMan._channels.get(_channel);
if (info != null) {
info.participants.add(bodyId);
} else if (_channelMan._resolving.containsKey(_channel)) {
log.warning("Oh for fuck's sake, distributed systems are complicated",
"channel", _channel);
}
}
});
}
/**
* When a body loses channel membership, this method should be called so that any server that
* happens to be hosting that channel can be told that the body in question is now a
* participant.
*/
@AnyThread
public void bodyRemovedFromChannel (ChatChannel channel, final int bodyId)
{
_peerMan.invokeNodeAction(new ChannelAction(channel) {
@Override protected void execute () {
ChannelInfo info = _channelMan._channels.get(_channel);
if (info != null) {
info.participants.remove(bodyId);
} else if (_channelMan._resolving.containsKey(_channel)) {
log.warning("Oh for fuck's sake, distributed systems are complicated",
"channel", _channel);
}
}
});
}
/**
* Collects all chat messages heard by the given user on all peers.
*/
@AnyThread
public void collectChatHistory (final Name user, final ResultListener lner)
{
_peerMan.invokeNodeRequest(new NodeRequest() {
public boolean isApplicable (NodeObject nodeobj) {
return true; // poll all nodes
}
@Override protected void execute (InvocationService.ResultListener listener) {
// find all the UserMessages for the given user and send them back
listener.requestProcessed(Lists.newArrayList(Iterables.filter(
_chatHistory.get(user), IS_USER_MESSAGE)));
}
@Inject protected transient ChatHistory _chatHistory;
}, new NodeRequestsListener>() {
public void requestsProcessed (NodeRequestsResult> rRes) {
ChatHistoryResult chRes = new ChatHistoryResult();
chRes.failedNodes = rRes.getNodeErrors().keySet();
chRes.history = Lists.newArrayList(
Iterables.concat(rRes.getNodeResults().values()));
Collections.sort(chRes.history, SORT_BY_TIMESTAMP);
lner.requestCompleted(chRes);
}
public void requestFailed (String cause) {
lner.requestFailed(new InvocationException(cause));
}
});
}
// from interface ChannelSpeakProvider
public void speak (ClientObject caller, final ChatChannel channel, String message, byte mode)
{
BodyObject body = _locator.forClient(caller);
final UserMessage umsg = new UserMessage(body.getVisibleName(), null, message, mode);
// if we're hosting this channel, dispatch it directly
if (_channels.containsKey(channel)) {
dispatchSpeak(channel, umsg);
return;
}
// if we're resolving this channel, queue up our message for momentary deliver
List msgs = _resolving.get(channel);
if (msgs != null) {
msgs.add(umsg);
return;
}
// forward the speak request to the server that hosts the channel in question
_peerMan.invokeNodeAction(new ChannelAction(channel) {
@Override protected void execute () {
_channelMan.dispatchSpeak(_channel, umsg);
}
}, new Runnable() {
public void run () {
_resolving.put(channel, Lists.newArrayList(umsg));
resolveAndDispatch(channel);
}
});
}
/**
* Creates our singleton manager and registers our invocation service.
*/
@Inject protected ChatChannelManager (PresentsDObjectMgr omgr, InvocationManager invmgr)
{
invmgr.registerProvider(this, ChannelSpeakMarshaller.class, CrowdCodes.CROWD_GROUP);
// create and start our idle channel closer; this will run as long as omgr is alive
omgr.newInterval(new Runnable() {
public void run () {
closeIdleChannels();
}
}).schedule(IDLE_CHANNEL_CHECK_PERIOD, true);
}
/**
* Resolves the channel specified in the supplied action and then dispatches it.
*/
protected void resolveAndDispatch (final ChatChannel channel)
{
NodeObject.Lock lock = new NodeObject.Lock("ChatChannel", channel.getLockName());
_peerMan.performWithLock(lock, new PeerManager.LockedOperation() {
public void run () {
((CrowdNodeObject)_peerMan.getNodeObject()).addToHostedChannels(channel);
finishResolveAndDispatch(channel);
}
public void fail (String peerName) {
final List msgs = _resolving.remove(channel);
if (peerName == null) {
log.warning("Failed to resolve chat channel due to lock failure",
"channel", channel);
} else {
// some other peer resolved this channel first, so forward any queued messages
// directly to that node
_peerMan.invokeNodeAction(peerName, new ChannelAction(channel) {
@Override protected void execute () {
for (final UserMessage msg : msgs) {
_channelMan.dispatchSpeak(_channel, msg);
}
}
});
}
}
});
}
/**
* Resolves the participant set for the specified chat channel and dispatches all pending
* messages to the channel. End users of the chat channel system should override this method
* and do what is necessary to resolve the channel's participant set and call {@link
* #resolutionComplete} or {@link #resolutionFailed}.
*/
protected void finishResolveAndDispatch (ChatChannel channel)
{
resolutionComplete(channel, new ArrayIntSet());
}
/**
* This should be called when a channel's participant set has been resolved.
*/
protected void resolutionComplete (ChatChannel channel, Set parts)
{
// map the participants of our now resolved channel
ChannelInfo info = new ChannelInfo();
info.channel = channel;
info.participants = parts;
_channels.put(channel, info);
// dispatch any pending messages now that we know where they go
for (UserMessage msg : _resolving.remove(channel)) {
dispatchSpeak(channel, msg);
}
}
/**
* This should be called if channel resolution fails.
*/
protected void resolutionFailed (ChatChannel channel, Exception cause)
{
log.warning("Failed to resolve chat channel", "channel", channel, cause);
// alas, we just drop all pending messages because we're hosed
_resolving.remove(channel);
}
/**
* Requests that we dispatch the supplied message to all participants of the specified chat
* channel. The speaker will be validated prior to dispatching the message as the originating
* server does not have the information it needs to validate the speaker and must leave that to
* us, the channel hosting server.
*/
protected void dispatchSpeak (ChatChannel channel, final UserMessage message)
{
final ChannelInfo info = _channels.get(channel);
if (info == null) {
// TODO: maybe we should just reresolve the channel...
log.warning("Requested to dispatch speak on unhosted channel", "channel", channel,
"msg", message);
return;
}
// validate the speaker
if (!info.participants.contains(getBodyId(message.speaker))) {
log.warning("Dropping channel chat message from non-speaker", "channel", channel,
"message", message);
return;
}
// note that we're dispatching a message on this channel
info.lastMessage = System.currentTimeMillis();
// generate a mapping from node name to an array of body ids for the participants that are
// currently on the node in question
final Map partMap = Maps.newHashMap();
for (NodeObject nodeobj : _peerMan.getNodeObjects()) {
ArrayIntSet nodeBodyIds = new ArrayIntSet();
for (ClientInfo clinfo : nodeobj.clients) {
int bodyId = getBodyId(((CrowdClientInfo)clinfo).visibleName);
if (info.participants.contains(bodyId)) {
nodeBodyIds.add(bodyId);
}
}
partMap.put(nodeobj.nodeName, nodeBodyIds.toIntArray());
}
for (Map.Entry entry : partMap.entrySet()) {
final int[] bodyIds = entry.getValue();
_peerMan.invokeNodeAction(entry.getKey(), new ChannelAction(channel) {
@Override protected void execute () {
_channelMan.deliverSpeak(_channel, message, bodyIds);
}
});
}
}
/**
* Delivers the supplied chat channel message to the specified bodies.
*/
protected void deliverSpeak (ChatChannel channel, UserMessage message, int[] bodyIds)
{
channel = intern(channel);
for (int bodyId : bodyIds) {
BodyObject bobj = getBodyObject(bodyId);
if (bobj != null && shouldDeliverSpeak(channel, message, bobj)) {
_chatHistory.record(channel, message, bobj.getVisibleName());
bobj.postMessage(ChatCodes.CHAT_CHANNEL_NOTIFICATION, channel, message);
}
}
}
/**
* Called periodically to check for and close any channels that have been idle too long.
*/
protected void closeIdleChannels ()
{
long now = System.currentTimeMillis();
Iterator> iter = _channels.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = iter.next();
if (now - entry.getValue().lastMessage > IDLE_CHANNEL_CLOSE_TIME) {
((CrowdNodeObject)_peerMan.getNodeObject()).removeFromHostedChannels(
entry.getKey());
iter.remove();
}
}
}
/**
* Ratifies the delivery of the supplied chat channel message to the specified body. Derived
* classes can override this method to implement channel disabling, mute lists or any other
* suppression they might need.
*/
protected boolean shouldDeliverSpeak (ChatChannel channel, UserMessage message, BodyObject body)
{
return true;
}
/**
* Returns a widely referenced instance equivalent to the given channel, if one is available.
* This reduces memory usage since clients send new channel instances with each message.
*/
protected ChatChannel intern (ChatChannel channel)
{
ChannelInfo chinfo = _channels.get(channel);
if (chinfo != null) {
return chinfo.channel;
}
return channel;
}
/**
* Converts a speaker's visible name into a unique integer id. This is not the oid for this
* speaker but rather a persistent integer identifier that can be passed between servers and
* used to look up the speaker on the target server via a call to {@link #getBodyObject}. We
* use this rather than names to avoid having to send (large) {@link Name} objects for every
* channel participant to each individual peer that will be forwarding messages.
*/
protected abstract int getBodyId (Name speaker);
/**
* Locates a body object from the given unique id. May return null.
*/
protected abstract BodyObject getBodyObject (int bodyId);
/** Forwards a channel speak request from the server hosting the message originator to the
* server that is hosting the channel. */
protected abstract static class ChannelAction extends PeerManager.NodeAction
{
public ChannelAction (ChatChannel channel) {
_channel = channel;
}
public boolean isApplicable (NodeObject nodeobj) {
return ((CrowdNodeObject)nodeobj).hostedChannels.contains(_channel);
}
protected ChatChannel _channel;
@Inject protected transient ChatChannelManager _channelMan;
}
protected static final Predicate IS_USER_MESSAGE =
new Predicate() {
public boolean apply (ChatHistory.Entry entry) {
return entry.message instanceof UserMessage;
}
};
protected static final Comparator SORT_BY_TIMESTAMP =
new Comparator() {
public int compare (ChatHistory.Entry e1, ChatHistory.Entry e2) {
return Longs.compare(e1.message.timestamp, e2.message.timestamp);
}
};
/** Contains metadata for a particular channel. */
protected static class ChannelInfo
{
/** The channel this info is for. */
public ChatChannel channel;
/** The body ids of the participants of this channel. */
public Set participants;
/** The time at which a message was last dispatched on this channel. */
public long lastMessage;
}
/** Contains pending messages for all channels currently being resolved. */
protected Map> _resolving = Maps.newHashMap();
/** A map of resolved channels to metadata records. */
protected Map _channels = Maps.newHashMap();
/** Provides peer services. */
@Inject protected CrowdPeerManager _peerMan;
/** Used for acquiring BodyObject references from Names and ClientObjects. */
@Inject protected BodyLocator _locator;
/** Used for recording chat history. */
@Inject protected ChatHistory _chatHistory;
/** The period on which we check for idle channels. */
protected static final long IDLE_CHANNEL_CHECK_PERIOD = 5 * 1000L;
/** The amount of idle time (in milliseconds) after which we close a channel. */
protected static final long IDLE_CHANNEL_CLOSE_TIME = 5 * 60 * 1000L;
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy