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

org.jvnet.hk2.config.Transactions Maven / Gradle / Ivy

There is a newer version: 7.2024.1.Alpha1
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2007-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package org.jvnet.hk2.config;

import java.util.*;
import java.util.logging.Logger;
import java.util.logging.Level;

import java.beans.PropertyChangeEvent;
import java.lang.reflect.Proxy;
import java.util.concurrent.*;

import org.jvnet.hk2.annotations.Optional;
import org.jvnet.hk2.annotations.Service;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Provider;

import org.glassfish.hk2.api.PostConstruct;
import org.glassfish.hk2.api.PreDestroy;

/**
 * Transactions is a singleton service that receives transaction notifications and dispatch these
 * notifications asynchronously to listeners.
 *
 * @author Jerome Dochez
 */

@Service
public final class Transactions implements PostConstruct, PreDestroy {

    // each transaction listener has a notification pump.
    private final List>> listeners =
            new ArrayList>>();

    private final Map> typeListeners = new HashMap>();

    @Inject @Named("transactions-executor") @Optional
    private ExecutorService executor;

    // all configuration listeners are notified though one notifier.
    private final Provider configListenerNotifier = new Provider() {

            private final ConfigListenerNotifier configListenerNotifier = new ConfigListenerNotifier();
            private final CountDownLatch initialized = new CountDownLatch(1);

            public ConfigListenerNotifier get() {
                //synchronized(initialized) {
                    if (initialized.getCount()>0) {
                        configListenerNotifier.start();
                        initialized.countDown();
                    }

                return configListenerNotifier;
            //}
        }
    };

    public void postConstruct() {
        if (executor==null) {
            executor = Executors.newCachedThreadPool();
        }
    }

    public void preDestroy() {
       for (Provider> listener : listeners) {
           listener.get().stop();
       }
       configListenerNotifier.get().stop();
       executor.shutdown();
    }

    /**
     * Abstract notification pump, it adds jobs to the queue and process them in the order
     * jobs were added.
     *
     * Jobs are just a wrapper for events of type  and a notification mechanism for
     * completion notification
     *
     * @param  type of listener interface
     * @param  type of events the listener methods are expecting
     * @param  return type of the listener interface methods.
     */
    private abstract class Notifier {
        
        private final BlockingQueue pendingJobs = new ArrayBlockingQueue(50);
        private CountDownLatch latch = new CountDownLatch(1);

        /**
         * Creates the task that will notify the listeners of a particular job.
         * @param job contains the specifics of the notification like the events that need to be notified.
         * @return a task that can be run and return an optional value.
         */
        protected abstract FutureTask prepare(final Job job);

        /**
         * Adds a job to the notification pump. This job will be processed as soon as all other pending
         * jobs have completed.
         *
         * @param job new notification job.
         * @return a future on the return value.
         */
        public Future add(final Job job) {
                
            // NOTE that this is put() which blocks, *not* add() which will not block and will
            // throw an IllegalStateException if the queue is full.
            if (latch.getCount()==0) {
                throw new RuntimeException("TransactionListener is inactive, yet jobs are published to it");
            }
            try {
                pendingJobs.put(prepare(job));
            } catch (InterruptedException e ) {
                throw new RuntimeException(e);
            }
            return null;
        }
        
        protected void start() {

            executor.submit(new Runnable() {

                public void run() {
                    while (latch.getCount()>0) {
                        try {
                            final FutureTask job = pendingJobs.take();
                            // when listeners start a transaction themselves, several jobs try to get published
                            // simultaneously so we cannot block the pump while delivering the messages. 
                            executor.submit(new Runnable() {
                                public void run() {
                                    job.run();
                                }
                            });
                        }

                        catch (InterruptedException e) {
                            // do anything here?
                        }
                    }
                }
                
            });
        }

        void stop() {
            latch.countDown();
            // last event to force the close
            pendingJobs.add(prepare(new Job(null, null) {
                public V process(T target) {
                    return null;
                }
            }));
        }
    }

    /**
     * Default listener notification pump. One thread per listener, jobs processed in
     * the order it was received.
     *
     * @param  type of listener interface
     * @param  type of events the listener methods are expecting
     * @param  return type of the listener interface methods.
     */
    private class ListenerNotifier extends Notifier {
        
        final T listener;

        public ListenerNotifier(T listener) {
            this.listener = listener;
        }

        protected FutureTask prepare(final Job job) {
            return new FutureTask(new Callable() {
                    public V call() throws Exception {
                        try {
                            if ( job.mEvents.size() != 0 ) {
                                return job.process(listener);
                           }
                        } finally {
                            job.releaseLatch();
                        }
                        return null;
                    }
                });
        }   
        
    }

