Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[META] Java Annotations for BWC and API enforcement #8127

Open
2 of 5 tasks
nknize opened this issue Jun 16, 2023 · 12 comments
Open
2 of 5 tasks

[META] Java Annotations for BWC and API enforcement #8127

nknize opened this issue Jun 16, 2023 · 12 comments
Assignees
Labels
API Issues with external APIs distributed framework enhancement Enhancement or improvement to existing feature or request Roadmap:Modular Architecture Project-wide roadmap label

Comments

@nknize
Copy link
Collaborator

nknize commented Jun 16, 2023

Is your feature request related to a problem? Please describe.
We provide @opensearch.internal @opensearch.experimental and @opensearch.api to define code that provides a certain set of BWC guarantees. The problem is, this isn't enforceable without maintainers reviewing and manually rejecting PRs.

Describe the solution you'd like
Similar to Apache Spark's annotations, OpenSearch should introduce a new o.o.core.annotation package that (at minimum) defines enforceable java annotations as follows:

@Internal
@Api
@Experimental

And provides a mechanism to define new annotations.

Describe alternatives you've considered
We're currently using javadoc annotations. Let's make these more enforceable through java public @interface Api {}.

@nknize nknize added enhancement Enhancement or improvement to existing feature or request untriaged labels Jun 16, 2023
@nknize nknize changed the title [Feature] Java Annotations [Feature] Java Annotations for BWC and API enforcement Jun 16, 2023
@reta
Copy link
Collaborator

reta commented Aug 2, 2023

@nknize do you mind if I pick it up? I think we could start from the Plugin APIs (those are by definition public APIs) and go up the dependency graph to build the complete picture. Sounds like a plan?

@reta reta self-assigned this Aug 2, 2023
@nknize
Copy link
Collaborator Author

nknize commented Aug 2, 2023

Thank you for taking this one @reta! It will be super helpful!

@reta
Copy link
Collaborator

reta commented Aug 3, 2023

Had a time to give it some thoughts, I envision this feature as a 4 step process:

  1. Define the initial set of annotations, their meaning and relations between them
  2. Decorate the existing APIs with proper annotations
  3. Introduce checks for enforcing the API restrictions
  4. Introduce checks for enforcing the API backward compatibility

Define the initial set of annotations, their meaning and relations between them

As @nknize suggested, I think it would make sense to start with this 4 annotations (please see their description for the guarantees):

/**
 * Stable public APIs that retain source and binary compatibility within a minor release.
 * These interfaces can change from one major release to another major release
 * (e.g. from 1.0 to 2.0). The types marked with this annotations could only expose
 * other {@link PublicApi} or {@link ExperimentalApi} types as public members.
 */
@Documented
@Target({ ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD, ElementType.CONSTRUCTOR })
public @interface PublicApi {

}
/**
 * Experimental APIs that may not retain source and binary compatibility within major,
 * minor or patch releases. The types marked with this annotations could only expose
 * other {@link PublicApi} or {@link ExperimentalApi} types as public members.
 */
@Documented
@Target({ ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD, ElementType.CONSTRUCTOR })
public @interface ExperimentalApi {

}
/**
 * Internal APIs that have no compatibility guarantees and should not be used outside
 * of OpenSearch core components.
 */
@Documented
@Target({ ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD, ElementType.CONSTRUCTOR })
public @interface InternalApi {

}

/**
 * Marks the public APIs as deprecated and scheduled for removal in one of the upcoming
 * major release. The types marked with this annotations could only be other {@link PublicApi}s.
 */
@Documented
@Target({ ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD, ElementType.CONSTRUCTOR })
public @interface DeprecatedApi {
    String since();
    String forRemoval();
}

Alternative arrangement under Api class:

public final class Api {
    private Api() {
    }

    /**
     * Experimental APIs that may not retain source and binary compatibility within major,
     * minor or patch releases. The types marked with this annotations could only expose
     * other {@link Public} or {@link Experimental} types as public members.
     */
    @Documented
    @Target({ ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD, ElementType.CONSTRUCTOR })
    public @interface Experimental {

    }

    /**
     * Internal APIs that have no compatibility guarantees and should be not used outside
     * of OpenSearch core components.
     */
    @Documented
    @Target({ ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD, ElementType.CONSTRUCTOR })
    public @interface Internal {

    }

    /**
     * Stable public APIs that retain source and binary compatibility within a major release.
     * These interfaces can change from one major release to another major release
     * (e.g. from 1.0 to 2.0). The types marked with this annotations could only expose
     * other {@link Public} or {@link Experimental} types as public members.
     */
    @Documented
    @Target({ ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD, ElementType.CONSTRUCTOR })
    public @interface Public {

    }

    /**
     * Marks the public APIs as deprecated and scheduled for removal in one of the upcoming
     * major release. The types marked with this annotations could only be other {@link Public}s.
     */
    @Documented
    @Target({ ElementType.TYPE, ElementType.PACKAGE, ElementType.METHOD, ElementType.CONSTRUCTOR })
    public @interface Deprecated {
        String since();
        String forRemoval();
    }
}

Decorate the existing APIs with proper annotations

The natural starting point is org.opensearch.plugins.* that by definition should be public and available to 3rd party developers (I suspect the footprint will be huge).

Introduce checks for enforcing the API restrictions

Ideally, the public API should never expose anything that is internal. To enforce that (or at least to understand the limits of how far we can go), we have to introspect each type annotated with @Api or @Exprimental and make sure it leaks only @Api or @Exprimental types as well (exceptions, method return values, method arguments, annotations). How can we do that?

  • craft annotation processor
    @SupportedAnnotationTypes("org.opensearch.common.annotation.*")
    public class ApiAnnotationProcessor extends AbstractProcessor {
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latest();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment round) {
            final Set<? extends Element> elements = round.getElementsAnnotatedWithAny(Set.of(Api.class, Experimental.class));
            processingEnv.getMessager().printMessage(Kind.NOTE, "Processing Api annotations");
    
            for (var element : elements) {
                if (element instanceof TypeElement) {
                    final TypeElement type = (TypeElement) element;
                    process(type);
                } 
                 ....
            } 
    
            return false;
        }
    }
  • use something like ArchUnit and introduce the API level tests (pseudo rule is below but it is very close to what we could have as real one)
    public class ApiRules {
        @ArchTest
        public static final ArchRule PUBLIC_API_METHODS_USE_ONLY_PUBLIC_OR_EXPERIMENTAL_API_TYPES =
            freeze(
                methods()
                    .that()
                    .areDeclaredInClassesThat().resideInAPackage("org.opensearch..")
                    .and().arePublic()
                    .should(
                        haveArgsOrReturnTypes()
                            .that()
                            .resideOutsideOfPackage("org.opensearch..")
                            .or()
                                .resideInAPackage("org.opensearch..")
                                .and(areAnnotatedWithAtLeastOneOf(Public.class, Experimental.class)))
                    .as("Return and argument types of methods annotated with @Api or @Experimental must be annotated with @Api or @Experimental."));
    }

Introduce checks for enforcing the API backward compatibility

This is the final goal, once we have the API outlined (annotated with @Api), we could enforce the compatibility rules that have to be applied between version. The japicmp, that was explored here #9044, is one of the most advanced tools we could use for these purposes.

@peternied
Copy link
Member

@reta Thanks for digging in to this, really like the approach, couple of standouts, happy to move these comments to another issue if you are going to put together an RFC(s).

  • I would prefer a more descriptive annotation name @PublicSupportedApi, other names @SemverBreakingApi, @BackwardsCompatiableApi.
  • What kinds of things do we want to put the @Internal annotation on vs items with no annotation, are there examples?
  • There is a concern about marking the Api as ready when it isn't, thoughts on how we will gate this?
  • OK we need to make a breaking change to an API, what do we do?

@peternied
Copy link
Member

One more:

  • Rather than work backward from all of plugins namespace, maybe we can subdivide that effort further, by starting with the APIs consumed by the Extensions SDK [repo], as those interfaces have had the most recent thought about exposing. If there are components that should not be exposed we would need to alter the SDK design.

@andrross
Copy link
Member

andrross commented Aug 4, 2023

@reta What will this look like in a post-JPMS world? If my understanding is correct, everything exported by a JPMS module would be implicitly an @Api and probably not need an annotation, whereas everything not exported would be inaccessible to downstream dependencies and also not need an annotation. The @Experimental annotation would probably still be useful though. Is that right?

@reta
Copy link
Collaborator

reta commented Aug 7, 2023

@reta What will this look like in a post-JPMS world? If my understanding is correct, everything exported by a JPMS module would be implicitly an @Api and probably not need an annotation, whereas everything not exported would be inaccessible to downstream dependencies and also not need an annotation. The @Experimental annotation would probably still be useful though. Is that right?

Thanks @andrross , yes, the @Api & @Experimental in JPMS world would fall into "export to everyone" category, however the catch here is that exports are by packages, not individual classes so the @Internal may still be useful in some places, at least till we move the affected classes under *.internal.* packages.

@reta
Copy link
Collaborator

reta commented Aug 7, 2023

@reta Thanks for digging in to this, really like the approach, couple of standouts, happy to move these comments to another issue if you are going to put together an RFC(s).

Thanks @peternied

I would prefer a more descriptive annotation name @PublicSupportedApi, other names @SemverBreakingApi, @BackwardsCompatiableApi.

I think embedding everything into the name is too much (to me those names look very overloaded). For example, the @SemverBreakingApi does not bear anything to explain why the APIs is it treated like that, vs @Experimental is super clear - work in progress, may change. The @PublicSupportedApi and @BackwardsCompatiableApi are mutually inclusive I think, the public API should always be backward compatible (within minor / patch versions). But I agree that @Api may not be a good name, what about @PublicApi instead?

What kinds of things do we want to put the @internal annotation on vs items with no annotation, are there examples?

Till JPMS, we have only Java visibility rules to control the API exports. Basically everything which is public but should not be so (there are literally thousands of such classes).

There is a concern about marking the Api as ready when it isn't, thoughts on how we will gate this?

No sure I get that, sorry, the Api which is not ready is @Experimental, no?

OK we need to make a breaking change to an API, what do we do?

That should be within major versions only, we document that (preferably deprecate one major version before if it makes sense), and go with the change.

@reta
Copy link
Collaborator

reta commented Aug 7, 2023

  • Rather than work backward from all of plugins namespace, maybe we can subdivide that effort further, by starting with the APIs consumed by the Extensions SDK [repo], a

Thanks, @peternied, the Extensions is experimental feature [1] (as of latest release at least), we may not gain much from it by annotating everything with @Experimental but we could do that as well along with the plugins.

[1] https://opensearch.org/blog/introducing-extensions-for-opensearch/

@peternied
Copy link
Member

There is a concern about marking the Api as ready when it isn't, thoughts on how we will gate this?

No sure I get that, sorry, the Api which is not ready is @experimental, no?

I mean this in the context of new functionality is being built out or when we first do the 'classification' pass. Are there ways that we will guard against just marking everything @Api?

While that would be fast and easy to mark everything @Api level. I would posit that even existing public members marked as @opensearch.api correctly trace back that there are no experimental features / expected breaking associated with them.

Maybe this is more of a philosophical problem, if we don't have rules governing how to know if public should be a public API is it might be easy to get stuck into the same problem space we have today.

Potential rules:

  • After introduced, the class or interface API surface must have no methods added or remove and no updates to existing methods for at least 2 minor release before being eligible to be marked part of the public API.
  • Features marked as internal, cannot be promoted to part of the public API without first being released as experimental for at least 2 minor releases.

@peternied
Copy link
Member

We might also be missing a category for deprecation, reusing the existing annotation should work, but we'd also want the rules to prevent picking up deprecated components.

@reta
Copy link
Collaborator

reta commented Aug 7, 2023

I mean this in the context of new functionality is being built out or when we first do the 'classification' pass. Are there ways that we will guard against just marking everything @Api?

That's should be done by feature implementors I think (besides that, all other checks would guard against exposing the API inappropriately).

Maybe this is more of a philosophical problem, if we don't have rules governing how to know if public should be a public API is it might be easy to get stuck into the same problem space we have today.

There are some shady APIs for sure, that is why the suggestion is to start with what we know (plugins, 100% public) and I am sure, more surprises would be on the way.

We might also be missing a category for deprecation, reusing the existing annotation should work, but we'd also want the rules to prevent picking up deprecated components.

This is (to me) not very clear, the deprecation comes to public APIs which means that those API will be using deprecated components for some time. One of the options I see - introduce new annotation @DeprecatedApi(forRemoval = <OS Version>"). This would give us the following rule: a) the public API in current major version could use the @DeprecatedApi that are scheduled to be removed in the next major version; b) the the public API in current major version could not use @DeprecatedApi that are scheduled to be removed in the current major version;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API Issues with external APIs distributed framework enhancement Enhancement or improvement to existing feature or request Roadmap:Modular Architecture Project-wide roadmap label
Projects
Status: New
Development

No branches or pull requests

6 participants