Skip to content

Add @RestrictedApi to D2ClientBuilder constructor and build()#1155

Draft
singhsanjay12 wants to merge 1 commit intolinkedin:masterfrom
singhsanjay12:singhsanjay12/restrict-d2-client-builder
Draft

Add @RestrictedApi to D2ClientBuilder constructor and build()#1155
singhsanjay12 wants to merge 1 commit intolinkedin:masterfrom
singhsanjay12:singhsanjay12/restrict-d2-client-builder

Conversation

@singhsanjay12
Copy link
Copy Markdown

@singhsanjay12 singhsanjay12 commented Mar 25, 2026

Summary

Annotate D2ClientBuilder's constructor and build() method with Error Prone's @RestrictedApi to produce compile-time errors when LinkedIn-internal callers instantiate or invoke the builder directly outside of blessed paths. External open-source callers are unaffected.


Motivation

D2ClientBuilder is a raw, low-level API. LinkedIn container apps must use D2ClientFactory instead, which automatically applies INDIS configuration, future platform migrations, and framework features.

A LOG.warn / LOG.error at runtime already exists but:

  • It only fires when the process runs, not when code is written
  • It is easy to miss in log noise
  • It cannot block new regressions at code-review time

A compile-time Error Prone checker closes the feedback loop at build time.


Changes

build.gradle (root)

  • Added errorProneAnnotations: 'com.google.errorprone:error_prone_annotations:2.26.1' to externalDependency

d2/build.gradle

  • Added compileOnly externalDependency.errorProneAnnotations

AllowRawD2ClientBuilder.java (new)

  • Opt-in marker annotation for LinkedIn-internal callers that legitimately need direct access during migration
  • Has a required reason() field to force documentation at every exception
  • @Retention(CLASS) — compile-time only, no runtime cost
  • @Target({TYPE, METHOD, CONSTRUCTOR}) — callers can be surgical about scope

D2ClientBuilder.java

  • Added import com.google.errorprone.annotations.RestrictedApi
  • Added explicit no-arg constructor annotated with @RestrictedApi
  • Annotated build() with @RestrictedApi

Allowed callers

Caller Path contains com/linkedin? Result
D2ClientFactory.java yes ✅ allowed (explicit path match)
Any test/ file either ✅ allowed (explicit path match)
External user (org.foo, com.example, etc.) no ✅ allowed (^(?!.*com/linkedin).*$)
LinkedIn-internal app (com.linkedin.container) yes ❌ blocked → compile error
LinkedIn-internal app with @AllowRawD2ClientBuilder yes ✅ allowed (annotation escape hatch)
LinkedIn-internal app with @SuppressWarnings("RestrictedApiChecker") yes ✅ allowed (emergency escape hatch)

allowedOnPath breakdown

allowedOnPath = ".*/factory/D2ClientFactory\\.java"  // LinkedIn's blessed factory
    + "|.*/test/.*"                                   // all test code
    + "|^(?!.*com/linkedin).*$",                      // all non-LinkedIn (open-source) callers

Migration tracking

Search cu:AllowRawD2ClientBuilder in Jarvis codesearch to get a live inventory of remaining exceptions and drive them to zero over time.


Testing & Verification

⚠️ Prerequisite: Error Prone compiler plugin

@RestrictedApi is enforced by Error Prone's RestrictedApiChecker — it requires the net.ltgt.errorprone Gradle plugin to be configured as the compiler plugin. Without it, the annotation compiles cleanly but is not enforced. This PR adds the annotation and allowlist infrastructure; wiring in the Gradle plugin is the necessary follow-up to activate enforcement.

Local demo — all 6 cases verified

A standalone project with Error Prone configured was used to verify all enforcement scenarios end-to-end:

# File Caller type Expected Actual
1 com/linkedin/demo/BadCaller.java LinkedIn-internal, no annotation ❌ BLOCKED [RestrictedApi] error
2 org/external/ExternalUser.java External open-source ✅ ALLOWED ✅ compiles clean
3 com/linkedin/demo/AllowedInternalCaller.java LinkedIn-internal + @AllowRawD2ClientBuilder ✅ ALLOWED ✅ compiles clean
4 com/linkedin/demo/SuppressedCaller.java LinkedIn-internal + @SuppressWarnings ✅ ALLOWED ✅ compiles clean
5 com/linkedin/factory/D2ClientFactory.java Blessed factory path ✅ ALLOWED ✅ compiles clean
6 com/linkedin/test/D2ClientBuilderTest.java test/ path ✅ ALLOWED ✅ compiles clean

Case 1 — LinkedIn-internal caller blocked:

// com/linkedin/demo/BadCaller.java
public class BadCaller {
    public void go() { new D2ClientBuilder().build(); }
}
BadCaller.java:4: error: [RestrictedApi] Restricted for LinkedIn-internal callers.
    Use D2ClientFactory. See go/onboardindis.
        public void go() { new D2ClientBuilder().build(); }
                                                 ^

Case 2 — External open-source caller allowed (no annotation needed):

// org/external/ExternalUser.java
public class ExternalUser {
    public void go() { new D2ClientBuilder().build(); } // compiles clean
}

Case 3 — LinkedIn-internal with @AllowRawD2ClientBuilder allowed:

@AllowRawD2ClientBuilder(reason = "Legacy service, migrating via JIRA-1234")
public class AllowedInternalCaller {
    public void go() { new D2ClientBuilder().build(); } // compiles clean
}

D2ClientBuilderTest — passes locally (Java 11, Gradle 6.9.4)

BUILD SUCCESSFUL
D2ClientBuilderTest: all tests pass (test/ matches allowedOnPath — no restriction triggered)

Related

  • go/onboardindis — INDIS migration guide
  • D2ClientFactory in pegasus-d2-client-factory
  • Follow-up: add net.ltgt.errorprone Gradle plugin to activate compile-time enforcement

@singhsanjay12 singhsanjay12 force-pushed the singhsanjay12/restrict-d2-client-builder branch 3 times, most recently from a46e4cd to e7ff079 Compare March 25, 2026 22:25
Annotate D2ClientBuilder's constructor and build() method with Error
Prone's @RestrictedApi to produce compile-time errors when callers
instantiate or invoke the builder directly, outside of blessed paths.

Why:
- D2ClientBuilder is a raw, low-level API. LinkedIn container apps
  must use D2ClientFactory instead, which auto-applies INDIS config,
  future migrations, and platform features.
- Runtime LOG.warn / LOG.error already exist but are easy to miss.
  A compile-time checker closes the feedback loop earlier.

Changes:
- build.gradle: add error_prone_annotations 2.26.1 to externalDependency
- d2/build.gradle: wire in compileOnly errorProneAnnotations
- AllowRawD2ClientBuilder.java: new opt-in marker annotation with a
  required 'reason' field, for customers that legitimately need direct
  access during migration
- D2ClientBuilder.java:
  - import RestrictedApi
  - explicit no-arg constructor annotated with @RestrictedApi
  - build() annotated with @RestrictedApi
  - both annotations allowlist D2ClientFactory.java, test paths,
    @AllowRawD2ClientBuilder, and @SuppressWarnings('RestrictedApiChecker')

Allowed callers:
  allowedOnPath  : .*/factory/D2ClientFactory\.java | .*/test/.*
  allowlistAnnotations: @AllowRawD2ClientBuilder (with required reason),
                        @SuppressWarnings('RestrictedApiChecker')

Migration tracking:
  Search cu:AllowRawD2ClientBuilder in Jarvis to audit remaining
  exceptions and drive them to zero over time.
@singhsanjay12 singhsanjay12 force-pushed the singhsanjay12/restrict-d2-client-builder branch from e7ff079 to 552dbec Compare March 25, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant