All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.azure.documentdb.internal.SessionContainer Maven / Gradle / Ivy

/* 
 * Copyright (c) Microsoft Corporation.  All rights reserved.
 */

package com.microsoft.azure.documentdb.internal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.microsoft.azure.documentdb.DocumentClientException;
import com.microsoft.azure.documentdb.DocumentCollection;
import com.microsoft.azure.documentdb.Error;
import com.microsoft.azure.documentdb.PartitionKeyRange;
import com.microsoft.azure.documentdb.internal.directconnectivity.GatewayAddressCache;
import com.microsoft.azure.documentdb.internal.routing.ClientCollectionCache;
import com.microsoft.azure.documentdb.internal.routing.PartitionKeyRangeCache;

/**
 * Used internally to cache the collections' session tokens in the Azure Cosmos DB database service.
 */
public final class SessionContainer {
    private static final String EMPTY_SESSION_TOKEN = "";
    private static final char SESSION_TOKEN_SEPARATOR = ',';
    private static final char SESSION_TOKEN_PARTITION_SPLITTER = ':';
    private static final Logger logger = LoggerFactory.getLogger(SessionContainer.class);

    private ClientCollectionCache collectionCache;
    private PartitionKeyRangeCache partitionKeyRangeCache;

    /**
     * Session token cache that maps collection ResourceID to session tokens
     */
    private final ConcurrentHashMap> collectionResourceIdToSessionTokens;
    /**
     * Collection ResourceID cache that maps collection name to collection ResourceID
     * When collection name is provided instead of self-link, this is used in combination with
     * collectionResourceIdToSessionTokens to retrieve the session token for the collection by name
     */
    private final ConcurrentHashMap collectionNameToCollectionResourceId;
    private final String hostName;

    public SessionContainer(String hostName,
            ClientCollectionCache collectionCache,
            PartitionKeyRangeCache partitionKeyRangeCache) {
        this(hostName,
                collectionCache,
                partitionKeyRangeCache,
                new ConcurrentHashMap(),
                new ConcurrentHashMap>());
    }

    public SessionContainer(String hostName,
            ClientCollectionCache collectionCache,
            PartitionKeyRangeCache partitionKeyRangeCache,
            ConcurrentHashMap nameToRidMap,
            ConcurrentHashMap> ridToTokensMap) {
        this.hostName = hostName;
        this.collectionCache = collectionCache;
        this.partitionKeyRangeCache = partitionKeyRangeCache;
        this.collectionResourceIdToSessionTokens = ridToTokensMap;
        this.collectionNameToCollectionResourceId = nameToRidMap;
    }

    public String getHostName() {
        return this.hostName;
    }

    private ConcurrentHashMap getPartitionKeyRangeIdToTokenMap(DocumentServiceRequest request) {
        return getPartitionKeyRangeIdToTokenMap(request.getIsNameBased(), request.getResourceId(), request.getResourceAddress());
    }

    private ConcurrentHashMap getPartitionKeyRangeIdToTokenMap(boolean isNameBased, String rId, String resourceAddress) {
        ConcurrentHashMap rangeIdToTokenMap = null;
        if (!isNameBased) {
            if (!StringUtils.isEmpty(rId)) {
                ResourceId resourceId = ResourceId.parse(rId);
                if (resourceId.getDocumentCollection() != 0) {
                    rangeIdToTokenMap =
                            this.collectionResourceIdToSessionTokens.get(resourceId.getUniqueDocumentCollectionId());
                }
            }
        } else {
            String collectionName = Utils.getCollectionName(resourceAddress);
            if (!StringUtils.isEmpty(collectionName) && this.collectionNameToCollectionResourceId.containsKey(collectionName)) {
                rangeIdToTokenMap = this.collectionResourceIdToSessionTokens.get(
                        this.collectionNameToCollectionResourceId.get(collectionName));
            }
        }
        return rangeIdToTokenMap;
    }

    /**
     * Resolves a session token for request. This should be invoked for read only requests which require a session token in session consistency.
     * 
     * It attempts to resolve the target partition key range.
     * 1) If partition key range is resolved: it will find the local session token for the given partition or if not found uses parent partition session token.
     * 2) If partition key range is not resolved: it passes the global session token.
     * 
     * @param request
     * @return
     * @throws DocumentClientException 
     */
    public String resolveSessionToken(DocumentServiceRequest request) throws DocumentClientException {
        if (request == null) {
            throw new IllegalArgumentException("request cannot be null");
        }

        String userSessionToken = request.getHeaders().get(HttpConstants.HttpHeaders.SESSION_TOKEN);

        PartitionKeyRange partitionKeyRange = null;

        if (request.getResourceType().isPartitioned()) {
            partitionKeyRange = resolvePartitionKeyRange(request, false);
        }

        if (!StringUtils.isEmpty(userSessionToken)) {
            return parseLocalSessionToken(userSessionToken, partitionKeyRange);
        }
        
        return resolveSessionToken(request.getIsNameBased(), request.getResourceId(), request.getResourceAddress(), 
                partitionKeyRange);
    }
    
