/* * Copyright 2011 Google Inc. * * 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.ipc.invalidation.external.client.contrib; import com.google.ipc.invalidation.external.client.types.ObjectId; import com.google.ipc.invalidation.ticl.ProtoConverter; import com.google.ipc.invalidation.ticl.TiclExponentialBackoffDelayGenerator; import com.google.ipc.invalidation.util.Bytes; import com.google.ipc.invalidation.util.Marshallable; import com.google.ipc.invalidation.util.TypedUtil; import com.google.protobuf.ByteString; import com.google.protos.ipc.invalidation.AndroidListenerProtocol; import com.google.protos.ipc.invalidation.AndroidListenerProtocol.AndroidListenerState.RetryRegistrationState; import com.google.protos.ipc.invalidation.ClientProtocol.ObjectIdP; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.UUID; /** * Encapsulates state to simplify persistence and tracking of changes. Internally maintains an * {@link #isDirty} bit. Call {@link #resetIsDirty} to indicate that changes have been persisted. * * <p>Notes on the {@link #desiredRegistrations} (DR) and {@link #delayGenerators} (DG) collections: * When the client application registers for an object, it is immediately added to DR. Similarly, * an object is removed from DR when the application unregisters. If a registration failure is * reported, the object is removed from DR if it exists and a delay generator is added to DG if one * does not already exist. (In the face of a failure, we assume that the registration is not desired * by the application unless/until the application retries.) When there is a successful * registration, the corresponding DG entry is removed. There are two independent collections rather * than one since we may be applying exponential backoff for an object when it is not in DR, and we * may have no reason to delay operations against an object in DR as well. * * <p>By removing objects from the {@link #desiredRegistrations} collection on failures, we are * essentially assuming that the client application doesn't care about the registration until we're * told otherwise -- by a subsequent call to register or unregister. * */ final class AndroidListenerState implements Marshallable<AndroidListenerProtocol.AndroidListenerState> { /** * Exponential backoff delay generators used to determine delay before registration retries. * There is a delay generator for every failing object. */ private final Map<ObjectId, TiclExponentialBackoffDelayGenerator> delayGenerators = new HashMap<ObjectId, TiclExponentialBackoffDelayGenerator>(); /** The set of registrations for which the client wants to be registered. */ private final Set<ObjectId> desiredRegistrations; /** Random generator used for all delay generators. */ private final Random random = new Random(); /** Initial maximum retry delay for exponential backoff. */ private final int initialMaxDelayMs; /** Maximum delay factor for exponential backoff (relative to {@link #initialMaxDelayMs}). */ private final int maxDelayFactor; /** Sequence number for alarm manager request codes. */ private int requestCodeSeqNum; /** * Dirty flag. {@code true} whenever changes are made, reset to false when {@link #resetIsDirty} * is called. State initialized from a proto is assumed to be initially clean. */ private boolean isDirty; /** * The identifier for the current client. The ID is randomly generated and is used to ensure that * messages are not handled by the wrong client instance. */ private final ByteString clientId; /** Initializes state for a new client. */ AndroidListenerState(int initialMaxDelayMs, int maxDelayFactor) { desiredRegistrations = new HashSet<ObjectId>(); clientId = createGloballyUniqueClientId(); // Assigning a client ID dirties the state because calling the constructor twice produces // different results. isDirty = true; requestCodeSeqNum = 0; this.initialMaxDelayMs = initialMaxDelayMs; this.maxDelayFactor = maxDelayFactor; } /** Initializes state from proto. */ AndroidListenerState(int initialMaxDelayMs, int maxDelayFactor, AndroidListenerProtocol.AndroidListenerState state) { desiredRegistrations = new HashSet<ObjectId>(); for (ObjectIdP objectIdProto : state.getRegistrationList()) { desiredRegistrations.add(ProtoConverter.convertFromObjectIdProto(objectIdProto)); } for (RetryRegistrationState retryState : state.getRetryRegistrationStateList()) { ObjectId objectId = ProtoConverter.convertFromObjectIdProto(retryState.getObjectId()); delayGenerators.put(objectId, new TiclExponentialBackoffDelayGenerator(random, initialMaxDelayMs, maxDelayFactor, retryState.getExponentialBackoffState())); } clientId = state.getClientId(); requestCodeSeqNum = state.getRequestCodeSeqNum(); isDirty = false; this.initialMaxDelayMs = initialMaxDelayMs; this.maxDelayFactor = maxDelayFactor; } /** Increments and returns sequence number for alarm manager request codes. */ int getNextRequestCode() { isDirty = true; return ++requestCodeSeqNum; } /** * See specs for {@link TiclExponentialBackoffDelayGenerator#getNextDelay}. Gets next delay for * the given {@code objectId}. If a delay generator does not yet exist for the object, one is * created. */ int getNextDelay(ObjectId objectId) { TiclExponentialBackoffDelayGenerator delayGenerator = delayGenerators.get(objectId); if (delayGenerator == null) { delayGenerator = new TiclExponentialBackoffDelayGenerator(random, initialMaxDelayMs, maxDelayFactor); delayGenerators.put(objectId, delayGenerator); } // Requesting a delay from a delay generator modifies its internal state. isDirty = true; return delayGenerator.getNextDelay(); } /** Inform that there has been a successful registration for an object. */ void informRegistrationSuccess(ObjectId objectId) { // Since registration was successful, we can remove exponential backoff (if any) for the given // object. resetDelayGeneratorFor(objectId); } /** * Inform that there has been a registration failure. * * <p>Remove the object from the desired registrations collection whenever there's a failure. We * don't care if the op that failed was actually an unregister because we never suppress an * unregister request (even if the object is not in the collection). See * {@link AndroidListener#issueRegistration}. */ public void informRegistrationFailure(ObjectId objectId, boolean isTransient) { removeDesiredRegistration(objectId); if (!isTransient) { // There should be no retries for the object, so remove any backoff state associated with it. resetDelayGeneratorFor(objectId); } } /** * If there is a backoff delay generator for the given object, removes it and sets dirty flag. */ private void resetDelayGeneratorFor(ObjectId objectId) { if (TypedUtil.remove(delayGenerators, objectId) != null) { isDirty = true; } } /** Adds the given registration. Returns {@code true} if it was not already tracked. */ boolean addDesiredRegistration(ObjectId objectId) { if (desiredRegistrations.add(objectId)) { isDirty = true; return true; } return false; } /** Removes the given registration. Returns {@code true} if it was actually tracked. */ boolean removeDesiredRegistration(ObjectId objectId) { if (desiredRegistrations.remove(objectId)) { isDirty = true; return true; } return false; } /** * Resets the {@link #isDirty} flag to {@code false}. Call after marshalling and persisting state. */ void resetIsDirty() { isDirty = false; } @Override public AndroidListenerProtocol.AndroidListenerState marshal() { return AndroidListenerProtos.newAndroidListenerState(clientId, requestCodeSeqNum, delayGenerators, desiredRegistrations); } /** * Gets the identifier for the current client. Used to determine if registrations commands are * relevant to this instance. */ ByteString getClientId() { return clientId; } /** Returns {@code true} iff registration is desired for the given object. */ boolean containsDesiredRegistration(ObjectId objectId) { return TypedUtil.contains(desiredRegistrations, objectId); } /** * Returns {@code true} if changes have been made since the last successful call to * {@link #resetIsDirty}. */ boolean getIsDirty() { return isDirty; } @Override public int hashCode() { // Since the client ID is globally unique, it's sufficient as a hashCode. return clientId.hashCode(); } /** * Overridden for tests which compare listener states to verify that they have been correctly * (un)marshalled. We implement equals rather than exposing private data. */ @Override public boolean equals(Object object) { if (this == object) { return true; } if (!(object instanceof AndroidListenerState)) { return false; } AndroidListenerState that = (AndroidListenerState) object; return (this.isDirty == that.isDirty) && (this.requestCodeSeqNum == that.requestCodeSeqNum) && (this.desiredRegistrations.size() == that.desiredRegistrations.size()) && (this.desiredRegistrations.containsAll(that.desiredRegistrations)) && (this.clientId.equals(that.clientId)) && equals(this.delayGenerators, that.delayGenerators); } /** Compares the contents of two {@link #delayGenerators} maps. */ private static boolean equals(Map<ObjectId, TiclExponentialBackoffDelayGenerator> x, Map<ObjectId, TiclExponentialBackoffDelayGenerator> y) { if (x.size() != y.size()) { return false; } for (Entry<ObjectId, TiclExponentialBackoffDelayGenerator> xEntry : x.entrySet()) { TiclExponentialBackoffDelayGenerator yGenerator = y.get(xEntry.getKey()); if ((yGenerator == null) || !xEntry.getValue().marshal().toByteString().equals( yGenerator.marshal().toByteString())) { return false; } } return true; } @Override public String toString() { return String.format("AndroidListenerState[%s]: isDirty = %b, " + "desiredRegistrations.size() = %d, delayGenerators.size() = %d, requestCodeSeqNum = %d", Bytes.toString(clientId), isDirty, desiredRegistrations.size(), delayGenerators.size(), requestCodeSeqNum); } /** * Constructs a new globally unique ID for the client. Can be used to determine if commands * originated from this instance of the listener. */ private static ByteString createGloballyUniqueClientId() { UUID guid = UUID.randomUUID(); byte[] bytes = new byte[16]; ByteBuffer buffer = ByteBuffer.wrap(bytes); buffer.putLong(guid.getLeastSignificantBits()); buffer.putLong(guid.getMostSignificantBits()); return ByteString.copyFrom(bytes); } }