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

ru.progrm_jarvis.javacommons.service.AbstractManagedPendingService Maven / Gradle / Ivy

package ru.progrm_jarvis.javacommons.service;

import lombok.*;
import lombok.experimental.Accessors;
import lombok.experimental.FieldDefaults;
import org.jetbrains.annotations.NotNull;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.function.Consumer;

/**
 * Abstract implementation of {@link ManagedPendingService}.
 *
 * @param  type of owner of the service
 * @param  type of service owned while pending
 * @param  type of value passed to hooks indicating that the service's pending has ended (the service became ready)
 */
@FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true)
public abstract class AbstractManagedPendingService implements ManagedPendingService {

    /**
     * Current state if this service
     */
    @NonNull AtomicReference state;

    /**
     * Lock used for synchronizing lifecycle operations
     */
    @NonNull Lock lifecycleLock;

    /**
     * Map of owned service instances by their owners
     */
    @NonNull Map> owners;

    /**
     * Map of callbacks to be called once the server gets started by their owners
     */
    @NonNull Map>> readyCallbacks;

    /**
     * Creates a new managed pending service.
     *
     * @param lifecycleLock lock used for synchronizing lifecycle operations
     * @param owners empty map of owned service instances by their owners
     * @param readyCallbacks empty map of callbacks to be called once the server gets started by their owners
     *
     * @throws IllegalArgumentException if {@code owners} or {@code readyCallbacks} is not empty
     */
    protected AbstractManagedPendingService(final @NonNull Lock lifecycleLock,
                                            final @NonNull Map> owners,
                                            final @NonNull Map>> readyCallbacks) {
        if (!owners.isEmpty()) throw new IllegalArgumentException(
                "Map of owners should be empty"
        );
        if (!readyCallbacks.isEmpty()) throw new IllegalArgumentException(
                "Multi-map of ready callbacks should be empty"
        );

        state = new AtomicReference<>(State.PENDING);

        this.lifecycleLock = lifecycleLock;
        this.owners = owners;
        this.readyCallbacks = readyCallbacks;
    }

    @Override
    public @NotNull OwnedService request(final @NonNull O owner) {
        final Lock lock;
        (lock = lifecycleLock).lock();
        try {
            if (state.get().canRequest()) return owners.computeIfAbsent(
                    owner, freshOwner -> new SafeOwnedService(owner, newService(freshOwner))
            );
            throw new IllegalStateException("Services cannot be requested any longer");
        } finally {
            lock.unlock();
        }
    }

    protected abstract S newService(@NonNull O owner);

    @SuppressWarnings("resource") // close() is already the cause of this method being called
    protected void markAsReady(final @NonNull O owner) {
        final Lock lock;
        (lock = lifecycleLock).lock();
        try {
            if (!state.get().canClose()) throw new IllegalStateException("Owned services can no longer be closed");

            if (owners.remove(owner) != null) tryStart();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Attempts to start this service.
     */
    protected void tryStart() {
        if (owners.isEmpty() && state.compareAndSet(State.READY, State.STARTED)) runReadyCallbacks(start());
    }

    /**
     * Actually tarts this service returning it.
     *
     * @return started service
     */
    protected abstract R start();

    /**
     * Runs all {@link #readyCallbacks ready-callbacks}.
     *
     * @param service service to be passed to callbacks
     */
    protected void runReadyCallbacks(final R service) {
        for (val callbacks : readyCallbacks.values()) for (val callback : callbacks) callback.accept(service);
    }

    /**
     * Adds a ready-callback for the given owner.
     *
     * @param owner owner of the callback
     * @param readyCallback callback to be added to the given owner
     */
    protected void addReadyCallback(final @NonNull O owner, final @NonNull Consumer readyCallback) {
        final Lock lock;
        (lock = lifecycleLock).lock();
        try {
            readyCallbacks.computeIfAbsent(owner, key -> ConcurrentHashMap.newKeySet()).add(readyCallback);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void ready() {
        final Lock lock;
        (lock = lifecycleLock).lock();
        try {
            if (!state.compareAndSet(State.PENDING, State.READY)) throw new IllegalStateException(
                    "Could not set state to READY from PENDING"
            );
            tryStart();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public boolean isSafelyStarted() {
        final Lock lock;
        (lock = lifecycleLock).lock();
        try {
            return state.get() == State.STARTED;
        } finally {
            lock.unlock();
        }
    }

    /**
     * States in which this service can be.
     */
    @Getter
    @RequiredArgsConstructor
    @Accessors(fluent = true)
    @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
    protected enum State {
        /**
         * This service accepts new owners.
         */
        PENDING(true, true),
        /**
         * This service is ready to start thus not accepting any new owners.
         */
        READY(false, true),
        /**
         * This service is already started and no longer usable directly.
         */
        STARTED(false, false);

        /**
         * Flag indicating whether or not new {@link #request(Object) own-requests} can happen
         */
        boolean canRequest,
        /**
         * Flag indicating whether or not owned services can still be {@link OwnedService#close() closed}
         */
        canClose;
    }

    /**
     * Safe {@link OwnedService owned service} guaranteeing no undefined behaviour due to illegal state.
     */
    @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true)
    protected class SafeOwnedService implements OwnedService {

        /**
         * Owner of this service
         */
        @NotNull O owner;

        /**
         * Actual owned service
         */
        S service;

        /**
         * Flag indicating whether or not this service can no longer be used.
         */
        @NotNull AtomicBoolean closed;

        /**
         * Creates a new safe owned service.
         *
         * @param owner owner of this service
         * @param service actual owned service
         */
        protected SafeOwnedService(final @NotNull O owner, final S service) {
            this.owner = owner;
            this.service = service;
            closed = new AtomicBoolean();
        }

        protected void failOnClosed() {
            throw new IllegalStateException("This managed service has already been closed");
        }

        @Override
        public @NotNull S service() {
            if (closed.get()) failOnClosed();

            return service;
        }

        @Override
        public void onceReady(final @NonNull Consumer serviceCallback) {
            if (closed.get()) failOnClosed();

            addReadyCallback(owner, serviceCallback);
        }

        @Override
        public void close() {
            if (closed.compareAndSet(false, true)) markAsReady(owner);
            else failOnClosed();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy