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

io.bdeploy.jersey.actions.ActionService Maven / Gradle / Ivy

Go to download

Public API including dependencies, ready to be used for integrations and plugins.

There is a newer version: 7.4.0
Show newest version
package io.bdeploy.jersey.actions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;

import io.bdeploy.common.audit.AuditRecord;
import io.bdeploy.common.audit.Auditor;
import io.bdeploy.common.util.JacksonHelper;
import io.bdeploy.common.util.NamedDaemonThreadFactory;
import io.bdeploy.jersey.ws.change.ObjectChangeBroadcaster;
import io.bdeploy.jersey.ws.change.msg.ObjectChangeDto;
import io.bdeploy.jersey.ws.change.msg.ObjectEvent;
import io.bdeploy.jersey.ws.change.msg.ObjectScope;
import jakarta.inject.Singleton;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response.Status;

@Singleton
public class ActionService {

    @FunctionalInterface
    public interface ActionHandle extends AutoCloseable {

        @Override
        public void close();
    }

    public static final String ACTIONS_TYPE = "SERVER_ACTIONS";
    public static final String ACTIONS_PAYLOAD = "SERVER_ACTION";

    private static final Logger log = LoggerFactory.getLogger(ActionService.class);

    private final ObjectChangeBroadcaster bc;
    private final Auditor auditor;

    private final Map> running = new TreeMap<>();

    private final ScheduledExecutorService cleaner = Executors
            .newSingleThreadScheduledExecutor(new NamedDaemonThreadFactory("Action-Cleanup"));

    public ActionService(ObjectChangeBroadcaster bc, Auditor auditor) {
        this.bc = bc;
        this.auditor = auditor;

        cleaner.scheduleAtFixedRate(this::cleanup, 1, 1, TimeUnit.MINUTES);
    }

    public ActionHandle start(Action action, ActionExecution exec) {
        internalAdd(action, exec, true);

        auditor.audit(
                new AuditRecord.Builder().setWho(exec.getName()).setMethod("Begin").setMessage(action.getType().getDescription())
                        .addParameter("type", action.getType().name()).addParameter("bhive", action.getBHive())
                        .addParameter("instance", action.getInstance()).addParameter("item", action.getItem()).build());

        return () -> stop(action, exec);
    }

    private void stop(Action action, ActionExecution exec) {
        // auditing happens on stop, which should be called for all action *except* for those that are
        // open ended by design (restart, install update, ...).
        auditor.audit(
                new AuditRecord.Builder().setWho(exec.getName()).setMethod("Done").setMessage(action.getType().getDescription())
                        .addParameter("type", action.getType().name()).addParameter("bhive", action.getBHive())
                        .addParameter("instance", action.getInstance()).addParameter("item", action.getItem())
                        .addParameter("duration", System.currentTimeMillis() - exec.getStart() + "ms").build());

        internalRemove(action, exec);
    }

    private void withExecutions(Action action, Consumer> consumer) {
        synchronized (running) {
            consumer.accept(running.computeIfAbsent(action, a -> new TreeSet<>()));
        }
    }

    private void cleanup() {
        synchronized (running) {
            List notRunning = running.entrySet().stream().filter(e -> e.getValue().isEmpty()).map(Map.Entry::getKey)
                    .collect(Collectors.toList());
            notRunning.forEach(running::remove);
        }
    }

    public List getRunningActions(String bhive, String instance) {
        List result = new ArrayList<>();

        synchronized (running) {
            for (Map.Entry> entry : running.entrySet()) {
                if (bhive != null && !bhive.equals(entry.getKey().getBHive())) {
                    // skip :) the bhive is not as requested. null request matches all.
                    continue;
                }

                if (instance != null && !instance.equals(entry.getKey().getInstance())) {
                    // skip :) the instance is not as requested. null request matches all.
                    continue;
                }

                for (ActionExecution exec : entry.getValue()) {
                    result.add(new ActionBroadcastDto(entry.getKey(), exec));
                }
            }
        }

        return result;
    }

    private void internalAdd(Action action, ActionExecution exec, boolean failOnConflict) {
        if (action == null || exec == null) {
            throw new IllegalArgumentException("Neither action nor execution may be null");
        }
        withExecutions(action, execs -> {
            // if exclusive, either the set is empty, or the only entry is our own execution.
            if (action.getType().isExclusive() && !execs.isEmpty() && !(execs.size() == 1 && execs.contains(exec))) {
                if (failOnConflict) {
                    throw new WebApplicationException("Operation is already running: " + action, Status.CONFLICT);
                } else {
                    // we cannot *prevent* it, we just log the fact. we can now no longer start that
                    // action ourselves without the remote action going away.
                    log.warn("Action conflict while adding {} by {}. Already running: {}", action, exec, execs);
                }
            }

            if (!execs.contains(exec)) {
                execs.add(exec);
                broadcast(action, exec, ObjectEvent.CREATED);
            }
        });
    }

    private void internalRemove(Action action, ActionExecution exec) {
        withExecutions(action, execs -> {
            if (!execs.contains(exec)) {
                log.warn("Cannot remove execution which is not existing: {} in {}", exec, execs);
            } else {
                execs.remove(exec);
                broadcast(action, exec, ObjectEvent.REMOVED);
            }
        });
    }

    protected void add(ActionBroadcastDto... actions) {
        for (ActionBroadcastDto dto : actions) {
            internalAdd(dto.action, dto.execution, false);
        }
    }

    protected void remove(ActionBroadcastDto... actions) {
        for (ActionBroadcastDto dto : actions) {
            internalRemove(dto.action, dto.execution);
        }
    }

    protected void removeSource(String source) {
        synchronized (running) {
            for (Map.Entry> entry : running.entrySet()) {
                List owned = entry.getValue().stream().filter(ex -> source.equals(ex.getSource()))
                        .collect(Collectors.toList());

                owned.forEach(x -> internalRemove(entry.getKey(), x));
            }
        }
    }

    private void broadcast(Action action, ActionExecution exec, ObjectEvent type) {
        if (bc == null) {
            return;
        }

        try {
            ActionBroadcastDto act = new ActionBroadcastDto(action, exec);
            List scopes = new ArrayList<>();
            if (act.action.getBHive() != null) {
                scopes.add(act.action.getBHive());
                if (act.action.getInstance() != null) {
                    scopes.add(act.action.getInstance());
                    // no further scoping as the web ui maximum refined scope is instance level.
                }
            }

            ObjectScope scope = new ObjectScope(scopes);
            bc.send(new ObjectChangeDto(ACTIONS_TYPE, scope, type, Collections.singletonMap(ACTIONS_PAYLOAD, serialize(act))));

        } catch (Exception e) {
            log.warn("Cannot broadcast server action", e);
        }
    }

    private static String serialize(ActionBroadcastDto dtos) {
        try {
            return JacksonHelper.getDefaultJsonObjectMapper().writeValueAsString(dtos);
        } catch (JsonProcessingException e) {
            throw new IllegalStateException("Cannot serialize server actions", e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy