/* * RED5 Open Source Flash Server - http://code.google.com/p/red5/ * * Copyright 2006-2012 by respective authors (see below). All rights reserved. * * 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.red5.server.so; import java.io.IOException; 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.atomic.AtomicInteger; import org.red5.io.object.Deserializer; import org.red5.io.object.Input; import org.red5.io.object.Output; import org.red5.io.object.Serializer; import org.red5.server.AttributeStore; import org.red5.server.api.IAttributeStore; import org.red5.server.api.IConnection.Encoding; import org.red5.server.api.event.IEventListener; import org.red5.server.api.persistence.IPersistable; import org.red5.server.api.persistence.IPersistenceStore; import org.red5.server.api.scope.ScopeType; import org.red5.server.api.statistics.ISharedObjectStatistics; import org.red5.server.api.statistics.support.StatisticsCounter; import org.red5.server.net.rtmp.Channel; import org.red5.server.net.rtmp.RTMPConnection; import org.red5.server.net.rtmp.message.Constants; import org.red5.server.so.ISharedObjectEvent.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 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, IPersistable, 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; /** * Object that is delegated with all storage work for persistent SOs */ protected IPersistenceStore storage; /** * 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; /** * 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(); /** Constructs a new SharedObject. */ public SharedObject() { // This is used by the persistence framework super(); ownerMessage = new SharedObjectMessage(null, null, -1, false); creationTime = System.currentTimeMillis(); } /** * Constructs new SO from Input object * @param input Input source * @throws IOException I/O exception * * @see org.red5.io.object.Input */ public SharedObject(Input input) throws IOException { this(); deserialize(input); } /** * Creates new SO from given data map, name, path and persistence option * * @param name SO name * @param path SO path * @param persistent SO persistence */ public SharedObject(String name, String path, boolean persistent) { super(); this.name = name; this.path = path; this.persistent = persistent; ownerMessage = new SharedObjectMessage(null, name, 0, persistent); creationTime = System.currentTimeMillis(); } /** * Creates new SO from given data map, name, path, storage object and persistence option * * @param name SO name * @param path SO path * @param persistent SO persistence * @param storage Persistence storage */ public SharedObject(String name, String path, boolean persistent, IPersistenceStore storage) { this(name, path, persistent); setStore(storage); } /** * 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) { this(name, path, persistent); attributes.putAll(data); } /** * Creates new SO from given data map, name, path, storage object and persistence option * * @param data Data * @param name SO name * @param path SO path * @param persistent SO persistence * @param storage Persistence storage */ public SharedObject(Map<String, Object> data, String name, String path, boolean persistent, IPersistenceStore storage) { this(data, name, path, persistent); setStore(storage); } /** {@inheritDoc} */ public String getName() { return name; } /** {@inheritDoc} */ public void setName(String name) { throw new UnsupportedOperationException("Shared objects don't support setting of their name"); } /** {@inheritDoc} */ public String getPath() { return path; } /** {@inheritDoc} */ public void setPath(String path) { this.path = path; } /** {@inheritDoc} */ public String getType() { return ScopeType.SHARED_OBJECT.toString(); } /** {@inheritDoc} */ public long getLastModified() { return lastModified; } /** {@inheritDoc} */ public boolean isPersistent() { return persistent; } /** {@inheritDoc} */ public void setPersistent(boolean persistent) { log.debug("setPersistent: {}", persistent); this.persistent = persistent; } /** * Send update notification over data channel of RTMP connection */ protected void sendUpdates() { log.debug("sendUpdates"); //get the current version int currentVersion = getVersion(); //get the name String name = getName(); //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 if (source != null) { // Only send updates when issued through RTMP request final SharedObjectMessage syncOwner = ((RTMPConnection) source).getEncoding() == Encoding.AMF3 ? new FlexSharedObjectMessage(null, name, currentVersion, persistent) : new SharedObjectMessage(null, name, currentVersion, persistent); syncOwner.addEvents(events); Channel channel = ((RTMPConnection) source).getChannel((byte) 3); if (channel != null) { //ownerMessage.acquire(); log.debug("Send to (owner) {}", channel); try { channel.write(syncOwner); } catch (Exception e) { log.warn("Exception sending shared object sync to owner", e); } } 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 //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 RTMPConnection) { //get the channel for so updates final Channel channel = ((RTMPConnection) listener).getChannel((byte) 3); //create a new sync message for every client to avoid //concurrent access through multiple threads final SharedObjectMessage syncMessage = ((RTMPConnection) listener).getEncoding() == Encoding.AMF3 ? new FlexSharedObjectMessage(null, name, currentVersion, persistent) : new SharedObjectMessage(null, name, currentVersion, persistent); syncMessage.addEvents(events); //create a worker Runnable worker = new Runnable() { public void run() { log.debug("Send to {}", channel); try { channel.write(syncMessage); } catch (Exception e) { log.warn("Exception sending shared object sync", e); } } }; SharedObjectService.SHAREDOBJECT_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() { log.debug("notifyModified"); if (updateCounter.get() == 0) { if (modified) { // The client sent at least one update -> increase version of SO updateVersion(); lastModified = System.currentTimeMillis(); if (storage != null) { if (!storage.save(this)) { log.error("Could not store shared object."); } } } else { log.debug("Not modified"); } sendUpdates(); //APPSERVER-291 modified = false; } else { log.debug("Update counter: {}", updateCounter.get()); } } /** * 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) { log.debug("getAttribute - name: {} value: {}", name, value); Object result = null; if (name != null) { 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) { log.debug("setAttribute - name: {} value: {}", name, 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) { 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) { 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) { log.debug("register - listener: {}", listener); listeners.add(listener); listenerStats.increment(); // prepare response for new client ownerMessage.addEvent(Type.CLIENT_INITIAL_DATA, null, null); if (!isPersistent()) { 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(); } /** * Unregister event listener * @param listener Event listener */ protected void unregister(IEventListener listener) { log.debug("unregister - listener: {}", listener); listeners.remove(listener); listenerStats.decrement(); checkRelease(); } /** * Check if shared object must be released. */ protected void checkRelease() { //part 3 of fix for TRAC #360 if (!isPersistent() && listeners.isEmpty() && !isAcquired()) { log.info("Deleting shared object {} because all clients disconnected and it is no longer acquired.", name); if (storage != null) { if (!storage.remove(this)) { log.error("Could not remove shared object"); } } close(); } } /** * 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() { log.debug("beginUpdate"); beginUpdate(source); } /** * Begin update of this Shared Object and setting listener * @param listener Update with listener */ protected void beginUpdate(IEventListener listener) { log.debug("beginUpdate - listener: {}", 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() { log.debug("endUpdate"); // Decrease number of pending updates if (updateCounter.decrementAndGet() == 0) { notifyModified(); source = null; } } /** {@inheritDoc} */ public void serialize(Output output) throws IOException { log.debug("serialize"); Serializer ser = new Serializer(); ser.serialize(output, getName()); ser.serialize(output, getAttributes()); } /** {@inheritDoc} */ @SuppressWarnings({ "unchecked", "rawtypes" }) public void deserialize(Input input) throws IOException { log.debug("deserialize"); Deserializer deserializer = new Deserializer(); name = deserializer.deserialize(input, String.class); persistent = true; super.setAttributes(deserializer.<Map> deserialize(input, Map.class)); ownerMessage.setName(name); ownerMessage.setPersistent(persistent); } /** {@inheritDoc} */ public void setStore(IPersistenceStore store) { this.storage = store; } /** {@inheritDoc} */ public IPersistenceStore getStore() { return storage; } /** * 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() { log.debug("clear"); super.removeAttributes(); // send confirmation to client ownerMessage.addEvent(Type.CLIENT_CLEAR_DATA, name, null); notifyModified(); changeStats.incrementAndGet(); 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() { log.debug("close"); // clear collections super.removeAttributes(); listeners.clear(); syncEvents.clear(); ownerMessage.getEvents().clear(); } /** * 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() { log.debug("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() { log.debug("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(); } }