49
49
import org .junit .jupiter .api .BeforeAll ;
50
50
import org .junit .jupiter .api .BeforeEach ;
51
51
import org .junit .jupiter .api .Disabled ;
52
+ import org .junit .jupiter .api .RepeatedTest ;
52
53
import org .junit .jupiter .api .Test ;
53
54
import org .junit .jupiter .api .TestInfo ;
54
55
import org .junit .jupiter .api .extension .ExtendWith ;
68
69
import java .util .Set ;
69
70
import java .util .Stack ;
70
71
import java .util .UUID ;
72
+ import java .util .concurrent .CountDownLatch ;
71
73
import java .util .concurrent .ExecutionException ;
74
+ import java .util .concurrent .ExecutorService ;
75
+ import java .util .concurrent .Executors ;
72
76
import java .util .concurrent .Future ;
73
77
import java .util .concurrent .TimeUnit ;
74
78
import java .util .concurrent .TimeoutException ;
80
84
81
85
import static io .strimzi .api .ResourceAnnotations .ANNO_STRIMZI_IO_PAUSE_RECONCILIATION ;
82
86
import static io .strimzi .operator .topic .v2 .BatchingTopicController .isPaused ;
87
+ import static io .strimzi .operator .topic .v2 .TopicOperatorTestUtil .findKafkaTopicByName ;
83
88
import static java .lang .String .format ;
84
89
import static org .junit .jupiter .api .Assertions .assertEquals ;
85
90
import static org .junit .jupiter .api .Assertions .assertFalse ;
@@ -476,6 +481,42 @@ private KafkaTopic createTopic(KafkaCluster kc, KafkaTopic kt, Predicate<KafkaTo
476
481
return waitUntil (created , condition );
477
482
}
478
483
484
+ private List <KafkaTopic > createTopicsConcurrently (KafkaCluster kc , KafkaTopic ... kts ) throws InterruptedException , ExecutionException {
485
+ if (kts == null || kts .length == 0 ) {
486
+ throw new IllegalArgumentException ("You need pass at least one topic to be created" );
487
+ }
488
+ String ns = namespace (kts [0 ].getMetadata ().getNamespace ());
489
+ maybeStartOperator (topicOperatorConfig (ns , kc ));
490
+ ExecutorService executor = Executors .newFixedThreadPool (Runtime .getRuntime ().availableProcessors ());
491
+ CountDownLatch latch = new CountDownLatch (kts .length );
492
+ List <KafkaTopic > result = new ArrayList <>();
493
+ for (KafkaTopic kt : kts ) {
494
+ executor .submit (() -> {
495
+ try {
496
+ var created = Crds .topicOperation (client ).resource (kt ).create ();
497
+ LOGGER .info ("Test created KafkaTopic {} with creationTimestamp {}" ,
498
+ created .getMetadata ().getName (),
499
+ created .getMetadata ().getCreationTimestamp ());
500
+ var reconciled = waitUntil (created , readyIsTrueOrFalse ());
501
+ result .add (reconciled );
502
+ } catch (Exception e ) {
503
+ throw new RuntimeException (e );
504
+ }
505
+ latch .countDown ();
506
+ });
507
+ }
508
+ latch .await (1 , TimeUnit .MINUTES );
509
+ try {
510
+ executor .shutdown ();
511
+ executor .awaitTermination (5 , TimeUnit .SECONDS );
512
+ } catch (InterruptedException e ) {
513
+ if (!executor .isTerminated ()) {
514
+ executor .shutdownNow ();
515
+ }
516
+ }
517
+ return result ;
518
+ }
519
+
479
520
private KafkaTopic pauseTopic (String namespace , String topicName ) {
480
521
var current = Crds .topicOperation (client ).inNamespace (namespace ).withName (topicName ).get ();
481
522
var paused = Crds .topicOperation (client ).resource (new KafkaTopicBuilder (current )
@@ -686,7 +727,7 @@ public void shouldNotUpdateTopicInKafkaWhenKafkaTopicBecomesUnselected(
686
727
assertEquals (Set .of (kt .getSpec ().getReplicas ()), replicationFactors (topicDescription ));
687
728
assertEquals (Map .of (), topicConfigMap (expectedTopicName ));
688
729
689
- Map <String , Set <KubeRef >> topics = new HashMap <>(operator .controller .topics );
730
+ Map <String , List <KubeRef >> topics = new HashMap <>(operator .controller .topics );
690
731
assertFalse (topics .containsKey ("foo" )
691
732
|| topics .containsKey ("FOO" ),
692
733
"Transition to a non-selected resource should result in removal from topics map: " + topics );
@@ -1834,20 +1875,15 @@ public void shouldFailDeleteIfNoTopicAuthz(KafkaTopic kt,
1834
1875
1835
1876
@ Test
1836
1877
public void shouldFailIfNumPartitionsDivergedWithConfigChange (@ BrokerConfig (name = "auto.create.topics.enable" , value = "false" )
1837
- KafkaCluster kafkaCluster ) throws ExecutionException , InterruptedException , TimeoutException {
1838
- // Scenario from https://github.com/strimzi/strimzi-kafka-operator/pull/8627#pullrequestreview-1477513413
1839
-
1840
- // given
1878
+ KafkaCluster kafkaCluster ) throws ExecutionException , InterruptedException , TimeoutException {
1879
+ // scenario from https://github.com/strimzi/strimzi-kafka-operator/pull/8627#pullrequestreview-1477513413
1841
1880
1842
1881
// create foo
1843
1882
var topicName = randomTopicName ();
1844
1883
LOGGER .info ("Create foo" );
1845
1884
var foo = kafkaTopic (NAMESPACE , "foo" , null , null , 1 , 1 );
1846
1885
var createdFoo = createTopicAndAssertSuccess (kafkaCluster , foo );
1847
1886
1848
- // TODO remove after fixing https://github.com/strimzi/strimzi-kafka-operator/issues/9270
1849
- Thread .sleep (1000 );
1850
-
1851
1887
// create conflicting bar
1852
1888
LOGGER .info ("Create conflicting bar" );
1853
1889
var bar = kafkaTopic (NAMESPACE , "bar" , SELECTOR , null , null , "foo" , 1 , 1 ,
@@ -1861,29 +1897,72 @@ public void shouldFailIfNumPartitionsDivergedWithConfigChange(@BrokerConfig(name
1861
1897
// increase partitions of foo
1862
1898
LOGGER .info ("Increase partitions of foo" );
1863
1899
var editedFoo = modifyTopicAndAwait (createdFoo , theKt ->
1864
- new KafkaTopicBuilder (theKt ).editSpec ().withPartitions (3 ).endSpec ().build (),
1865
- readyIsTrue ());
1900
+ new KafkaTopicBuilder (theKt ).editSpec ().withPartitions (3 ).endSpec ().build (),
1901
+ readyIsTrue ());
1866
1902
1867
1903
// unmanage foo
1868
1904
LOGGER .info ("Unmanage foo" );
1869
1905
var unmanagedFoo = modifyTopicAndAwait (editedFoo , theKt ->
1870
- new KafkaTopicBuilder (theKt ).editMetadata ().addToAnnotations (BatchingTopicController .MANAGED , "false" ).endMetadata ().build (),
1871
- readyIsTrue ());
1906
+ new KafkaTopicBuilder (theKt ).editMetadata ().addToAnnotations (BatchingTopicController .MANAGED , "false" ).endMetadata ().build (),
1907
+ readyIsTrue ());
1872
1908
1873
1909
// when: delete foo
1874
1910
LOGGER .info ("Delete foo" );
1875
1911
Crds .topicOperation (client ).resource (unmanagedFoo ).delete ();
1876
1912
LOGGER .info ("Test deleted KafkaTopic {} with resourceVersion {}" ,
1877
- unmanagedFoo .getMetadata ().getName (), BatchingTopicController .resourceVersion (unmanagedFoo ));
1913
+ unmanagedFoo .getMetadata ().getName (), BatchingTopicController .resourceVersion (unmanagedFoo ));
1878
1914
Resource <KafkaTopic > resource = Crds .topicOperation (client ).resource (unmanagedFoo );
1879
1915
TopicOperatorTestUtil .waitUntilCondition (resource , Objects ::isNull );
1880
1916
1881
1917
// then: expect bar's unreadiness to be due to mismatching #partitions
1882
1918
waitUntil (createdBar , readyIsFalseAndReasonIs (
1883
- TopicOperatorException .Reason .NOT_SUPPORTED .reason ,
1884
- "Decreasing partitions not supported" ));
1919
+ TopicOperatorException .Reason .NOT_SUPPORTED .reason ,
1920
+ "Decreasing partitions not supported" ));
1921
+ }
1922
+ @ RepeatedTest (10 )
1923
+ public void shouldDetectConflictingKafkaTopicCreations (
1924
+ @ BrokerConfig (name = "auto.create.topics.enable" , value = "false" )
1925
+ KafkaCluster kafkaCluster ) throws ExecutionException , InterruptedException {
1926
+ var foo = kafkaTopic ("ns" , "foo" , null , null , 1 , 1 );
1927
+ var bar = kafkaTopic ("ns" , "bar" , SELECTOR , null , null , "foo" , 1 , 1 ,
1928
+ Map .of (TopicConfig .COMPRESSION_TYPE_CONFIG , "snappy" ));
1929
+
1930
+ LOGGER .info ("Create conflicting topics: foo and bar" );
1931
+ var reconciledTopics = createTopicsConcurrently (kafkaCluster , foo , bar );
1932
+ var reconciledFoo = findKafkaTopicByName (reconciledTopics , "foo" );
1933
+ var reconciledBar = findKafkaTopicByName (reconciledTopics , "bar" );
1934
+
1935
+ // only one resource with the same topicName should be reconciled
1936
+ var fooFailed = readyIsFalse ().test (reconciledFoo );
1937
+ var barFailed = readyIsFalse ().test (reconciledBar );
1938
+ assertTrue (fooFailed ^ barFailed );
1939
+
1940
+ if (fooFailed ) {
1941
+ assertKafkaTopicConflict (reconciledFoo , reconciledBar );
1942
+ } else {
1943
+ assertKafkaTopicConflict (reconciledBar , reconciledFoo );
1944
+ }
1885
1945
}
1886
1946
1947
+ private void assertKafkaTopicConflict (KafkaTopic failed , KafkaTopic ready ) {
1948
+ // the error message should refer to the ready resource name
1949
+ var condition = assertExactlyOneCondition (failed );
1950
+ assertEquals (TopicOperatorException .Reason .RESOURCE_CONFLICT .reason , condition .getReason ());
1951
+ assertEquals (format ("Managed by Ref{namespace='ns', name='%s'}" , ready .getMetadata ().getName ()), condition .getMessage ());
1952
+
1953
+ // the failed resource should become ready after we unmanage and delete the other
1954
+ LOGGER .info ("Unmanage {}" , ready .getMetadata ().getName ());
1955
+ var unmanagedBar = modifyTopicAndAwait (ready , theKt -> new KafkaTopicBuilder (theKt )
1956
+ .editMetadata ().addToAnnotations (BatchingTopicController .MANAGED , "false" ).endMetadata ().build (),
1957
+ readyIsTrue ());
1958
+
1959
+ LOGGER .info ("Delete {}" , ready .getMetadata ().getName ());
1960
+ Crds .topicOperation (client ).resource (unmanagedBar ).delete ();
1961
+ Resource <KafkaTopic > resource = Crds .topicOperation (client ).resource (unmanagedBar );
1962
+ TopicOperatorTestUtil .waitUntilCondition (resource , Objects ::isNull );
1963
+
1964
+ waitUntil (failed , readyIsTrue ());
1965
+ }
1887
1966
private static <T > KafkaFuture <T > failedFuture (Throwable error ) {
1888
1967
var future = new KafkaFutureImpl <T >();
1889
1968
future .completeExceptionally (error );
0 commit comments