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

io.bdeploy.jersey.activity.JerseyBroadcastingActivityReporter Maven / Gradle / Ivy

Go to download

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

There is a newer version: 7.3.6
Show newest version
package io.bdeploy.jersey.activity;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.LongSupplier;
import java.util.stream.Collectors;

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

import com.fasterxml.jackson.core.JsonProcessingException;

import io.bdeploy.common.ActivityReporter;
import io.bdeploy.common.ActivitySnapshot;
import io.bdeploy.common.NoThrowAutoCloseable;
import io.bdeploy.common.security.RemoteService;
import io.bdeploy.common.util.JacksonHelper;
import io.bdeploy.common.util.UuidHelper;
import io.bdeploy.jersey.JerseyScopeService;
import io.bdeploy.jersey.JerseyServer;
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.Inject;
import jakarta.inject.Named;

/**
 * An activity reporter which exposes currently running activities to be broadcasted via SSE
 */
@Service
public class JerseyBroadcastingActivityReporter implements ActivityReporter {

    /** Needs to be in line with ObjectChangeType for the Web UI */
    public static final String OCT_ACTIVIES = "ACTIVITIES";

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

    /**
     * All state must be static (VM global) as this service might be instantiated from multiple Jersey applications (plug-ins).
     * It seems that HK2 has a bug where it changes the registration for a singleton service in a locator if the service is
     * registered as singleton in ANOTHER locator...
     */
    private static final List globalActivities = new CopyOnWriteArrayList<>();
    private static final ThreadLocal currentActivity = new ThreadLocal<>();
    private static final Set activeScopes = new TreeSet<>();

    @Inject
    private JerseyScopeService jss;

    @Inject
    @Optional
    private ObjectChangeBroadcaster bc;

    @Inject
    public JerseyBroadcastingActivityReporter(@Named(JerseyServer.BROADCAST_EXECUTOR) ScheduledExecutorService scheduler) {
        scheduler.scheduleAtFixedRate(this::sendUpdate, 1, 1, TimeUnit.SECONDS);
    }

    private void sendUpdate() {
        if (bc == null) {
            return;
        }

        try {
            List list = getGlobalActivities().stream().filter(Objects::nonNull)
                    .map(JerseyRemoteActivity::snapshot).collect(Collectors.toList());

            // figure out all different scopes which are there.
            List scopes = list.stream().map(a -> new ObjectScope(a.scope)).distinct().collect(Collectors.toList());

            Map> perScope = new HashMap<>();

            for (ObjectScope scope : scopes) {
                List forScope = new ArrayList<>();
                for (ActivitySnapshot snapshot : list) {
                    if (snapshot.parentUuid != null) {
                        // this is a child, not a root - skip
                        continue;
                    }

                    if (scope.matches(new ObjectScope(snapshot.scope))) {
                        forScope.add(snapshot);

                        while (addChildren(forScope, list) != 0) {
                            // intentionally left blank :)
                        }
                    }
                }
                if (!forScope.isEmpty()) {
                    perScope.put(scope, forScope);
                }
            }
            activeScopes.addAll(perScope.keySet());

            List scopesToRemove = new ArrayList<>();
            for (ObjectScope active : activeScopes) {
                if (!perScope.containsKey(active)) {
                    scopesToRemove.add(active);
                }
            }

            // This will send an empty list to all the consumers that match this scope.
            // This will in some cases result in a reset of activities to empty, even though
            // this client will receive events from *another* scope. we cannot determine
            // it here, since we don't know about the actual registrations. Thus we send
            // the "update to empty" first and *right* after that the updates which may
            // contain activities for that clients as well.
            for (ObjectScope toRemove : scopesToRemove) {
                activeScopes.remove(toRemove);
                bc.send(new ObjectChangeDto(OCT_ACTIVIES, toRemove, ObjectEvent.CHANGED,
                        Collections.singletonMap(OCT_ACTIVIES, "[]")));
            }

            List allMessages = new ArrayList<>();
            for (Map.Entry> e : perScope.entrySet()) {
                allMessages.add(new ObjectChangeDto(OCT_ACTIVIES, e.getKey(), ObjectEvent.CHANGED,
                        Collections.singletonMap(OCT_ACTIVIES, serialize(e.getValue()))));
            }
            bc.sendBestMatching(allMessages);
        } catch (Exception e) {
            log.error("Error while broadcasting activities", e);
        }
    }

    private String serialize(List snap) {
        try {
            return JacksonHelper.getDefaultJsonObjectMapper().writeValueAsString(snap);
        } catch (JsonProcessingException e) {
            throw new IllegalStateException("Cannot serialize activities", e);
        }
    }

    private int addChildren(List activities, List pool) {
        Set haveUuids = activities.stream().map(s -> s.uuid).collect(Collectors.toCollection(TreeSet::new));

        List children = new ArrayList<>();
        for (ActivitySnapshot root : activities) {
            for (ActivitySnapshot potentialChild : pool) {
                if (haveUuids.contains(potentialChild.uuid)) {
                    // have it already.
                    continue;
                }

                if (potentialChild.parentUuid != null && potentialChild.parentUuid.equals(root.uuid)) {
                    children.add(potentialChild);
                }
            }
        }
        activities.addAll(children);
        return children.size();
    }

    @Override
    public Activity start(String activity) {
        return start(activity, -1l);
    }

    @Override
    public Activity start(String activity, long maxWork) {
        return start(activity, () -> maxWork, null);
    }

    @Override
    public synchronized Activity start(String activity, LongSupplier maxValue, LongSupplier currentValue) {
        List scope = JerseyRemoteActivityScopeServerFilter.getRequestActivityScope(jss);
        String user = jss.getUser();

        // wire activities by UUID. this is done so that serialization of activity "trees" stays
        // as flat as it is - otherwise too much traffic to clients would be produced. Clients
        // need to convert the flat list of activities to a tree representation when interested.
        JerseyRemoteActivity parent = currentActivity.get();
        String parentUuid = null;
        if (parent != null) {
            parentUuid = parent.getUuid();
        }

        JerseyRemoteActivity act = new JerseyRemoteActivity(this::done, null, activity, maxValue, currentValue, scope, user,
                System.currentTimeMillis(), UuidHelper.randomId(), parentUuid);

        if (log.isTraceEnabled()) {
            log.trace("Begin: [{}] {}", act.getUuid(), activity);
        }

        currentActivity.set(act);
        globalActivities.add(act);
        return act;
    }

    private synchronized void done(JerseyRemoteActivity act) {
        if (!globalActivities.contains(act)) {
            return; // already done.
        }

        JerseyRemoteActivity current = currentActivity.get();
        if (current != null && current.getUuid().equals(act.getUuid())) {
            // current is the one we're finishing
            if (act.getParentUuid() != null) {
                JerseyRemoteActivity parent = getActivityById(act.getParentUuid());
                if (parent != null) {
                    currentActivity.set(parent);
                } else {
                    log.warn("Parent activity no longer available: {} for {}", act.getParentUuid(), act);
                }
            } else {
                // no parent set - we are top-level.
                currentActivity.remove();
            }
        } else if (current != null) {
            // we're finishing something which is not current -> warn & ignore
            log.warn("Finished activity is not current for this thread: {}, current: {}", act, current);
        } else {
            log.warn("Finished activity but there is no current activity for this thread: {}", act);
        }

        globalActivities.remove(act);
    }

    @Override
    public NoThrowAutoCloseable proxyActivities(RemoteService service) {
        return new JerseyRemoteActivityProxy(service, this);
    }

    /**
     * @return (a copy of) all currently known activities
     */
    synchronized List getGlobalActivities() {
        return new ArrayList<>(globalActivities);
    }

    synchronized void addProxyActivity(JerseyRemoteActivity act) {
        globalActivities.add(act);
    }

    synchronized void removeProxyActivity(JerseyRemoteActivity act) {
        globalActivities.remove(act);
    }

    /**
     * @return the current activity on the calling thread.
     */
    JerseyRemoteActivity getCurrentActivity() {
        return currentActivity.get();
    }

    void resetCurrentActivity() {
        currentActivity.remove();
    }

    JerseyRemoteActivity getActivityById(String uuid) {
        return globalActivities.stream().filter(Objects::nonNull).filter(a -> a.getUuid().equals(uuid)).findAny().orElse(null);
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy