
io.fabric8.kubernetes.client.dsl.internal.AbstractWatchManager 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.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.fabric8.kubernetes.api.model.*;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.Watch;
import io.fabric8.kubernetes.client.Watcher;
import io.fabric8.kubernetes.client.Watcher.Action;
import io.fabric8.kubernetes.client.WatcherException;
import io.fabric8.kubernetes.client.dsl.base.BaseOperation;
import io.fabric8.kubernetes.client.http.HttpClient;
import io.fabric8.kubernetes.client.utils.ExponentialBackoffIntervalCalculator;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.fabric8.kubernetes.client.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import static java.net.HttpURLConnection.HTTP_GONE;
public abstract class AbstractWatchManager implements Watch {
private static final Logger logger = LoggerFactory.getLogger(AbstractWatchManager.class);
final Watcher watcher;
final AtomicReference resourceVersion;
final AtomicBoolean forceClosed;
private final int reconnectLimit;
private final ExponentialBackoffIntervalCalculator retryIntervalCalculator;
final AtomicInteger currentReconnectAttempt;
private ScheduledFuture> reconnectAttempt;
protected final HttpClient client;
private BaseOperation baseOperation;
private ListOptions listOptions;
private URL requestUrl;
private final AtomicBoolean reconnectPending = new AtomicBoolean(false);
private final boolean receiveBookmarks;
AbstractWatchManager(
Watcher watcher, BaseOperation baseOperation, ListOptions listOptions, int reconnectLimit, int reconnectInterval, int maxIntervalExponent, Supplier clientSupplier
) throws MalformedURLException {
this.watcher = watcher;
this.reconnectLimit = reconnectLimit;
this.retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(reconnectInterval, maxIntervalExponent);
this.resourceVersion = new AtomicReference<>(listOptions.getResourceVersion());
this.currentReconnectAttempt = new AtomicInteger(0);
this.forceClosed = new AtomicBoolean();
this.receiveBookmarks = Boolean.TRUE.equals(listOptions.getAllowWatchBookmarks());
// opt into bookmarks by default
if (listOptions.getAllowWatchBookmarks() == null) {
listOptions.setAllowWatchBookmarks(true);
}
this.baseOperation = baseOperation;
this.requestUrl = baseOperation.getNamespacedUrl();
this.listOptions = listOptions;
this.client = clientSupplier.get();
runWatch();
}
protected abstract void run(URL url, Map headers);
protected abstract void closeRequest();
final void close(WatcherException cause) {
// proactively close the request (it will be called again in close)
// for reconnecting watchers, we may not complete onClose for a while
closeRequest();
if (forceClosed.get()) {
logger.debug("Ignoring duplicate firing of onClose event");
} else {
boolean success = false;
try {
watcher.onClose(cause);
success = true;
} finally {
if (success || !watcher.reconnecting()) {
forceClosed.set(true);
}
}
}
close();
}
final void closeEvent() {
if (forceClosed.getAndSet(true)) {
logger.debug("Ignoring duplicate firing of onClose event");
return;
}
watcher.onClose();
}
final synchronized void cancelReconnect() {
if (reconnectAttempt != null) {
reconnectAttempt.cancel(true);
}
}
void scheduleReconnect() {
if (!reconnectPending.compareAndSet(false, true)) {
logger.debug("Reconnect already scheduled");
return;
}
if (isForceClosed()) {
logger.debug("Ignoring error for already closed/closing connection");
return;
}
if (cannotReconnect()) {
close(new WatcherException("Exhausted reconnects"));
return;
}
logger.debug("Scheduling reconnect task");
long delay = nextReconnectInterval();
synchronized (this) {
reconnectAttempt = Utils.schedule(Utils.getCommonExecutorSerive(), () -> {
try {
runWatch();
if (isForceClosed()) {
closeRequest();
}
} catch (Exception e) {
// An unexpected error occurred and we didn't even get an onFailure callback.
logger.error("Exception in reconnect", e);
close(new WatcherException("Unhandled exception in reconnect attempt", e));
} finally {
reconnectPending.set(false);
}
}, delay, TimeUnit.MILLISECONDS);
if (isForceClosed()) {
cancelReconnect();
}
}
}
final boolean cannotReconnect() {
return !watcher.reconnecting() && currentReconnectAttempt.get() >= reconnectLimit && reconnectLimit >= 0;
}
final long nextReconnectInterval() {
int exponentOfTwo = currentReconnectAttempt.getAndIncrement();
long ret = retryIntervalCalculator.getInterval(exponentOfTwo);
logger.debug("Current reconnect backoff is {} milliseconds (T{})", ret, exponentOfTwo);
return ret;
}
void resetReconnectAttempts() {
currentReconnectAttempt.set(0);
}
boolean isForceClosed() {
return forceClosed.get();
}
void eventReceived(Watcher.Action action, HasMetadata resource) {
if (!receiveBookmarks && action == Action.BOOKMARK) {
// the user didn't ask for bookmarks, just filter them
return;
}
// the WatchEvent deserialization is not specifically typed
// modify the type here if needed
if (resource != null && !baseOperation.getType().isAssignableFrom(resource.getClass())) {
resource = Serialization.jsonMapper().convertValue(resource, baseOperation.getType());
}
watcher.eventReceived(action, (T)resource);
}
void updateResourceVersion(final String newResourceVersion) {
resourceVersion.set(newResourceVersion);
}
protected void runWatch() {
listOptions.setResourceVersion(resourceVersion.get());
URL url = BaseOperation.appendListOptionParams(requestUrl, listOptions);
String origin = requestUrl.getProtocol() + "://" + requestUrl.getHost();
if (requestUrl.getPort() != -1) {
origin += ":" + requestUrl.getPort();
}
Map headers = new HashMap<>();
headers.put("Origin", origin);
logger.debug("Watching {}...", url);
closeRequest(); // only one can be active at a time
run(url, headers);
}
@Override
public void close() {
logger.debug("Force closing the watch {}", this);
closeEvent();
closeRequest();
cancelReconnect();
}
private WatchEvent contextAwareWatchEventDeserializer(String messageSource) {
try {
return Serialization.unmarshal(messageSource, WatchEvent.class);
} catch (Exception ex1) {
try {
JsonNode json = Serialization.jsonMapper().readTree(messageSource);
JsonNode objectJson = null;
if (json instanceof ObjectNode && json.has("object")) {
objectJson = ((ObjectNode) json).remove("object");
}
WatchEvent watchEvent = Serialization.jsonMapper().treeToValue(json, WatchEvent.class);
KubernetesResource object = Serialization.jsonMapper().treeToValue(objectJson, baseOperation.getType());
watchEvent.setObject(object);
return watchEvent;
} catch (JsonProcessingException ex2) {
throw new IllegalArgumentException("Failed to deserialize WatchEvent", ex2);
}
}
}
protected WatchEvent readWatchEvent(String messageSource) {
WatchEvent event = contextAwareWatchEventDeserializer(messageSource);
KubernetesResource object = null;
if (event != null) {
object = event.getObject();
}
// when watching API Groups we don't get a WatchEvent resource
// so the object will be null
// so lets try parse the message as a KubernetesResource
// as it will probably be a list of resources like a BuildList
if (object == null) {
object = Serialization.unmarshal(messageSource, KubernetesResource.class);
if (event == null) {
event = new WatchEvent(object, "MODIFIED");
} else {
event.setObject(object);
}
}
if (event.getType() == null) {
event.setType("MODIFIED");
}
return event;
}
protected void onMessage(String message) {
try {
WatchEvent event = readWatchEvent(message);
Object object = event.getObject();
if (object instanceof Status) {
Status status = (Status) object;
onStatus(status);
} else if (object instanceof KubernetesResourceList) {
// Dirty cast - should always be valid though
KubernetesResourceList list = (KubernetesResourceList) object;
updateResourceVersion(list.getMetadata().getResourceVersion());
Action action = Action.valueOf(event.getType());
List items = list.getItems();
if (items != null) {
for (HasMetadata item : items) {
eventReceived(action, item);
}
}
} else if (object instanceof HasMetadata) {
@SuppressWarnings("unchecked")
T obj = (T) object;
updateResourceVersion(obj.getMetadata().getResourceVersion());
Action action = Action.valueOf(event.getType());
eventReceived(action, obj);
} else {
logger.error("Unknown message received: {}", message);
}
} catch (ClassCastException e) {
logger.error("Received wrong type of object for watch", e);
} catch (IllegalArgumentException e) {
logger.error("Invalid event type", e);
} catch (Exception e) {
logger.error("Unhandled exception encountered in watcher event handler", e);
}
}
protected boolean onStatus(Status status) {
// The resource version no longer exists - this has to be handled by the caller.
if (status.getCode() == HTTP_GONE) {
close(new WatcherException(status.getMessage(), new KubernetesClientException(status)));
return true;
}
eventReceived(Action.ERROR, null);
logger.error("Error received: {}", status);
return false;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy