Package com.google.walkaround.slob.server

Source Code of com.google.walkaround.slob.server.SlobMessageRouter$ListenerAlreadyPresent

/*
* Copyright 2011 Google Inc. 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 com.google.walkaround.slob.server;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.appengine.api.channel.ChannelFailureException;
import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelService;
import com.google.appengine.api.memcache.Expiration;
import com.google.appengine.api.memcache.MemcacheService.SetPolicy;
import com.google.common.base.Objects;
import com.google.common.collect.Sets;
import com.google.inject.BindingAnnotation;
import com.google.inject.Inject;
import com.google.walkaround.slob.shared.ClientId;
import com.google.walkaround.slob.shared.SlobId;
import com.google.walkaround.util.server.Util;
import com.google.walkaround.util.server.appengine.MemcacheTable;

import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Router that connects client channels as listeners to objects in an m:n
* fashion, and provides the token required for channel set up.
*
* <p>
* Messages are not guaranteed to be delivered, nor are they guaranteed to be in
* order, nor is there any guaranteed about lack of duplicate messages. The
* message contents should provide enough information to allow clients to deal
* with these situations.
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class SlobMessageRouter {

  @BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
  public @interface SlobChannelExpirationSeconds {}

  public static class TooManyListenersException extends Exception {
    private static final long serialVersionUID = 455819249880278222L;

    public TooManyListenersException(String message, Throwable cause) {
      super(message, cause);
    }

    public TooManyListenersException(String message) {
      super(message);
    }

    public TooManyListenersException(Throwable cause) {
      super(cause);
    }
  }

  private static class ListenerAlreadyPresent extends Exception {
    private static final long serialVersionUID = 144800949601544909L;

    private final ListenerKey key;
    private final ClientId clientId;
    private ListenerAlreadyPresent(ListenerKey key, ClientId clientId) {
      super(key + " " + clientId);
      this.key = key;
      this.clientId = clientId;
    }
  }

  private static class ListenerKey implements Serializable {
    private static final long serialVersionUID = 601407287266649008L;

    private final SlobId id;
    private final int listenerNum;

    public ListenerKey(SlobId id, int listenerNum) {
      this.id = checkNotNull(id, "Null id");
      this.listenerNum = listenerNum;
    }

    @Override public String toString() {
      return "ListenerKey(" + id + ", " + listenerNum + ")";
    }

    @Override public final boolean equals(Object o) {
      if (o == this) { return true; }
      if (!(o instanceof ListenerKey)) { return false; }
      ListenerKey other = (ListenerKey) o;
      return listenerNum == other.listenerNum
          && Objects.equal(id, other.id);
    }

    @Override public final int hashCode() {
      return Objects.hashCode(id, listenerNum);
    }
  }

  private static final Logger log = Logger.getLogger(SlobMessageRouter.class.getName());

  private static final int MAX_LISTENERS = 51;

  private static final String LISTENER_MEMCACHE_TAG = "ORL";
  private static final String CLIENTS_MEMCACHE_TAG = "ORC";
  private final MemcacheTable<ListenerKey, ClientId> objectListeners;
  private final MemcacheTable<ClientId, String> clientTokens;
  private final ChannelService channelService;
  private final int expirationSeconds;

  @Inject
  public SlobMessageRouter(MemcacheTable.Factory memcacheFactory, ChannelService channelService,
      @SlobChannelExpirationSeconds int expirationSeconds) {
    this.objectListeners = memcacheFactory.create(LISTENER_MEMCACHE_TAG);
    this.clientTokens = memcacheFactory.create(CLIENTS_MEMCACHE_TAG);
    this.channelService = channelService;
    this.expirationSeconds = expirationSeconds;
  }

  /**
   * Publishes messages to clients listening on an object.
   */
  public void publishMessages(SlobId object, String jsonString) {
    if (jsonString.length() > 8000) {
      // Channel API has a limit of 32767 UTF-8 bytes.  It's OK for us not to
      // publish large messages; we can let clients poll.  TODO(ohler): 8000 is
      // probably overly conservative, make a better estimate.
      log.warning(object + ": Message too large ("
          + jsonString.length() + " chars), not publishing: " + jsonString);
      return;
    } else {
      log.info("Publishing " + object + " " + jsonString);
    }
    Map<?, ClientId> takenMappings = getMappings(object);
    for (ClientId listener : takenMappings.values()) {
      sendData(listener, jsonString);
    }
  }

  /**
   * Connects a client as a listener to an object. A client may listen to more
   * than one object.
   *
   * <p>
   * Returns the token the client should use to set up its browser channel. A
   * client will only use one token, even if it is listening to multiple
   * objects. The router keeps track of this, and will return the client's
   * existing token if it already has one.
   */
  public String connectListener(SlobId objectId, ClientId clientId)
      throws TooManyListenersException {
    log.info("Connecting " + clientId + " to " + objectId);

    int maxAttempts = 10;
    for (int attempt = 0; attempt < maxAttempts; attempt++) {
      boolean success;
      try {
        int freeId = getFreeKeyForListener(objectId, clientId);
        assert freeId >= 0 && freeId < MAX_LISTENERS;

        success = objectListeners.put(new ListenerKey(objectId, freeId), clientId,
            Expiration.byDeltaSeconds(expirationSeconds),
            SetPolicy.ADD_ONLY_IF_NOT_PRESENT);

        if (success) {
          log.info("Created new listener: " + clientId);
        }
      } catch (ListenerAlreadyPresent e) {
        // This is just an expiry refresh, ideally we would rather not clobber
        // a different listener in the unlikely event it got changed in this
        // brief instant, but that doesn't matter.
        objectListeners.put(e.key, e.clientId,
            Expiration.byDeltaSeconds(expirationSeconds),
            SetPolicy.SET_ALWAYS);
        success = true;
        log.info("Refreshed listener: " + e.clientId);
      }

      if (success) {
        return tokenFor(clientId);
      }

      log.info("Failed to create listener, might retry...");
    }

    log.warning("Max attempts to set a listener exceeded");
    throw new TooManyListenersException("Max attempts to set a listener exceeded");
  }

  private String tokenFor(ClientId clientId) {
    String existing = clientTokens.get(clientId);
    if (existing != null) {
      log.info("Got existing token for client " + clientId + ": " + existing);
      return existing;
    }

    // This might screw up a concurrent attempt to do the same thing but
    // doesn't really matter.
    String token = channelService.createChannel(clientId.getId());
    clientTokens.put(clientId, token);

    log.info("Got new token for client " + clientId + ": " + token);
    return token;
  }

  private void sendData(ClientId clientId, String data) {
    log.info("Sending to " + clientId + ", " + Util.abbrev(data, 50));
    try {
      channelService.sendMessage(new ChannelMessage(clientId.getId(), data));
    } catch (ChannelFailureException e) {
      // Channel service is best-effort anyway, so it's safe to discard the
      // exception after taking note of it.
      log.log(Level.SEVERE, "Channel service failed for " + clientId, e);
    }
  }

  private int getFreeKeyForListener(SlobId object, ClientId clientId)
      throws TooManyListenersException, ListenerAlreadyPresent {
    Map<ListenerKey, ClientId> takenMappings = getMappings(object);

    for (Map.Entry<ListenerKey, ClientId> entry : takenMappings.entrySet()) {
      if (clientId.equals(entry.getValue())) {
        throw new ListenerAlreadyPresent(entry.getKey(), entry.getValue());
      }
    }

    // MAX_LISTENERS is small, and we need to iterate up to it anyway
    // in getMappings()
    for (int i = 0; i < MAX_LISTENERS; i++) {
      if (!takenMappings.containsKey(new ListenerKey(object, i))) {
        return i;
      }
    }

    throw new TooManyListenersException(object + " has too many listeners");
  }

  private Map<ListenerKey, ClientId> getMappings(SlobId object) {
    Set<ListenerKey> keys = Sets.newHashSet();
    for (int i = 0; i < MAX_LISTENERS; i++) {
      keys.add(new ListenerKey(object, i));
    }
    return objectListeners.getAll(keys);
  }
}
TOP

Related Classes of com.google.walkaround.slob.server.SlobMessageRouter$ListenerAlreadyPresent

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.