    /**
     * Parses combinedSessionToken and returns local session token scoped to the given partition or one of its parents.
     * 
     * @param combinedSessionToken
     * @param partitionKeyRange
     * @return
     */
    private static String parseLocalSessionToken(String combinedSessionToken, PartitionKeyRange partitionKeyRange) {
        if (combinedSessionToken == null) {
            return null;
        }
        
        if (partitionKeyRange == null) {
            return combinedSessionToken;
        }
        
        String[] localSessionTokens = StringUtils.split(combinedSessionToken, SESSION_TOKEN_SEPARATOR);
        Map partitionToLSN = new HashMap<>();
        for(String localSessionToken: localSessionTokens) {
            String[] parts = StringUtils.split(localSessionToken, SESSION_TOKEN_PARTITION_SPLITTER);
            if (parts.length == 2) {
                partitionToLSN.put(parts[0], parts[1]);
            }
        }
        
        return findLocalSessionToken(partitionToLSN, partitionKeyRange);
    }
    
    /**
     * given the partition to lsn map and partition key range, it finds the token associated with the partition if not found returns the parent partition session.
     * 
     * If no session associated to partition or its parents found, returns empty session token.
     * 
     * @param partitionToLSN
     * @param partitionKeyRange
     * @return
     */
    private static String findLocalSessionToken(Map partitionToLSN, PartitionKeyRange partitionKeyRange) {
        if (partitionKeyRange == null) {
            throw new IllegalArgumentException("partitionKeyRange is null");
        }
        
        // if local session token for the partition is found use it
        if (partitionToLSN.containsKey(partitionKeyRange.getId())) {
            return partitionKeyRange.getId() + SESSION_TOKEN_PARTITION_SPLITTER + getSessionTokenString(partitionToLSN.get(partitionKeyRange.getId()));
        }

        // if the local session token for any of parents is found use it.
        Collection parents = partitionKeyRange.getParents();
        List parentList = new ArrayList<>(parents);
        for (int i = parentList.size() -1; i >= 0; i--) {
            String parentId = parentList.get(i);
            if (partitionToLSN.containsKey(parentId)) {
                return parentId + SESSION_TOKEN_PARTITION_SPLITTER + getSessionTokenString(partitionToLSN.get(parentId));
            }
        }
        
        return EMPTY_SESSION_TOKEN;
    }
    
    private static String getSessionTokenString(Object sessionToken) {
        if (sessionToken instanceof VectorSessionToken) {
            return ((VectorSessionToken) sessionToken).convertToString();
        } else {
            return (String) sessionToken;
        }
    }
    
    /**
     * Resolves the partition key range
     *
     * @param request
     * @param refreshCache
     * @return
     * @throws DocumentClientException
     */
    private PartitionKeyRange resolvePartitionKeyRange(AbstractDocumentServiceRequest request, boolean refreshCache) throws DocumentClientException {
        if (refreshCache) {
            request.setForceAddressRefresh(true);
            request.setForceNameCacheRefresh(true);
        }
        
        PartitionKeyRange partitionKeyRange = null;
        DocumentCollection collection = this.collectionCache.resolveCollection(request);
        
        String partitionKeyRangeId = null;

        if (request.getHeaders().get(HttpConstants.HttpHeaders.PARTITION_KEY) != null) {
            partitionKeyRange = GatewayAddressCache.tryResolveServerPartitionByPartitionKey(this.partitionKeyRangeCache,
                    request.getHeaders().get(HttpConstants.HttpHeaders.PARTITION_KEY),
                    collection,
                    request.isForcePartitionKeyRangeRefresh());

            if(partitionKeyRange != null)
            {
                partitionKeyRangeId = partitionKeyRange.getId();
            }
        } else if (request.getPartitionKeyRangeIdentity() != null) {
            partitionKeyRangeId = request.getPartitionKeyRangeIdentity().getPartitionKeyRangeId();
            partitionKeyRange = partitionKeyRangeCache.getPartitionKeyRangeById(
            collection.getSelfLink(), partitionKeyRangeId, request.isForcePartitionKeyRangeRefresh());
        }

        if (partitionKeyRangeId == null) {
            return null;
        }

        logger.debug("request.isForcePartitionKeyRangeRefresh={}, partitionKeyRange={}", request.isForcePartitionKeyRangeRefresh(), partitionKeyRange);
        
        if (partitionKeyRange == null) {
            if (refreshCache) {
                // we already refreshed cache but still couldn't resolve partition key range
                Map responseHeaders = new HashMap();
                responseHeaders.put(HttpConstants.HttpHeaders.SUB_STATUS, String.valueOf(HttpConstants.SubStatusCodes.PARTITION_KEY_RANGE_GONE));
                logger.error("Invalid Partition Key Range");
                throw new DocumentClientException(HttpConstants.StatusCodes.GONE,
                        new Error("{ 'message': 'Invalid partition key range' }"), responseHeaders);
            }
            
            // need to refresh cache, maybe split happened
            return resolvePartitionKeyRange(request, true);
        } else {
            request.setResolvedPartitionKeyRange(partitionKeyRange);
            return partitionKeyRange;
        }
    }

