Skip to content

Commit b7ee026

Browse files
committed
wip
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 3ef78ed commit b7ee026

19 files changed

Lines changed: 483 additions & 36 deletions

File tree

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.fabric8.kubernetes.client.KubernetesClient;
2525
import io.javaoperatorsdk.operator.ReconcilerUtilsInternal;
2626
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
27+
import io.javaoperatorsdk.operator.processing.event.source.informer.pool.InformerPool;
2728

2829
/**
2930
* An abstract implementation of {@link ConfigurationService} meant to ease custom implementations
@@ -35,6 +36,7 @@ public class AbstractConfigurationService implements ConfigurationService {
3536
private KubernetesClient client;
3637
private Cloner cloner;
3738
private ExecutorServiceManager executorServiceManager;
39+
private InformerPool informerPool;
3840

3941
protected AbstractConfigurationService(Version version) {
4042
this(version, null);
@@ -190,4 +192,14 @@ public ExecutorServiceManager getExecutorServiceManager() {
190192
}
191193
return executorServiceManager;
192194
}
195+
196+
@Override
197+
public InformerPool informerPool() {
198+
// cached so that all controllers backed by this ConfigurationService share the same pool and
199+
// can therefore share the underlying informers
200+
if (informerPool == null) {
201+
informerPool = ConfigurationService.super.informerPool();
202+
}
203+
return informerPool;
204+
}
193205
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/FieldSelector.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Arrays;
1919
import java.util.List;
20+
import java.util.Objects;
2021

2122
public class FieldSelector {
2223
private final List<Field> fields;
@@ -38,4 +39,16 @@ public Field(String path, String value) {
3839
this(path, value, false);
3940
}
4041
}
42+
43+
@Override
44+
public boolean equals(Object o) {
45+
if (o == null || getClass() != o.getClass()) return false;
46+
FieldSelector that = (FieldSelector) o;
47+
return Objects.equals(fields, that.fields);
48+
}
49+
50+
@Override
51+
public int hashCode() {
52+
return Objects.hashCode(fields);
53+
}
4154
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,11 @@ public void changeNamespaces(Set<String> namespaces) {
120120
// remove from pool
121121
sourcesToRemove.forEach(
122122
k -> {
123-
informerPool.releaseInformer(
124-
configuration.getInformerConfig().getName(), getClassifier(k));
123+
var optionalInformer =
124+
informerPool.releaseInformer(
125+
configuration.getInformerConfig().getName(), getClassifier(k));
125126
sources.remove(k);
127+
optionalInformer.ifPresent(i -> i.removeEventHandler(eventHandler));
126128
});
127129

128130
var newNamespaces =

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/AlwaysCreateInformerPool.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@
1616
package io.javaoperatorsdk.operator.processing.event.source.informer.pool;
1717

1818
import java.util.Map;
19+
import java.util.Optional;
1920
import java.util.concurrent.ConcurrentHashMap;
2021

22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
2125
import io.fabric8.kubernetes.api.model.HasMetadata;
2226
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
2327

2428
@SuppressWarnings({"unchecked", "rawtypes"})
2529
public class AlwaysCreateInformerPool extends AbstractInformerPool {
2630

31+
private static final Logger log = LoggerFactory.getLogger(AlwaysCreateInformerPool.class);
32+
2733
private Map<ClassifierWithName, SharedIndexInformer> informers = new ConcurrentHashMap();
2834

2935
@Override
@@ -38,9 +44,15 @@ public <R extends HasMetadata> SharedIndexInformer<R> getInformer(
3844
// todo controller name should be added here? for better naming => referencing and have guaranteed
3945
// uniqueness
4046
@Override
41-
public <R extends HasMetadata> void releaseInformer(
47+
public <R extends HasMetadata> Optional<SharedIndexInformer<R>> releaseInformer(
4248
String name, InformerClassifier<R> classifier) {
43-
informers.get(new ClassifierWithName(name, classifier)).stop();
49+
var informer = informers.get(new ClassifierWithName(name, classifier));
50+
if (informer != null) {
51+
informer.stop();
52+
} else {
53+
log.warn("Informer was not found for classifier: {}", classifier);
54+
}
55+
return Optional.ofNullable(informer);
4456
}
4557

4658
public record ClassifierWithName<R extends HasMetadata>(

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/DefaultInformerPool.java

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.HashMap;
1919
import java.util.Map;
20+
import java.util.Optional;
2021
import java.util.concurrent.atomic.AtomicInteger;
2122

2223
import org.slf4j.Logger;
@@ -41,6 +42,7 @@ public DefaultInformerPool(KubernetesClient client, ConfigurationService service
4142
}
4243

4344
@SuppressWarnings("unchecked")
45+
@Override
4446
public <R extends HasMetadata> SharedIndexInformer<R> getInformer(
4547
String name, InformerClassifier<R> classifier) {
4648
SharedIndexInformer<R> informer;
@@ -51,29 +53,58 @@ public <R extends HasMetadata> SharedIndexInformer<R> getInformer(
5153
informers.put(classifier, informer);
5254
counters.put(classifier, new AtomicInteger(1));
5355
} else {
56+
informers.keySet().stream()
57+
.filter(existing -> existing.differsOnlyByInformerListLimit(classifier))
58+
.findFirst()
59+
.ifPresent(
60+
existing ->
61+
log.warn(
62+
"Reusing informer for classifier {} that differs only by informerListLimit"
63+
+ " (existing: {}, requested: {}). The existing informerListLimit is"
64+
+ " kept.",
65+
classifier,
66+
existing.informerListLimit(),
67+
classifier.informerListLimit()));
5468
counters.get(classifier).incrementAndGet();
5569
}
5670
}
5771
start(informer, classifier);
5872
return informer;
5973
}
6074

61-
@SuppressWarnings("rawtypes")
62-
public synchronized <R extends HasMetadata> void releaseInformer(
75+
@SuppressWarnings("unchecked")
76+
public synchronized <R extends HasMetadata> Optional<SharedIndexInformer<R>> releaseInformer(
6377
String name, InformerClassifier<R> classifier) {
64-
SharedIndexInformer informer = null;
78+
SharedIndexInformer<R> informer = null;
6579
synchronized (this) {
6680
var counter = counters.get(classifier);
81+
informer = (SharedIndexInformer<R>) informers.get(classifier);
6782
if (counter != null && counter.decrementAndGet() == 0) {
68-
informer = informers.get(classifier);
6983
counters.remove(classifier);
7084
informers.remove(classifier);
85+
// todo check if we can remove event handled if informer stopped
86+
informer.stop();
7187
} else {
7288
log.warn("No informer found in the pool.");
7389
}
7490
}
75-
if (informer != null) {
76-
informer.stop();
77-
}
91+
return Optional.ofNullable(informer);
92+
}
93+
94+
/** Total number of distinct informers currently held in the pool. */
95+
public synchronized int size() {
96+
return informers.size();
97+
}
98+
99+
/**
100+
* Number of distinct informers currently held in the pool for the given resource type. When
101+
* multiple controllers share a single informer for a resource, this returns {@code 1} for that
102+
* resource type regardless of how many controllers use it.
103+
*/
104+
public synchronized long numberOfInformersForResource(
105+
Class<? extends HasMetadata> resourceClass) {
106+
return informers.keySet().stream()
107+
.filter(classifier -> resourceClass.equals(classifier.resourceClass()))
108+
.count();
78109
}
79110
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/InformerClassifier.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
*/
1616
package io.javaoperatorsdk.operator.processing.event.source.informer.pool;
1717

18+
import java.util.Objects;
19+
1820
import io.fabric8.kubernetes.api.model.HasMetadata;
1921
import io.fabric8.kubernetes.client.informers.cache.ItemStore;
2022
import io.javaoperatorsdk.operator.api.config.informer.FieldSelector;
2123
import io.javaoperatorsdk.operator.processing.GroupVersionKind;
2224

23-
// todo add this to docs
2425
/**
2526
* Indexers are not part of classifier since those can be added dynamically to an informer, also can
2627
* live side by side from different controllers, just users have to avoid naming collision.
@@ -33,4 +34,42 @@ public record InformerClassifier<R extends HasMetadata>(
3334
GroupVersionKind groupVersionKind,
3435
FieldSelector fieldSelector,
3536
Long informerListLimit,
36-
ItemStore<R> itemStore) {}
37+
ItemStore<R> itemStore) {
38+
39+
@Override
40+
public boolean equals(Object o) {
41+
if (this == o) {
42+
return true;
43+
}
44+
if (!(o instanceof InformerClassifier<?> that)) {
45+
return false;
46+
}
47+
return Objects.equals(labelSelector, that.labelSelector)
48+
&& Objects.equals(shardSelector, that.shardSelector)
49+
&& Objects.equals(namespaceIdentifier, that.namespaceIdentifier)
50+
&& Objects.equals(resourceClass, that.resourceClass)
51+
&& Objects.equals(groupVersionKind, that.groupVersionKind)
52+
&& Objects.equals(fieldSelector, that.fieldSelector)
53+
&& Objects.equals(itemStore, that.itemStore);
54+
}
55+
56+
@Override
57+
public int hashCode() {
58+
return Objects.hash(
59+
labelSelector,
60+
shardSelector,
61+
namespaceIdentifier,
62+
resourceClass,
63+
groupVersionKind,
64+
fieldSelector,
65+
itemStore);
66+
}
67+
68+
/**
69+
* Checks whether this classifier and the other are equal in every attribute except for the {@link
70+
* #informerListLimit()}, which differs between them.
71+
*/
72+
public boolean differsOnlyByInformerListLimit(InformerClassifier<?> other) {
73+
return equals(other) && !Objects.equals(informerListLimit, other.informerListLimit);
74+
}
75+
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/InformerPool.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package io.javaoperatorsdk.operator.processing.event.source.informer.pool;
1717

18+
import java.util.Optional;
19+
1820
import io.fabric8.kubernetes.api.model.HasMetadata;
1921
import io.fabric8.kubernetes.client.KubernetesClient;
2022
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
@@ -29,7 +31,8 @@ public interface InformerPool {
2931
<R extends HasMetadata> SharedIndexInformer<R> getInformer(
3032
String name, InformerClassifier<R> classifier);
3133

32-
<R extends HasMetadata> void releaseInformer(String name, InformerClassifier<R> classifier);
34+
<R extends HasMetadata> Optional<SharedIndexInformer<R>> releaseInformer(
35+
String name, InformerClassifier<R> classifier);
3336

3437
void setClient(KubernetesClient client);
3538

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import io.fabric8.kubernetes.client.dsl.AnyNamespaceOperation;
3232
import io.fabric8.kubernetes.client.dsl.ApiextensionsAPIGroupDSL;
3333
import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable;
34-
import io.fabric8.kubernetes.client.dsl.Informable;
3534
import io.fabric8.kubernetes.client.dsl.MixedOperation;
3635
import io.fabric8.kubernetes.client.dsl.NamespaceableResource;
3736
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
@@ -112,9 +111,9 @@ public static <T extends HasMetadata> KubernetesClient client(
112111

113112
when(filterable.runnableInformer(anyLong())).thenReturn(informer);
114113

115-
Informable<T> informable = mock(Informable.class);
116-
when(filterable.withLimit(anyLong())).thenReturn(informable);
117-
when(informable.runnableInformer(anyLong())).thenReturn(informer);
114+
// The informer pool casts the result of withLimit() back to FilterWatchListDeletable, so it has
115+
// to return the filterable mock (which is one) rather than a plain Informable mock.
116+
when(filterable.withLimit(anyLong())).thenReturn(filterable);
118117

119118
when(client.resources(clazz)).thenReturn(resources);
120119
when(client.leaderElector())

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ class ControllerTest {
6262
@Test
6363
void crdShouldNotBeCheckedForNativeResources() {
6464
final var client = MockKubernetesClient.client(Secret.class);
65+
final var configurationService =
66+
ConfigurationService.newOverriddenConfigurationService(
67+
this.configurationService, o -> o.withKubernetesClient(client));
6568
final var configuration =
6669
MockControllerConfiguration.forResource(Secret.class, configurationService);
6770
final var controller = new Controller<Secret>(reconciler, configuration, client);
@@ -75,7 +78,8 @@ void notifiesMetricsWhenEventProcessorStarts() {
7578
final var metrics = mock(Metrics.class);
7679
final var configurationService =
7780
ConfigurationService.newOverriddenConfigurationService(
78-
new BaseConfigurationService(), o -> o.withMetrics(metrics));
81+
new BaseConfigurationService(),
82+
o -> o.withMetrics(metrics).withKubernetesClient(client));
7983
final var configuration =
8084
MockControllerConfiguration.forResource(Secret.class, configurationService);
8185
final var controller = new Controller<Secret>(reconciler, configuration, client);
@@ -95,7 +99,8 @@ void doesNotNotifyMetricsWhenEventProcessorNotStarted() {
9599
final var metrics = mock(Metrics.class);
96100
final var configurationService =
97101
ConfigurationService.newOverriddenConfigurationService(
98-
new BaseConfigurationService(), o -> o.withMetrics(metrics));
102+
new BaseConfigurationService(),
103+
o -> o.withMetrics(metrics).withKubernetesClient(client));
99104
final var configuration =
100105
MockControllerConfiguration.forResource(Secret.class, configurationService);
101106
final var controller = new Controller<Secret>(reconciler, configuration, client);
@@ -110,7 +115,8 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() {
110115
final var client = MockKubernetesClient.client(TestCustomResource.class);
111116
ConfigurationService configurationService =
112117
ConfigurationService.newOverriddenConfigurationService(
113-
new BaseConfigurationService(), o -> o.checkingCRDAndValidateLocalModel(false));
118+
new BaseConfigurationService(),
119+
o -> o.checkingCRDAndValidateLocalModel(false).withKubernetesClient(client));
114120

115121
final var configuration =
116122
MockControllerConfiguration.forResource(TestCustomResource.class, configurationService);

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.javaoperatorsdk.operator.MockKubernetesClient;
2525
import io.javaoperatorsdk.operator.OperatorException;
2626
import io.javaoperatorsdk.operator.api.config.BaseConfigurationService;
27+
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
2728
import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration;
2829
import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration;
2930
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
@@ -202,12 +203,14 @@ void changesNamespacesOnControllerAndInformerEventSources() {
202203

203204
private EventSourceManager initManager() {
204205
final var configuration = MockControllerConfiguration.forResource(ConfigMap.class);
205-
final var configService = new BaseConfigurationService();
206+
final var mockClient = MockKubernetesClient.client(ConfigMap.class);
207+
final var configService =
208+
ConfigurationService.newOverriddenConfigurationService(
209+
new BaseConfigurationService(),
210+
overrider -> overrider.withKubernetesClient(mockClient));
206211
when(configuration.getConfigurationService()).thenReturn(configService);
207212

208-
final Controller controller =
209-
new Controller(
210-
mock(Reconciler.class), configuration, MockKubernetesClient.client(ConfigMap.class));
213+
final Controller controller = new Controller(mock(Reconciler.class), configuration, mockClient);
211214
return new EventSourceManager(controller);
212215
}
213216
}

0 commit comments

Comments
 (0)