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

org.eclipse.jetty.io.ManagedSelector Maven / Gradle / Ivy

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
// 
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// 
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
// 
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// 
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
// 
package org.eclipse.jetty.io;

import java.io.Closeable;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.statistic.SampleStatistic;
import org.eclipse.jetty.util.thread.ExecutionStrategy;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.thread.strategy.EatWhatYouKill;

/**
 *  

{@link ManagedSelector} wraps a {@link Selector} simplifying non-blocking operations on channels.

*

{@link ManagedSelector} runs the select loop, which waits on {@link Selector#select()} until events * happen for registered channels. When events happen, it notifies the {@link EndPoint} associated * with the channel.

* * @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. */ @Deprecated(since = "2021-05-27") public class ManagedSelector extends ContainerLifeCycle implements Dumpable { private static final Logger LOG = Log.getLogger(ManagedSelector.class); private static final boolean FORCE_SELECT_NOW; static { String property = System.getProperty("org.eclipse.jetty.io.forceSelectNow"); if (property != null) { FORCE_SELECT_NOW = Boolean.parseBoolean(property); } else { property = System.getProperty("os.name"); FORCE_SELECT_NOW = property != null && property.toLowerCase(Locale.ENGLISH).contains("windows"); } } private final AtomicBoolean _started = new AtomicBoolean(false); private boolean _selecting; private final SelectorManager _selectorManager; private final int _id; private final ExecutionStrategy _strategy; private Selector _selector; private Deque _updates = new ArrayDeque<>(); private Deque _updateable = new ArrayDeque<>(); private final SampleStatistic _keyStats = new SampleStatistic(); public ManagedSelector(SelectorManager selectorManager, int id) { _selectorManager = selectorManager; _id = id; SelectorProducer producer = new SelectorProducer(); Executor executor = selectorManager.getExecutor(); _strategy = new EatWhatYouKill(producer, executor); addBean(_strategy, true); setStopTimeout(5000); } public Selector getSelector() { return _selector; } @Override protected void doStart() throws Exception { super.doStart(); _selector = _selectorManager.newSelector(); // The producer used by the strategies will never // be idle (either produces a task or blocks). // The normal strategy obtains the produced task, schedules // a new thread to produce more, runs the task and then exits. _selectorManager.execute(_strategy::produce); // Set started only if we really are started Start start = new Start(); submit(start); start._started.await(); } @Override protected void doStop() throws Exception { // doStop might be called for a failed managedSelector, // We do not want to wait twice, so we only stop once for each start if (_started.compareAndSet(true, false) && _selector != null) { // Close connections, but only wait a single selector cycle for it to take effect CloseConnections closeConnections = new CloseConnections(); submit(closeConnections); closeConnections._complete.await(); // Wait for any remaining endpoints to be closed and the selector to be stopped StopSelector stopSelector = new StopSelector(); submit(stopSelector); stopSelector._stopped.await(); } super.doStop(); } @ManagedAttribute(value = "Total number of keys", readonly = true) public int getTotalKeys() { return _selector.keys().size(); } @ManagedAttribute(value = "Average number of selected keys", readonly = true) public double getAverageSelectedKeys() { return _keyStats.getMean(); } @ManagedAttribute(value = "Maximum number of selected keys", readonly = true) public double getMaxSelectedKeys() { return _keyStats.getMax(); } @ManagedAttribute(value = "Total number of select() calls", readonly = true) public long getSelectCount() { return _keyStats.getCount(); } @ManagedOperation(value = "Resets the statistics", impact = "ACTION") public void resetStats() { _keyStats.reset(); } protected int nioSelect(Selector selector, boolean now) throws IOException { return now ? selector.selectNow() : selector.select(); } protected int select(Selector selector) throws IOException { try { int selected = nioSelect(selector, false); if (selected == 0) { if (LOG.isDebugEnabled()) LOG.debug("Selector {} woken with none selected", selector); if (Thread.interrupted() && !isRunning()) throw new ClosedSelectorException(); if (FORCE_SELECT_NOW) selected = nioSelect(selector, true); } return selected; } catch (ClosedSelectorException x) { throw x; } catch (Throwable x) { handleSelectFailure(selector, x); return 0; } } protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException { LOG.info("Caught select() failure, trying to recover: {}", failure.toString()); if (LOG.isDebugEnabled()) LOG.debug(failure); Selector newSelector = _selectorManager.newSelector(); for (SelectionKey oldKey : selector.keys()) { SelectableChannel channel = oldKey.channel(); int interestOps = safeInterestOps(oldKey); if (interestOps >= 0) { try { Object attachment = oldKey.attachment(); SelectionKey newKey = channel.register(newSelector, interestOps, attachment); if (attachment instanceof Selectable) ((Selectable) attachment).replaceKey(newKey); oldKey.cancel(); if (LOG.isDebugEnabled()) LOG.debug("Transferred {} iOps={} att={}", channel, interestOps, attachment); } catch (Throwable t) { if (LOG.isDebugEnabled()) LOG.debug("Could not transfer {}", channel, t); IO.close(channel); } } else { if (LOG.isDebugEnabled()) LOG.debug("Invalid interestOps for {}", channel); IO.close(channel); } } IO.close(selector); _selector = newSelector; } protected void onSelectFailed(Throwable cause) { // override to change behavior } public int size() { Selector s = _selector; if (s == null) return 0; Set keys = s.keys(); if (keys == null) return 0; return keys.size(); } /** * Submit an {@link SelectorUpdate} to be acted on between calls to {@link Selector#select()} * * @param update The selector update to apply at next wakeup */ public void submit(SelectorUpdate update) { submit(update, false); } private void submit(SelectorUpdate update, boolean lazy) { if (LOG.isDebugEnabled()) LOG.debug("Queued change lazy={} {} on {}", lazy, update, this); Selector selector = null; synchronized (ManagedSelector.this) { _updates.offer(update); if (_selecting && !lazy) { selector = _selector; // To avoid the extra select wakeup. _selecting = false; } } if (selector != null) { if (LOG.isDebugEnabled()) LOG.debug("Wakeup on submit {}", this); selector.wakeup(); } } private void wakeup() { if (LOG.isDebugEnabled()) LOG.debug("Wakeup {}", this); Selector selector = null; synchronized (ManagedSelector.this) { if (_selecting) { selector = _selector; _selecting = false; } } if (selector != null) selector.wakeup(); } private void execute(Runnable task) { try { _selectorManager.execute(task); } catch (RejectedExecutionException x) { if (task instanceof Closeable) IO.close((Closeable) task); } } private void processConnect(SelectionKey key, Connect connect) { SelectableChannel channel = key.channel(); try { key.attach(connect.attachment); boolean connected = _selectorManager.doFinishConnect(channel); if (LOG.isDebugEnabled()) LOG.debug("Connected {} {}", connected, channel); if (connected) { if (connect.timeout.cancel()) { key.interestOps(0); execute(new CreateEndPoint(connect, key)); } else { throw new SocketTimeoutException("Concurrent Connect Timeout"); } } else { throw new ConnectException(); } } catch (Throwable x) { connect.failed(x); } } protected void endPointOpened(EndPoint endPoint) { _selectorManager.endPointOpened(endPoint); } protected void endPointClosed(EndPoint endPoint) { _selectorManager.endPointClosed(endPoint); } private void createEndPoint(SelectableChannel channel, SelectionKey selectionKey) throws IOException { EndPoint endPoint = _selectorManager.newEndPoint(channel, this, selectionKey); Connection connection = _selectorManager.newConnection(channel, endPoint, selectionKey.attachment()); endPoint.setConnection(connection); submit(selector -> { SelectionKey key = selectionKey; if (key.selector() != selector) { key = channel.keyFor(selector); if (key != null && endPoint instanceof Selectable) ((Selectable) endPoint).replaceKey(key); } if (key != null) key.attach(endPoint); }, true); endPoint.onOpen(); endPointOpened(endPoint); _selectorManager.connectionOpened(connection); if (LOG.isDebugEnabled()) LOG.debug("Created {}", endPoint); } void destroyEndPoint(EndPoint endPoint) { // Waking up the selector is necessary to clean the // cancelled-key set and tell the TCP stack that the // socket is closed (so that senders receive RST). wakeup(); execute(new DestroyEndPoint(endPoint)); } private int getActionSize() { synchronized (ManagedSelector.this) { return _updates.size(); } } static int safeReadyOps(SelectionKey selectionKey) { try { return selectionKey.readyOps(); } catch (Throwable x) { LOG.ignore(x); return -1; } } static int safeInterestOps(SelectionKey selectionKey) { try { return selectionKey.interestOps(); } catch (Throwable x) { LOG.ignore(x); return -1; } } @Override public void dump(Appendable out, String indent) throws IOException { List keys; List updates; Selector selector = _selector; if (selector != null && selector.isOpen()) { DumpKeys dump = new DumpKeys(); String updatesAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()); synchronized (ManagedSelector.this) { updates = new ArrayList<>(_updates); _updates.addFirst(dump); _selecting = false; } if (LOG.isDebugEnabled()) LOG.debug("wakeup on dump {}", this); selector.wakeup(); keys = dump.get(5, TimeUnit.SECONDS); String keysAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()); if (keys == null) keys = Collections.singletonList("No dump keys retrieved"); dumpObjects(out, indent, new DumpableCollection("updates @ " + updatesAt, updates), new DumpableCollection("keys @ " + keysAt, keys)); } else { dumpObjects(out, indent); } } @Override public String toString() { Selector selector = _selector; return String.format("%s id=%s keys=%d selected=%d updates=%d", super.toString(), _id, selector != null && selector.isOpen() ? selector.keys().size() : -1, selector != null && selector.isOpen() ? selector.selectedKeys().size() : -1, getActionSize()); } /** * A {@link Selectable} is an {@link EndPoint} that wish to be * notified of non-blocking events by the {@link ManagedSelector}. * * @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. */ @Deprecated(since = "2021-05-27") public interface Selectable { /** * Callback method invoked when a read or write events has been * detected by the {@link ManagedSelector} for this endpoint. * * @return a job that may block or null */ Runnable onSelected(); /** * Callback method invoked when all the keys selected by the * {@link ManagedSelector} for this endpoint have been processed. */ void updateKey(); /** * Callback method invoked when the SelectionKey is replaced * because the channel has been moved to a new selector. * * @param newKey the new SelectionKey */ void replaceKey(SelectionKey newKey); } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") private class SelectorProducer implements ExecutionStrategy.Producer { private Set _keys = Collections.emptySet(); private Iterator _cursor = Collections.emptyIterator(); @Override public Runnable produce() { while (true) { Runnable task = processSelected(); if (task != null) return task; processUpdates(); updateKeys(); if (!select()) return null; } } private void processUpdates() { synchronized (ManagedSelector.this) { Deque updates = _updates; _updates = _updateable; _updateable = updates; } if (LOG.isDebugEnabled()) LOG.debug("updateable {}", _updateable.size()); for (SelectorUpdate update : _updateable) { if (_selector == null) break; try { if (LOG.isDebugEnabled()) LOG.debug("update {}", update); update.update(_selector); } catch (Throwable ex) { LOG.warn(ex); } } _updateable.clear(); Selector selector; int updates; synchronized (ManagedSelector.this) { updates = _updates.size(); _selecting = updates == 0; selector = _selecting ? null : _selector; } if (LOG.isDebugEnabled()) LOG.debug("updates {}", updates); if (selector != null) { if (LOG.isDebugEnabled()) LOG.debug("wakeup on updates {}", this); selector.wakeup(); } } private boolean select() { try { Selector selector = _selector; if (selector != null) { if (LOG.isDebugEnabled()) LOG.debug("Selector {} waiting with {} keys", selector, selector.keys().size()); int selected = ManagedSelector.this.select(selector); // The selector may have been recreated. selector = _selector; if (selector != null) { if (LOG.isDebugEnabled()) LOG.debug("Selector {} woken up from select, {}/{}/{} selected", selector, selected, selector.selectedKeys().size(), selector.keys().size()); int updates; synchronized (ManagedSelector.this) { // finished selecting _selecting = false; updates = _updates.size(); } _keys = selector.selectedKeys(); int selectedKeys = _keys.size(); if (selectedKeys > 0) _keyStats.record(selectedKeys); _cursor = selectedKeys > 0 ? _keys.iterator() : Collections.emptyIterator(); if (LOG.isDebugEnabled()) LOG.debug("Selector {} processing {} keys, {} updates", selector, selectedKeys, updates); return true; } } } catch (Throwable x) { IO.close(_selector); _selector = null; if (isRunning()) { LOG.warn("Fatal select() failure", x); onSelectFailed(x); } else { LOG.warn(x.toString()); if (LOG.isDebugEnabled()) LOG.debug(x); } } return false; } private Runnable processSelected() { while (_cursor.hasNext()) { SelectionKey key = _cursor.next(); Object attachment = key.attachment(); SelectableChannel channel = key.channel(); if (key.isValid()) { if (LOG.isDebugEnabled()) LOG.debug("selected {} {} {} ", safeReadyOps(key), key, attachment); try { if (attachment instanceof Selectable) { // Try to produce a task Runnable task = ((Selectable) attachment).onSelected(); if (task != null) return task; } else if (key.isConnectable()) { processConnect(key, (Connect) attachment); } else { throw new IllegalStateException("key=" + key + ", att=" + attachment + ", iOps=" + safeInterestOps(key) + ", rOps=" + safeReadyOps(key)); } } catch (CancelledKeyException x) { if (LOG.isDebugEnabled()) LOG.debug("Ignoring cancelled key for channel {}", channel); IO.close(attachment instanceof EndPoint ? (EndPoint) attachment : channel); } catch (Throwable x) { LOG.warn("Could not process key for channel {}", channel, x); IO.close(attachment instanceof EndPoint ? (EndPoint) attachment : channel); } } else { if (LOG.isDebugEnabled()) LOG.debug("Selector loop ignoring invalid key for channel {}", channel); IO.close(attachment instanceof EndPoint ? (EndPoint) attachment : channel); } } return null; } private void updateKeys() { // Do update keys for only previously selected keys. // This will update only those keys whose selection did not cause an // updateKeys update to be submitted. for (SelectionKey key : _keys) { Object attachment = key.attachment(); if (attachment instanceof Selectable) ((Selectable) attachment).updateKey(); } _keys.clear(); } @Override public String toString() { return String.format("%s@%x", getClass().getSimpleName(), hashCode()); } } /** * A selector update to be done when the selector has been woken. * * @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. */ @Deprecated(since = "2021-05-27") public interface SelectorUpdate { void update(Selector selector); } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") private class Start implements SelectorUpdate { private final CountDownLatch _started = new CountDownLatch(1); @Override public void update(Selector selector) { ManagedSelector.this._started.set(true); _started.countDown(); } } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") private static class DumpKeys implements SelectorUpdate { private final CountDownLatch latch = new CountDownLatch(1); private List keys; @Override public void update(Selector selector) { Set selectionKeys = selector.keys(); List list = new ArrayList<>(selectionKeys.size()); for (SelectionKey key : selectionKeys) { if (key != null) list.add(String.format("SelectionKey@%x{i=%d}->%s", key.hashCode(), safeInterestOps(key), key.attachment())); } keys = list; latch.countDown(); } public List get(long timeout, TimeUnit unit) { try { latch.await(timeout, unit); } catch (InterruptedException x) { LOG.ignore(x); } return keys; } } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") class Acceptor implements SelectorUpdate, Selectable, Closeable { private final SelectableChannel _channel; private SelectionKey _key; Acceptor(SelectableChannel channel) { _channel = channel; } @Override public void update(Selector selector) { try { _key = _channel.register(selector, SelectionKey.OP_ACCEPT, this); if (LOG.isDebugEnabled()) LOG.debug("{} acceptor={}", this, _channel); } catch (Throwable x) { IO.close(_channel); LOG.warn(x); } } @Override public Runnable onSelected() { SelectableChannel channel = null; try { while (true) { channel = _selectorManager.doAccept(_channel); if (channel == null) break; _selectorManager.accepted(channel); } } catch (Throwable x) { LOG.warn("Accept failed for channel {}", channel, x); IO.close(channel); } return null; } @Override public void updateKey() { } @Override public void replaceKey(SelectionKey newKey) { _key = newKey; } @Override public void close() throws IOException { // May be called from any thread. // Implements AbstractConnector.setAccepting(boolean). submit(selector -> _key.cancel()); } } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") class Accept implements SelectorUpdate, Runnable, Closeable { private final SelectableChannel channel; private final Object attachment; private SelectionKey key; Accept(SelectableChannel channel, Object attachment) { this.channel = channel; this.attachment = attachment; _selectorManager.onAccepting(channel); } @Override public void close() { if (LOG.isDebugEnabled()) LOG.debug("closed accept of {}", channel); IO.close(channel); } @Override public void update(Selector selector) { try { key = channel.register(selector, 0, attachment); execute(this); } catch (Throwable x) { IO.close(channel); _selectorManager.onAcceptFailed(channel, x); if (LOG.isDebugEnabled()) LOG.debug(x); } } @Override public void run() { try { createEndPoint(channel, key); _selectorManager.onAccepted(channel); } catch (Throwable x) { if (LOG.isDebugEnabled()) LOG.debug(x); failed(x); } } protected void failed(Throwable failure) { IO.close(channel); LOG.warn(String.valueOf(failure)); if (LOG.isDebugEnabled()) LOG.debug(failure); _selectorManager.onAcceptFailed(channel, failure); } @Override public String toString() { return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), channel); } } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") class Connect implements SelectorUpdate, Runnable { private final AtomicBoolean failed = new AtomicBoolean(); private final SelectableChannel channel; private final Object attachment; private final Scheduler.Task timeout; Connect(SelectableChannel channel, Object attachment) { this.channel = channel; this.attachment = attachment; long timeout = ManagedSelector.this._selectorManager.getConnectTimeout(); if (timeout > 0) this.timeout = ManagedSelector.this._selectorManager.getScheduler().schedule(this, timeout, TimeUnit.MILLISECONDS); else this.timeout = null; } @Override public void update(Selector selector) { try { channel.register(selector, SelectionKey.OP_CONNECT, this); } catch (Throwable x) { failed(x); } } @Override public void run() { if (_selectorManager.isConnectionPending(channel)) { if (LOG.isDebugEnabled()) LOG.debug("Channel {} timed out while connecting, closing it", channel); failed(new SocketTimeoutException("Connect Timeout")); } } public void failed(Throwable failure) { if (failed.compareAndSet(false, true)) { if (timeout != null) timeout.cancel(); IO.close(channel); ManagedSelector.this._selectorManager.connectionFailed(channel, failure, attachment); } } @Override public String toString() { return String.format("Connect@%x{%s,%s}", hashCode(), channel, attachment); } } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") private class CloseConnections implements SelectorUpdate { private final Set _closed; private final CountDownLatch _complete = new CountDownLatch(1); private CloseConnections() { this(null); } private CloseConnections(Set closed) { _closed = closed; } @Override public void update(Selector selector) { if (LOG.isDebugEnabled()) LOG.debug("Closing {} connections on {}", selector.keys().size(), ManagedSelector.this); for (SelectionKey key : selector.keys()) { if (key != null && key.isValid()) { Closeable closeable = null; Object attachment = key.attachment(); if (attachment instanceof EndPoint) { EndPoint endPoint = (EndPoint) attachment; Connection connection = endPoint.getConnection(); if (connection != null) closeable = connection; else closeable = endPoint; } if (closeable != null) { if (_closed == null) { IO.close(closeable); } else if (!_closed.contains(closeable)) { _closed.add(closeable); IO.close(closeable); } } } } _complete.countDown(); } } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") private class StopSelector implements SelectorUpdate { private final CountDownLatch _stopped = new CountDownLatch(1); @Override public void update(Selector selector) { for (SelectionKey key : selector.keys()) { // Key may be null when using the UnixSocket selector. if (key == null) continue; Object attachment = key.attachment(); if (attachment instanceof Closeable) IO.close((Closeable) attachment); } _selector = null; IO.close(selector); _stopped.countDown(); } } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") private final class CreateEndPoint implements Runnable { private final Connect _connect; private final SelectionKey _key; private CreateEndPoint(Connect connect, SelectionKey key) { _connect = connect; _key = key; } @Override public void run() { try { createEndPoint(_connect.channel, _key); } catch (Throwable failure) { IO.close(_connect.channel); LOG.warn(String.valueOf(failure)); if (LOG.isDebugEnabled()) LOG.debug(failure); _connect.failed(failure); } } @Override public String toString() { return String.format("CreateEndPoint@%x{%s}", hashCode(), _connect); } } // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported. @Deprecated(since = "2021-05-27") private class DestroyEndPoint implements Runnable, Closeable { private final EndPoint endPoint; public DestroyEndPoint(EndPoint endPoint) { this.endPoint = endPoint; } @Override public void run() { if (LOG.isDebugEnabled()) LOG.debug("Destroyed {}", endPoint); Connection connection = endPoint.getConnection(); if (connection != null) _selectorManager.connectionClosed(connection); ManagedSelector.this.endPointClosed(endPoint); } @Override public void close() { run(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy