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

com.vmware.xenon.common.OperationJoin Maven / Gradle / Ivy

There is a newer version: 1.6.18
Show newest version
/*
 * Copyright (c) 2014-2015 VMware, 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.vmware.xenon.common;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

/**
 * The {@link OperationJoin} construct is a handler for {@link OperationJoin#create(Operation...)}
 * functionality. After multiple parallel requests have
 * completed, only then will invoked all {@link com.vmware.xenon.common.Operation.CompletionHandler}s providing all operations and
 * failures as part of the execution context.
 */
public class OperationJoin {
    private static final int APPROXIMATE_EXPECTED_CAPACITY = 4;
    public static final String ERROR_MSG_BATCH_LIMIT_VIOLATED = "batch limit violated";
    public static final String ERROR_MSG_INVALID_BATCH_SIZE = "batch size must be greater than 0";
    public static final String ERROR_MSG_OPERATIONS_ALREADY_SET = "operations have already been set";
    private final ConcurrentHashMap operations;
    private ConcurrentHashMap failures;
    JoinedCompletionHandler joinedCompletion;
    private OperationContext opContext;
    private final AtomicInteger pendingCount = new AtomicInteger();
    private final AtomicInteger batchSizeGuard = new AtomicInteger();
    private int batchSize = 0;
    private Iterator operationIterator;
    private ServiceRequestSender sender;
    private final Object failuresLock = new Object();
    private String transactionId;

    private OperationJoin() {
        this.operations = new ConcurrentHashMap<>(APPROXIMATE_EXPECTED_CAPACITY);
        this.opContext = OperationContext.getOperationContext();
    }

    /**
     * Create {@link OperationJoin} with no operations in preparation for {@link Operation}s to
     * be added at a later time.
     */
    public static OperationJoin create() {
        return new OperationJoin();
    }

    /**
     * Create {@link OperationJoin} with an array of {@link Operation}s to be joined together in
     * parallel execution.
     */
    public static OperationJoin create(Operation... ops) {
        OperationJoin joinOp = new OperationJoin();
        joinOp.setOperations(ops);
        return joinOp;
    }

    /**
     * Create {@link OperationJoin} with a collection of {@link Operation}s to be joined together in
     * parallel execution.
     */
    public static OperationJoin create(Collection ops) {
        OperationJoin joinOp = new OperationJoin();
        joinOp.setOperations(ops);
        return joinOp;
    }

    /**
     * Create {@link OperationJoin} with a stream of {@link Operation}s to be joined together in
     * parallel execution.
     */
    public static OperationJoin create(Stream ops) {
        OperationJoin joinOp = new OperationJoin();
        joinOp.setOperations(ops);
        return joinOp;
    }

    /**
     * Set the {@link Operation}s for the current {@link OperationJoin}. This is a one-time
     * operation.
     */
    public OperationJoin setOperations(Operation... ops) {
        if (ops.length == 0) {
            throw new IllegalArgumentException("At least one operation to join expected");
        }

        if (this.operationIterator != null) {
            throw new IllegalStateException(ERROR_MSG_OPERATIONS_ALREADY_SET);
        }

        for (Operation op : ops) {
            prepareOperation(op);
        }

        this.operationIterator = this.operations.values().iterator();
        return this;
    }

    /**
     * Set the {@link Operation}s for the current {@link OperationJoin}. This is a one-time
     * operation.
     */
    public OperationJoin setOperations(Collection ops) {
        if (ops.isEmpty()) {
            throw new IllegalArgumentException("At least one operation to join expected");
        }

        if (this.operationIterator != null) {
            throw new IllegalStateException(ERROR_MSG_OPERATIONS_ALREADY_SET);
        }

        for (Operation op : ops) {
            prepareOperation(op);
        }

        this.operationIterator = this.operations.values().iterator();
        return this;
    }



    /**
     * Set the {@link Operation}s for the current {@link OperationJoin}. This is a one-time
     * operation.
     */
    public OperationJoin setOperations(Stream ops) {
        if (this.operationIterator != null) {
            throw new IllegalStateException(ERROR_MSG_OPERATIONS_ALREADY_SET);
        }

        ops.forEach(this::prepareOperation);
        this.operationIterator = this.operations.values().iterator();

        if (isEmpty()) {
            throw new IllegalArgumentException("At least one operation to join expected");
        }

        return this;
    }

    private void prepareOperation(Operation op) {
        this.operations.put(op.getId(), op);

        if (this.transactionId != null) {
            op.setTransactionId(this.transactionId);
        }

        op.nestCompletion(this::parentCompletion);
        this.pendingCount.incrementAndGet();
    }

    private void parentCompletion(Operation o, Throwable e) {
        if (e != null) {
            synchronized (this.failuresLock) {
                if (this.failures == null) {
                    this.failures = new ConcurrentHashMap<>();
                }
            }
            this.failures.put(o.getId(), e);
        }

        Operation originalOp = getOperation(o.getId());
        originalOp.setStatusCode(o.getStatusCode())
                .transferResponseHeadersFrom(o)
                .setBodyNoCloning(o.getBodyRaw());

        this.batchSizeGuard.decrementAndGet();
        sendNext();

        if (this.pendingCount.decrementAndGet() != 0) {
            return;
        }

        OperationContext.restoreOperationContext(this.opContext);
        // call each operation completion individually
        for (Operation op : this.operations.values()) {
            Throwable t = null;
            if (this.failures != null) {
                t = this.failures.get(op.getId());
            }
            if (t != null) {
                op.fail(t);
            } else {
                op.complete();
            }
        }

        if (this.joinedCompletion != null) {
            this.joinedCompletion.handle(this.operations, this.failures);
        }
    }

    private void sendWithBatch() {
        if (this.operationIterator == null || !this.operationIterator.hasNext()) {
            throw new IllegalStateException("No operations to be sent");
        }

        // Move the operations to local list to avoid concurrency issues with iterator
        // when sendNext could be called from handler of returning operation
        // before we get out of this method.
        ArrayList batch = new ArrayList<>();
        int count = 0;
        while (this.operationIterator.hasNext()) {
            batch.add(this.operationIterator.next());
            count++;
            if (this.batchSize > 0 && count == this.batchSize) {
                break;
            }
        }

        for (Operation op : batch) {
            sendOperation(op);
            if (this.batchSize > 0 && this.batchSizeGuard.incrementAndGet() > this.batchSize) {
                throw new IllegalStateException((ERROR_MSG_BATCH_LIMIT_VIOLATED));
            }
        }
    }

    private void sendOperation(Operation op) {
        this.sender.sendRequest(op);
    }

    private void sendNext() {
        if (this.sender == null) {
            return;
        }

        Operation op = null;
        synchronized (this.operationIterator) {
            if (this.operationIterator.hasNext()) {
                op = this.operationIterator.next();
            }
        }

        if (op != null) {
            if (this.batchSize > 0 && this.batchSizeGuard.incrementAndGet() > this.batchSize) {
                throw new IllegalStateException((ERROR_MSG_BATCH_LIMIT_VIOLATED));
            }
            sendOperation(op);
        }
    }

    /**
     * Send the join operations using the {@link ServiceRequestSender}. A sender can be
     * a ServiceHost, ServiceClient or a Service.
     * Caller can also provide batch size to control the rate at which operations are sent.
     */
    public void sendWith(ServiceRequestSender sender, int batchSize) {
        if (batchSize <= 0) {
            throw new IllegalArgumentException(ERROR_MSG_INVALID_BATCH_SIZE);
        }

        this.batchSize = batchSize;
        this.sendWith(sender);
    }

    /**
     * Send the join operations using the {@link ServiceHost}.
     */
    public void sendWith(ServiceRequestSender sender) {
        if (sender == null) {
            throw new IllegalArgumentException("host must not be null.");
        }

        this.sender = sender;
        sendWithBatch();
    }

    public OperationJoin setCompletion(JoinedCompletionHandler joinedCompletion) {
        this.joinedCompletion = joinedCompletion;
        return this;
    }

    OperationContext getOperationContext() {
        return this.opContext;
    }

    /**
     * Sets (overwrites) the operation context of this operation join instance
     *
     * The visibility of this method is intentionally package-local. It is intended to
     * only be called by functions in this package, so that we can apply whitelisting
     * to limit the set of services that is able to set it.
     *
     * @param opContext the operation context to set.
     */
    void setOperationContext(OperationContext opContext) {
        this.opContext = opContext;
    }

    public boolean isEmpty() {
        return this.operations.isEmpty();
    }

    public Collection getOperations() {
        return this.operations.values();
    }

    public Map getFailures() {
        return this.failures;
    }

    public Operation getOperation(long id) {
        return this.operations.get(id);
    }

    public String getTransactionId() {
        return this.transactionId;
    }

    public OperationJoin setTransactionId(String transactionId) {
        this.transactionId = transactionId;
        return this;
    }

    @FunctionalInterface
    public interface JoinedCompletionHandler {
        void handle(Map ops, Map failures);
    }


    /**
     * WARNING: This method is unsafe. If called when some operation o in this.operations has been
     * sent but neither completed nor failed, o will be failed immediately, which may cause immediate
     * success/fail of all operations in this.operations (if it was the last pending operation due to the latch in
     * parentCompletion), and separately the actual outstanding async work will when it calls complete or fail, cause
     * the original callback to be called, potentially succeeding even though this method was intended to fail
     * everything.
     *
     * @param t The exception to fail operations with.
     */
    void fail(Throwable t) {
        this.failures = new ConcurrentHashMap<>();
        this.failures.put(this.operations.keys().nextElement(), t);
        OperationContext origContext = OperationContext.getOperationContext();
        OperationContext.restoreOperationContext(this.opContext);
        for (Operation op : this.operations.values()) {
            op.fail(t);
        }
        OperationContext.restoreOperationContext(origContext);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy