[NETCONF-938] Cannot generate API docs for Junos device Created: 05/Jan/23  Updated: 01/Feb/24

Status: In Review
Project: netconf
Component/s: restconf-openapi
Affects Version/s: None
Fix Version/s: 7.0.0

Type: Bug Priority: Highest
Reporter: Ivan Hrasko Assignee: Ivan Hrasko
Resolution: Unresolved Votes: 0
Labels: pt
Σ Remaining Estimate: Not Specified Remaining Estimate: Not Specified
Σ Time Spent: Not Specified Time Spent: Not Specified
Σ Original Estimate: Not Specified Original Estimate: Not Specified

Attachments: PNG File Image Pasted at 2023-1-9 10-08.png     PNG File Image Pasted at 2023-1-9 10-09.png     PNG File Image Pasted at 2023-1-9 10-10.png     PNG File Image Pasted at 2023-1-9 10-11.png     PNG File Screenshot_2023-01-04_11-03-36.png     PNG File image-2023-03-15-09-23-20-869.png     PNG File image-2023-03-15-09-47-49-518.png    
Issue Links:
Blocks
is blocked by NETCONF-1241 OpenAPI: Iterate over models in Paths... Resolved
is blocked by NETCONF-1242 OpenAPI: Iterate over models in Schem... Resolved
Duplicate
is duplicated by NETCONF-480 GET on /apidoc/apis takes long time t... Resolved
Relates
relates to NETCONF-1054 OpenApi: POST examples are incorrect Resolved
relates to NETCONF-1056 OpenApi: Do not use ArrayNode to stor... Resolved
relates to NETCONF-1057 OpenApi: Eliminate TOP schemas Resolved
relates to NETCONF-1234 Slow API docs generation for Junos de... Resolved
relates to NETCONF-982 Reduce the number of schema objects Resolved
relates to NETCONF-997 OpenAPI: Use Java Path object instead... Resolved
relates to NETCONF-1023 OpenAPI: Limit usage of JsonUtil#copy... Resolved
relates to NETCONF-1024 OpenAPI: Use Java Schema object inste... Resolved
relates to NETCONF-1025 OpenAPI: Eliminate JsonUtil#addFields... Resolved
relates to NETCONF-1036 OpenApi: Reduce XML elements inside O... Resolved
relates to NETCONF-1041 OpenApi: Transform model to Java records Resolved
relates to NETCONF-1050 OpenApi: Introduce Operation object Resolved
Sub-Tasks:
Key
Summary
Type
Status
Assignee
NETCONF-939 Create swagger benchmark with Junos m... Sub-task In Progress Peter Suna  
NETCONF-982 Reduce the number of schema objects Sub-task Resolved Samuel Schneider  

 Description   

When we have device that contains Junos [common models|https://github.com/Juniper/yang/tree/master/22.3/22.3R1/common] and [junos family models|https://github.com/Juniper/yang/tree/master/22.3/22.3R1/junos] its not possible to access [its mount point documentation|http://localhost:8181/apidoc/explorer/index.html?urls.primaryName=17830-sim-device%20resources%20-%20RestConf%20RFC%208040].

The docs are never loaded and we can see that Java VM is out of heap space. After some time the running ODL instance is killed.



 Comments   
Comment by Peter Suna [ 15/Mar/23 ]

The DefinitionGenerator class is encountering a heap space issue while processing the "junos-conf-root" model.
This issue arises when processing JSON body examples. Despite not being particularly large, this model only creates 100 JSON body examples when a simple test is created with the "junos-conf-root" model and its two import models.

The problem lies in the fact that other models create approximately 62 augmentations to the configuration container within the "junos-conf-root" model. This results in the creation of a large number of JSON body examples, around 1 million, which contain a total of 10 million JSON elements.

The Swagger API docs process each model and append all examples from every model to an object called "definition" inside SwaggerObject. This involves appending 10 million elements, which can take up all available memory.

It is likely that creating 1 million examples for augmentations is not correct and needs to be addressed to fix the issue.

Code in master branch after creating configuration containers.

I have replaced ObjectNodes with DefinitionObjects and results in numbers for this model:

https://git.opendaylight.org/gerrit/c/netconf/+/104878/2

Comment by Ivan Hrasko [ 14/Jun/23 ]

We assume that the high memory consumption is caused by:

  1. Generating unnecessary schema objects, for _XML, _TOP, etc.
  2. Unnecessary usage of many ArraNode(s) and ObjectNode(s).
  3. Recursion.
Comment by Robert Varga [ 27/Jul/23 ]

Right, but at the end of the day the core problem is how JSON serialization is implemented.

What we have is:

    @Override
    public synchronized Response getAllModulesDoc(final UriInfo uriInfo) {
        final DefinitionNames definitionNames = new DefinitionNames();
        final OpenApiObject doc = openApiGeneratorRFC8040.getAllModulesDoc(uriInfo, definitionNames);
        return Response.ok(doc).build();
    }

which then routes to JaxbContextResolver, which then uses ObjectMapper to turn OpenApiObject into JSON (and uses reflection to determine how to do that).

The problem is the fact OpenApiObject exists. It is the moral equivalent of doing Stream.collect() before doing further processing of the stream contents. You have to realize that on input there is EffectiveModelContext and on output there are bytes (which happen to represent a JSON a document).

Response.ok(doc) does this:

    public static ResponseBuilder ok(Object entity) {
        ResponseBuilder b = ok();
        b.entity(entity);
        return b;
    }

and ResponseBuilder.entity() explicitly says:

Note that the entity can be also set as an {@link java.io.InputStream input stream}.

So we really want to integrate at this level – e.g. provide an InputStream whose bytes are the JSON representation of what OpenApiObject models. At the end of the day, BaseYangOpenApiGenerator, needs to return an OpenApiInputStream from getFilledDoc() – the constructor of which just captures stuff from UriInfo and the EffectiveModelContext.

As for the implementation of said OpenApiInputStream, think of it as a more complicated java.util.Iterator. Essentially it iterates over EffectiveModelContext (maybe multiple times) and each time it gets a piece of the JSON document, which it caches internally and gives out its bytes on InputStream.read(). Whenever it runs out of bytes to give out, it moves to the next bit of EffectiveModelContext, turns it into a JSON fragment, and hands that out – until it runs out of EffectiveModelContext, at which point it reports -1 to indicate it is all done.

I think the core interface here ends up being https://www.javadoc.io/static/com.fasterxml.jackson.core/jackson-core/2.15.2/com/fasterxml/jackson/core/JsonGenerator.html, where the generator is pointed to something that holds bytes for OpenApiInputStream to consume and the EffectiveModelContext-processing logic is provided with the JsonGenerator to use methods writeStartObject() and similar. This obviously needs further investigation, but the goal is clear I think.

We already have stuff working on this general principle: see how https://github.com/opendaylight/yangtools/blob/master/model/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/DeclaredStatementFormatter.java works once it is configured. toYangTextSnippet() takes either a module, or a submodule and returns an Iterable – and that in turn defers to https://github.com/opendaylight/yangtools/blob/master/model/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippetIterator.java – which converts a DeclaredStatement (and its subtree) to a sequence of Strings.

We want to do a similar thing, except:

  • it is not DeclaredStatement tree but an EffectiveModelContext tree on input
  • it is not YANG text snippets buts JSON snippet bytes on output
  • the machinery is driven by a request for a byte
  • there is Jackson in between

Perhaps a critical thing to understand: OpenApiInputStream here is consumed by framework is a similar fashion that YangTextSnippet users consume it. InputStream should be viewed as Interator<Integer>, where 'int read()' combines the function of hasNext() and next() (via the return of -1). Plus, obviously, it provides methods to consume bytes in bulk.

At the end of the day, there must be no ObjectMapper or com.fasterxml.jackson.annotation in sight.

Also, to drive this point home even more, think of EffectiveModelContext as a source of events (like "we have a module", "we have an RPC", "we have an RPC input", "we have a leaf", "we ended the RPC input", "we have ended the RPC", "we have ended the module") and we are turning them into JSON events (like beginObject(), endObject(), etc.) and then turning them into bytes. This is stream processing at its purest.

Comment by Robert Varga [ 31/Jul/23 ]

Assuming JAX-RS, we can eliminate some of the complexity by shifing computation to a MessageBodyWriter, when we have the OutputStream available. https://git.opendaylight.org/gerrit/c/netconf/+/107149 provides the basic things, but it highlights we really should think about how the model is actually laid out (centered around OpenApiEntity and its specializations).

Comment by Ivan Hrasko [ 17/Jan/24 ]

With https://git.opendaylight.org/gerrit/c/netconf/+/109803 we can generate and download OpenAPI docs for junos device within 30 seconds with:

curl -o /home/odl/Desktop/openapi.json --location 'http://localhost:8181/openapi/api/v3/mounts/1' --header 'Authorization: Basic YWRtaW46YWRtaW4='
Comment by Ivan Hrasko [ 17/Jan/24 ]

Anyway the whole junos model (1.3 GiB) is not parsable by browsers. We wil try to handle this in NETCONF-1225.

Generated at Wed Feb 07 20:16:17 UTC 2024 using Jira 8.20.10#820010-sha1:ace47f9899e9ee25d7157d59aa17ab06aee30d3d.