
io.fabric8.kubernetes.client.dsl.internal.WatchHTTPManager Maven / Gradle / Ivy
/**
* Copyright (C) 2015 Red Hat, 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.fabric8.kubernetes.client.dsl.internal;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
import io.fabric8.kubernetes.api.model.Status;
import io.fabric8.kubernetes.api.model.WatchEvent;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.Watch;
import io.fabric8.kubernetes.client.Watcher;
import io.fabric8.kubernetes.client.dsl.base.BaseOperation;
import io.fabric8.kubernetes.client.dsl.base.OperationSupport;
import okhttp3.*;
import okhttp3.logging.HttpLoggingInterceptor;
import okio.BufferedSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static io.fabric8.kubernetes.client.utils.Utils.isNotNullOrEmpty;
import static java.net.HttpURLConnection.HTTP_GONE;
public class WatchHTTPManager> implements
Watch {
private static final Logger logger = LoggerFactory.getLogger(WatchHTTPManager.class);
private static final ObjectMapper mapper = new ObjectMapper();
private final BaseOperation baseOperation;
private final Watcher watcher;
private final AtomicBoolean forceClosed = new AtomicBoolean();
private final AtomicReference resourceVersion;
private final int reconnectLimit;
private final int reconnectInterval;
private final AtomicBoolean reconnectPending = new AtomicBoolean(false);
private final static int maxIntervalExponent = 5; // max 32x slowdown from base interval
private final URL requestUrl;
private final AtomicInteger currentReconnectAttempt = new AtomicInteger(0);
private OkHttpClient clonedClient;
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread ret = new Thread(r, "Executor for Watch " + System.identityHashCode(WatchHTTPManager.this));
ret.setDaemon(true);
return ret;
}
});
public WatchHTTPManager(final OkHttpClient client,
final BaseOperation baseOperation,
final String version, final Watcher watcher, final int reconnectInterval,
final int reconnectLimit, long connectTimeout)
throws MalformedURLException {
if (version == null) {
L currentList = baseOperation.list();
this.resourceVersion = new AtomicReference<>(currentList.getMetadata().getResourceVersion());
} else {
this.resourceVersion = new AtomicReference<>(version);
}
this.baseOperation = baseOperation;
this.watcher = watcher;
this.reconnectInterval = reconnectInterval;
this.reconnectLimit = reconnectLimit;
OkHttpClient clonedClient = client.newBuilder()
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.cache(null)
.build();
// If we set the HttpLoggingInterceptor's logging level to Body (as it is by default), it does
// not let us stream responses from the server.
for (Interceptor i : clonedClient.networkInterceptors()) {
if (i instanceof HttpLoggingInterceptor) {
HttpLoggingInterceptor interceptor = (HttpLoggingInterceptor) i;
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
}
}
this.clonedClient = clonedClient;
requestUrl = baseOperation.getNamespacedUrl();
runWatch();
}
private final void runWatch() {
logger.debug("Watching via HTTP GET ... {}", this);
HttpUrl.Builder httpUrlBuilder = HttpUrl.get(requestUrl).newBuilder();
String labelQueryParam = baseOperation.getLabelQueryParam();
if (isNotNullOrEmpty(labelQueryParam)) {
httpUrlBuilder.addQueryParameter("labelSelector", labelQueryParam);
}
String fieldQueryString = baseOperation.getFieldQueryParam();
String name = baseOperation.getName();
if (name != null && name.length() > 0) {
if (fieldQueryString.length() > 0) {
fieldQueryString += ",";
}
fieldQueryString += "metadata.name=" + name;
}
if (isNotNullOrEmpty(fieldQueryString)) {
httpUrlBuilder.addQueryParameter("fieldSelector", fieldQueryString);
}
httpUrlBuilder
.addQueryParameter("resourceVersion", this.resourceVersion.get())
.addQueryParameter("watch", "true");
final Request request = new Request.Builder()
.get()
.url(httpUrlBuilder.build())
.addHeader("Origin", requestUrl.getProtocol() + "://" + requestUrl.getHost() + ":" + requestUrl.getPort())
.build();
clonedClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
logger.info("Watch connection failed. reason: {}", e.getMessage());
scheduleReconnect();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
throw OperationSupport.requestFailure(request,
OperationSupport.createStatus(response.code(), response.message()));
}
try {
BufferedSource source = response.body().source();
while (!source.exhausted()) {
String message = source.readUtf8LineStrict();
onMessage(message);
}
} catch (Exception e) {
logger.info("Watch terminated unexpectedly. reason: {}", e.getMessage());
}
// if we get here, the source is exhausted, so, we have lost our "watch".
// we must reconnect.
if (response != null) {
response.body().close();
}
scheduleReconnect();
}
});
}
private void scheduleReconnect() {
if (forceClosed.get()) {
logger.warn("Ignoring error for already closed/closing connection");
return;
}
if (currentReconnectAttempt.get() >= reconnectLimit && reconnectLimit >= 0) {
watcher.onClose(new KubernetesClientException("Connection unexpectedly closed"));
return;
}
logger.debug("Submitting reconnect task to the executor");
// make sure that whichever thread calls this method, the tasks are
// performed serially in the executor.
executor.submit(new Runnable() {
@Override
public void run() {
if (!reconnectPending.compareAndSet(false, true)) {
logger.debug("Reconnect already scheduled");
return;
}
try {
// actual reconnect only after the back-off time has passed, without
// blocking the thread
logger.debug("Scheduling reconnect task");
executor.schedule(new Runnable() {
@Override
public void run() {
try {
WatchHTTPManager.this.runWatch();
reconnectPending.set(false);
} catch (Exception e) {
// An unexpected error occurred and we didn't even get an onFailure callback.
logger.error("Exception in reconnect", e);
close();
watcher.onClose(new KubernetesClientException("Unhandled exception in reconnect attempt", e));
}
}
}, nextReconnectInterval(), TimeUnit.MILLISECONDS);
} catch (RejectedExecutionException e) {
logger.error("Exception in reconnect", e);
reconnectPending.set(false);
}
}
});
}
public void onMessage(String messageSource) throws IOException {
try {
WatchEvent event = mapper.readValue(messageSource, WatchEvent.class);
if (event.getObject() instanceof HasMetadata) {
@SuppressWarnings("unchecked")
T obj = (T) event.getObject();
// Dirty cast - should always be valid though
String currentResourceVersion = resourceVersion.get();
String newResourceVersion = ((HasMetadata) obj).getMetadata().getResourceVersion();
if (currentResourceVersion.compareTo(newResourceVersion) < 0) {
resourceVersion.compareAndSet(currentResourceVersion, newResourceVersion);
}
Watcher.Action action = Watcher.Action.valueOf(event.getType());
watcher.eventReceived(action, obj);
} else if (event.getObject() instanceof Status) {
Status status = (Status) event.getObject();
// The resource version no longer exists - this has to be handled by the caller.
if (status.getCode() == HTTP_GONE) {
// exception
// shut down executor, etc.
close();
watcher.onClose(new KubernetesClientException(status));
return;
}
logger.error("Error received: {}", status.toString());
} else {
logger.error("Unknown message received: {}", messageSource);
}
} catch (IOException e) {
logger.error("Could not deserialize watch event: {}", messageSource, e);
} catch (ClassCastException e) {
logger.error("Received wrong type of object for watch", e);
} catch (IllegalArgumentException e) {
logger.error("Invalid event type", e);
}
}
private long nextReconnectInterval() {
int exponentOfTwo = currentReconnectAttempt.getAndIncrement();
if (exponentOfTwo > maxIntervalExponent)
exponentOfTwo = maxIntervalExponent;
long ret = reconnectInterval * (1 << exponentOfTwo);
logger.info("Current reconnect backoff is " + ret + " milliseconds (T" + exponentOfTwo + ")");
return ret;
}
@Override
public void close() {
logger.debug("Force closing the watch {}", this);
forceClosed.set(true);
if (!executor.isShutdown()) {
try {
executor.shutdown();
if (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
logger.warn("Executor didn't terminate in time after shutdown in close(), killing it in: {}", this);
executor.shutdownNow();
}
} catch (Throwable t) {
throw KubernetesClientException.launderThrowable(t);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy