Marc Pellmann created NIFI-16474:
------------------------------------

             Summary: Nested versioned flows are fetched from the Flow Registry 
an exponential number of times when loading a flow's contents
                 Key: NIFI-16474
                 URL: https://issues.apache.org/jira/browse/NIFI-16474
             Project: Apache NiFi
          Issue Type: Bug
    Affects Versions: 2.12.0
            Reporter: Marc Pellmann


When NiFi loads a versioned flow together with its nested versioned flows, each 
nested flow is fetched from the Flow Registry Client many times instead of 
once. This happens through StandardFlowRegistryClientNode.getFlowContents(..., 
fetchRemoteFlows = true). The number of fetches grows exponentially with 
nesting depth.

 

This affects every operation that resolves the full snapshot, for example:
 * GET /process-groups/\{id}/local-modifications 
(StandardNiFiServiceFacade.getLocalModifications)
 * putting an existing process group under version control (PUT 
/process-groups/\{id} with versionControlInformation)
 * importing a versioned flow into the canvas

 

The Git-based registry clients (GitHub, GitLab, Bitbucket, Azure DevOps) talk 
to a hosted REST API. With them, a single request takes minutes and issues 
hundreds of API calls.

 

*Cause 1: duplicated call in getFlowContents*
{code:java}
for (final VersionedProcessGroup child : contents.getProcessGroups()) {
    final Map<String, VersionedParameterContext> childParameterContexts = 
populateVersionedContentsRecursively(context, child, snapshotContainer);
    
    for (final Map.Entry<String, VersionedParameterContext> 
childParameterContext : childParameterContexts.entrySet()) {
        
flowSnapshot.getParameterContexts().putIfAbsent(childParameterContext.getKey(), 
childParameterContext.getValue());
    }
    populateVersionedContentsRecursively(context, child, snapshotContainer);  
// <-- second, identical call
} {code}
populateVersionedContentsRecursively is called twice for every child. The 
result of the second call is discarded. The line was introduced with NIFI-11464.

 

*Cause 2: nested flows are resolved twice per level*

For a group that has VersionedFlowCoordinates, 
populateVersionedContentsRecursively calls fetchFlowContents(context, 
coordinates, true). That goes through clientNode.getFlowContents(..., 
fetchRemoteFlows = true), which already resolves every nested flow below that 
group. The group's contents are then copied over, and the method recurses into 
group.getProcessGroups() again. Those children still carry their coordinates, 
so every nested flow below is fetched again.

Combining both causes, reading the code suggests that a flow at nesting depth d 
is fetched on the order of 2·3^(d−1) times. If the same flow occurs several 
times in the tree, that number is multiplied again. Nested contents are also 
added to the FlowSnapshotContainer repeatedly.

 

*Cause 3: every fetch also lists the file's commit history (Git-based clients)*

AbstractGitFlowRegistryClient.getFlowContents calls getLatestVersion on every 
fetch, only to set RegisteredFlowSnapshot.isLatest. That lists the commit 
history of the snapshot file (GitRepositoryClient.getCommits), which may span 
several pages. Each fetch therefore costs at least two REST calls: content plus 
history. 

 

*Observed*

A flow with three levels of nesting, where one nested flow is used three times, 
on Bitbucket Cloud. A single GET /process-groups/\{id}/local-modifications ran 
for more than 4.5 minutes. During that time it fetched individual nested flows 
dozens of times (e.g. Nested.json 44 times, still running). One request thread 
produced more than 1,200 Bitbucket client log entries. Earlier, putting the 
same group under version control fetched the deepest nested flows 108 times 
each and took ~35 s.

 

*Impact*
 * getLocalModifications holds the flow read lock 
(NiFiServiceFacadeLock.proceedWithReadLock) for the whole time. Every write 
request waits behind it, e.g. deleting the very process group. 
 * Synchronous REST calls such as starting version control and importing a flow 
can outlast client read timeouts. The operation still completes on the server, 
and a client retry then fails with 409 ("process group is already under version 
control") or creates a duplicate group.
 * The hosted Git APIs see hundreds of calls per user action, which risks rate 
limiting.

 

*Steps to reproduce*
 #  Configure a Git-based Flow Registry Client, e.g. the Bitbucket or GitHub 
client.
 #  Version flows B and C. Version flow A so that it contains B, and B contains 
C. Optionally reference B more than once inside A.
 #  Import A into the canvas, or call GET 
/nifi-api/process-groups/\{id}/local-modifications on it.
 #  Enable DEBUG logging for the registry client. Each nested snapshot file is 
requested many times, and the count grows with depth.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to