| 
 | 1 | +/*  | 
 | 2 | + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one  | 
 | 3 | + * or more contributor license agreements. Licensed under the Elastic License  | 
 | 4 | + * 2.0; you may not use this file except in compliance with the Elastic License  | 
 | 5 | + * 2.0.  | 
 | 6 | + */  | 
 | 7 | + | 
 | 8 | +package org.elasticsearch.xpack.core.action;  | 
 | 9 | + | 
 | 10 | +import org.apache.logging.log4j.LogManager;  | 
 | 11 | +import org.apache.logging.log4j.Logger;  | 
 | 12 | +import org.elasticsearch.ElasticsearchStatusException;  | 
 | 13 | +import org.elasticsearch.ElasticsearchTimeoutException;  | 
 | 14 | +import org.elasticsearch.action.ActionListener;  | 
 | 15 | +import org.elasticsearch.action.support.ActionFilters;  | 
 | 16 | +import org.elasticsearch.action.support.master.AcknowledgedResponse;  | 
 | 17 | +import org.elasticsearch.action.support.master.AcknowledgedTransportMasterNodeAction;  | 
 | 18 | +import org.elasticsearch.cluster.ClusterState;  | 
 | 19 | +import org.elasticsearch.cluster.ClusterStateTaskListener;  | 
 | 20 | +import org.elasticsearch.cluster.SimpleBatchedExecutor;  | 
 | 21 | +import org.elasticsearch.cluster.block.ClusterBlockException;  | 
 | 22 | +import org.elasticsearch.cluster.block.ClusterBlockLevel;  | 
 | 23 | +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;  | 
 | 24 | +import org.elasticsearch.cluster.service.ClusterService;  | 
 | 25 | +import org.elasticsearch.cluster.service.MasterServiceTaskQueue;  | 
 | 26 | +import org.elasticsearch.common.Priority;  | 
 | 27 | +import org.elasticsearch.common.util.concurrent.EsExecutors;  | 
 | 28 | +import org.elasticsearch.core.Strings;  | 
 | 29 | +import org.elasticsearch.core.Tuple;  | 
 | 30 | +import org.elasticsearch.rest.RestStatus;  | 
 | 31 | +import org.elasticsearch.tasks.Task;  | 
 | 32 | +import org.elasticsearch.threadpool.ThreadPool;  | 
 | 33 | +import org.elasticsearch.transport.TransportService;  | 
 | 34 | + | 
 | 35 | +import java.util.concurrent.atomic.AtomicBoolean;  | 
 | 36 | + | 
 | 37 | +public abstract class AbstractTransportSetUpgradeModeAction extends AcknowledgedTransportMasterNodeAction<SetUpgradeModeActionRequest> {  | 
 | 38 | + | 
 | 39 | +    private static final Logger logger = LogManager.getLogger(AbstractTransportSetUpgradeModeAction.class);  | 
 | 40 | +    private final AtomicBoolean isRunning = new AtomicBoolean(false);  | 
 | 41 | +    private final MasterServiceTaskQueue<UpdateModeStateListener> taskQueue;  | 
 | 42 | + | 
 | 43 | +    public AbstractTransportSetUpgradeModeAction(  | 
 | 44 | +        String actionName,  | 
 | 45 | +        String taskQueuePrefix,  | 
 | 46 | +        TransportService transportService,  | 
 | 47 | +        ClusterService clusterService,  | 
 | 48 | +        ThreadPool threadPool,  | 
 | 49 | +        ActionFilters actionFilters,  | 
 | 50 | +        IndexNameExpressionResolver indexNameExpressionResolver  | 
 | 51 | +    ) {  | 
 | 52 | +        super(  | 
 | 53 | +            actionName,  | 
 | 54 | +            transportService,  | 
 | 55 | +            clusterService,  | 
 | 56 | +            threadPool,  | 
 | 57 | +            actionFilters,  | 
 | 58 | +            SetUpgradeModeActionRequest::new,  | 
 | 59 | +            indexNameExpressionResolver,  | 
 | 60 | +            EsExecutors.DIRECT_EXECUTOR_SERVICE  | 
 | 61 | +        );  | 
 | 62 | + | 
 | 63 | +        this.taskQueue = clusterService.createTaskQueue(taskQueuePrefix + " upgrade mode", Priority.NORMAL, new UpdateModeExecutor());  | 
 | 64 | +    }  | 
 | 65 | + | 
 | 66 | +    @Override  | 
 | 67 | +    protected void masterOperation(  | 
 | 68 | +        Task task,  | 
 | 69 | +        SetUpgradeModeActionRequest request,  | 
 | 70 | +        ClusterState state,  | 
 | 71 | +        ActionListener<AcknowledgedResponse> listener  | 
 | 72 | +    ) throws Exception {  | 
 | 73 | +        // Don't want folks spamming this endpoint while it is in progress, only allow one request to be handled at a time  | 
 | 74 | +        if (isRunning.compareAndSet(false, true) == false) {  | 
 | 75 | +            String msg = Strings.format(  | 
 | 76 | +                "Attempted to set [upgrade_mode] for feature name [%s] to [%s] from [%s] while previous request was processing.",  | 
 | 77 | +                featureName(),  | 
 | 78 | +                request.enabled(),  | 
 | 79 | +                upgradeMode(state)  | 
 | 80 | +            );  | 
 | 81 | +            logger.info(msg);  | 
 | 82 | +            Exception detail = new IllegalStateException(msg);  | 
 | 83 | +            listener.onFailure(  | 
 | 84 | +                new ElasticsearchStatusException(  | 
 | 85 | +                    "Cannot change [upgrade_mode] for feature name [{}]. Previous request is still being processed.",  | 
 | 86 | +                    RestStatus.TOO_MANY_REQUESTS,  | 
 | 87 | +                    detail,  | 
 | 88 | +                    featureName()  | 
 | 89 | +                )  | 
 | 90 | +            );  | 
 | 91 | +            return;  | 
 | 92 | +        }  | 
 | 93 | + | 
 | 94 | +        // Noop, nothing for us to do, simply return fast to the caller  | 
 | 95 | +        var upgradeMode = upgradeMode(state);  | 
 | 96 | +        if (request.enabled() == upgradeMode) {  | 
 | 97 | +            logger.info("Upgrade mode noop");  | 
 | 98 | +            isRunning.set(false);  | 
 | 99 | +            listener.onResponse(AcknowledgedResponse.TRUE);  | 
 | 100 | +            return;  | 
 | 101 | +        }  | 
 | 102 | + | 
 | 103 | +        logger.info(  | 
 | 104 | +            "Starting to set [upgrade_mode] for feature name [{}] to [{}] from [{}]",  | 
 | 105 | +            featureName(),  | 
 | 106 | +            request.enabled(),  | 
 | 107 | +            upgradeMode  | 
 | 108 | +        );  | 
 | 109 | + | 
 | 110 | +        ActionListener<AcknowledgedResponse> wrappedListener = ActionListener.wrap(r -> {  | 
 | 111 | +            logger.info("Finished setting [upgrade_mode] for feature name [{}]", featureName());  | 
 | 112 | +            isRunning.set(false);  | 
 | 113 | +            listener.onResponse(r);  | 
 | 114 | +        }, e -> {  | 
 | 115 | +            logger.info("Failed to set [upgrade_mode] for feature name [{}]", featureName());  | 
 | 116 | +            isRunning.set(false);  | 
 | 117 | +            listener.onFailure(e);  | 
 | 118 | +        });  | 
 | 119 | + | 
 | 120 | +        ActionListener<AcknowledgedResponse> setUpgradeModeListener = wrappedListener.delegateFailure((delegate, ack) -> {  | 
 | 121 | +            if (ack.isAcknowledged()) {  | 
 | 122 | +                upgradeModeSuccessfullyChanged(task, request, state, delegate);  | 
 | 123 | +            } else {  | 
 | 124 | +                logger.info("Cluster state update is NOT acknowledged");  | 
 | 125 | +                wrappedListener.onFailure(new ElasticsearchTimeoutException("Unknown error occurred while updating cluster state"));  | 
 | 126 | +            }  | 
 | 127 | +        });  | 
 | 128 | + | 
 | 129 | +        taskQueue.submitTask(featureName(), new UpdateModeStateListener(request, setUpgradeModeListener), request.ackTimeout());  | 
 | 130 | +    }  | 
 | 131 | + | 
 | 132 | +    /**  | 
 | 133 | +     * Define the feature name, used in log messages and naming the task on the task queue.  | 
 | 134 | +     */  | 
 | 135 | +    protected abstract String featureName();  | 
 | 136 | + | 
 | 137 | +    /**  | 
 | 138 | +     * Parse the ClusterState for the implementation's {@link org.elasticsearch.cluster.metadata.Metadata.Custom} and find the upgradeMode  | 
 | 139 | +     * boolean stored there.  We will compare this boolean with the request's desired state to determine if we should change the metadata.  | 
 | 140 | +     */  | 
 | 141 | +    protected abstract boolean upgradeMode(ClusterState state);  | 
 | 142 | + | 
 | 143 | +    /**  | 
 | 144 | +     * This is called from the ClusterState updater and is expected to return quickly.  | 
 | 145 | +     */  | 
 | 146 | +    protected abstract ClusterState createUpdatedState(SetUpgradeModeActionRequest request, ClusterState state);  | 
 | 147 | + | 
 | 148 | +    /**  | 
 | 149 | +     * This method is only called when the cluster state was successfully changed.  | 
 | 150 | +     * If we failed to update for any reason, this will not be called.  | 
 | 151 | +     * The ClusterState param is the previous ClusterState before we called update.  | 
 | 152 | +     */  | 
 | 153 | +    protected abstract void upgradeModeSuccessfullyChanged(  | 
 | 154 | +        Task task,  | 
 | 155 | +        SetUpgradeModeActionRequest request,  | 
 | 156 | +        ClusterState state,  | 
 | 157 | +        ActionListener<AcknowledgedResponse> listener  | 
 | 158 | +    );  | 
 | 159 | + | 
 | 160 | +    @Override  | 
 | 161 | +    protected ClusterBlockException checkBlock(SetUpgradeModeActionRequest request, ClusterState state) {  | 
 | 162 | +        return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE);  | 
 | 163 | +    }  | 
 | 164 | + | 
 | 165 | +    private record UpdateModeStateListener(SetUpgradeModeActionRequest request, ActionListener<AcknowledgedResponse> listener)  | 
 | 166 | +        implements  | 
 | 167 | +            ClusterStateTaskListener {  | 
 | 168 | + | 
 | 169 | +        @Override  | 
 | 170 | +        public void onFailure(Exception e) {  | 
 | 171 | +            listener.onFailure(e);  | 
 | 172 | +        }  | 
 | 173 | +    }  | 
 | 174 | + | 
 | 175 | +    private class UpdateModeExecutor extends SimpleBatchedExecutor<UpdateModeStateListener, Void> {  | 
 | 176 | +        @Override  | 
 | 177 | +        public Tuple<ClusterState, Void> executeTask(UpdateModeStateListener clusterStateListener, ClusterState clusterState) {  | 
 | 178 | +            return Tuple.tuple(createUpdatedState(clusterStateListener.request(), clusterState), null);  | 
 | 179 | +        }  | 
 | 180 | + | 
 | 181 | +        @Override  | 
 | 182 | +        public void taskSucceeded(UpdateModeStateListener clusterStateListener, Void unused) {  | 
 | 183 | +            clusterStateListener.listener().onResponse(AcknowledgedResponse.TRUE);  | 
 | 184 | +        }  | 
 | 185 | +    }  | 
 | 186 | +}  | 
0 commit comments