mcgilman commented on code in PR #11677:
URL: https://github.com/apache/nifi/pull/11677#discussion_r4020218523


##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connectors-listing/connectors-listing.effects.ts:
##########
@@ -433,6 +440,82 @@ export class ConnectorsListingEffects {
         )
     );
 
+    openChangeConnectorVersionDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(openChangeConnectorVersionDialog),
+                map((action) => action.request),
+                switchMap((request) =>
+                    
from(this.extensionTypesService.getConnectorVersionsForType(request.type, 
request.bundle)).pipe(
+                        map(
+                            (response) =>
+                                ({
+                                    fetchRequest: request,
+                                    componentVersions: response.connectorTypes
+                                }) as OpenChangeComponentVersionDialogRequest
+                        ),
+                        catchError((errorResponse: HttpErrorResponse) => {
+                            this.store.dispatch(
+                                ErrorActions.snackBarError({
+                                    error: 
this.errorHelper.getErrorString(errorResponse)
+                                })
+                            );
+                            return EMPTY;
+                        })
+                    )
+                ),
+                tap((request) => {
+                    const dialogRequest = 
this.dialog.open(ChangeComponentVersionDialog, {
+                        ...LARGE_DIALOG,
+                        data: request,
+                        autoFocus: false
+                    });
+
+                    
dialogRequest.componentInstance.changeVersion.pipe(take(1)).subscribe((newVersion)
 => {
+                        this.store.dispatch(
+                            changeConnectorVersion({
+                                request: {
+                                    id: request.fetchRequest.id,
+                                    uri: request.fetchRequest.uri,
+                                    payload: {
+                                        component: {
+                                            bundle: newVersion.bundle,
+                                            id: request.fetchRequest.id
+                                        },
+                                        revision: request.fetchRequest.revision
+                                    }
+                                }
+                            })
+                        );
+                        dialogRequest.close();
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    changeConnectorVersion$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(changeConnectorVersion),
+            map((action) => action.request),
+            concatMap((request) =>

Review Comment:
   **Medium — prevent queuing a stale revision**
   
   The dialog closes immediately after dispatch, but the Change Version table 
action remains available while the existing `saving` state is true. A second 
invocation can capture the same revision, and `concatMap` queues it until the 
first succeeds, guaranteeing a stale-revision failure. Please wire the existing 
listing `saving` state specifically to this new table action and disable it 
while the request is pending. This does not require changing the shared dialog 
or other table actions.



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java:
##########
@@ -5409,7 +5409,19 @@ public ConnectorDTO createConnectorDto(final 
ConnectorNode connector) {
         dto.setType(connector.getCanonicalClassName());
         dto.setExtensionMissing(connector.isExtensionMissing());
 
-        dto.setBundle(createBundleDto(connector.getBundleCoordinate()));
+        final BundleCoordinate bundleCoordinate = 
connector.getBundleCoordinate();
+        final List<Bundle> availableBundles = 
extensionManager.getBundles(connector.getCanonicalClassName());
+        int compatibleBundleCount = 0;
+        for (final Bundle bundle : availableBundles) {
+            final BundleCoordinate coordinate = 
bundle.getBundleDetails().getCoordinate();
+            if (bundleCoordinate.getGroup().equals(coordinate.getGroup()) && 
bundleCoordinate.getId().equals(coordinate.getId())) {
+                compatibleBundleCount++;
+            }
+        }
+
+        dto.setMultipleVersionsAvailable(connector.isExtensionMissing() ? 
compatibleBundleCount > 0 : compatibleBundleCount > 1);

Review Comment:
   **Medium — merge version availability across cluster nodes**
   
   This value is calculated independently on each node, but 
`ConnectorEntityMerger` retains whichever node supplied the client entity. The 
UI can therefore offer Change Version when another node reports no compatible 
alternative. Cluster validation prevents a partial update, but the offered 
operation will fail. Please merge this conservatively, as 
`ProcessorEntityMerger` does: any null/false node result should produce false. 
A mixed-node merger test would cover this.



##########
nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java:
##########
@@ -1310,6 +1318,126 @@ public Connector getConnector() {
         return connectorDetails.getConnector();
     }
 
+    @Override
+    public void verifyCanReload() {
+        if (!isStopped()) {
+            throw new IllegalStateException("Cannot reload " + this + " 
because its state is " + getCurrentState());
+        }
+    }
+
+    @Override
+    public void verifyCanUpdateBundle(final BundleCoordinate 
incomingCoordinate) {
+        final BundleCoordinate currentCoordinate = getBundleCoordinate();
+        if 
(!currentCoordinate.getGroup().equals(incomingCoordinate.getGroup()) || 
!currentCoordinate.getId().equals(incomingCoordinate.getId())) {
+            throw new IllegalArgumentException("Cannot update " + this + " 
from " + currentCoordinate.getCoordinate() + " to "
+                + incomingCoordinate.getCoordinate() + " because the bundle 
group and artifact must be unchanged");
+        }
+    }
+
+    @Override
+    public void replaceConnector(final Connector replacement, final 
BundleCoordinate replacementCoordinate, final ComponentLog replacementLog) 
throws FlowUpdateException {
+        verifyCanReload();
+
+        final FrameworkConnectorInitializationContext 
replacementInitializationContext = new 
StandardConnectorInitializationContext.Builder()
+            .identifier(identifier)
+            .name(name)
+            .componentLog(replacementLog)
+            .secretsManager(initializationContext.getSecretsManager())
+            .assetManager(initializationContext.getAssetManager())
+            
.componentBundleLookup(initializationContext.getComponentBundleLookup())
+            .build();
+
+        try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(replacement.getClass().getClassLoader())) {
+            replacement.initialize(replacementInitializationContext);
+        }
+
+        final WorkingFlowContextState workingContextState = 
acquireWorkingFlowContext();
+        final FrameworkFlowContext workingContext = 
workingContextState.getContext();
+        boolean workingContextReleased = false;
+        try {
+            final Map<String, StepConfiguration> originalActiveConfiguration = 
toConfigurationMap(activeFlowContext.getConfigurationContext().toConnectorConfiguration());
+            final Map<String, StepConfiguration> originalWorkingConfiguration 
= workingContext == null ? originalActiveConfiguration
+                : 
toConfigurationMap(workingContext.getConfigurationContext().toConnectorConfiguration());
+
+            final Map<String, StepConfiguration> activeConfiguration = 
migrateProperties(replacement, originalActiveConfiguration);
+            final Map<String, StepConfiguration> workingConfiguration = 
migrateProperties(replacement, originalWorkingConfiguration);
+            final Bundle bundle = new Bundle(replacementCoordinate.getGroup(), 
replacementCoordinate.getId(), replacementCoordinate.getVersion());
+
+            replaceConfiguration(activeFlowContext.getConfigurationContext(), 
activeConfiguration);

Review Comment:
   **High — incomplete rollback during connector replacement**
   
   Rollback currently covers only failures from `notifyStepConfigured()`. 
`replaceConfiguration()` can fail during secret/asset resolution after earlier 
steps have been mutated, and either flow-context `reload()` can also fail after 
configuration or connector metadata has changed. These paths can leave migrated 
configuration under the old connector or leave the node referencing the 
replacement whose candidate classloader is subsequently closed. Please stage 
the replacement and publish it atomically, or restore every mutated 
field/context for all failure paths. Tests should inject failures during 
configuration resolution and both context reloads.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-table/connector-table.component.ts:
##########
@@ -157,6 +159,20 @@ export class ConnectorTable {
         return isConnectorActionAllowed(entity, 'PURGE_FLOWFILES');
     }
 
+    canChangeVersion(entity: ConnectorEntity): boolean {
+        const versionChangeEligibleStates = [
+            ConnectorState.STOPPED,
+            ConnectorState.UPDATED,
+            ConnectorState.UPDATE_FAILED
+        ];
+        return (
+            this.canRead(entity) &&
+            this.canModify(entity) &&
+            versionChangeEligibleStates.includes(entity.component.state as 
ConnectorState) &&

Review Comment:
   **Medium — account for active threads in eligible states**
   
   `UPDATED` and `UPDATE_FAILED` are considered eligible unconditionally, but 
backend `verifyCanReload()` treats those states as stopped only when the active 
flow has no active threads. This can expose Change Version while the request 
will be rejected with 409. Please require 
`entity.status.aggregateSnapshot.activeThreadCount === 0` for these two states 
and add active/quiescent test cases. `STOPPED` can retain its current behavior.



-- 
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