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

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

Go to download

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

The newest version!
package io.bdeploy.jersey.actions;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.jvnet.hk2.annotations.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;

import io.bdeploy.common.security.RemoteService;
import io.bdeploy.common.util.JacksonHelper;
import io.bdeploy.common.util.NamedDaemonThreadFactory;
import io.bdeploy.common.util.StringHelper;
import io.bdeploy.common.util.UuidHelper;
import io.bdeploy.jersey.JerseyClientFactory;
import io.bdeploy.jersey.resources.ActionResource;
import io.bdeploy.jersey.ws.change.client.ObjectChangeClientWebSocket;
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.NotFoundException;

/**
 * Bridges actions from managed servers to central server.
 */
@Singleton
@Service
public class ActionBridge {

    private static final class ActionBridgeHandle {

        private final RemoteService svc;
        private final long begin;
        private ObjectChangeClientWebSocket ws;

        public ActionBridgeHandle(ObjectChangeClientWebSocket ws, RemoteService svc, long begin) {
            this.svc = svc;
            this.begin = begin;
            this.ws = ws;
        }
    }

    private static final Logger log = LoggerFactory.getLogger(ActionBridge.class);
    /** The same timeout as used in the web ui, see servers.service.ts -> SYNC_TIMEOUT */
    private static final long SYNC_TIMEOUT = TimeUnit.MINUTES.toMillis(15);

    private final Map handles = new TreeMap<>();
    private final ScheduledExecutorService reaper = Executors
            .newSingleThreadScheduledExecutor(new NamedDaemonThreadFactory("ActionBridge Reaper"));
    private final ActionService actions;

    public ActionBridge(ActionService svc) {
        this.actions = svc;
        reaper.scheduleAtFixedRate(this::cleanup, 5, 5, TimeUnit.SECONDS);
    }

    private void cleanup() {
        synchronized (handles) {
            List toRemove = new ArrayList<>();
            for (Map.Entry entry : handles.entrySet()) {
                String name = entry.getKey();
                ActionBridgeHandle handle = entry.getValue();

                if (System.currentTimeMillis() > (handle.begin + SYNC_TIMEOUT)) {
                    // timed out. cleanup.
                    closeHandle(handles.get(name));

                    log.debug("Action bridge timed out to {}", name);

                    actions.removeSource(name);
                    toRemove.add(name);
                } else if (!handle.ws.isOpen()) {
                    // not timed out, but websocket closed! re-establish
                    closeHandle(handles.get(name));

                    log.atDebug().log("Action bridge to {} unexpectedly closed", name);

                    if (fetchCurrent(name, handle.svc)) {
                        // renew subscription.
                        handle.ws = fetchSubscription(name, handle.svc);
                    } else {
                        // cannot fetch anymore?! don't try to keep track anymore!
                        log.warn("Existing action bridge to {} cannot be renewed", name);
                        actions.removeSource(name);
                        toRemove.add(name);
                    }
                }
            }
            toRemove.forEach(handles::remove);
        }
    }

    private boolean fetchCurrent(String name, RemoteService service) {
        // initial fetch
        try {
            // get *all* actions we can get a hold of.
            List dtos = JerseyClientFactory.get(service).getProxyClient(ActionResource.class).getActions(null,
                    null);

            dtos.forEach(d -> d.execution.setSource(name));
            actions.removeSource(name); // remove existing.
            actions.add(dtos.toArray(new ActionBroadcastDto[dtos.size()]));

            log.debug("Established action bridge to {}. Initial actions: {}", name, dtos);

            return true;
        } catch (Exception e) {
            var l = log.atInfo();
            if (e instanceof NotFoundException) {
                l = log.atDebug();
            }
            // either not supported or another problem...?
            l.log("Cannot bridge actions from {}: {}", name, e);
            return false;
        }
    }

    private ObjectChangeClientWebSocket fetchSubscription(String name, RemoteService service) {
        String id = UuidHelper.randomId();
        ObjectChangeClientWebSocket ws = JerseyClientFactory.get(service).getObjectChangeWebSocket(e -> onEvent(id, name, e));
        ws.subscribe(ActionService.ACTIONS_TYPE, ObjectScope.EMPTY);
        return ws;
    }

    private static void closeHandle(ActionBridgeHandle handle) {
        if (handle.ws != null) {
            handle.ws.close();
        }
    }

    public void onSync(String name, RemoteService service) {
        // if we have that already, just update the start time to begin again.
        synchronized (handles) {
            actions.removeSource(name);

            if (handles.containsKey(name)) {
                closeHandle(handles.get(name));
            }

            if (!fetchCurrent(name, service)) {
                return; // not supported or not possible.
            }

            log.debug("Sync {}", name);

            // prepare websocket and handle.
            ActionBridgeHandle handle = new ActionBridgeHandle(fetchSubscription(name, service), service,
                    System.currentTimeMillis());

            handles.put(name, handle);
        }
    }

    private void onEvent(String id, String source, ObjectChangeDto change) {
        // extract event - inverse operation from ActionService#broadcast's serialize.
        String event = change.details.get(ActionService.ACTIONS_PAYLOAD);

        if (StringHelper.isNullOrEmpty(event)) {
            return; // meh.
        }

        ActionBroadcastDto dto = deserialize(event);
        dto.execution.setSource(source);

        log.debug("{} Bridge event [{}] {} by {}", id, change.event.name(), dto.action, dto.execution);

        if (change.event == ObjectEvent.CREATED) {
            actions.add(dto);
        } else if (change.event == ObjectEvent.REMOVED) {
            actions.remove(dto);
        }
    }

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

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy