Files
narya/src/java/com/threerings/crowd/chat/server/ChatChannelManager.java
T
Michael Bayne c0c68f98d3 Added RootDObjectManager.newInterval() for creating intervals that are
automatically canceled when the omgr is shutdown (they actually cancel
themselves if they fire after the omgr has been shutdown, which stock intervals
also do, but these guys do so quietly because we know they mean to work that
way).

Made use of that new method and the fluent schedule methods in various places.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5880 542714f4-19e9-0310-aa3c-eee0fc999fb1
2009-07-20 22:43:00 +00:00

396 lines
15 KiB
Java

//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2009 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.Iterator;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.IntSet;
import com.threerings.util.Name;
import com.threerings.presents.annotation.AnyThread;
import com.threerings.presents.data.ClientObject;
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.server.InvocationManager;
import com.threerings.presents.server.PresentsDObjectMgr;
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 static com.threerings.crowd.Log.log;
/**
* Handles chat channel services.
*/
public abstract class ChatChannelManager
implements ChannelSpeakProvider
{
/**
* 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, int bodyId)
{
_peerMan.invokeNodeAction(new ParticipantChanged(channel, bodyId, true));
}
/**
* 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, int bodyId)
{
_peerMan.invokeNodeAction(new ParticipantChanged(channel, bodyId, false));
}
// from interface ChannelSpeakProvider
public void speak (ClientObject caller, final ChatChannel channel, String message, byte mode)
{
final UserMessage umsg = new UserMessage(
((BodyObject)caller).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<UserMessage> 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 ForwardChannelSpeak(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.registerDispatcher(new ChannelSpeakDispatcher(this), 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) {
List<UserMessage> 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
for (UserMessage msg : msgs) {
_peerMan.invokeNodeAction(peerName, new ForwardChannelSpeak(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, IntSet parts)
{
// map the participants of our now resolved channel
ChannelInfo info = new ChannelInfo();
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, 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<String,int[]> 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<String,int[]> entry : partMap.entrySet()) {
_peerMan.invokeNodeAction(
entry.getKey(), new DispatchChannelSpeak(channel, message, entry.getValue()));
}
}
/**
* Delivers the supplied chat channel message to the specified bodies.
*/
protected void deliverSpeak (ChatChannel channel, UserMessage message, int[] bodyIds)
{
for (int bodyId : bodyIds) {
BodyObject bobj = getBodyObject(bodyId);
if (bobj != null && shouldDeliverSpeak(channel, message, bobj)) {
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<Map.Entry<ChatChannel, ChannelInfo>> iter = _channels.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<ChatChannel, ChannelInfo> 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;
}
/**
* 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 ChannelAction () {
}
@Override public boolean isApplicable (NodeObject nodeobj) {
return ((CrowdNodeObject)nodeobj).hostedChannels.contains(_channel);
}
protected ChatChannel _channel;
@Inject protected transient ChatChannelManager _channelMan;
}
/** Informs the server hosting a channel that a body has been added to or removed from the
* channel's participants set. */
protected static class ParticipantChanged extends ChannelAction
{
public ParticipantChanged (ChatChannel channel, int bodyId, boolean added) {
super(channel);
_bodyId = bodyId;
_added = added;
}
public ParticipantChanged () {
}
@Override protected void execute () {
ChannelInfo info = _channelMan._channels.get(_channel);
if (info != null) {
if (_added) {
info.participants.add(_bodyId);
} else {
info.participants.remove(_bodyId);
}
} else if (_channelMan._resolving.containsKey(_channel)) {
log.warning("Oh for fuck's sake, distributed systems are complicated",
"channel", _channel);
}
}
protected int _bodyId;
protected boolean _added;
}
/** Forwards a channel speak request from the server hosting the message originator to the
* server that is hosting the channel. */
protected static class ForwardChannelSpeak extends ChannelAction
{
public ForwardChannelSpeak (ChatChannel channel, UserMessage message) {
super(channel);
_message = message;
}
public ForwardChannelSpeak () {
}
@Override protected void execute () {
_channelMan.dispatchSpeak(_channel, _message);
}
protected UserMessage _message;
}
/** Forwards a chat channel message to the server to which some subset of the channel
* participants are connected so that it can dispatch the message on their body objects. */
protected static class DispatchChannelSpeak extends ForwardChannelSpeak
{
public DispatchChannelSpeak (ChatChannel channel, UserMessage message, int[] bodyIds) {
super(channel, message);
_bodyIds = bodyIds;
}
public DispatchChannelSpeak () {
}
@Override public boolean isApplicable (NodeObject nodeobj) {
return true; // not used
}
@Override protected void execute () {
_channelMan.deliverSpeak(_channel, _message, _bodyIds);
}
protected int[] _bodyIds;
}
/** Contains metadata for a particular channel. */
protected static class ChannelInfo
{
/** The body ids of the participants of this channel. */
public IntSet 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<ChatChannel,List<UserMessage>> _resolving = Maps.newHashMap();
/** A map of resolved channels to metadata records. */
protected Map<ChatChannel,ChannelInfo> _channels = Maps.newHashMap();
/** Provides peer services. */
@Inject protected CrowdPeerManager _peerMan;
/** 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;
}