    private String resolveSessionToken(boolean isNameBased, String rId, String resourceAddress, PartitionKeyRange partitionKeyRange) {
        ConcurrentHashMap rangeIdToTokenMap = this.getPartitionKeyRangeIdToTokenMap(isNameBased, rId, resourceAddress);
        if (rangeIdToTokenMap == null) {
            // we don't know about this collection.
            return EMPTY_SESSION_TOKEN;
        }

        if (partitionKeyRange == null) {
            // there is no information for target partition key range id
            // using global session token
            return getCombinedSessionToken(rangeIdToTokenMap);
        }

        // getting the partition session token
        VectorSessionToken token = rangeIdToTokenMap.get(partitionKeyRange.getId());
        if (token != null) {
            return partitionKeyRange.getId() + SESSION_TOKEN_PARTITION_SPLITTER + token.convertToString();
        }

        // the partition session token is not cached, using the parents if any
        Collection parentPartitions = partitionKeyRange.getParents();

        if (parentPartitions == null || parentPartitions.isEmpty()) {
            // if no parents then return empty session token
            return EMPTY_SESSION_TOKEN;
        }

        return findLocalSessionToken(rangeIdToTokenMap, partitionKeyRange);
    }

    public String resolveGlobalSessionToken(String collectionLink) {
        if (StringUtils.isEmpty(collectionLink)) {
            throw new IllegalArgumentException("collectionLink cannot be null");
        }

        PathInfo pathInfo = PathsHelper.parsePathSegments(collectionLink);

        if (pathInfo == null) {
            return EMPTY_SESSION_TOKEN;
        }

        return this.resolveSessionToken(pathInfo.isNameBased, pathInfo.resourceIdOrFullName, pathInfo.resourcePath, null);
    }

    public void clearToken(final DocumentServiceRequest request) {
        Long collectionResourceId = null;
        if (!request.getIsNameBased()) {
            if (!StringUtils.isEmpty(request.getResourceId())) {
                ResourceId resourceId = ResourceId.parse(request.getResourceId());
                if (resourceId.getDocumentCollection() != 0) {
                    collectionResourceId = resourceId.getUniqueDocumentCollectionId();
                }
            }
        } else {
            String collectionName = Utils.getCollectionName(request.getResourceAddress());
            if (!StringUtils.isEmpty(collectionName)) {
                collectionResourceId = this.collectionNameToCollectionResourceId.get(collectionName);
                this.collectionNameToCollectionResourceId.remove(collectionName);
            }
        }
        if (collectionResourceId != null) {
            this.collectionResourceIdToSessionTokens.remove(collectionResourceId);
        }
    }