    /**
     * Configuration listener notification pump. All Listeners are notified within their own thread, only on thread
     * takes care of the job pump.
     * 
     */
    private class ConfigListenerNotifier extends Notifier {

        protected FutureTask
            prepare(final Job job) {

        // first, calculate the recipients.
        final Set configListeners = new HashSet();
            if (job.mEvents != null) {
                for (PropertyChangeEvent event : job.mEvents) {
                    final Dom dom = (Dom) ((ConfigView) Proxy.getInvocationHandler(event.getSource())).getMasterView();
                    configListeners.addAll(dom.getListeners());

                    // we also notify the parent.
                    if (dom.parent()!=null) {
                        configListeners.addAll(dom.parent().getListeners());
                    }

                    // and now, notify all listeners for the changed types.
                    Set listeners = typeListeners.get(dom.getProxyType());
                    if (listeners!=null) {
                        configListeners.addAll(listeners);
                    }

                    // we need to check if elements are removed to ensure
                    // the typed listeners are notified.
                    if (event.getNewValue()==null) {
                        Object oldValue = event.getOldValue();
                        if (oldValue instanceof ConfigBeanProxy) {
                            Dom domOldValue = Dom.unwrap((ConfigBeanProxy) oldValue);
                            Set typedListeners = typeListeners.get(domOldValue.getProxyType());
                            if (typedListeners!=null) {
                                configListeners.addAll(typedListeners);
                            }
                        }
                    }
                }
            }

            return new FutureTask(new Callable() {
            public UnprocessedChangeEvents call() throws Exception {

                try {
                    // temporary structure to store our future notifications with pointer to the
                    // originator config listener
                    Map, ConfigListener> futures = new HashMap, ConfigListener>();

                    for (final ConfigListener listener : configListeners) {
                        // each listener is notified in it's own thread.
                        futures.put(executor.submit(new Callable() {
                            public UnprocessedChangeEvents call() throws Exception {
                                UnprocessedChangeEvents e = job.process(listener);
                                return e;

                            }
                        }), listener);
                    }
                    List unprocessed = new ArrayList(futures.size());
                    for (Map.Entry, ConfigListener> futureEntry : futures.entrySet()) {
                        Future future = futureEntry.getKey();
                        try {
                            UnprocessedChangeEvents result = future.get(200, TimeUnit.SECONDS);
                            if (result!=null && result.getUnprocessed()!=null && result.getUnprocessed().size()>0) {
                                for (UnprocessedChangeEvent event : result.getUnprocessed()) {
                                    Logger.getAnonymousLogger().log(Level.WARNING, "Unprocessed event : " + event);
                                }
                                unprocessed.add(result);
                            }
                        } catch (InterruptedException e) {
                            Logger.getAnonymousLogger().log(Level.SEVERE, "Config Listener notification got interrupted", e);
                        } catch (ExecutionException e) {
                            Logger.getAnonymousLogger().log(Level.SEVERE, "Config Listener notification got interrupted", e);
                        } catch (TimeoutException e) {
                            ConfigListener cl = futureEntry.getValue();
                            Logger.getAnonymousLogger().log(Level.SEVERE, "Config Listener " + cl.getClass() + " notification took too long", e);
                        }
                    }

                    // all notification have been successful, I just need to notify the unprocessed events.
                    // note these events are always synchronous so far.
                    if (!unprocessed.isEmpty()) {
                        Job unprocessedJob = new UnprocessedEventsJob(unprocessed, null);
                        for (Provider> listener : Transactions.this.listeners) {
                            listener.get().add(unprocessedJob);
                        }
                    }
                } finally {
                    job.releaseLatch();
                }

                // in theory I should aggregate my unprocessed events but nobody cares.
                return null;
            }
        });
    }

    }


    /**
        A job contains an optional CountdownLatch so that a caller can learn when the
        transaction has "cleared" by blocking until that time.
     */
    private abstract static class Job {

        private final CountDownLatch mLatch;
        protected final List mEvents;
        
        public Job(List events, final CountDownLatch latch ) {
            mLatch  = latch;
            mEvents = events;
        }
        
        public void waitForLatch() throws InterruptedException {
            if ( mLatch != null ) {
                mLatch.await();
            }
        }
        
        public void releaseLatch() {
            if ( mLatch != null ) {
                mLatch.countDown();
            }
        }
        
        public abstract V process(T target);
    }
    
    private static class TransactionListenerJob extends Job {

        public TransactionListenerJob(List events, CountDownLatch latch) {
            super(events,  latch);
        }
        
        @Override
        public Void process(TransactionListener listener) {
            try {
                listener.transactionCommited(mEvents);
            } catch(Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }

    private static class UnprocessedEventsJob extends Job {

        public UnprocessedEventsJob(List events, CountDownLatch latch) {
            super(events, latch);
        }

        @Override
        public Void process(TransactionListener listener) {
            try {
                listener.unprocessedTransactedEvents(mEvents);
            } catch(Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }

    private class ConfigListenerJob extends Job {

        final PropertyChangeEvent[] eventsArray;

        public ConfigListenerJob(List events, CountDownLatch latch) {
            super(events, latch);
            eventsArray = mEvents.toArray(new PropertyChangeEvent[mEvents.size()]);            
        }

        public UnprocessedChangeEvents process(ConfigListener target) {
            return target.changed(eventsArray);
        }
    }

    /**
     * adds a listener for a particular config type
     * @param listenerType the config type
     * @param listener the config listener
     */
    public synchronized void addListenerForType(Class listenerType, ConfigListener listener) {
        Set listeners = typeListeners.get(listenerType);
        if (listeners==null) {
            listeners = new HashSet();
            typeListeners.put(listenerType, listeners);
        }
        listeners.add(listener);
    }

    /**
     * removes a listener for a particular config type
     *
     * @param listenerType the config type
     * @param listener the config listener
     * @return true if the listener was removed successfully, false otherwise.
     */
    public synchronized boolean removeListenerForType(Class listenerType, ConfigListener listener) {
        Set listeners = typeListeners.get(listenerType);
        if (listeners==null) {
            return false;
        }
        return listeners.remove(listener);


    }
    /**
     * add a new listener to all transaction events.
     *
     * @param listener to be added.
     */
    public void addTransactionsListener(final TransactionListener listener) {
        synchronized(listeners) {
            listeners.add(new Provider>() {

                final ListenerNotifier tsListener = new ListenerNotifier(listener);
                final CountDownLatch initialized = new CountDownLatch(1);

                public ListenerNotifier get() {
                    //synchronized(initialized) {
                        if (initialized.getCount()>0) {
                            tsListener.start();
                            initialized.countDown();
                        }
                    //}
                    return tsListener;
                }
            });
        }
    }

    /**
     * Removes an existing listener for transaction events
     * @param listener the registered listener
     * @return true if the listener unregistration was successful
     */
    public boolean removeTransactionsListener(TransactionListener listener) {
        synchronized(listeners) {
            for (Provider> holder : listeners) {
                ListenerNotifier info = holder.get();
                if (info.listener==listener) {
                    info.stop();
                    return listeners.remove(holder);
                }
            }
        }
        return false;
    }
    
    public List currentListeners() {
        synchronized(listeners) {            
            List l = new ArrayList();
            for (Provider> holder : listeners) {
                ListenerNotifier info = holder.get();
                l.add(info.listener);
            }
            return l;
        }
    }


    /**
     * Synchronous notification of a new transactional configuration change operation.
     *
     * @param events list of changes 
     */
    void addTransaction( final List events) {
        addTransaction(events, true);
    }
        
    /**
     * Notification of a new transaction completion
     *
     * @param events accumulated list of changes
     * @param waitTillCleared  synchronous semantics; wait until all change events are sent
     */
    @SuppressWarnings("cast")
    void addTransaction(
        final List events,
        final boolean waitTillCleared ) {
        
        final List> listInfos = new ArrayList>();
        for (Provider> holder : listeners) {
            ListenerNotifier info = holder.get();
            listInfos.add(info);
        }
        
        // create a CountDownLatch to implement waiting for events to actually be sent
        final Job job = new TransactionListenerJob( events,
                                waitTillCleared ? new CountDownLatch(listInfos.size()) : null);
        
        final ConfigListenerJob configJob = new ConfigListenerJob(events,
                waitTillCleared? new CountDownLatch(1):null);
        
        // NOTE that this is put() which blocks, *not* add() which will not block and will
        // throw an IllegalStateException if the queue is full.
        try {
            for (ListenerNotifier listener : listInfos) {
                listener.add(job);
            }

            configListenerNotifier.get().add(configJob);

            job.waitForLatch();
            configJob.waitForLatch();
        } catch (InterruptedException e ) {
            throw new RuntimeException(e);
        }
    }

    public void waitForDrain() {
        // insert a dummy Job and block until is has been processed.  This guarantees
        // that all prior jobs have finished
        addTransaction( new ArrayList(), true );
        // at this point all prior transactions are guaranteed to have cleared
    }    
}