package sk.stuba.fiit.perconik.elasticsearch; import java.net.InetSocketAddress; import java.util.Map; import java.util.concurrent.ExecutorService; import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multiset; import org.elasticsearch.client.transport.TransportClient; import org.elasticsearch.common.settings.ImmutableSettings.Builder; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import sk.stuba.fiit.perconik.elasticsearch.preferences.ElasticsearchOptions; import sk.stuba.fiit.perconik.utilities.concurrent.TimeValue; import sk.stuba.fiit.perconik.utilities.configuration.Options; import static java.lang.Integer.toHexString; import static java.lang.String.format; import static java.util.concurrent.Executors.newSingleThreadExecutor; import static java.util.concurrent.TimeUnit.SECONDS; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableMap.copyOf; import static com.google.common.collect.Iterables.toArray; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.util.concurrent.MoreExecutors.shutdownAndAwaitTermination; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; import static sk.stuba.fiit.perconik.elasticsearch.preferences.ElasticsearchOptions.Schema.clientTransportAddresses; import static sk.stuba.fiit.perconik.utilities.MoreStrings.toDefaultString; import static sk.stuba.fiit.perconik.utilities.concurrent.TimeValue.of; import static sk.stuba.fiit.perconik.utilities.configuration.MapOptions.from; public class SharedElasticsearchProxy extends AbstractElasticsearchProxy { private static final TimeValue waitBeforeClientClose = of(12, SECONDS); protected final ElasticsearchOptions options; private final ElasticsearchReporter reporter; private SharedSecrets secrets; private ImmutableList<TransportAddress> addresses; private Settings settings; public SharedElasticsearchProxy(final ElasticsearchOptions options) { this.options = checkNotNull(options); this.reporter = new ElasticsearchReporter(options); this.reload(); } private static final class SharedSecrets { private static final SharedSecrets instance = new SharedSecrets(); private final Multiset<Settings> counters; private final Map<Settings, TransportClient> clients; private SharedSecrets() { this.counters = HashMultiset.create(); this.clients = newHashMap(); } static SharedSecrets obtain(final ElasticsearchReporter reporter, final Settings settings) { synchronized (instance) { return instance.connect(reporter, settings); } } static String identify(final Settings settings) { return toHexString(settings.hashCode()); } private SharedSecrets connect(final ElasticsearchReporter reporter, final Settings settings) { assert settings != null; int count = this.counters.add(settings, 1) + 1; assert count > 0; reporter.logNotice(format("connect to %s -> %d connections", identify(settings), count)); return this; } private SharedSecrets disconnect(final ElasticsearchReporter reporter, final Settings settings, final TimeValue wait) { assert settings != null; int count = this.counters.remove(settings, 1) - 1; assert count >= 0; reporter.logNotice(format("disconnect from %s -> %d connections", identify(settings), count)); if (count == 0) { TransportClient client = this.clients.remove(settings); if (client != null) { close(reporter, settings, client, wait); } } return this; } private static TransportClient open(final ElasticsearchReporter reporter, final Settings settings, final Iterable<TransportAddress> addresses) { assert settings != null; reporter.logNotice(format("opening shared client for %s", identify(settings))); try { TransportClient client = new TransportClient(settings); client.addTransportAddresses(toArray(addresses, TransportAddress.class)); reporter.logNotice(format("shared client for %s opened -> %s", identify(settings), toDefaultString(client))); return client; } catch (Exception failure) { reporter.logError(format("unable to open shared client for %s", identify(settings)), failure); throw failure; } } private static void close(final ElasticsearchReporter reporter, final Settings settings, final TransportClient client, final TimeValue wait) { assert client != null; checkNotNull(wait); reporter.logNotice(format("closing shared client for %s -> %s", identify(settings), toDefaultString(client))); ExecutorService service = newSingleThreadExecutor(); service.execute(new Runnable() { public void run() { sleepUninterruptibly(wait.duration(), wait.unit()); try { client.close(); reporter.logNotice(format("shared client for %s closed", identify(settings))); } catch (Exception failure) { reporter.logError(format("unable to close shared client for %s -> %s", identify(settings), toDefaultString(client)), failure); } } }); shutdownAndAwaitTermination(service, wait.duration(), wait.unit()); } synchronized void release(final ElasticsearchReporter reporter, final Settings settings, final TimeValue wait) { assert settings != null; this.disconnect(reporter, settings, wait); } synchronized TransportClient client(final ElasticsearchReporter reporter, final Settings settings, final Iterable<TransportAddress> addresses) { assert settings != null; TransportClient client = this.clients.get(settings); if (client == null) { client = open(reporter, settings, addresses); this.clients.put(settings, client); } return client; } } private static ElasticsearchOptions safeOptions(final Options options) { return ElasticsearchOptions.View.of(from(copyOf(options.toMap()))); } private static ImmutableList<TransportAddress> readAddresses(final Options options) { ImmutableList.Builder<TransportAddress> builder = ImmutableList.builder(); for (InetSocketAddress address: clientTransportAddresses.getValue(options)) { builder.add(new InetSocketTransportAddress(address)); } return builder.build(); } private static Settings normalizeSettings(final Settings settings) { Builder builder = settingsBuilder(); builder.put(settings); // automatically overridden builder.put("client.type", "transport"); builder.put("node.client", true); builder.put("network.server", false); // ensure behavior builder.put("node.master", false); builder.put("node.data", false); // disable HTTP builder.put("http.enabled", false); return builder.build(); } private void reload() { ElasticsearchOptions options = safeOptions(this.options); Settings update = normalizeSettings(options.toSettings()); if (!update.equals(this.settings)) { if (this.settings != null) { this.secrets.release(this.reporter, this.settings, waitBeforeClientClose); } this.settings = update; this.addresses = readAddresses(options); this.secrets = SharedSecrets.obtain(this.reporter, update); } } public final ImmutableList<TransportAddress> addresses() { synchronized (this) { if (this.addresses == null) { this.reload(); } return this.addresses; } } public final Settings settings() { synchronized (this) { if (this.settings == null) { this.reload(); } return this.settings; } } public final Settings update() { synchronized (this) { Settings settings = this.settings; this.reload(); return settings; } } @Override protected final TransportClient client() { synchronized (this) { if (this.settings == null || this.addresses == null) { this.reload(); } return this.secrets.client(this.reporter, this.settings, this.addresses); } } @Override protected void reportMessage(final String message) { this.reporter.logNotice(message); } @Override protected void reportFailure(final String message, final Exception failure) { this.reporter.logError(message, failure); this.reporter.displayError(message, failure); } public final void close() { synchronized (this) { if (this.settings == null) { this.reload(); } this.secrets.release(this.reporter, this.settings, waitBeforeClientClose); this.closeHook(); } } protected void closeHook() {} }