001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2016-2026 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
013 * License for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.core.internal;
020
021import java.util.Collections;
022import java.util.IdentityHashMap;
023import java.util.Map;
024import java.util.logging.Level;
025import java.util.logging.Logger;
026import org.jgrapes.core.ComponentType;
027
028/**
029 * A registry for generators. Used to track generators and determine
030 * whether the application has stopped.
031 */
032@SuppressWarnings({ "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal",
033    "PMD.AvoidSynchronizedStatement" })
034public class GeneratorRegistry {
035
036    @SuppressWarnings("PMD.FieldNamingConventions")
037    private static final Logger generatorTracking
038        = Logger.getLogger(ComponentType.class.getPackage().getName()
039            + ".generatorTracking");
040
041    private long running;
042    private Thread keepAlive;
043    private Map<Object, Object> generators;
044
045    /**
046     * Holds a generator instance.
047     */
048    private static final class InstanceHolder {
049        private static final GeneratorRegistry INSTANCE
050            = new GeneratorRegistry();
051    }
052
053    private GeneratorRegistry() {
054        if (generatorTracking.isLoggable(Level.FINE)) {
055            generators = Collections.synchronizedMap(new IdentityHashMap<>());
056        }
057    }
058
059    /**
060     * Returns the singleton instance of the registry.
061     *
062     * @return the generator registry
063     */
064    public static GeneratorRegistry instance() {
065        return InstanceHolder.INSTANCE;
066    }
067
068    /**
069     * Adds a generator.
070     *
071     * @param obj the obj
072     */
073    public void add(Object obj) {
074        synchronized (this) {
075            running += 1;
076            if (generators != null) {
077                generators.put(obj, null);
078                generatorTracking.finest(() -> "Added generator " + obj
079                    + ", " + generators.size() + " generators registered: "
080                    + generators.keySet());
081            }
082            if (running == 1) { // NOPMD, no, not using a constant for this.
083                keepAlive = new Thread("GeneratorRegistry") {
084                    @Override
085                    @SuppressWarnings("PMD.EmptyCatchBlock")
086                    public void run() {
087                        try {
088                            while (true) {
089                                sleep(Long.MAX_VALUE);
090                            }
091                        } catch (InterruptedException e) {
092                            // Okay, then stop
093                        }
094                    }
095                };
096                keepAlive.start();
097            }
098        }
099    }
100
101    /**
102     * Removes the generator.
103     *
104     * @param obj the generator
105     */
106    public void remove(Object obj) {
107        synchronized (this) {
108            running -= 1;
109            if (generators != null) {
110                generators.remove(obj);
111                generatorTracking.finest(() -> "Removed generator " + obj
112                    + ", " + generators.size() + " generators registered: "
113                    + generators.keySet());
114            }
115            if (running == 0) {
116                generatorTracking
117                    .finest(() -> "Zero generators, notifying all.");
118                keepAlive.interrupt();
119                notifyAll();
120            }
121        }
122    }
123
124    /**
125     * Checks if is exhausted (no generators left)
126     *
127     * @return true, if is exhausted
128     */
129    public boolean isExhausted() {
130        return running == 0;
131    }
132
133    /**
134     * Await exhaustion.
135     *
136     * @throws InterruptedException the interrupted exception
137     */
138    @SuppressWarnings({ "PMD.GuardLogStatement" })
139    public void awaitExhaustion() throws InterruptedException {
140        if (generators != null) {
141            synchronized (this) {
142                if (running != generators.size()) {
143                    generatorTracking
144                        .severe(() -> "Generator count doesn't match tracked.");
145                }
146            }
147        }
148        while (running > 0) {
149            // generators.keySet() may call EventProcessor.toString()
150            // which locks on the EventProcessor which may want a lock
151            // on the registry (deadlock). So keep this out of the
152            // synchronized.
153            if (generators != null) {
154                generatorTracking
155                    .fine(() -> "Thread " + Thread.currentThread().getName()
156                        + " is waiting, " + generators.size()
157                        + " generators registered: "
158                        + generators.keySet());
159            }
160            synchronized (this) {
161                if (running > 0) {
162                    wait();
163                }
164            }
165        }
166        generatorTracking
167            .finest("Thread " + Thread.currentThread().getName()
168                + " continues.");
169    }
170
171    /**
172     * Await exhaustion with a timeout.
173     *
174     * @param timeout the timeout
175     * @return true, if successful
176     * @throws InterruptedException the interrupted exception
177     */
178    @SuppressWarnings({ "PMD.CollapsibleIfStatements" })
179    public boolean awaitExhaustion(long timeout)
180            throws InterruptedException {
181        synchronized (this) {
182            if (generators != null) {
183                if (running != generators.size()) {
184                    generatorTracking.severe(
185                        "Generator count doesn't match tracked.");
186                }
187            }
188            if (isExhausted()) {
189                return true;
190            }
191            if (generators != null) {
192                generatorTracking
193                    .fine(() -> "Waiting, generators: " + generators.keySet());
194            }
195            wait(timeout);
196            if (generators != null) {
197                generatorTracking
198                    .fine(() -> "Waited, generators: " + generators.keySet());
199            }
200            return isExhausted();
201        }
202    }
203}