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

io.bdeploy.common.ActivityReporter 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
/*
 * Copyright (c) SSI Schaefer IT Solutions GmbH
 */
package io.bdeploy.common;

import java.io.PrintStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.LongSupplier;
import java.util.function.Supplier;

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

import io.bdeploy.common.cli.data.DataFormat;
import io.bdeploy.common.cli.data.DataTable;
import io.bdeploy.common.security.RemoteService;
import io.bdeploy.common.util.NamedDaemonThreadFactory;

/**
 * Allows reporting and tracking of activities.
 */
public interface ActivityReporter {

    /**
     * Start an {@link Activity} without any associated work amount.
     */
    public Activity start(String activity);

    /**
     * Start an {@link Activity} with a static work amount. Use
     * {@link Activity#worked(long)} to updated the already consumed work amount.
     * 

* Special maxWork values: *

    *
  • -1: don't report work at all. only time will be reported *
  • 0: don't report any maximum, just report current amount if possible. *
*/ public Activity start(String activity, long maxWork); /** * Start an {@link Activity} with a dynamic maximum and current work amount. The * {@link Supplier}s are queried periodically when reporting progress, to catch * updates on the values. */ public Activity start(String activity, LongSupplier maxValue, LongSupplier currentValue); /** * Proxy remote activities to this {@link ActivityReporter} implementation if possible. */ public NoThrowAutoCloseable proxyActivities(RemoteService service); /** * An Activity allows updating progress and signaling when the {@link Activity} * is finished. */ public interface Activity extends NoThrowAutoCloseable { /** * Updates the name of the activity */ public void activity(String activity); /** * Update the local work amount. Has no effect if the {@link Activity} was * started using a dynamic amount {@link Supplier}. */ public void worked(long amount); /** * Signals that this {@link Activity} is done, removing it from the managing * {@link ActivityReporter}. */ public void done(); /** * Return the amount of milliseconds since the activity has been started. */ public long duration(); /** * @return whether cancellation is requested */ public boolean isCancelRequested(); public default void workAndCancelIfRequested(long amount) { worked(amount); if (isCancelRequested()) { done(); // make sure the activity is actually finished. throw new ActivityCancelledException(); } } @Override default void close() { done(); } } /** * Exception to be thrown when an activity has been cancelled from outside. */ public static class ActivityCancelledException extends RuntimeException { private static final long serialVersionUID = 1L; } /** * An {@link ActivityReporter} implementation which reports progress to a given * {@link PrintStream}. */ public static final class Stream implements ActivityReporter { private static final Logger log = LoggerFactory.getLogger(Stream.class); private final PrintStream output; private ScheduledExecutorService updater; private ScheduledFuture scheduled; private boolean verbose; private BiFunction>, NoThrowAutoCloseable> proxyConnector; private final Deque activities = new ArrayDeque<>(); private final List allActivities = new ArrayList<>(); private String lastReportedActivity = "init"; private long lastReportedAmount; public Stream(PrintStream output) { this.output = output; } /** * A verbose summary will include more details about single activity * duration(s). */ public void setVerboseSummary(boolean verbose) { this.verbose = verbose; } /** * Begins the reporting process. Reporting runs asynchronously (periodically) to * avoid stalling of running actions while catching up with dynamic updates to * progress values. */ public synchronized void beginReporting() { if (updater == null) { updater = Executors .newSingleThreadScheduledExecutor(new NamedDaemonThreadFactory(() -> "Activity Reporter (Console)")); } scheduled = updater.scheduleAtFixedRate(() -> catchAll(this::report), 0, 200, TimeUnit.MILLISECONDS); } /** * Stops (and cleans up resources associated with) progress reporting. No * further {@link Activity} processing will be done after this method finishes. * All remaining {@link Activity} instances are cleared (but not finished). */ public synchronized void stopReporting() { if (scheduled != null) { scheduled.cancel(false); } scheduled = null; if (updater != null) { updater.shutdownNow(); updater = null; } reportSummary(); activities.clear(); output.flush(); } private void catchAll(Runnable x) { try { x.run(); } catch (Exception e) { if (log.isTraceEnabled()) { log.trace("Exception in stream reporter", e); } } } private synchronized void report() { AsyncActivity current = activities.peek(); if (current == null) { return; } Long currentAmount = current.getCurrentAmount(); boolean activityChanged = lastReportedActivity != null && !current.activity.equals(lastReportedActivity); boolean amountChanged = currentAmount != lastReportedAmount; if (!activityChanged && !amountChanged) { return; } output.print('\r'); Long max = current.getMaxAmount(); if (max < 0) { output.print(String.format("[%1$08d] %2$-70s", current.duration(), current.activity)); } else if (max == 0) { output.print( String.format("[%1$08d] %2$-70s /%3$8d", current.duration(), current.activity, currentAmount)); } else { output.print( String.format("[%1$08d] %2$-70s %3$8d/%4$8d", current.duration(), current.activity, currentAmount, max)); } } private synchronized void reportDone(AsyncActivity act) { output.print('\r'); Long max = act.getMaxAmount(); if (max < 0) { output.print(String.format("[%1$08d] %2$-70s DONE%n", act.duration(), act.activity)); } else if (max == 0) { output.print( String.format("[%1$08d] %2$-70s DONE/%3$8d%n", act.duration(), act.activity, act.getCurrentAmount())); } else { output.print(String.format("[%1$08d] %2$-70s DONE/%3$8d%n", act.duration(), act.activity, max)); } output.flush(); lastReportedActivity = null; // avoid newline on next report :) } private void reportSummary() { if (allActivities.isEmpty()) { return; } if (verbose) { DataTable table = DataFormat.TEXT.createTable(output); table.column("Activity", 80).column("Duration", 15); for (AsyncActivity act : allActivities) { table.row().cell((act.isNested ? " " : "") + act.activity).cell(act.duration() + " ms").build(); } table.render(); } } @Override public Activity start(String activity) { return start(activity, -1); } @Override public synchronized Activity start(String activity, long maxWork) { return start(activity, () -> maxWork, null); } @Override public synchronized Activity start(String activity, LongSupplier maxValue, LongSupplier currentValue) { AsyncActivity act = new AsyncActivity(activity, maxValue, currentValue); activities.push(act); allActivities.add(act); return act; } private synchronized void done(AsyncActivity act) { activities.remove(act); reportDone(act); } private final class AsyncActivity implements ActivityReporter.Activity { private final LongAdder localCurrent = new LongAdder(); private final LongSupplier currentAmount; private final LongSupplier maxAmount; private final long startTime; private String activity; private long stopTime; private boolean isNested = false; public AsyncActivity(String activity, LongSupplier maxValue, LongSupplier currentValue) { this.activity = activity; this.maxAmount = maxValue; this.currentAmount = currentValue != null ? currentValue : localCurrent::sum; this.startTime = System.currentTimeMillis(); this.isNested = !activities.isEmpty(); } long getCurrentAmount() { if (stopTime != 0 && maxAmount.getAsLong() > 0) { return maxAmount.getAsLong(); // already stopped. } return currentAmount.getAsLong(); } long getMaxAmount() { return maxAmount.getAsLong(); } @Override public boolean isCancelRequested() { return false; // no cancel support for streams } @Override public void worked(long amount) { localCurrent.add(amount); } @Override public void activity(String activity) { this.activity = activity; } @Override public void done() { stopTime = System.currentTimeMillis(); ActivityReporter.Stream.this.done(this); } @Override public long duration() { return (stopTime > 0 ? stopTime : System.currentTimeMillis()) - startTime; } } public void setProxyConnector( BiFunction>, NoThrowAutoCloseable> proxyConnector) { this.proxyConnector = proxyConnector; } @Override public NoThrowAutoCloseable proxyActivities(RemoteService service) { return new StreamRemoteActivityProxy(service); } private class StreamRemoteActivityProxy implements NoThrowAutoCloseable { private NoThrowAutoCloseable source; public StreamRemoteActivityProxy(RemoteService svc) { // getKeyStore is only set for real remotes. local hives will have no key store. if (svc != null && svc.getKeyStore() != null && proxyConnector != null) { source = proxyConnector.apply(svc, this::onMessage); } } private void onMessage(List message) { try { for (ActivitySnapshot act : message) { output.println("SRV: " + act); } } catch (Exception e) { output.println("Cannot read server activities:"); e.printStackTrace(output); } } @Override public void close() { if (source != null) { source.close(); } } } } /** * No-op {@link ActivityReporter}, which does nothing, except being * null. */ public static class Null implements ActivityReporter { @Override public Activity start(String activity) { return new NullActivity(); } @Override public Activity start(String activity, long maxWork) { return new NullActivity(); } @Override public Activity start(String activity, LongSupplier maxValue, LongSupplier currentValue) { return new NullActivity(); } private static final class NullActivity implements ActivityReporter.Activity { long startTime = System.currentTimeMillis(); @Override public void worked(long amount) { // nothing } @Override public void activity(String activity) { // nothing } @Override public void done() { // nothing } @Override public long duration() { return System.currentTimeMillis() - startTime; } @Override public boolean isCancelRequested() { return false; } } @Override public NoThrowAutoCloseable proxyActivities(RemoteService service) { return () -> { }; } } /** * Delegator {@link ActivityReporter} which can change destination lazily once a proper instance * is available (after server startup, ...). */ public class Delegating implements ActivityReporter { private ActivityReporter delegate = new ActivityReporter.Null(); public void setDelegate(ActivityReporter delegate) { this.delegate = delegate; } @Override public Activity start(String activity) { return delegate.start(activity); } @Override public Activity start(String activity, long maxWork) { return delegate.start(activity, maxWork); } @Override public Activity start(String activity, LongSupplier maxValue, LongSupplier currentValue) { return delegate.start(activity, maxValue, currentValue); } @Override public NoThrowAutoCloseable proxyActivities(RemoteService service) { return delegate.proxyActivities(service); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy