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

org.jboss.threads.QueueExecutor Maven / Gradle / Ivy

/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2017 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jboss.threads;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.jboss.threads.management.BoundedQueueThreadPoolExecutorMBean;
import org.wildfly.common.Assert;

/**
 * An executor which uses a regular queue to hold tasks.  The executor may be tuned at runtime in many ways.
 *
 * @deprecated Use {@link EnhancedQueueExecutor} instead.
 */
@Deprecated
public final class QueueExecutor extends AbstractExecutorService implements BlockingExecutorService, BoundedQueueThreadPoolExecutorMBean, ShutdownListenable {

    private final SimpleShutdownListenable shutdownListenable = new SimpleShutdownListenable();

    private final Lock lock = new ReentrantLock();
    // signal when a task is written to the queue
    private final Condition enqueueCondition = lock.newCondition();
    // signal when the queue is read
    private final Condition removeCondition = lock.newCondition();
    // signalled when threads terminate
    private final Condition threadExitCondition = lock.newCondition();
    private final ThreadFactory threadFactory;
    private final DirectExecutor taskExecutor;

    // all protected by poolLock...
    private int coreThreads;
    private int maxThreads;
    private int largestPoolSize;
    private int rejectCount;
    private boolean allowCoreThreadTimeout;
    private long keepAliveTime;
    private TimeUnit keepAliveTimeUnit;
    private boolean blocking;
    private Executor handoffExecutor;

    private int threadCount;
    private Set workers = new HashSet();

    private boolean stop;

    private Queue queue;

    /**
     * Create a new instance.
     *
     * @param coreThreads the number of threads to create before enqueueing tasks
     * @param maxThreads the maximum number of threads to create
     * @param keepAliveTime the amount of time that an idle thread should remain active
     * @param keepAliveTimeUnit the unit of time for {@code keepAliveTime}
     * @param queue the queue to use for tasks
     * @param threadFactory the thread factory to use for new threads
     * @param blocking {@code true} if the executor should block when the queue is full and no threads are available, {@code false} to use the handoff executor
     * @param handoffExecutor the executor which is called when blocking is disabled and a task cannot be accepted, or {@code null} to reject the task
     * @param taskExecutor the executor to use to execute tasks
     */
    public QueueExecutor(final int coreThreads, final int maxThreads, final long keepAliveTime, final TimeUnit keepAliveTimeUnit, final Queue queue, final ThreadFactory threadFactory, final boolean blocking, final Executor handoffExecutor, final DirectExecutor taskExecutor) {
        Assert.checkNotNullParam("threadFactory", threadFactory);
        Assert.checkNotNullParam("queue", queue);
        Assert.checkNotNullParam("keepAliveTimeUnit", keepAliveTimeUnit);
        final Lock lock = this.lock;
        lock.lock();
        try {
            this.threadFactory = threadFactory;
            // configurable...
            this.keepAliveTime = keepAliveTime;
            this.keepAliveTimeUnit = keepAliveTimeUnit;
            this.coreThreads = coreThreads;
            this.maxThreads = maxThreads > coreThreads ? maxThreads : coreThreads;
            this.queue = queue;
            this.blocking = blocking;
            this.handoffExecutor = handoffExecutor;
            this.taskExecutor = taskExecutor;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Create a new instance.
     *
     * @param coreThreads the number of threads to create before enqueueing tasks
     * @param maxThreads the maximum number of threads to create
     * @param keepAliveTime the amount of time that an idle thread should remain active
     * @param keepAliveTimeUnit the unit of time for {@code keepAliveTime}
     * @param queue the queue to use for tasks
     * @param threadFactory the thread factory to use for new threads
     * @param blocking {@code true} if the executor should block when the queue is full and no threads are available, {@code false} to use the handoff executor
     * @param handoffExecutor the executor which is called when blocking is disabled and a task cannot be accepted, or {@code null} to reject the task
     */
    public QueueExecutor(final int coreThreads, final int maxThreads, final long keepAliveTime, final TimeUnit keepAliveTimeUnit, final Queue queue, final ThreadFactory threadFactory, final boolean blocking, final Executor handoffExecutor) {
        this(coreThreads, maxThreads, keepAliveTime, keepAliveTimeUnit, queue, threadFactory, blocking, handoffExecutor, JBossExecutors.directExecutor());
    }

    /**
     * Create a new instance.
     *
     * @param coreThreads the number of threads to create before enqueueing tasks
     * @param maxThreads the maximum number of threads to create
     * @param keepAliveTime the amount of time that an idle thread should remain active
     * @param keepAliveTimeUnit the unit of time for {@code keepAliveTime}
     * @param queueLength the fixed queue length to use for tasks
     * @param threadFactory the thread factory to use for new threads
     * @param blocking {@code true} if the executor should block when the queue is full and no threads are available, {@code false} to use the handoff executor
     * @param handoffExecutor the executor which is called when blocking is disabled and a task cannot be accepted, or {@code null} to reject the task
     */
    public QueueExecutor(final int coreThreads, final int maxThreads, final long keepAliveTime, final TimeUnit keepAliveTimeUnit, final int queueLength, final ThreadFactory threadFactory, final boolean blocking, final Executor handoffExecutor) {
        this(coreThreads, maxThreads, keepAliveTime, keepAliveTimeUnit, new ArrayQueue(queueLength), threadFactory, blocking, handoffExecutor);
    }

    /**
     * Execute a task.
     *
     * @param task the task to execute
     * @throws RejectedExecutionException when a task is rejected by the handoff executor
     * @throws StoppedExecutorException when the executor is terminating
     * @throws ExecutionInterruptedException when blocking is enabled and the current thread is interrupted before a task could be accepted
     */
    public void execute(final Runnable task) throws RejectedExecutionException {
        Assert.checkNotNullParam("task", task);
        final Executor executor;
        final Lock lock = this.lock;
        lock.lock();
        try {
            for (;;) {
                if (stop) {
                    throw Messages.msg.shutDownInitiated();
                }
                // Try core thread first, then queue, then extra thread
                final int count = threadCount;
                if (count < coreThreads) {
                    startNewThread(task);
                    threadCount = count + 1;
                    return;
                }
                // next queue...
                final Queue queue = this.queue;
                if (queue.offer(task)) {
                    enqueueCondition.signal();
                    return;
                }
                // extra threads?
                if (count < maxThreads) {
                    startNewThread(task);
                    threadCount = count + 1;
                    return;
                }
                if (blocking) {
                    try {
                        removeCondition.await();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw Messages.msg.executionInterrupted();
                    }
                } else {
                    // delegate the task outside of the lock.
                    rejectCount++;
                    executor = handoffExecutor;
                    break;
                }
            }
        } finally {
            lock.unlock();
        }
        if (executor != null) {
            executor.execute(task);
        } else {
            throw Messages.msg.executionRejected();
        }
        return;
    }

    /**
     * Execute a task, blocking until it can be accepted, or until the calling thread is interrupted.
     *
     * @param task the task to submit
     *
     * @throws org.jboss.threads.StoppedExecutorException if the executor was shut down before the task was accepted
     * @throws org.jboss.threads.ThreadCreationException if a thread could not be created for some reason
     * @throws java.util.concurrent.RejectedExecutionException if execution is rejected for some other reason
     * @throws InterruptedException if the current thread was interrupted before the task could be accepted
     * @throws NullPointerException if command is {@code null}
     */
    public void executeBlocking(final Runnable task) throws RejectedExecutionException, InterruptedException {
        Assert.checkNotNullParam("task", task);
        final Lock lock = this.lock;
        lock.lock();
        try {
            for (;;) {
                if (stop) {
                    throw Messages.msg.shutDownInitiated();
                }
                // Try core thread first, then queue, then extra thread
                final int count = threadCount;
                if (count < coreThreads) {
                    startNewThread(task);
                    threadCount = count + 1;
                    return;
                }
                // next queue...
                final Queue queue = this.queue;
                if (queue.offer(task)) {
                    enqueueCondition.signal();
                    return;
                }
                // extra threads?
                if (count < maxThreads) {
                    startNewThread(task);
                    threadCount = count + 1;
                    return;
                }
                removeCondition.await();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Execute a task, blocking until it can be accepted, a timeout elapses, or the calling thread is interrupted.
     *
     * @param task the task to submit
     * @param timeout the amount of time to wait
     * @param unit the unit of time
     *
     * @throws org.jboss.threads.ExecutionTimedOutException if the timeout elapsed before a task could be accepted
     * @throws org.jboss.threads.StoppedExecutorException if the executor was shut down before the task was accepted
     * @throws org.jboss.threads.ThreadCreationException if a thread could not be created for some reason
     * @throws java.util.concurrent.RejectedExecutionException if execution is rejected for some other reason
     * @throws InterruptedException if the current thread was interrupted before the task could be accepted
     * @throws NullPointerException if command is {@code null}
     */
    public void executeBlocking(final Runnable task, final long timeout, final TimeUnit unit) throws RejectedExecutionException, InterruptedException {
        Assert.checkNotNullParam("task", task);
        long now = System.currentTimeMillis();
        final long deadline = now + unit.toMillis(timeout);
        if (deadline < 0L) {
            executeBlocking(task);
            return;
        }
        final Lock lock = this.lock;
        lock.lock();
        try {
            for (;;) {
                if (stop) {
                    throw Messages.msg.shutDownInitiated();
                }
                // Try core thread first, then queue, then extra thread
                final int count = threadCount;
                if (count < coreThreads) {
                    startNewThread(task);
                    threadCount = count + 1;
                    return;
                }
                // next queue...
                final Queue queue = this.queue;
                if (queue.offer(task)) {
                    enqueueCondition.signal();
                    return;
                }
                // extra threads?
                if (count < maxThreads) {
                    startNewThread(task);
                    threadCount = count + 1;
                    return;
                }
                final long remaining = deadline - now;
                if (remaining <= 0L) {
                    throw Messages.msg.executionTimedOut();
                }
                removeCondition.await(remaining, TimeUnit.MILLISECONDS);
                now = System.currentTimeMillis();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Execute a task, without blocking.
     *
     * @param task the task to submit
     *
     * @throws org.jboss.threads.StoppedExecutorException if the executor was shut down before the task was accepted
     * @throws org.jboss.threads.ThreadCreationException if a thread could not be created for some reason
     * @throws java.util.concurrent.RejectedExecutionException if execution is rejected for some other reason
     * @throws NullPointerException if command is {@code null}
     */
    public void executeNonBlocking(final Runnable task) throws RejectedExecutionException {
        Assert.checkNotNullParam("task", task);
        final Executor executor;
        final Lock lock = this.lock;
        lock.lock();
        try {
            if (stop) {
                throw Messages.msg.shutDownInitiated();
            }
            // Try core thread first, then queue, then extra thread
            final int count = threadCount;
            if (count < coreThreads) {
                startNewThread(task);
                threadCount = count + 1;
                return;
            }
            // next queue...
            final Queue queue = this.queue;
            if (queue.offer(task)) {
                enqueueCondition.signal();
                return;
            }
            // extra threads?
            if (count < maxThreads) {
                startNewThread(task);
                threadCount = count + 1;
                return;
            }
            // delegate the task outside of the lock.
            rejectCount++;
            executor = handoffExecutor;
        } finally {
            lock.unlock();
        }
        if (executor != null) {
            executor.execute(task);
        }
        return;
    }

    /** {@inheritDoc} */
    public void shutdown() {
        boolean callShutdownListener = false;
        final Lock lock = this.lock;
        lock.lock();
        try {
            if (! stop) {
                stop = true;
                // wake up the whole town
                removeCondition.signalAll();
                enqueueCondition.signalAll();
                if (workers.isEmpty()) {
                    callShutdownListener = true;
                } else {
                    for (Thread worker : workers) {
                        worker.interrupt();
                    }
                }
            }
        } finally {
            lock.unlock();
            if (callShutdownListener)
                shutdownListenable.shutdown();
        }
    }

    /** {@inheritDoc} */
    public List shutdownNow() {
        boolean callShutdownListener = false;
        final Lock lock = this.lock;
        lock.lock();
        try {
            stop = true;
            removeCondition.signalAll();
            enqueueCondition.signalAll();
            if (workers.isEmpty()) {
                callShutdownListener = true;
            } else {
                for (Thread worker : workers) {
                    worker.interrupt();
                }
            }
            final Queue queue = this.queue;
            final ArrayList list = new ArrayList(queue);
            queue.clear();
            return list;
        } finally {
            lock.unlock();
            if (callShutdownListener)
                shutdownListenable.shutdown();
        }
    }

    /** {@inheritDoc} */
    public boolean isShutdown() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return stop;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public boolean isTerminated() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return stop && threadCount == 0;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public boolean awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException {
        final Lock lock = this.lock;
        lock.lockInterruptibly();
        try {
            if (workers.contains(Thread.currentThread())) {
                throw Messages.msg.cannotAwaitWithin();
            }
            final long start = System.currentTimeMillis();
            long elapsed = 0L;
            while (! stop && threadCount > 0) {
                final long remaining = timeout - elapsed;
                if (remaining <= 0) {
                    return false;
                }
                threadExitCondition.await(remaining, unit);
                elapsed = unit.convert(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public boolean isAllowCoreThreadTimeout() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return allowCoreThreadTimeout;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public void setAllowCoreThreadTimeout(boolean allowCoreThreadTimeout) {
        final Lock lock = this.lock;
        lock.lock();
        try {
            this.allowCoreThreadTimeout = allowCoreThreadTimeout;
            if (allowCoreThreadTimeout) {
                // wake everyone up so core threads can time out
                enqueueCondition.signalAll();
            }
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public int getCoreThreads() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return coreThreads;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public void setCoreThreads(final int coreThreads) {
        final Lock lock = this.lock;
        lock.lock();
        try {
            final int oldLimit = this.coreThreads;
            if (maxThreads < coreThreads) {
                // don't let the max thread limit be less than the core thread limit.
                // the called method will signal as needed
                setMaxThreads(coreThreads);
            } else if (oldLimit < coreThreads) {
                // we're growing the number of core threads
                // therefore signal anyone waiting to add tasks; there might be more threads to add
                removeCondition.signalAll();
            } else if (oldLimit > coreThreads) {
                // we're shrinking the number of core threads
                // therefore signal anyone waiting to remove tasks so the pool can shrink properly
                enqueueCondition.signalAll();
            } else {
                // we aren't changing anything...
                return;
            }
            this.coreThreads = coreThreads;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public int getMaxThreads() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return maxThreads;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public void setMaxThreads(final int maxThreads) {
        final Lock lock = this.lock;
        lock.lock();
        try {
            final int oldLimit = this.maxThreads;
            if (maxThreads < coreThreads) {
                // don't let the max thread limit be less than the core thread limit.
                // the called method will signal as needed
                setCoreThreads(maxThreads);
            } else if (oldLimit < maxThreads) {
                // we're growing the number of extra threads
                // therefore signal anyone waiting to add tasks; there might be more threads to add
                removeCondition.signalAll();
            } else if (oldLimit > maxThreads) {
                // we're shrinking the number of extra threads
                // therefore signal anyone waiting to remove tasks so the pool can shrink properly
                enqueueCondition.signalAll();
            } else {
                // we aren't changing anything...
                return;
            }
            this.maxThreads = maxThreads;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public long getKeepAliveTime() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return keepAliveTime;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Set the keep-alive time to the given amount of time.
     *
     * @param keepAliveTime the amount of time
     * @param keepAliveTimeUnit the unit of time
     */
    public void setKeepAliveTime(final long keepAliveTime, final TimeUnit keepAliveTimeUnit) {
        Assert.checkNotNullParam("keepAliveTimeUnit", keepAliveTimeUnit);
        Assert.checkMinimumParameter("keepAliveTime", 0L, keepAliveTime);
        final Lock lock = this.lock;
        lock.lock();
        try {
            this.keepAliveTime = keepAliveTimeUnit.convert(keepAliveTime, TimeUnit.MILLISECONDS);
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public void setKeepAliveTime(final long milliseconds) {
        setKeepAliveTime(milliseconds, TimeUnit.MILLISECONDS);
    }

    /**
     * Determine whether this thread pool executor is set to block when a task cannot be accepted immediately.
     *
     * @return {@code true} if blocking is enabled, {@code false} if the handoff executor is used
     */
    public boolean isBlocking() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return blocking;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Set whether this thread pool executor should be set to block when a task cannot be accepted immediately.
     *
     * @param blocking {@code true} if blocking is enabled, {@code false} if the handoff executor is used
     */
    public void setBlocking(boolean blocking) {
        final Lock lock = this.lock;
        lock.lock();
        try {
            this.blocking = blocking;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Get the handoff executor which is called when a task cannot be accepted immediately.
     *
     * @return the handoff executor
     */
    public Executor getHandoffExecutor() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return handoffExecutor;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Set the handoff executor which is called when a task cannot be accepted immediately.
     *
     * @param handoffExecutor the handoff executor
     */
    public void setHandoffExecutor(final Executor handoffExecutor) {
        final Lock lock = this.lock;
        lock.lock();
        try {
            this.handoffExecutor = handoffExecutor;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public  void addShutdownListener(final EventListener shutdownListener, final A attachment) {
        shutdownListenable.addShutdownListener(shutdownListener, attachment);
    }

    // call with lock held!
    private void startNewThread(final Runnable task) {
        final Thread thread = threadFactory.newThread(new Worker(task));
        if (thread == null) {
            throw Messages.msg.noThreadCreated();
        }
        workers.add(thread);
        final int size = workers.size();
        if (size > largestPoolSize) {
            largestPoolSize = size;
        }
        thread.start();
    }

    // call with lock held!
    private Runnable pollTask() {
        final Runnable task = queue.poll();
        if (task != null) {
            removeCondition.signal();
            return task;
        } else {
            if (-- threadCount == 0) {
                threadExitCondition.signalAll();
            }
            return null;
        }
    }

    // call with lock held!
    private Runnable takeTask() {
        final Condition removeCondition = this.removeCondition;
        Runnable task = queue.poll();
        if (task != null) {
            removeCondition.signal();
            return task;
        } else {
            final Condition enqueueCondition = this.enqueueCondition;
            final long start = System.currentTimeMillis();
            boolean intr = Thread.interrupted();
            try {
                long elapsed = 0L;
                for (;;) {
                    // these parameters may change on each iteration
                    final int threadCount = this.threadCount;
                    final int coreThreadLimit = coreThreads;
                    final boolean allowCoreThreadTimeout = this.allowCoreThreadTimeout;
                    if (stop || threadCount > maxThreads) {
                        // too many threads.  Handle a task if there is one, otherwise exit
                        return pollTask();
                    } else if (allowCoreThreadTimeout || threadCount > coreThreadLimit) {
                        final TimeUnit timeUnit = keepAliveTimeUnit;
                        final long time = keepAliveTime;
                        final long remaining = time - timeUnit.convert(elapsed, TimeUnit.MILLISECONDS);
                        if (remaining > 0L) {
                            try {
                                enqueueCondition.await(remaining, timeUnit);
                            } catch (InterruptedException e) {
                                intr = true;
                            }
                        } else {
                            // the timeout has expired
                            return pollTask();
                        }
                    } else {
                        // ignore timeout until we are not a core thread or until core threads are allowed to time out
                        try {
                            enqueueCondition.await();
                        } catch (InterruptedException e) {
                            intr = true;
                        }
                    }
                    task = queue.poll();
                    if (task != null) {
                        removeCondition.signal();
                        return task;
                    }
                    elapsed = System.currentTimeMillis() - start;
                }
            } finally {
                if (intr) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    /** {@inheritDoc} */
    public int getCurrentThreadCount() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return workers.size();
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public int getLargestThreadCount() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return largestPoolSize;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public int getRejectedCount() {
        final Lock lock = this.lock;
        lock.lock();
        try {
            return rejectCount;
        } finally {
            lock.unlock();
        }
    }

    /** {@inheritDoc} */
    public int getQueueSize() {
        return this.queue.size();
    }

    private void runTask(Runnable task) {
        if (task != null) try {
            taskExecutor.execute(task);
        } catch (Throwable t) {
            Messages.msg.executionFailed(t, task);
        }
    }

    private class Worker implements Runnable {

        private volatile Runnable first;

        public Worker(final Runnable command) {
            first = command;
        }

        public void run() {
            final Lock lock = QueueExecutor.this.lock;
            try {
                Runnable task = first;
                // Release reference to task
                first = null;
                runTask(task);
                for (;;) {
                    // don't hang on to task while we possibly block waiting for the next one
                    task = null;
                    lock.lock();
                    try {
                        if (stop) {
                            // drain queue
                            if ((task = pollTask()) == null) {
                                return;
                            }
                            Thread.currentThread().interrupt();
                        } else {
                            // get next task
                            if ((task = takeTask()) == null) {
                                return;
                            }
                        }
                    } finally {
                        lock.unlock();
                    }
                    runTask(task);
                    Thread.interrupted();
                }
            } finally {
                boolean last = false;
                lock.lock();
                try {
                    workers.remove(Thread.currentThread());
                    last = stop && workers.isEmpty();
                } finally {
                    lock.unlock();
                }
                if (last) {
                    shutdownListenable.shutdown();
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy