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

com.wl4g.component.support.cli.NodeProcessManagerImpl Maven / Gradle / Ivy

/*
 * Copyright 2017 ~ 2025 the original author or authors. 
 *
 * 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.wl4g.component.support.cli;

import static com.wl4g.component.common.lang.Exceptions.getRootCausesString;
import static com.wl4g.component.common.lang.ThreadUtils2.sleepRandom;
import static com.wl4g.component.support.cache.jedis.util.RedisSpecUtil.*;
import static com.wl4g.component.support.cli.destroy.DestroySignalMessage.DestroyState.*;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.Assert;

import com.wl4g.component.support.cache.jedis.JedisService;
import com.wl4g.component.support.cache.locks.JedisLockManager;
import com.wl4g.component.support.cli.TimeoutDestroyProcessException;
import com.wl4g.component.support.cli.destroy.DestroySignal;
import com.wl4g.component.support.cli.destroy.DestroySignalMessage;
import com.wl4g.component.support.cli.process.DestroableProcess;
import com.wl4g.component.support.cli.repository.ProcessRepository;

/**
 * Implementation of distributed destroable command process based on jedis
 * cluster.
 * 
 * @author Wangl.sir <[email protected], [email protected]>
 * @version v1.0.0 2019-10-20
 * @since
 */
public class NodeProcessManagerImpl extends GenericProcessManager {

    /** Application name. */
    @Value("${spring.application.name}")
    protected String appName;

    /** Jedis service. */
    @Autowired
    protected JedisService jedisService;

    /** Jedis locks manager. */
    @Autowired
    protected JedisLockManager lockManager;

    public NodeProcessManagerImpl(ProcessRepository repository) {
        super(repository);
    }

    @Override
    public void destroyForComplete(DestroySignal signal) throws TimeoutDestroyProcessException, IllegalStateException {
        // Send destruction signal.
        String signalKey = getDestroySignalKey(signal.getProcessId());
        if (log.isInfoEnabled()) {
            log.info("Send destruction signal:{} for processId:{}", signalKey, signal.getProcessId());
        }
        jedisService.setObjectAsJson(signalKey, signal, DEFAULT_SIGNAL_EXPIRED_SEC); // MARK1

        // Wait for complete.
        int sleepTotal = 0; // Total waiting time for destruction.
        DestroySignalMessage ret = null;
        while (isNull(ret = pollDestroyMessage(signal.getProcessId()))) {
            sleepTotal += sleepRandom(100, 800);
            if (sleepTotal >= signal.getTimeoutMs()) {
                throw new TimeoutDestroyProcessException(
                        String.format("Timeout destory command process for %s", signal.getProcessId()));
            }
        }

        // Check destory failure.
        if (ret.getState() == DESTROY_FAIL) {
            throw new IllegalStateException(
                    String.format("Failed to destory process for %s, caused by: %s", signal.getProcessId(), ret.getMessage()));
        }

    }

    @Override
    public void run() {
        getWorker().scheduleWithRandomDelay(() -> {
            try {
                doInspectForProcessesDestroy(getDestroyLockName());
            } catch (Exception e) {
                throw new IllegalStateException(
                        "Critical error! Killed node process watcher, commands process on this node will not be manual cancel.",
                        e);
            }
        }, 5000, DEFAULT_MIN_WATCH_MS, DEFAULT_MAX_WATCH_MS, TimeUnit.MILLISECONDS);
    }

    /**
     * Inspecting process destroy all.
* Akka can be used instead of distributed message transmission. This is for * a more lightweight implementation, so redis scanning is used * * @param destroyLockName * @throws InterruptedException */ private void doInspectForProcessesDestroy(String destroyLockName) throws InterruptedException { Lock lock = lockManager.getLock(safeFormat(destroyLockName)); try { // Let cluster this node process destroy, nodes that do not // acquire lock are on ready in place. if (lock.tryLock()) { Collection pss = repository.getProcessRegistry(); if (log.isDebugEnabled()) { log.debug("Destroable processes: {}", pss); } for (DestroableProcess ps : pss) { String signalKey = getDestroySignalKey(ps.getProcessId()); // Match & destroy process. See:[MARK1] DestroySignal signal = jedisService.getObjectAsJson(signalKey, DestroySignal.class); try { if (nonNull(signal)) { destroy(signal); publishDestroyMessage(signal, null); break; } } catch (Exception e) { log.error("Failed to destroy process.", e); publishDestroyMessage(signal, e); } finally { jedisService.del(signalKey); // Cleanup. } } } else if (log.isDebugEnabled()) { log.debug("Skip destroy processes ..."); } } catch (Throwable ex) { log.error("Destruction error", ex); } finally { lock.unlock(); } } /** * Publish destroy result message. * * @param signal * @param th */ private void publishDestroyMessage(DestroySignal signal, Throwable th) { DestroySignalMessage ret = new DestroySignalMessage(signal); if (nonNull(th)) { ret.setState(DESTROY_FAIL); ret.setMessage(getRootCausesString(th)); } jedisService.setObjectAsJson(getDestroyMessageKey(signal.getProcessId()), ret, 10_000); } /** * Poll destroy result message. * * @param signal * @param th */ private DestroySignalMessage pollDestroyMessage(String processId) { String destroyMsgKey = getDestroyMessageKey(processId); try { return jedisService.getObjectAsJson(destroyMsgKey, DestroySignalMessage.class); } finally { jedisService.del(destroyMsgKey); } } /** * Obtain process destruction lock name. * * @return */ private String getDestroyLockName() { return appName + "." + LOCK_CLI_PROCESS_DESTROY; } /** * Get processId destory signal key. * * @param processId * @return */ private String getDestroySignalKey(String processId) { Assert.hasText(processId, "ProcessId must not be empty."); return SIGNAL_PROCESS_DESTROY + processId; } /** * Get processId destory signal result key. * * @param processId * @return */ private String getDestroyMessageKey(String processId) { Assert.hasText(processId, "ProcessId must not be empty."); return SIGNAL_PROCESS_DESTROY_RET + processId; } /** Command-line process watcher locker. */ final public static String LOCK_CLI_PROCESS_DESTROY = "cli.process.destroy"; /** * Command-line process destroy signal. */ final public static String SIGNAL_PROCESS_DESTROY = "cli.process.destroy_"; /** * Command-line process destroy signal result. */ final public static String SIGNAL_PROCESS_DESTROY_RET = "cli.process.destroy.ret_"; final public static long DEFAULT_MIN_WATCH_MS = 2_00L; final public static long DEFAULT_MAX_WATCH_MS = 2_000L; /** Default destruction signal expired seconds. */ final public static int DEFAULT_SIGNAL_EXPIRED_SEC = (int) (3 * TimeUnit.MILLISECONDS.toSeconds(DEFAULT_MAX_WATCH_MS)); }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy