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

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

package com.microsoft.azure.documentdb.internal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

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

import com.microsoft.azure.documentdb.DocumentClientException;

/**
 * Used internally to provides helper functions to work with session tokens in the Azure Cosmos DB database service.
 */
public class SessionTokenHelper {
    private final static Logger logger = LoggerFactory.getLogger(SessionTokenHelper.class);

    public static void setPartitionLocalSessionToken(DocumentServiceRequest request, SessionContainer sessionContainer) throws DocumentClientException {
        String originalSessionToken = request.getHeaders().get(HttpConstants.HttpHeaders.SESSION_TOKEN);
        String partitionKeyRangeId = request.getResolvedPartitionKeyRange().getId();

        // Add support for partitioned collections
        if (StringUtils.isNotEmpty(originalSessionToken)) {
            VectorSessionToken sessionToken = getLocalSessionToken(request, originalSessionToken, partitionKeyRangeId);
            if (sessionToken != null) {
                request.setSessionToken(sessionToken);
            }
        } else {
            VectorSessionToken sessionToken = sessionContainer.resolvePartitionLocalSessionToken(request, partitionKeyRangeId);
            if (sessionToken != null) {
                request.setSessionToken(sessionToken);
            }
        }

        if (request.getSessionToken() == null) {
            request.getHeaders().remove(HttpConstants.HttpHeaders.SESSION_TOKEN);
            logger.trace("Removed partition local session token, partitionKeyRangeId: {}", partitionKeyRangeId);
        } else {
            String localSessionToken = String.format(
                    "%s:%s",
                    partitionKeyRangeId != null? partitionKeyRangeId: "0",
                            request.getSessionToken().convertToString());
            request.getHeaders().put(HttpConstants.HttpHeaders.SESSION_TOKEN, localSessionToken);
            logger.trace("Set partition local session token partitionKeyRangeId: {}, sessionToken: {}",
                    partitionKeyRangeId, localSessionToken);
        }
    }

    private static VectorSessionToken getLocalSessionToken(
            DocumentServiceRequest request,
            String sessionToken,
            String partitionKeyRangeId) throws DocumentClientException {

        if (partitionKeyRangeId == null || partitionKeyRangeId.isEmpty()) {
            // AddressCache/address resolution didn't produce partition key range id.
            // In this case it is a bug.
            throw new IllegalStateException("Partition key range Id is absent in the context.");
        }

        String[] partitionKeyRangesToken = StringUtils.split(sessionToken, ',');
        Set partitionKeyRangeSet = new HashSet<>();
        partitionKeyRangeSet.add(partitionKeyRangeId);
        VectorSessionToken highestSessionToken = null;

        if (request.getResolvedPartitionKeyRange() != null && request.getResolvedPartitionKeyRange().getParents() != null) {
            partitionKeyRangeSet.addAll(request.getResolvedPartitionKeyRange().getParents());
        }

        for (String partitionKeyRangeToken : partitionKeyRangesToken) {
            String[] items = StringUtils.split(partitionKeyRangeToken, ':');

            if (items.length != 2) {
                throw new DocumentClientException(HttpStatus.SC_BAD_REQUEST, "Invalid session token value.");
            }
            
            if (partitionKeyRangeSet.contains(items[0])) {
                VectorSessionToken parsedSessionToken = VectorSessionToken.create(items[1]);
                if (parsedSessionToken == null) {
                    throw new DocumentClientException(HttpStatus.SC_BAD_REQUEST, "Invalid session token value.");
                }
                if (highestSessionToken == null) {
                    highestSessionToken = parsedSessionToken;
                } else {
                    highestSessionToken = highestSessionToken.merge(parsedSessionToken);
                }
            }
        }

        return highestSessionToken;
    }

    static VectorSessionToken resolvePartitionLocalSessionToken(DocumentServiceRequest request,
                                                  String partitionKeyRangeId,
                                                  ConcurrentHashMap rangeIdToTokenMap) {
        if (rangeIdToTokenMap != null) {
            if (rangeIdToTokenMap.containsKey(partitionKeyRangeId)) {
                return rangeIdToTokenMap.get(partitionKeyRangeId);
            }
            else {
                Collection parents = request.getResolvedPartitionKeyRange().getParents();
                if (parents != null) {
                    List parentsList = new ArrayList<>(parents);
                    for (int i = parentsList.size() - 1; i >= 0; i--) {
                        String parentId = parentsList.get(i);
                        if (rangeIdToTokenMap.containsKey(parentId)) {
                            return rangeIdToTokenMap.get(parentId);
                        }
                    }
                }
            }
        }

        return null;
    }


    /**
     * Update session token by request and response headers. This also includes removing the session token if
     * it is collection deletion operation.
     *
     * @param sessionContainer the session container
     * @param request          the request object
     * @param responseHeaders  a map of response headers
     * @throws DocumentClientException 
     */
    public static void captureSessionToken(SessionContainer sessionContainer,
                                           DocumentServiceRequest request,
                                           Map responseHeaders) throws DocumentClientException {
        if (request.getResourceType() != ResourceType.DocumentCollection ||
                request.getOperationType() != OperationType.Delete) {
            sessionContainer.setSessionToken(request, responseHeaders);
        } else {
            sessionContainer.clearToken(request);
        }
    }

    /**
     * Handle the session token in case of DocumentClientException when message is processed from a StoreModel.
     * This method removes the session token when the collection name may be re-used, updates the collection session
     * token in Pre-condition or Conflict failures, as well as NotFound exceptions that are not ReadSessionNotAvailable.
     * There is no changes to the session cache in other cases.
     *
     * @param sessionContainer the session container
     * @param request          the request object
     * @param dce              the DocumentClientException instance
     * @throws DocumentClientException 
     */
    public static void updateSession(SessionContainer sessionContainer,
                                     DocumentServiceRequest request,
                                     DocumentClientException dce) throws DocumentClientException {
        if (request.getIsNameBased() &&
                HttpConstants.StatusCodes.NOTFOUND == dce.getStatusCode() &&
                dce.getSubStatusCode() != null &&
                HttpConstants.SubStatusCodes.READ_SESSION_NOT_AVAILABLE == dce.getSubStatusCode() &&
                request.shouldClearSessionTokenOnSessionReadFailure()) {
            // There are a few scenarios leading to this exception, such as:
            // 1. when the collection was deleted and recreated with the same name
            // 2. when a global account with read fail-over in progress and the requests reached the old endpoint
            // 3. when a replica is in copying or catchup state

            // When we get here, this is a name-based request, which gets NotFound - ReadSessionNotAvailable exception
            // from the backend even after a retry. In #2 the request would be redirected to the write endpoint and
            // succeed. We should clear the session token, because this might be #1, the collection name might be reused.
            // #3 is handled in the else clause corresponding to this.
            logger.debug("Clear the token for request {}", request.getResourceAddress());
            sessionContainer.clearToken(request);
        } else {
            if (!request.getResourceType().isMasterResource() &&
                    (dce.getStatusCode() == HttpConstants.StatusCodes.PRECONDITION_FAILED ||
                            dce.getStatusCode() == HttpConstants.StatusCodes.CONFLICT ||
                            (dce.getStatusCode() == HttpConstants.StatusCodes.NOTFOUND &&
                                    dce.getSubStatusCode() != null &&
                                    dce.getSubStatusCode() != HttpConstants.SubStatusCodes.READ_SESSION_NOT_AVAILABLE))) {
                SessionTokenHelper.captureSessionToken(sessionContainer, request, dce.getResponseHeaders());
            }
        }
    }
    
    public static VectorSessionToken parse(String sessionToken) throws DocumentClientException {
        String[] sessionTokenSegments = StringUtils.split(sessionToken, ':');
        String sessionTokenString = sessionTokenSegments[sessionTokenSegments.length - 1];
        VectorSessionToken vectorSessionToken = VectorSessionToken.create(sessionTokenString);
        if (vectorSessionToken == null) {
            throw new DocumentClientException(HttpConstants.StatusCodes.INTERNAL_SERVER_ERROR, String.format("Could not parse the received session token: %s", sessionTokenString));
        }
        return vectorSessionToken;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy