package org.red5.server.so;
/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright (c) 2006-2010 by respective authors (see below). All rights reserved.
*
* 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
*/
import static org.red5.server.api.so.ISharedObject.TYPE;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.red5.server.AttributeStore;
import org.red5.server.api.IAttributeStore;
import org.red5.server.api.event.IEventListener;
import org.red5.server.api.statistics.ISharedObjectStatistics;
import org.red5.server.api.statistics.support.StatisticsCounter;
import org.red5.server.net.rtmp.message.Constants;
import org.red5.server.so.ISharedObjectEvent.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.flazr.rtmp.RtmpHeader;
/**
* Represents shared object on server-side. Shared Objects in Flash are like cookies that are stored
* on client side. In Red5 and Flash Media Server there's one more special type of SOs : remote Shared Objects.
*
* These are shared by multiple clients and synchronized between them automatically on each data change. This is done
* asynchronously, used as events handling and is widely used in multiplayer Flash online games.
*
* Shared object can be persistent or transient. The difference is that first are saved to the disk and can be
* accessed later on next connection, transient objects are not saved and get lost each time they last client
* disconnects from it.
*
* Shared Objects has name identifiers and path on server's HD (if persistent). On deeper level server-side
* Shared Object in this implementation actually uses IPersistenceStore to delegate all (de)serialization work.
*
* SOs store data as simple map, that is, "name-value" pairs. Each value in turn can be complex object or map.
*
* All access to methods that change properties in the SO must be properly
* synchronized for multi-threaded access.
*/
public class SharedObject extends AttributeStore implements ISharedObjectStatistics, Constants {
/**
* Logger
*/
protected static Logger log = LoggerFactory.getLogger(SharedObject.class);
/**
* Shared Object name (identifier)
*/
protected String name = "";
/**
* SO path
*/
protected String path = "";
/**
* true if the SharedObject was stored by the persistence framework (NOT in database,
* just plain serialization to the disk) and can be used later on reconnection
*/
protected boolean persistent;
/**
* true if the client / server created the SO to be persistent
*/
protected boolean persistentSO;
/**
* Version. Used on synchronization purposes.
*/
protected AtomicInteger version = new AtomicInteger(1);
/**
* Number of pending update operations
*/
protected AtomicInteger updateCounter = new AtomicInteger();
/**
* Has changes? flag
*/
protected boolean modified;
/**
* Last modified timestamp
*/
protected long lastModified = -1;
/**
* Owner event
*/
protected SharedObjectMessage ownerMessage;
/**
* Synchronization events
*/
protected ConcurrentLinkedQueue<ISharedObjectEvent> syncEvents = new ConcurrentLinkedQueue<ISharedObjectEvent>();
/**
* Listeners
*/
protected CopyOnWriteArraySet<IEventListener> listeners = new CopyOnWriteArraySet<IEventListener>();
/**
* Event listener, actually RTMP connection
*/
protected IEventListener source;
protected Channel channel;
/**
* Number of times the SO has been acquired
*/
protected AtomicInteger acquireCount = new AtomicInteger();
/**
* Timestamp the scope was created.
*/
private long creationTime;
/**
* Manages listener statistics.
*/
protected StatisticsCounter listenerStats = new StatisticsCounter();
/**
* Counts number of "change" events.
*/
protected AtomicInteger changeStats = new AtomicInteger();
/**
* Counts number of "delete" events.
*/
protected AtomicInteger deleteStats = new AtomicInteger();
/**
* Counts number of "send message" events.
*/
protected AtomicInteger sendStats = new AtomicInteger();
/**
* Executor for sending messages to connections.
*/
protected ExecutorService executor;
/** Constructs a new SharedObject. */
public SharedObject() {
ownerMessage = new SharedObjectMessage(null, null, -1, false);
creationTime = System.currentTimeMillis();
}
/**
* Constructs new SO from a received message (header + buffer)
*/
public SharedObject(RtmpHeader header, ChannelBuffer in) {
ownerMessage = new SharedObjectMessage(header, in);
creationTime = System.currentTimeMillis();
}
/**
* Creates new SO from given data map, name, path and persistence option
*
* @param data Data
* @param name SO name
* @param path SO path
* @param persistent SO persistence
*/
public SharedObject(Map<String, Object> data, String name, String path, boolean persistent) {
super();
this.name = name;
this.path = path;
this.persistentSO = persistent;
ownerMessage = new SharedObjectMessage(null, name, 0, persistent);
creationTime = System.currentTimeMillis();
super.setAttributes(data);
}
/** {@inheritDoc} */
public String getName() {
return name;
}
/** {@inheritDoc} */
public void setName(String name) {
// Shared objects don't support setting of their names
}
/** {@inheritDoc} */
public String getPath() {
return path;
}
/** {@inheritDoc} */
public void setPath(String path) {
this.path = path;
}
/** {@inheritDoc} */
public String getType() {
return TYPE;
}
/** {@inheritDoc} */
public long getLastModified() {
return lastModified;
}
/**
* Getter for persistent object
*
* @return Persistent object
*/
public boolean isPersistentObject() {
return persistentSO;
}
/** {@inheritDoc} */
public boolean isPersistent() {
return persistent;
}
/** {@inheritDoc} */
public void setPersistent(boolean persistent) {
this.persistent = persistent;
}
/**
* Send update notification over data channel of RTMP connection
*/
protected void sendUpdates() {
//get the current version
int currentVersion = getVersion();
//get the name
String name = getName();
//is it persistent
boolean persist = isPersistentObject();
//used for notifying owner / consumers
ConcurrentLinkedQueue<ISharedObjectEvent> events = new ConcurrentLinkedQueue<ISharedObjectEvent>();
//get owner events
ConcurrentLinkedQueue<ISharedObjectEvent> ownerEvents = ownerMessage.getEvents();
//get all current owner events
do {
ISharedObjectEvent soe = ownerEvents.poll();
if (soe != null) {
events.add(soe);
}
} while (!ownerEvents.isEmpty());
//null out our ref
ownerEvents = null;
//
if (!events.isEmpty()) {
// Send update to "owner" of this update request
SharedObjectMessage syncOwner = new SharedObjectMessage(null, name, currentVersion, persist);
syncOwner.addEvents(events);
if (channel != null) {
//ownerMessage.acquire();
channel.write(syncOwner);
log.debug("Owner: {}", channel);
} else {
log.warn("No channel found for owner changes!?");
}
}
//clear owner events
events.clear();
//get all current sync events
do {
ISharedObjectEvent soe = syncEvents.poll();
if (soe != null) {
events.add(soe);
}
} while (!syncEvents.isEmpty());
//tell all the listeners
if (!events.isEmpty()) {
//dont create the executor until we need it
if (executor == null) {
executor = Executors.newCachedThreadPool();
}
//get the listeners
Set<IEventListener> listeners = getListeners();
//updates all registered clients of this shared object
for (IEventListener listener : listeners) {
if (listener != source) {
if (listener instanceof Channel) {
//get the channel for so updates
final Channel channel = ((Channel) listener);
//create a new sync message for every client to avoid
//concurrent access through multiple threads
final SharedObjectMessage syncMessage = new SharedObjectMessage(null, name, currentVersion, persist);
syncMessage.addEvents(events);
//create a worker
Runnable worker = new Runnable() {
public void run() {
log.debug("Send to {}", channel);
channel.write(syncMessage);
}
};
executor.execute(worker);
} else {
log.warn("Can't send sync message to unknown connection {}", listener);
}
} else {
// Don't re-send update to active client
log.debug("Skipped {}", source);
}
}
}
//clear events
events.clear();
}
/**
* Send notification about modification of SO
*/
protected void notifyModified() {
// does it make sense on client side?
//if (updateCounter.get() > 0) {
// we're inside a beginUpdate...endUpdate block
// return;
//}
if (modified) {
// The client sent at least one update -> increase version of SO
updateVersion();
lastModified = System.currentTimeMillis();
}
sendUpdates();
//APPSERVER-291
modified = false;
}
/**
* Return an error message to the client.
*
* @param message
*/
protected void returnError(String message) {
ownerMessage.addEvent(Type.CLIENT_STATUS, "error", message);
}
/**
* Return an attribute value to the owner.
*
* @param name
*/
protected void returnAttributeValue(String name) {
ownerMessage.addEvent(Type.CLIENT_UPDATE_DATA, name, getAttribute(name));
}
/**
* Return attribute by name and set if it doesn't exist yet.
* @param name Attribute name
* @param value Value to set if attribute doesn't exist
* @return Attribute value
*/
@Override
public Object getAttribute(String name, Object value) {
if (name == null) {
return null;
}
Object result = attributes.putIfAbsent(name, value);
if (result == null) {
// No previous value
modified = true;
ownerMessage.addEvent(Type.CLIENT_UPDATE_DATA, name, value);
syncEvents.add(new SharedObjectEvent(Type.CLIENT_UPDATE_DATA, name, value));
notifyModified();
changeStats.incrementAndGet();
result = value;
}
return result;
}
/**
* Set value of attribute with given name
* @param name Attribute name
* @param value Attribute value
* @return <code>true</code> if there's such attribute and value was set, <code>false</code> otherwise
*/
@Override
public boolean setAttribute(String name, Object value) {
boolean result = true;
ownerMessage.addEvent(Type.CLIENT_UPDATE_ATTRIBUTE, name, null);
if (value == null && super.removeAttribute(name)) {
// Setting a null value removes the attribute
modified = true;
syncEvents.add(new SharedObjectEvent(Type.CLIENT_DELETE_DATA, name, null));
deleteStats.incrementAndGet();
} else if (value != null && super.setAttribute(name, value)) {
// only sync if the attribute changed
modified = true;
syncEvents.add(new SharedObjectEvent(Type.CLIENT_UPDATE_DATA, name, value));
changeStats.incrementAndGet();
} else {
result = false;
}
notifyModified();
return result;
}
/**
* Set attributes as map.
*
* @param values Attributes.
*/
@Override
public void setAttributes(Map<String, Object> values) {
if (values == null) {
return;
}
beginUpdate();
try {
for (Map.Entry<String, Object> entry : values.entrySet()) {
setAttribute(entry.getKey(), entry.getValue());
}
} finally {
endUpdate();
}
}
/**
* Set attributes as attributes store.
*
* @param values Attributes.
*/
@Override
public void setAttributes(IAttributeStore values) {
if (values == null) {
return;
}
setAttributes(values.getAttributes());
}
/**
* Removes attribute with given name
* @param name Attribute
* @return <code>true</code> if there's such an attribute and it was removed, <code>false</code> otherwise
*/
@Override
public boolean removeAttribute(String name) {
boolean result = true;
// Send confirmation to client
ownerMessage.addEvent(Type.CLIENT_DELETE_DATA, name, null);
if (super.removeAttribute(name)) {
modified = true;
syncEvents.add(new SharedObjectEvent(Type.CLIENT_DELETE_DATA, name, null));
deleteStats.incrementAndGet();
} else {
result = false;
}
notifyModified();
return result;
}
/**
* Broadcast event to event handler
* @param handler Event handler
* @param arguments Arguments
*/
protected void sendMessage(String handler, List<?> arguments) {
// Forward
ownerMessage.addEvent(Type.CLIENT_SEND_MESSAGE, handler, arguments);
syncEvents.add(new SharedObjectEvent(Type.CLIENT_SEND_MESSAGE, handler, arguments));
sendStats.incrementAndGet();
}
/**
* Getter for data.
*
* @return SO data as unmodifiable map
*/
public Map<String, Object> getData() {
return getAttributes();
}
/**
* Getter for version.
*
* @return SO version.
*/
public int getVersion() {
return version.get();
}
/**
* Increases version by one
*/
private void updateVersion() {
version.incrementAndGet();
}
/**
* Remove all attributes (clear Shared Object)
*/
@Override
public void removeAttributes() {
// TODO: there must be a direct way to clear the SO on the client side...
Set<String> names = getAttributeNames();
for (String key : names) {
ownerMessage.addEvent(Type.CLIENT_DELETE_DATA, key, null);
syncEvents.add(new SharedObjectEvent(Type.CLIENT_DELETE_DATA, key, null));
}
deleteStats.addAndGet(names.size());
// Clear data
super.removeAttributes();
// Mark as modified
modified = true;
// Broadcast 'modified' event
notifyModified();
}
/**
* Register event listener
* @param listener Event listener
*/
protected void register(IEventListener listener) {
listeners.add(listener);
listenerStats.increment();
// prepare response for new client
ownerMessage.addEvent(Type.CLIENT_INITIAL_DATA, null, null);
if (!isPersistentObject()) {
ownerMessage.addEvent(Type.CLIENT_CLEAR_DATA, null, null);
}
if (!attributes.isEmpty()) {
ownerMessage.addEvent(new SharedObjectEvent(Type.CLIENT_UPDATE_DATA, null, getAttributes()));
}
// we call notifyModified here to send response if we're not in a
// beginUpdate block
notifyModified();
}
/**
* Check if shared object must be released.
*/
protected void checkRelease() {
//part 3 of fix for TRAC #360
if (!isPersistentObject() && listeners.isEmpty() && !isAcquired()) {
log.info("Deleting shared object {} because all clients disconnected and it is no longer acquired.", name);
close();
}
}
/**
* Unregister event listener
* @param listener Event listener
*/
protected void unregister(IEventListener listener) {
listeners.remove(listener);
listenerStats.decrement();
checkRelease();
}
/**
* Get event listeners.
*
* @return Value for property 'listeners'.
*/
public Set<IEventListener> getListeners() {
return listeners;
}
/**
* Begin update of this Shared Object.
* Increases number of pending update operations
*/
protected void beginUpdate() {
beginUpdate(source);
}
/**
* Begin update of this Shared Object and setting listener
* @param listener Update with listener
*/
protected void beginUpdate(IEventListener listener) {
source = listener;
// Increase number of pending updates
updateCounter.incrementAndGet();
}
/**
* End update of this Shared Object. Decreases number of pending update operations and
* broadcasts modified event if it is equal to zero (i.e. no more pending update operations).
*/
protected void endUpdate() {
// Decrease number of pending updates
if (updateCounter.decrementAndGet() == 0) {
notifyModified();
source = null;
}
}
/**
* Deletes all the attributes and sends a clear event to all listeners. The
* persistent data object is also removed from a persistent shared object.
*
* @return <code>true</code> on success, <code>false</code> otherwise
*/
protected boolean clear() {
super.removeAttributes();
// Send confirmation to client
ownerMessage.addEvent(Type.CLIENT_CLEAR_DATA, name, null);
notifyModified();
changeStats.incrementAndGet();
// Is it clear now?
return true;
}
/**
* Detaches a reference from this shared object, reset it's state, this will destroy the
* reference immediately. This is useful when you don't want to proxy a
* shared object any longer.
*/
protected void close() {
// clear collections
super.removeAttributes();
listeners.clear();
syncEvents.clear();
ownerMessage.getEvents().clear();
if (executor != null) {
//disable new tasks from being submitted
executor.shutdown();
try {
//wait a while for existing tasks to terminate
if (!executor.awaitTermination(250, TimeUnit.MILLISECONDS)) {
executor.shutdownNow(); // cancel currently executing tasks
}
} catch (InterruptedException ie) {
// re-cancel if current thread also interrupted
executor.shutdownNow();
// preserve interrupt status
Thread.currentThread().interrupt();
}
}
}
/**
* Prevent shared object from being released. Each call to <code>acquire</code>
* must be paired with a call to <code>release</code> so the SO isn't held
* forever. This is only valid for non-persistent SOs.
*/
public void acquire() {
acquireCount.incrementAndGet();
}
/**
* Check if shared object currently is acquired.
*
* @return <code>true</code> if the SO is acquired, otherwise <code>false</code>
*/
public boolean isAcquired() {
return acquireCount.get() > 0;
}
/**
* Release previously acquired shared object. If the SO is non-persistent,
* no more clients are connected the SO isn't acquired any more, the data
* is released.
*/
public void release() {
if (acquireCount.get() == 0) {
throw new RuntimeException("The shared object was not acquired before.");
}
if (acquireCount.decrementAndGet() == 0) {
checkRelease();
}
}
/** {@inheritDoc} */
public long getCreationTime() {
return creationTime;
}
/** {@inheritDoc} */
public int getTotalListeners() {
return listenerStats.getTotal();
}
/** {@inheritDoc} */
public int getMaxListeners() {
return listenerStats.getMax();
}
/** {@inheritDoc} */
public int getActiveListeners() {
return listenerStats.getCurrent();
}
/** {@inheritDoc} */
public int getTotalChanges() {
return changeStats.intValue();
}
/** {@inheritDoc} */
public int getTotalDeletes() {
return deleteStats.intValue();
}
/** {@inheritDoc} */
public int getTotalSends() {
return sendStats.intValue();
}
}