/* * 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.ticl; import com.google.ipc.invalidation.common.CommonProtoStrings2; import com.google.ipc.invalidation.common.CommonProtos2; import com.google.ipc.invalidation.common.DigestFunction; import com.google.ipc.invalidation.external.client.SystemResources.Logger; import com.google.ipc.invalidation.ticl.Statistics.ClientErrorType; import com.google.ipc.invalidation.ticl.TestableInvalidationClient.RegistrationManagerState; import com.google.ipc.invalidation.util.InternalBase; import com.google.ipc.invalidation.util.Marshallable; import com.google.ipc.invalidation.util.TextBuilder; import com.google.ipc.invalidation.util.TypedUtil; import com.google.protos.ipc.invalidation.ClientProtocol.ObjectIdP; import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationP; import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationP.OpType; import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationStatus; import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationSubtree; import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationSummary; import com.google.protos.ipc.invalidation.JavaClient.RegistrationManagerStateP; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Object to track desired client registrations. This class belongs to caller (e.g., * InvalidationClientImpl) and is not thread-safe - the caller has to use this class in a * thread-safe manner. * */ class RegistrationManager extends InternalBase implements Marshallable<RegistrationManagerStateP> { /** Prefix used to request all registrations. */ static final byte[] EMPTY_PREFIX = new byte[]{}; /** The set of regisrations that the application has requested for. */ private DigestStore<ObjectIdP> desiredRegistrations; /** Statistics objects to track number of sent messages, etc. */ private final Statistics statistics; /** Latest known server registration state summary. */ private ProtoWrapper<RegistrationSummary> lastKnownServerSummary; /** * Map of object ids and operation types for which we have not yet issued any registration-status * upcall to the listener. We need this so that we can synthesize success upcalls if registration * sync, rather than a server message, communicates to us that we have a successful * (un)registration. * <p> * This is a map from object id to type, rather than a set of {@code RegistrationP}, because * a set of {@code RegistrationP} would assume that we always get a response for every operation * we issue, which isn't necessarily true (i.e., the server might send back an unregistration * status in response to a registration request). */ private final Map<ProtoWrapper<ObjectIdP>, RegistrationP.OpType> pendingOperations = new HashMap<ProtoWrapper<ObjectIdP>, RegistrationP.OpType>(); private final Logger logger; public RegistrationManager(Logger logger, Statistics statistics, DigestFunction digestFn, RegistrationManagerStateP registrationManagerState) { this.logger = logger; this.statistics = statistics; this.desiredRegistrations = new SimpleRegistrationStore(digestFn); if (registrationManagerState == null) { // Initialize the server summary with a 0 size and the digest corresponding // to it. Using defaultInstance would wrong since the server digest will // not match unnecessarily and result in an info message being sent. this.lastKnownServerSummary = ProtoWrapper.of(getRegistrationSummary()); } else { this.lastKnownServerSummary = ProtoWrapper.of(registrationManagerState.getLastKnownServerSummary()); desiredRegistrations.add(registrationManagerState.getRegistrationsList()); for (RegistrationP regOp : registrationManagerState.getPendingOperationsList()) { pendingOperations.put(ProtoWrapper.of(regOp.getObjectId()), regOp.getOpType()); } } } /** * Returns a copy of the registration manager's state * <p> * Direct test code MUST not call this method on a random thread. It must be called on the * InvalidationClientImpl's internal thread. */ RegistrationManagerState getRegistrationManagerStateCopyForTest(DigestFunction digestFunction) { List<ObjectIdP> registeredObjects = new ArrayList<ObjectIdP>(); for (ObjectIdP oid : desiredRegistrations.getElements(EMPTY_PREFIX, 0)) { registeredObjects.add(oid); } return new RegistrationManagerState( RegistrationSummary.newBuilder(getRegistrationSummary()).build(), RegistrationSummary.newBuilder(lastKnownServerSummary.getProto()).build(), registeredObjects); } /** * Sets the digest store to be {@code digestStore} for testing purposes. * <p> * REQUIRES: This method is called before the Ticl has done any operations on this object. */ void setDigestStoreForTest(DigestStore<ObjectIdP> digestStore) { this.desiredRegistrations = digestStore; this.lastKnownServerSummary = ProtoWrapper.of(getRegistrationSummary()); } Collection<ObjectIdP> getRegisteredObjectsForTest() { return desiredRegistrations.getElements(EMPTY_PREFIX, 0); } /** Perform registration/unregistation for all objects in {@code objectIds}. */ Collection<ObjectIdP> performOperations(Collection<ObjectIdP> objectIds, RegistrationP.OpType regOpType) { // Record that we have pending operations on the objects. for (ObjectIdP objectId : objectIds) { pendingOperations.put(ProtoWrapper.of(objectId), regOpType); } // Update the digest appropriately. if (regOpType == RegistrationP.OpType.REGISTER) { return desiredRegistrations.add(objectIds); } else { return desiredRegistrations.remove(objectIds); } } /** * Returns a registration subtree for registrations where the digest of the object id begins with * the prefix {@code digestPrefix} of {@code prefixLen} bits. This method may also return objects * whose digest prefix does not match {@code digestPrefix}. */ RegistrationSubtree getRegistrations(byte[] digestPrefix, int prefixLen) { RegistrationSubtree.Builder builder = RegistrationSubtree.newBuilder(); for (ObjectIdP objectId : desiredRegistrations.getElements(digestPrefix, prefixLen)) { builder.addRegisteredObject(objectId); } return builder.build(); } /** * Handles registration operation statuses from the server. Returns a list of booleans, one per * registration status, that indicates whether the registration operation was both successful and * agreed with the desired client state (i.e., for each registration status, * (status.optype == register) == desiredRegistrations.contains(status.objectid)). * <p> * REQUIRES: the caller subsequently make an informRegistrationStatus or informRegistrationFailure * upcall on the listener for each registration in {@code registrationStatuses}. */ List<Boolean> handleRegistrationStatus(List<RegistrationStatus> registrationStatuses) { // Local-processing result code for each element of registrationStatuses. List<Boolean> localStatuses = new ArrayList<Boolean>(registrationStatuses.size()); for (RegistrationStatus registrationStatus : registrationStatuses) { ObjectIdP objectIdProto = registrationStatus.getRegistration().getObjectId(); // The object is no longer pending, since we have received a server status for it, so // remove it from the pendingOperations map. (It may or may not have existed in the map, // since we can receive spontaneous status messages from the server.) TypedUtil.remove(pendingOperations, ProtoWrapper.of(objectIdProto)); // We start off with the local-processing set as success, then potentially fail. boolean isSuccess = true; // if the server operation succeeded, then local processing fails on "incompatibility" as // defined above. if (CommonProtos2.isSuccess(registrationStatus.getStatus())) { boolean appWantsRegistration = desiredRegistrations.contains(objectIdProto); boolean isOpRegistration = registrationStatus.getRegistration().getOpType() == RegistrationP.OpType.REGISTER; boolean discrepancyExists = isOpRegistration ^ appWantsRegistration; if (discrepancyExists) { // Remove the registration and set isSuccess to false, which will cause the caller to // issue registration-failure to the application. desiredRegistrations.remove(objectIdProto); statistics.recordError(ClientErrorType.REGISTRATION_DISCREPANCY); logger.info("Ticl discrepancy detected: registered = %s, requested = %s. " + "Removing %s from requested", isOpRegistration, appWantsRegistration, CommonProtoStrings2.toLazyCompactString(objectIdProto)); isSuccess = false; } } else { // If the server operation failed, then also local processing fails. desiredRegistrations.remove(objectIdProto); logger.fine("Removing %s from committed", CommonProtoStrings2.toLazyCompactString(objectIdProto)); isSuccess = false; } localStatuses.add(isSuccess); } return localStatuses; } /** * Removes all desired registrations and pending operations. Returns all object ids * that were affected. * <p> * REQUIRES: the caller issue a permanent failure upcall to the listener for all returned object * ids. */ Collection<ProtoWrapper<ObjectIdP>> removeRegisteredObjects() { int numObjects = desiredRegistrations.size() + pendingOperations.size(); Set<ProtoWrapper<ObjectIdP>> failureCalls = new HashSet<ProtoWrapper<ObjectIdP>>(numObjects); for (ObjectIdP objectId : desiredRegistrations.removeAll()) { failureCalls.add(ProtoWrapper.of(objectId)); } failureCalls.addAll(pendingOperations.keySet()); pendingOperations.clear(); return failureCalls; } // // Digest-related methods // /** Returns a summary of the desired registrations. */ RegistrationSummary getRegistrationSummary() { return CommonProtos2.newRegistrationSummary(desiredRegistrations.size(), desiredRegistrations.getDigest()); } /** * Informs the manager of a new registration state summary from the server. * Returns a possibly-empty map of <object-id, reg-op-type>. For each entry in the map, * the caller should make an inform-registration-status upcall on the listener. */ Set<ProtoWrapper<RegistrationP>> informServerRegistrationSummary( RegistrationSummary regSummary) { if (regSummary != null) { this.lastKnownServerSummary = ProtoWrapper.of(regSummary); } if (isStateInSyncWithServer()) { // If we are now in sync with the server, then the caller should make inform-reg-status // upcalls for all operations that we had pending, if any; they are also no longer pending. Set<ProtoWrapper<RegistrationP>> upcallsToMake = new HashSet<ProtoWrapper<RegistrationP>>(pendingOperations.size()); for (Map.Entry<ProtoWrapper<ObjectIdP>, RegistrationP.OpType> entry : pendingOperations.entrySet()) { ObjectIdP objectId = entry.getKey().getProto(); boolean isReg = entry.getValue() == OpType.REGISTER; upcallsToMake.add(ProtoWrapper.of(CommonProtos2.newRegistrationP(objectId, isReg))); } pendingOperations.clear(); return upcallsToMake; } else { // If we are not in sync with the server, then the caller should make no upcalls. return Collections.emptySet(); } } /** * Returns whether the local registration state and server state agree, based on the last * received server summary (from {@link #informServerRegistrationSummary}). */ boolean isStateInSyncWithServer() { return TypedUtil.equals(lastKnownServerSummary, ProtoWrapper.of(getRegistrationSummary())); } @Override public void toCompactString(TextBuilder builder) { builder.appendFormat("Last known digest: %s, Requested regs: %s", lastKnownServerSummary, desiredRegistrations); } @Override public RegistrationManagerStateP marshal() { RegistrationManagerStateP.Builder builder = RegistrationManagerStateP.newBuilder(); builder.setLastKnownServerSummary(lastKnownServerSummary.getProto()); builder.addAllRegistrations(desiredRegistrations.getElements(EMPTY_PREFIX, 0)); for (Map.Entry<ProtoWrapper<ObjectIdP>, RegistrationP.OpType> pendingOp : pendingOperations.entrySet()) { ObjectIdP objectId = pendingOp.getKey().getProto(); boolean isReg = pendingOp.getValue() == OpType.REGISTER; builder.addPendingOperations(CommonProtos2.newRegistrationP(objectId, isReg)); } return builder.build(); } }