    /**
     * Updates the session cache with the new session token from response.
     * 
     * Also if partition key range cache is stale, it refreshes the partition key range cache 
     * 
     * @param request
     * @param responseHeaders
     * @throws DocumentClientException 
     */
    public void setSessionToken(AbstractDocumentServiceRequest request, Map responseHeaders) throws DocumentClientException {
        if (responseHeaders != null && !request.isReadingFromMaster()) {
            String sessionToken = responseHeaders.get(HttpConstants.HttpHeaders.SESSION_TOKEN);
            if (!StringUtils.isEmpty(sessionToken)) {
                if (request.getResourceType().isPartitioned()) {
                    String requestSessionToken = request.getHeaders().get(HttpConstants.HttpHeaders.SESSION_TOKEN);
                    String[] requestTokenParts = requestSessionToken == null ? null : StringUtils.split(requestSessionToken, SESSION_TOKEN_PARTITION_SPLITTER);
                    String[] responseTokenParts = StringUtils.split(sessionToken, SESSION_TOKEN_PARTITION_SPLITTER);
                    // check if split happened and partition key range require refresh
                    if (responseTokenParts.length == 2) {
                        if (StringUtils.isEmpty(requestSessionToken) 
                                || !StringUtils.equals(requestTokenParts[0], responseTokenParts[0])) {

                            DocumentCollection collection = this.collectionCache.resolveCollection(request);
                            PartitionKeyRange partitionKeyRangeCache =
                                    this.partitionKeyRangeCache.getPartitionKeyRangeById(collection.getSelfLink(), responseTokenParts[0], false);

                            if (partitionKeyRangeCache == null) {
                                // partition key range cache is stale refresh the cache.
                                this.partitionKeyRangeCache.getPartitionKeyRangeById(collection.getSelfLink(), responseTokenParts[0], true);
                            }
                        }
                    }
                }
            }

            String ownerFullName = responseHeaders.get(HttpConstants.HttpHeaders.OWNER_FULL_NAME);
            if (StringUtils.isEmpty(ownerFullName)) ownerFullName = request.getResourceAddress();

            String collectionName = Utils.getCollectionName(ownerFullName);

            String ownerId;
            if (!request.getIsNameBased()) {
                ownerId = request.getResourceId();

            } else {
                ownerId = responseHeaders.get(HttpConstants.HttpHeaders.OWNER_ID);
                if (StringUtils.isEmpty(ownerId)) ownerId = request.getResourceId();
            }

            if (!StringUtils.isEmpty(ownerId)) {
                ResourceId resourceId = ResourceId.parse(ownerId);

                if (resourceId.getDocumentCollection() != 0 && !StringUtils.isEmpty(collectionName)) {
                    Long uniqueDocumentCollectionId = resourceId.getUniqueDocumentCollectionId();
                    this.setSessionToken(uniqueDocumentCollectionId, collectionName, sessionToken);
                }
            }
        }
    }

    private void setSessionToken(long collectionRid, String collectionName, String sessionToken) throws DocumentClientException {
        logger.trace("Set session token: collectionRid = {}, collectionName = {}, sessionToken = {}",
                collectionRid, collectionName, sessionToken);
        this.collectionResourceIdToSessionTokens.putIfAbsent(collectionRid, 
                new ConcurrentHashMap());
        this.compareAndSetToken(sessionToken, this.collectionResourceIdToSessionTokens.get(collectionRid));
        this.collectionNameToCollectionResourceId.putIfAbsent(collectionName, collectionRid);
    }

    private String getCombinedSessionToken(ConcurrentHashMap tokens) {
        StringBuilder result = new StringBuilder();
        if (tokens != null) {
            for (Iterator> iterator = tokens.entrySet().iterator(); iterator.hasNext(); ) {
                Entry entry = iterator.next();
                result = result.append(entry.getKey()).append(SESSION_TOKEN_PARTITION_SPLITTER).append(entry.getValue().convertToString());
                if (iterator.hasNext()) {
                    result = result.append(SESSION_TOKEN_SEPARATOR);
                }
            }
        }

        return result.toString();
    }

    private void compareAndSetToken(String newToken, ConcurrentHashMap oldTokens) throws DocumentClientException {
        if (StringUtils.isNotEmpty(newToken)) {
            String[] newTokenParts = StringUtils.split(newToken, SESSION_TOKEN_PARTITION_SPLITTER);
            if (newTokenParts.length == 2) {
                String range = newTokenParts[0];
                VectorSessionToken newSessionToken = SessionTokenHelper.parse(newTokenParts[1]);
                boolean success;
                do {
                    VectorSessionToken oldSessionToken = oldTokens.putIfAbsent(range, newSessionToken);
                    // If there exists no previous session token, we're done.
                    success = (oldSessionToken == null);
                    if (!success) {
                        // Replace previous session token with merge of previous and current session tokens.
                        success = oldTokens.replace(range, oldSessionToken, oldSessionToken.merge(newSessionToken));
                    }
                } while (!success);
            }
        }
    }

    VectorSessionToken resolvePartitionLocalSessionToken(DocumentServiceRequest request, String partitionKeyRangeId) {
        return SessionTokenHelper.resolvePartitionLocalSessionToken(request, partitionKeyRangeId,
                this.getPartitionKeyRangeIdToTokenMap(request));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy