youngkermit8-coder opened a new issue, #7197:
URL: https://github.com/apache/rocketmq-dashboard/issues/7197

   ### Before Creating the Bug Report
   
   - [x] I searched all-state issues and PRs for import/interruption, 
importTopics/cancel, importConsumerGroups/interrupt and batch-import 
cancellation; the finite searches did not identify an equivalent outer-batch 
fix.
   - [x] This concerns Studio configuration-import orchestration, not a broker 
usage question.
   - [x] The exact source version and controlled reproduction are stated below.
   
   ### Studio Version
   
   Source-built `rocketmq-studio@127c47b06a268cc74224d5f34c56ec7c36ac0d55`. 
This was still the branch tip during the current reproduction/preflight. No 
production fix, POM change or dependency override was applied; only the test 
below was added to an isolated source export.
   
   ### Runtime Environment
   
   Windows, Temurin 21.0.12+8, Maven 3.9.9. The real MetadataService, 
AliyunInstanceProvider and AliyunClientFactory run with the official 
AsyncClient mocked at the network boundary. Instance routing, ownership guard 
and audit collaborators are mocked. No HTTP application, real cloud, database 
or broker is started. The fixture uses synthetic resource identifiers and never 
reads credentials.
   
   ### Connected RocketMQ Cluster
   
   None. Recorded SDK invocations are mock submissions, not confirmed cloud 
resource creation or a production incident.
   
   ### Build Toolchain
   
   JDK21 / Maven3.9.9. The current targeted test run intentionally exits1 with 
eight assertions showing unwanted later SDK requests; it is not a successful 
fixed regression, full suite or package run. Checkstyle reports zero 
violations. Existing effective-model/annotation-processing/deprecation warnings 
remain in the local log.
   
   ### Describe the Bug
   
   `MetadataService.importTopics` and `importConsumerGroups` catch each row's 
exception and proceed to the next row. If the real AliyunClientFactory is 
interrupted while waiting for a pending SDK Future, it cancels that Future and 
restores the interrupt flag. The import loop then still invokes the next row's 
SDK creation method. A completed Future can return successfully on an 
already-interrupted thread, so another client-factory wait does not reliably 
stop the batch.
   
   This is a cooperative row-boundary admission gap. It does not mean that 
closing a browser necessarily interrupts a server thread, or that an 
already-issued request can be rolled back.
   
   ### Steps to Reproduce
   
   At the pinned base, add the following exact fixture at 
`server/src/test/java/org/apache/rocketmq/studio/instance/topic/MetadataImportInterruptionTest.java`:
   
   ```java
   /*
    * Licensed to the Apache Software Foundation (ASF) under one or more
    * contributor license agreements.  See the NOTICE file distributed with
    * this work for additional information regarding copyright ownership.
    * The ASF licenses this file to You 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 org.apache.rocketmq.studio.instance.topic;
   
   import com.aliyun.sdk.service.rocketmq20220801.AsyncClient;
   import 
com.aliyun.sdk.service.rocketmq20220801.models.CreateConsumerGroupRequest;
   import 
com.aliyun.sdk.service.rocketmq20220801.models.CreateConsumerGroupResponse;
   import com.aliyun.sdk.service.rocketmq20220801.models.CreateTopicRequest;
   import com.aliyun.sdk.service.rocketmq20220801.models.CreateTopicResponse;
   import org.apache.rocketmq.studio.audit.OperationAuditService;
   import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
   import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
   import org.apache.rocketmq.studio.common.domain.enums.TopicType;
   import org.apache.rocketmq.studio.instance.InstanceRepository;
   import org.apache.rocketmq.studio.instance.InstanceResolver;
   import org.apache.rocketmq.studio.instance.InstanceVO;
   import org.apache.rocketmq.studio.instance.ResourceOwnershipGuard;
   import org.apache.rocketmq.studio.instance.group.CreateConsumerGroupDTO;
   import org.apache.rocketmq.studio.instance.message.MessageService;
   import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
   import org.apache.rocketmq.studio.provider.alibaba.AliyunClientFactory;
   import org.apache.rocketmq.studio.provider.alibaba.AliyunInstanceProvider;
   import org.apache.rocketmq.studio.provider.apache.AdminClient;
   import org.apache.rocketmq.studio.provider.apache.MetadataProvider;
   import 
org.apache.rocketmq.studio.provider.credential.CloudCredentialRepository;
   import org.junit.jupiter.api.AfterEach;
   import org.junit.jupiter.api.BeforeEach;
   import org.junit.jupiter.api.Timeout;
   import org.junit.jupiter.params.ParameterizedTest;
   import org.junit.jupiter.params.provider.ValueSource;
   
   import java.util.ArrayList;
   import java.util.List;
   import java.util.Optional;
   import java.util.concurrent.CompletableFuture;
   import java.util.concurrent.CountDownLatch;
   import java.util.concurrent.FutureTask;
   import java.util.concurrent.TimeUnit;
   import java.util.concurrent.atomic.AtomicBoolean;
   
   import static org.assertj.core.api.Assertions.assertThat;
   import static org.mockito.ArgumentMatchers.any;
   import static org.mockito.Mockito.mock;
   import static org.mockito.Mockito.when;
   
   /** Exercises real service, provider and future-interruption handling with a 
network-free SDK boundary. */
   @Timeout(10)
   class MetadataImportInterruptionTest {
       private final AsyncClient sdk = mock(AsyncClient.class);
       private final List<String> submitted = new ArrayList<>();
       private final AliyunClientFactory factory = new 
AliyunClientFactory(mock(CloudCredentialRepository.class)) {
           @Override
           public AsyncClient client(Long credentialId, String region) {
               return sdk;
           }
       };
       private MetadataService service;
       private String mode = "success";
       private CompletableFuture<?> interruptedFuture;
       private final CountDownLatch pendingRequest = new CountDownLatch(1);
   
       @BeforeEach
       void setUpTest() {
           InstanceVO instance = InstanceVO.builder().name("local-fixture")
                   
.vendor(InstanceVendor.ALIYUN).cloudInstanceId("synthetic-cloud-id")
                   .regionId("cn-hangzhou").credentialId(1L).build();
           InstanceRepository repository = mock(InstanceRepository.class);
           
when(repository.findByIdentifier("local-fixture")).thenReturn(Optional.of(instance));
           AliyunInstanceProvider provider = new 
AliyunInstanceProvider(factory, repository);
           InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
           
when(registry.byInstanceId("local-fixture")).thenReturn(Optional.of(provider));
           InstanceResolver resolver = mock(InstanceResolver.class);
           
when(resolver.findByIdentifier("local-fixture")).thenReturn(Optional.of(instance));
           ResourceOwnershipGuard guard = mock(ResourceOwnershipGuard.class);
           when(guard.requireInstance("local-fixture")).thenReturn(instance);
           service = new MetadataService(mock(MetadataProvider.class), 
mock(AdminClient.class), registry, resolver,
                   mock(OperationAuditService.class), 
mock(MessageService.class),
                   mock(RuntimeAdminClientResolver.class), guard);
           when(sdk.createTopic(any(CreateTopicRequest.class))).thenAnswer(call 
-> {
               CreateTopicRequest request = call.getArgument(0);
               return complete(request.getTopicName(), 
CreateTopicResponse.create().toBuilder().statusCode(200).build());
           });
           
when(sdk.createConsumerGroup(any(CreateConsumerGroupRequest.class))).thenAnswer(call
 -> {
               CreateConsumerGroupRequest request = call.getArgument(0);
               return complete(request.getConsumerGroupId(),
                       
CreateConsumerGroupResponse.create().toBuilder().statusCode(200).build());
           });
       }
   
       private <T> CompletableFuture<T> complete(String name, T response) {
           submitted.add(name);
           if ("row-1".equals(name)) {
               if ("interrupt".equals(mode) || 
"external-interrupt".equals(mode)) {
                   CompletableFuture<T> pending = new CompletableFuture<>();
                   interruptedFuture = pending;
                   // The actual factory's timed get must cancel this future 
and restore the flag.
                   if ("interrupt".equals(mode)) {
                       Thread.currentThread().interrupt();
                   }
                   pendingRequest.countDown();
                   return pending;
               }
               if ("failure".equals(mode)) {
                   return CompletableFuture.failedFuture(new 
IllegalStateException("ordinary SDK failure"));
               }
               if ("success-interrupted".equals(mode)) {
                   Thread.currentThread().interrupt();
               }
           }
           return CompletableFuture.completedFuture(response);
       }
   
       @AfterEach
       void clearFixtureInterruptTest() {
           Thread.interrupted();
           factory.close();
       }
   
       @ParameterizedTest
       @ValueSource(booleans = {false, true})
       void interruptionStopsLaterSdkRequestsTest(boolean groups) {
           mode = "interrupt";
           Outcome result = importRows(groups);
   
           assertThat(interruptedFuture).isNotNull();
           assertThat(interruptedFuture.isCancelled()).isTrue();
           assertThat(Thread.currentThread().isInterrupted()).isTrue();
           assertThat(submitted).containsExactly("row-0", "row-1");
           assertThat(result.imported()).isEqualTo(1);
           assertThat(result.failed()).isEqualTo(2);
           assertThat(result.failureIndices()).containsExactly(1, 2);
           assertThat(result.messages().getLast()).contains("not attempted");
       }
   
       @ParameterizedTest
       @ValueSource(booleans = {false, true})
       void 
anotherThreadCanInterruptPendingBatchWithoutFurtherWritesTest(boolean groups) 
throws Exception {
           mode = "external-interrupt";
           AtomicBoolean interruptRetained = new AtomicBoolean();
           FutureTask<Outcome> task = new FutureTask<>(() -> {
               Outcome result = importRows(groups);
               interruptRetained.set(Thread.currentThread().isInterrupted());
               return result;
           });
           Thread worker = new Thread(task, 
"metadata-import-interruption-fixture");
           worker.start();
           try {
               assertThat(pendingRequest.await(5, TimeUnit.SECONDS)).isTrue();
               worker.interrupt();
               Outcome result = task.get(5, TimeUnit.SECONDS);
   
               assertThat(interruptedFuture.isCancelled()).isTrue();
               assertThat(interruptRetained).isTrue();
               assertThat(submitted).containsExactly("row-0", "row-1");
               assertThat(result.imported()).isEqualTo(1);
               assertThat(result.failed()).isEqualTo(2);
               assertThat(result.failureIndices()).containsExactly(1, 2);
           } finally {
               worker.interrupt();
               worker.join(2_000);
               assertThat(worker.isAlive()).isFalse();
           }
       }
   
       @ParameterizedTest
       @ValueSource(booleans = {false, true})
       void completedRowIsRetainedWhenInterruptArrivesBeforeNextRowTest(boolean 
groups) {
           mode = "success-interrupted";
           Outcome result = importRows(groups);
   
           assertThat(Thread.currentThread().isInterrupted()).isTrue();
           assertThat(submitted).containsExactly("row-0", "row-1");
           assertThat(result.imported()).isEqualTo(2);
           assertThat(result.failed()).isEqualTo(1);
           assertThat(result.failureIndices()).containsExactly(2);
       }
   
       @ParameterizedTest
       @ValueSource(booleans = {false, true})
       void alreadyInterruptedBatchDoesNotSubmitSdkRequestsTest(boolean groups) 
{
           Thread.currentThread().interrupt();
           Outcome result = importRows(groups);
   
           assertThat(Thread.currentThread().isInterrupted()).isTrue();
           assertThat(submitted).isEmpty();
           assertThat(result.imported()).isZero();
           assertThat(result.failed()).isEqualTo(3);
           assertThat(result.failureIndices()).containsExactly(0, 1, 2);
       }
   
       @ParameterizedTest
       @ValueSource(booleans = {false, true})
       void ordinarySdkFailureStillAllowsOtherRowsTest(boolean groups) {
           mode = "failure";
           Outcome result = importRows(groups);
   
           assertThat(Thread.currentThread().isInterrupted()).isFalse();
           assertThat(submitted).containsExactly("row-0", "row-1", "row-2");
           assertThat(result.imported()).isEqualTo(2);
           assertThat(result.failed()).isEqualTo(1);
           assertThat(result.failureIndices()).containsExactly(1);
       }
   
       @ParameterizedTest
       @ValueSource(booleans = {false, true})
       void uninterruptedBatchImportsAllRowsTest(boolean groups) {
           Outcome result = importRows(groups);
   
           assertThat(submitted).containsExactly("row-0", "row-1", "row-2");
           assertThat(result.imported()).isEqualTo(3);
           assertThat(result.failed()).isZero();
       }
   
       private Outcome importRows(boolean groups) {
           List<String> names = List.of("row-0", "row-1", "row-2");
           if (groups) {
               var result = service.importConsumerGroups("local-fixture", 
names.stream().map(name -> {
                   CreateConsumerGroupDTO dto = new CreateConsumerGroupDTO();
                   dto.setName(name);
                   return dto;
               }).toList());
               return new Outcome(result.getImported(), result.getFailed(),
                       result.getFailures().stream().map(failure -> 
failure.getIndex()).toList(),
                       result.getFailures().stream().map(failure -> 
failure.getMessage()).toList());
           }
           var result = service.importTopics("local-fixture", 
names.stream().map(name -> {
               CreateTopicDTO dto = new CreateTopicDTO();
               dto.setName(name);
               dto.setType(TopicType.NORMAL);
               return dto;
           }).toList());
           return new Outcome(result.getImported(), result.getFailed(),
                   result.getFailures().stream().map(failure -> 
failure.getIndex()).toList(),
                   result.getFailures().stream().map(failure -> 
failure.getMessage()).toList());
       }
   
       private record Outcome(int imported, int failed, List<Integer> 
failureIndices, List<String> messages) {
       }
   }
   ```
   
   Run from `server/` using Java21:
   
   ```sh
   mvn -B -ntp -Dtest=MetadataImportInterruptionTest 
-Dsurefire.failIfNoSpecifiedTests=true test
   ```
   
   The current Windows invocation used offline cached dependencies, bounded 
heaps and the existing Mockito agent explicitly. The external-interrupt cases 
wait on a latch for row1's pending Future, then interrupt the actual importing 
worker and join it with a bound; there is no network call or timing sleep. 
Cleanup clears only the test thread's flag and closes the fixture factory.
   
   ### What Did You Expect to See?
   
   Once an interrupt is visible at a row boundary, do not start provider 
mutations for later rows; preserve the flag and already-confirmed successes. A 
non-interruption row failure should continue to permit subsequent rows.
   
   The representation of unattempted rows needs agreement: should the existing 
`failed`/`failures` fields include them with original indices and an explicit 
"not attempted" reason, or should the batch use another established response 
policy? The fixture contains assertions for the first option, but the baseline 
failures occur earlier at unwanted SDK submission assertions and do not 
establish maintainer agreement on that response design.
   
   ### What Did You See Instead?
   
   Twelve invocations on the unchanged current production implementation: eight 
interruption assertions fail, four healthy/ordinary-failure controls pass, zero 
errors or skips.
   
   - Pending row1 interrupted in the importing thread: both import paths still 
submit row2; the pending Future is cancelled and the flag is retained.
   - A separate thread interrupts the pending row1: both paths still submit 
row2.
   - Row1 completes while the flag becomes set: both paths still submit row2 
instead of stopping after confirmed success.
   - The batch begins interrupted: both paths submit rows0,1,2 rather than no 
SDK requests.
   - With no interruption, all three rows succeed; with an ordinary exceptional 
row1 Future, row2 proceeds. These four controls pass.
   
   An independent local audit verified the 1142 original exported files against 
frozen archive/disk and pinned Git blobs, allowing only the export's CRLF-to-LF 
conversion. The initial auditor rejected raw-byte identity because `git 
archive` inherited `core.autocrlf=true`; that failed audit is preserved, and 
the follow-up proved only line-ending differences without changing source, XML 
or rerunning tests. The three compiled production classes match a separately 
built pure baseline at the same commit. This provenance audit is not additional 
behavioral coverage or a fixed/full-build result.
   
   ### Additional Context
   
   This differs from #1519/#1520, which address interruption of an individual 
Aliyun SDK Future and are already represented in the baseline. #5544 concerns 
DLQ processing, not topic/consumer-group configuration imports. Both import 
methods share one root cause and belong in one contribution, not separate PRs. 
Searches are bounded, not proof that no differently worded related work exists.
   
   Earlier local prototypes and old-snapshot regressions predate this Issue. 
They are not claimed as current-base implementation validation or consensus. An 
older separate real-MySQL/Hikari audit probe observed that an interrupted 
failure audit can be lost when the connection pool is occupied; unattempted 
rows were not individually audited. The reproduction above mocks audit and does 
not retest or solve that limitation. A row-boundary check is cooperative, not 
atomic fencing, rollback, exactly-once execution or an audit-durability 
guarantee. The already-issued interrupted row's actual cloud state must be 
checked before retrying in a real deployment.
   
   Could maintainers confirm the intended partial-result and audit semantics 
before a code PR? No API/schema/dependency rewrite is proposed here. No code 
PR, binaries, raw logs, database, credentials or unrelated patches are 
attached. AI assistance was used for investigation and verification. No contest 
score or merge outcome is claimed.
   
   ### Are You Willing to Submit a Pull Request?
   
   - [x] Yes, after the necessary response/audit feedback, fresh implementation 
regression and normal packaging checks.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to