io.airlift.discovery.client.Announcer Maven / Gradle / Ivy
/*
* Copyright 2010 Proofpoint, Inc.
*
* 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 io.airlift.discovery.client;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MapMaker;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import io.airlift.concurrent.ThreadPoolExecutorMBean;
import io.airlift.log.Logger;
import io.airlift.units.Duration;
import org.weakref.jmx.Managed;
import org.weakref.jmx.Nested;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import java.net.ConnectException;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
import static io.airlift.concurrent.MoreFutures.getFutureValue;
import static io.airlift.concurrent.Threads.daemonThreadsNamed;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
public final class Announcer
{
private static final Logger log = Logger.get(Announcer.class);
private final ConcurrentMap announcements = new MapMaker().makeMap();
private final DiscoveryAnnouncementClient announcementClient;
private final ScheduledExecutorService executor;
private final ThreadPoolExecutorMBean executorMBean;
private final AtomicBoolean started = new AtomicBoolean(false);
private final ExponentialBackOff errorBackOff = new ExponentialBackOff(
new Duration(1, MILLISECONDS),
new Duration(1, SECONDS),
"Discovery server connect succeeded for announce",
"Cannot connect to discovery server for announce",
log);
@Inject
public Announcer(DiscoveryAnnouncementClient announcementClient, Set serviceAnnouncements)
{
requireNonNull(announcementClient, "client is null");
requireNonNull(serviceAnnouncements, "serviceAnnouncements is null");
this.announcementClient = announcementClient;
serviceAnnouncements.forEach(this::addServiceAnnouncement);
executor = new ScheduledThreadPoolExecutor(5, daemonThreadsNamed("Announcer-%s"));
executorMBean = new ThreadPoolExecutorMBean((ThreadPoolExecutor) executor);
}
@Managed
@Nested
public ThreadPoolExecutorMBean getExecutor()
{
return executorMBean;
}
public void start()
{
Preconditions.checkState(!executor.isShutdown(), "Announcer has been destroyed");
if (started.compareAndSet(false, true)) {
// announce immediately, if discovery is running
ListenableFuture announce = announce(System.nanoTime(), new Duration(0, SECONDS));
try {
announce.get(30, SECONDS);
}
catch (Exception ignored) {
}
}
}
@PreDestroy
public void destroy()
{
executor.shutdownNow();
try {
executor.awaitTermination(30, SECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// unannounce
try {
getFutureValue(announcementClient.unannounce(), DiscoveryException.class);
}
catch (DiscoveryException e) {
if (e.getCause() instanceof ConnectException) {
log.error("Cannot connect to discovery server for unannounce: %s", e.getCause().getMessage());
}
else {
log.error(e);
}
}
}
public void addServiceAnnouncement(ServiceAnnouncement serviceAnnouncement)
{
requireNonNull(serviceAnnouncement, "serviceAnnouncement is null");
announcements.put(serviceAnnouncement.getId(), serviceAnnouncement);
}
public void removeServiceAnnouncement(UUID serviceId)
{
announcements.remove(serviceId);
}
public Set getServiceAnnouncements()
{
return ImmutableSet.copyOf(announcements.values());
}
private ListenableFuture announce(long delayStart, Duration expectedDelay)
{
// log announcement did not happen within 5 seconds of expected delay
if (System.nanoTime() - (delayStart + expectedDelay.roundTo(NANOSECONDS)) > SECONDS.toNanos(5)) {
log.error("Expected service announcement after %s, but announcement was delayed %s", expectedDelay, Duration.nanosSince(delayStart));
}
long requestStart = System.nanoTime();
ListenableFuture future = announcementClient.announce(getServiceAnnouncements());
Futures.addCallback(future, new FutureCallback()
{
@Override
public void onSuccess(Duration expectedDelay)
{
errorBackOff.success();
// wait 80% of the suggested delay
expectedDelay = new Duration(expectedDelay.toMillis() * 0.8, MILLISECONDS);
log.debug("Service announcement succeeded after %s. Next request will happen within %s", Duration.nanosSince(requestStart), expectedDelay);
scheduleNextAnnouncement(expectedDelay);
}
@Override
public void onFailure(Throwable t)
{
Duration duration = errorBackOff.failed(t);
// todo this is a duplicate log message and should be remove after root cause of announcement delay is determined
log.error("Service announcement failed after %s. Next request will happen within %s", Duration.nanosSince(requestStart), expectedDelay);
scheduleNextAnnouncement(duration);
}
}, executor);
return future;
}
public ListenableFuture> forceAnnounce()
{
return announcementClient.announce(getServiceAnnouncements());
}
private void scheduleNextAnnouncement(Duration expectedDelay)
{
// already stopped? avoids rejection exception
if (executor.isShutdown()) {
return;
}
long delayStart = System.nanoTime();
executor.schedule(() -> announce(delayStart, expectedDelay), expectedDelay.toMillis(), MILLISECONDS);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy