diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/LeaderElector.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/LeaderElector.java index c0442deb839..dba196d49cc 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/LeaderElector.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/LeaderElector.java @@ -29,6 +29,7 @@ import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Objects; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; @@ -99,6 +100,9 @@ public CompletableFuture start() { } }); } else { + if (!(t instanceof CancellationException)) { + LOGGER.error("Exception during leader election", t); + } // there's a possibility that we'll obtain the lock, but get cancelled // before completing the future stopLeading(); diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLock.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLock.java index 9e0fd299249..20c7019f474 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLock.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLock.java @@ -18,41 +18,29 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.utils.Serialization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Objects; import java.util.Optional; -public class ConfigMapLock implements Lock { +public class ConfigMapLock extends ResourceLock { private static final Logger LOGGER = LoggerFactory.getLogger(ConfigMapLock.class); - private final String configMapNamespace; - private final String configMapName; - private final String identity; - public ConfigMapLock(String configMapNamespace, String configMapName, String identity) { - this.configMapNamespace = Objects.requireNonNull(configMapNamespace, "configMapNamespace is required"); - this.configMapName = Objects.requireNonNull(configMapName, "configMapName is required"); - this.identity = Objects.requireNonNull(identity, "identity is required"); + super(configMapNamespace, configMapName, identity); + } + + @Override + protected Class getKind() { + return ConfigMap.class; } - /** - * {@inheritDoc} - * - * @throws LockException - */ @Override - public LeaderElectionRecord get(KubernetesClient client) { - final ConfigMap configMap = client - .configMaps().inNamespace(configMapNamespace).withName(configMapName).get(); - return Optional.ofNullable(configMap) - .map(ConfigMap::getMetadata) - .map(ObjectMeta::getAnnotations) + protected LeaderElectionRecord toRecord(ConfigMap resource) { + return Optional.ofNullable(resource.getMetadata().getAnnotations()) .map(annotations -> annotations.get(LEADER_ELECTION_RECORD_ANNOTATION_KEY)) .map(annotation -> { try { @@ -63,55 +51,19 @@ public LeaderElectionRecord get(KubernetesClient client) { } }) .map(record -> { - record.setVersion(configMap.getMetadata().getResourceVersion()); + record.setVersion(resource.getMetadata().getResourceVersion()); return record; }) .orElse(null); } - /** - * {@inheritDoc} - */ @Override - public void create( - KubernetesClient client, LeaderElectionRecord leaderElectionRecord) { - - client.configMaps().inNamespace(configMapNamespace).withName(configMapName).create(new ConfigMapBuilder() - .editOrNewMetadata().withNamespace(configMapNamespace).withName(configMapName) + protected ConfigMap toResource(LeaderElectionRecord leaderElectionRecord, ObjectMeta meta, ConfigMap current) { + ConfigMapBuilder builder = Optional.ofNullable(current).map(ConfigMapBuilder::new).orElse(new ConfigMapBuilder()); + return builder.withMetadata(meta).editMetadata() .addToAnnotations(LEADER_ELECTION_RECORD_ANNOTATION_KEY, Serialization.asJson(leaderElectionRecord)) .endMetadata() - .build()); - } - - /** - * {@inheritDoc} - */ - @Override - public void update( - KubernetesClient client, LeaderElectionRecord leaderElectionRecord) { - - final ConfigMap toReplace = client.configMaps().inNamespace(configMapNamespace).withName(configMapName).get(); - toReplace.getMetadata().getAnnotations() - .put(LEADER_ELECTION_RECORD_ANNOTATION_KEY, Serialization.asJson(leaderElectionRecord)); - // Use replace instead of edit to avoid concurrent modifications, resourceVersion is locked to original record version - client.configMaps().inNamespace(configMapNamespace).withName(configMapName) - .lockResourceVersion((String) Objects.requireNonNull(leaderElectionRecord.getVersion())) - .replace(toReplace); + .build(); } - /** - * {@inheritDoc} - */ - @Override - public String identity() { - return identity; - } - - /** - * {@inheritDoc} - */ - @Override - public String describe() { - return String.format("ConfigMapLock: %s - %s (%s)", configMapNamespace, configMapName, identity); - } } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLock.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLock.java index a3703c69992..2d7279c215f 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLock.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/LeaseLock.java @@ -15,57 +15,29 @@ */ package io.fabric8.kubernetes.client.extended.leaderelection.resourcelock; +import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.coordination.v1.Lease; import io.fabric8.kubernetes.api.model.coordination.v1.LeaseBuilder; -import io.fabric8.kubernetes.client.KubernetesClient; import java.time.Duration; import java.time.temporal.ChronoUnit; -import java.util.Objects; import java.util.Optional; -public class LeaseLock implements Lock { - - private final String leaseNamespace; - private final String leaseName; - private final String identity; +public class LeaseLock extends ResourceLock { public LeaseLock(String leaseNamespace, String leaseName, String identity) { - this.leaseNamespace = Objects.requireNonNull(leaseNamespace, "leaseNamespace is required"); - this.leaseName = Objects.requireNonNull(leaseName, "leaseName is required"); - this.identity = Objects.requireNonNull(identity, "identity is required"); + super(leaseNamespace, leaseName, identity); } - /** - * {@inheritDoc} - */ @Override - public LeaderElectionRecord get(KubernetesClient client) { - final Lease lease = client.leases().inNamespace(leaseNamespace).withName(leaseName).get(); - return Optional.ofNullable(lease) - .map(Lease::getSpec) - .map(spec -> { - final LeaderElectionRecord ret = new LeaderElectionRecord( - spec.getHolderIdentity(), - Duration.ofSeconds(spec.getLeaseDurationSeconds()), - spec.getAcquireTime(), - spec.getRenewTime(), - Optional.ofNullable(spec.getLeaseTransitions()).orElse(0)); - ret.setVersion(lease.getMetadata().getResourceVersion()); - return ret; - }) - .orElse(null); + protected Class getKind() { + return Lease.class; } - /** - * {@inheritDoc} - */ @Override - public void create( - KubernetesClient client, LeaderElectionRecord leaderElectionRecord) { - - client.leases().inNamespace(leaseNamespace).withName(leaseName).create(new LeaseBuilder() - .withNewMetadata().withNamespace(leaseNamespace).withName(leaseName).endMetadata() + protected Lease toResource(LeaderElectionRecord leaderElectionRecord, ObjectMeta meta, Lease current) { + LeaseBuilder builder = Optional.ofNullable(current).map(LeaseBuilder::new).orElse(new LeaseBuilder()); + return builder.withMetadata(meta) .withNewSpec() .withHolderIdentity(leaderElectionRecord.getHolderIdentity()) .withLeaseDurationSeconds((int) leaderElectionRecord.getLeaseDuration().get(ChronoUnit.SECONDS)) @@ -73,41 +45,21 @@ public void create( .withRenewTime(leaderElectionRecord.getRenewTime()) .withLeaseTransitions(leaderElectionRecord.getLeaderTransitions()) .endSpec() - .build()); - } - - /** - * {@inheritDoc} - */ - @Override - public void update( - KubernetesClient client, LeaderElectionRecord leaderElectionRecord) { - - final Lease toReplace = client.leases().inNamespace(leaseNamespace).withName(leaseName).get(); - toReplace.getSpec().setHolderIdentity(leaderElectionRecord.getHolderIdentity()); - toReplace.getSpec().setLeaseDurationSeconds((int) leaderElectionRecord.getLeaseDuration().get(ChronoUnit.SECONDS)); - toReplace.getSpec().setAcquireTime(leaderElectionRecord.getAcquireTime()); - toReplace.getSpec().setRenewTime(leaderElectionRecord.getRenewTime()); - toReplace.getSpec().setLeaseTransitions(leaderElectionRecord.getLeaderTransitions()); - // Use replace instead of edit to avoid concurrent modifications, resourceVersion is locked to original record version - client.leases().inNamespace(leaseNamespace).withName(leaseName) - .lockResourceVersion((String) Objects.requireNonNull(leaderElectionRecord.getVersion())) - .replace(toReplace); + .build(); } - /** - * {@inheritDoc} - */ @Override - public String identity() { - return identity; + protected LeaderElectionRecord toRecord(Lease resource) { + return Optional.ofNullable(resource.getSpec()).map(spec -> { + final LeaderElectionRecord ret = new LeaderElectionRecord( + spec.getHolderIdentity(), + Duration.ofSeconds(spec.getLeaseDurationSeconds()), + spec.getAcquireTime(), + spec.getRenewTime(), + Optional.ofNullable(spec.getLeaseTransitions()).orElse(0)); + ret.setVersion(resource.getMetadata().getResourceVersion()); + return ret; + }).orElse(null); } - /** - * {@inheritDoc} - */ - @Override - public String describe() { - return String.format("LeaseLock: %s - %s (%s)", leaseNamespace, leaseName, identity); - } } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/Lock.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/Lock.java index 745f9110d36..0e4528dcd57 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/Lock.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/Lock.java @@ -35,8 +35,7 @@ public interface Lock { * @param client used to retrieve the LeaderElectionRecord * @param leaderElectionRecord to update */ - void create( - KubernetesClient client, LeaderElectionRecord leaderElectionRecord); + void create(KubernetesClient client, LeaderElectionRecord leaderElectionRecord); /** * Attempts to update the current {@link LeaderElectionRecord}. @@ -44,8 +43,7 @@ void create( * @param client used to retrieve the LeaderElectionRecord * @param leaderElectionRecord to update */ - void update( - KubernetesClient client, LeaderElectionRecord leaderElectionRecord); + void update(KubernetesClient client, LeaderElectionRecord leaderElectionRecord); /** * Returns the unique id of the lock holder. diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ResourceLock.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ResourceLock.java new file mode 100644 index 00000000000..e021e82228f --- /dev/null +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ResourceLock.java @@ -0,0 +1,96 @@ +/** + * 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.extended.leaderelection.resourcelock; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; + +import java.util.Objects; +import java.util.Optional; + +public abstract class ResourceLock implements Lock { + + private final String namespace; + private final String name; + private final String identity; + + public ResourceLock(String namespace, String name, String identity) { + this.namespace = Objects.requireNonNull(namespace, "namespace is required"); + this.name = Objects.requireNonNull(name, "name is required"); + this.identity = Objects.requireNonNull(identity, "identity is required"); + } + + protected abstract Class getKind(); + + @Override + public LeaderElectionRecord get(KubernetesClient client) { + return getResource(client).map(this::toRecord).orElse(null); + } + + private Optional getResource(KubernetesClient client) { + return Optional.ofNullable(client.resources(getKind()).inNamespace(namespace).withName(name).get()); + } + + @Override + public void create(KubernetesClient client, LeaderElectionRecord leaderElectionRecord) { + client.resource(toResource(leaderElectionRecord, getObjectMeta(null), null)).create(); + } + + @Override + public void update(KubernetesClient client, LeaderElectionRecord leaderElectionRecord) { + // this should be an edit, but we've disabled the ability for it to have optimistic locking + client.resource(getResource(client).map(r -> toResource(leaderElectionRecord, getObjectMeta(r), r)) + .orElseThrow(() -> new NullPointerException())).lockResourceVersion().replace(); + } + + /** + * Convert the record to a resource + * + * @param leaderElectionRecord + * @param meta not null + * @param current may be null + * @return + */ + protected abstract T toResource(LeaderElectionRecord leaderElectionRecord, ObjectMeta meta, T current); + + protected abstract LeaderElectionRecord toRecord(T resource); + + protected ObjectMeta getObjectMeta(T current) { + ObjectMetaBuilder builder = Optional.ofNullable(current).map(HasMetadata::getMetadata).map(ObjectMetaBuilder::new) + .orElse(new ObjectMetaBuilder()); + return builder.withNamespace(namespace).withName(name).build(); + } + + /** + * {@inheritDoc} + */ + @Override + public String identity() { + return identity; + } + + /** + * {@inheritDoc} + */ + @Override + public String describe() { + return String.format("%sLock: %s - %s (%s)", getKind().getSimpleName(), namespace, name, identity); + } + +} diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/LeaderElectorTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/LeaderElectorTest.java index 2f9c9d77aac..37cf6b069dc 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/LeaderElectorTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/LeaderElectorTest.java @@ -19,7 +19,6 @@ import io.fabric8.kubernetes.client.NamespacedKubernetesClient; import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.LeaderElectionRecord; import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.Lock; -import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.LockException; import io.fabric8.kubernetes.client.utils.CommonThreadPool; import io.fabric8.kubernetes.client.utils.Utils; import org.awaitility.Awaitility; @@ -69,7 +68,7 @@ void runShouldAbortAfterRenewDeadlineExpired() throws Exception { doNothing().doAnswer(invocation -> { // Sleep so that RENEW DEADLINE is reached Thread.sleep(renewDeadlineMillis * 2); - throw new LockException(""); + throw new KubernetesClientException(""); }).when(mockedLock).update(any(), any()); // When CompletableFuture future = new LeaderElector(mock(NamespacedKubernetesClient.class), lec, CommonThreadPool.get()) @@ -92,7 +91,7 @@ void runShouldEndlesslyRun() throws Exception { final CountDownLatch signal = new CountDownLatch(1); final LeaderElectionConfig lec = mockLeaderElectionConfiguration(); final Lock mockedLock = lec.getLock(); - doNothing().doThrow(new LockException("Exception won't affect execution")).doNothing().doAnswer(invocation -> { + doNothing().doThrow(new KubernetesClientException("Exception won't affect execution")).doNothing().doAnswer(invocation -> { // Force dedicated thread to gracefully end after a couple of updates signal.countDown(); return null;