ServiceProviderRefreshablePlace.java

package emissary.place;

import emissary.config.ConfigUtil;
import emissary.config.Configurator;
import emissary.core.Factory;

import jakarta.annotation.Nullable;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * ServiceProviderPlace that supports on-demand refresh of its configuration
 */
public abstract class ServiceProviderRefreshablePlace extends ServiceProviderPlace {

    private static final Logger logger = LoggerFactory.getLogger(ServiceProviderRefreshablePlace.class);

    private final Object allocatorLock = new Object();
    private final AtomicBoolean invalidated = new AtomicBoolean(false);
    private final AtomicBoolean defunct = new AtomicBoolean(false);

    public ServiceProviderRefreshablePlace() throws IOException {}

    public ServiceProviderRefreshablePlace(final String thePlaceLocation) throws IOException {
        super(thePlaceLocation);
    }

    protected ServiceProviderRefreshablePlace(final String configFile, @Nullable final String theDir, final String thePlaceLocation)
            throws IOException {
        super(configFile, theDir, thePlaceLocation);
    }

    protected ServiceProviderRefreshablePlace(final InputStream configStream, @Nullable final String theDir, final String thePlaceLocation)
            throws IOException {
        super(configStream, theDir, thePlaceLocation);
    }

    protected ServiceProviderRefreshablePlace(final InputStream configStream) throws IOException {
        super(configStream);
    }

    protected ServiceProviderRefreshablePlace(final String configFile, final String placeLocation) throws IOException {
        super(configFile, placeLocation);
    }

    protected ServiceProviderRefreshablePlace(final InputStream configStream, final String placeLocation) throws IOException {
        super(configStream, placeLocation);
    }

    /**
     * Refresh specific constructor, basically clones a place and swaps out the Namespace ref
     *
     * @param place the ServiceProviderRefreshablePlace to clone
     * @param register set true to register with directory place, false otherwise
     * @throws IOException if there is an issue trying to create the place
     */
    public ServiceProviderRefreshablePlace(final ServiceProviderRefreshablePlace place, final boolean register) throws IOException {
        super(place);

        if (!register) {
            // not registering so reuse some stuff
            this.placeLocation = place.placeLocation;
            this.serviceDescription = place.serviceDescription;
            this.keys.addAll(place.keys);
            this.denyList.addAll(place.denyList);
        }

        // reload the config files and get the place updates
        this.configLocs.addAll(place.configLocs);
        this.configG = loadConfigurator(configLocs, place.placeLocation);
        setupPlace(null, place.placeLocation, register);
    }

    /**
     * Get the invalid flag of the place. An invalidated place may indicate that the place has changes, such as new
     * configuration, and may trigger a follow-on process to reconfigure, reinitialize, or re-create the place.
     *
     * @return true if the place has been invalidated, false otherwise
     */
    public final boolean isInvalidated() {
        return this.invalidated.get();
    }

    /**
     * Invalidate a place that need to be refreshed.
     */
    public final void invalidate() {
        logger.info("Place[{}] being marked as invalidated", this.getPlaceName());
        this.invalidated.set(true);
    }

    /**
     * Reinitialize the place by reloading the configurator and reconfiguring the place. Must call {@link #invalidate()}
     * before attempting to refresh the place.
     */
    public final void refresh() {
        refresh(false);
    }

    /**
     * Reinitialize the place by reloading the configurator and reconfiguring the place. Must call {@link #invalidate()}
     * before attempting to refresh the place.
     *
     * @param full true unbind from the namespace and rebuild all directory keys, false to reuse existing keys and just
     *        refresh place configs
     */
    public final void refresh(final boolean full) {
        refresh(full, true);
    }

    /**
     * Reinitialize the place by reloading the configurator and reconfiguring the place. Must call {@link #invalidate()}
     * before attempting to refresh the place.
     *
     * @param full true unbind from the namespace and rebuild all directory keys, false to reuse existing keys and just
     *        refresh place configs
     * @param silent true to just log any issues that arise, false to throw runtime exceptions
     */
    public final void refresh(final boolean full, final boolean silent) {
        logger.trace("Waiting for lock in refresh()");
        synchronized (this.allocatorLock) {
            final String placeName = this.getPlaceName();
            logger.debug("Attempting to refresh place[{}]...", placeName);
            if (!this.defunct.get()) {
                if (isInvalidated()) {
                    if (full) {
                        unbindFromNamespace();
                        new ArrayList<>(this.keys).forEach(this::removeKey);
                    }
                    Factory.create(this.getClass().getName(), this, full);
                    this.defunct.set(true);
                    logger.info("Place[{}] refresh performed successfully", placeName);
                } else {
                    if (!silent) {
                        throw new IllegalStateException(
                                "Cannot refresh place without first calling invalidate; no refresh performed for " + placeName);
                    }
                    logger.warn("Cannot refresh place without first calling invalidate; no refresh performed for {}", placeName);
                }
            } else {
                if (!silent) {
                    throw new IllegalStateException("Error refreshing, DEFUNCT<" + placeName + ">");
                }
                logger.warn("DEFUNCT<{}>", placeName);
            }
        }
    }

    /**
     * Reload the {@link Configurator}
     *
     * @param configLocations the list of configuration files to load
     * @throws IOException if there is an issue loading the config
     */
    private Configurator loadConfigurator(@Nullable final List<String> configLocations, final String placeLocation) throws IOException {
        logger.info("Reloading configurator using locations {}", configLocations);
        if (CollectionUtils.isNotEmpty(configLocations)) {
            return ConfigUtil.getConfigInfo(configLocations);
        }
        return loadConfigurator(placeLocation);
    }
}