io.bdeploy.jersey.actions.ActionService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of api Show documentation
Show all versions of api Show documentation
Public API including dependencies, ready to be used for integrations and plugins.
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