Skip to content

Instrumentation methods should have more flexible generic return type #3609

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

Closed
paulbakker opened this issue May 31, 2024 · 6 comments
Closed

Comments

@paulbakker
Copy link

Describe the bug
Methods in Instrumentation currently return InstrumentationContext<Object>. This won't allow to return a more concrete object type. E.g. returning InstrumentationContext<String> does not compile.

I think these methods should return InstrumentationContext<?>.

To demonstrate this further, take this (plain Java) example:

interface Base {
    Wrapper<Object> wrapped();
}

class Wrapper<T> {
    T value;
}

class Impl implements Base {
    @Override
//Won't Compile!!!!!
    public Wrapper<String> wrapped() {
        return new Wrapper<String>();
    }
}

This won't compile, String is an incompatible return type here.
However, if you change from object to ?, it's fine.

interface Base {
    Wrapper<?> wrapped();
}

class Wrapper<T> {
    T value;
}

class Impl implements Base {
    @Override
    //Now it's fine.
    public Wrapper<String> wrapped() {
        return new Wrapper<String>();
    }
}
@bbakerman
Copy link
Member

Well technically some methods return a specific type

    default InstrumentationContext<Document> beginParse(InstrumentationExecutionParameters parameters, InstrumentationState state) {
        return noOp();
    }

but I am assuming you mean the "field fetching" methods like

    default InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
        return noOp();
    }

The reason these are like this is because that is what the engine has at the point it makes the "callback"

        InstrumentationContext<Object> fetchCtx = nonNullCtx(instrumentation.beginFieldFetch(instrumentationFieldFetchParams,
                executionContext.getInstrumentationState())
        );

        Object fetchedObject = invokeDataFetcher(executionContext, parameters, fieldDef, dataFetchingEnvironment, dataFetcher);

        fetchCtx.onDispatched();

        .handle((result, exception) -> {
                        fetchCtx.onCompleted(result, exception);

Sometimes the object is a CompleteableFuture and sometimes its a plain old object - but its only ever visible to the engine as an Object.

Can you please expand more on use cases where you as the SPI implementor will know what more specific type the "engine returned value" will be

We can make it InstrumentationContext<?> say but I am not clear on when you will know its going to be a more specific case.

@paulbakker
Copy link
Author

I was helping update some existing instrumentation that was using beginField (which is gone). I think beginFieldFetch is the correct alternative?

The code was using the whenCompleted method (from SimpleInstrumentationContext).

 @Override
  public @Nullable InstrumentationContext<Object> beginFieldFetch(
      InstrumentationFieldFetchParameters p, InstrumentationState s) {

    //Doesn't compile, needs to be explitily returning Object, not ExecutionResult.
    return whenCompleted(
        (ExecutionResult r, Throwable t) -> {
          if (s instanceof State state) {
            state.increment(coordinates(p), result(r, t));
          }
        });
  }

This might be completely incorrect, so maybe I'm just looking the wrong direction.
The following change works around the issue.

return whenCompleted(
        (Object r, Throwable t) -> {
          if (s instanceof State state && r instanceof ExecutionResult r1) {
            state.increment(coordinates(p), result(r1, t));
          }
        });

@bbakerman
Copy link
Member

in v22.1 - graphql.execution.instrumentation.Instrumentation#beginFieldFetching is the right method to replace beginFieldFetch

I think the modern Java instanceof mechanism is the way to go

@paulbakker
Copy link
Author

Ok, experimenting with beginFieldFetching works nicely this way (code in onComplete is just an example):

public @Nullable FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
        return new FieldFetchingInstrumentationContext() {
            @Override
            public void onDispatched() {
            }

            @Override
            public void onCompleted(Object result, Throwable t) {

                var parent = GraphQLTypeUtil.unwrapAll(parameters.getExecutionStepInfo().getParent().getType()).getName();
                var field = String.format("%s.%s", parent, parameters.getField().getName());

                if(t != null) {
                    System.out.printf("%s %s\n", field, Result.EXCEPTION);
                } else if(result == null) {
                    System.out.printf("%s %s\n", field, Result.NULL);
                } else {
                    System.out.printf("%s %s\n", field, Result.OK);
                }
            }
        };
    }

What doesn't work is using SimpleInstrumentationContext.whenCompleted.

public @Nullable FieldFetchingInstrumentationContext beginFieldFetching(InstrumentationFieldFetchParameters parameters, InstrumentationState state) {
        return SimpleInstrumentationContext.whenCompleted((Object result, Throwable t) -> {

In that case SimpleInstrumentationContext is not the expected return type of FieldFetchingInstrumentationContext. Not a huge issue, but I think it's some ergonomics that don't quite work well anymore.

Can you please expand more on use cases where you as the SPI implementor will know what more specific type the "engine returned value" will be

I think I was just trying the wrong thing. Possibly the javadoc can be a bit more clear on the intended use, it's not obvious how to go from the old APIs to the new one.

@bbakerman
Copy link
Member

Fair point on the better Java doc - we could do better there on saying to use the other one - I will fix that up - the beginFieldFetch method was deprecated in 22.1 for a good reason. Before you could work out at "dispatch time" that the value was always a CompletableFuture that wrapped the fetched value -

in v22.0 we improved this so the fetched value could be either a plain old java object unwrapped or a CF if thats what came back. However you could no longer know if the value was a CF or not. This broke libs like Expedia's smart data loader.

So via #3571 we introduced a new beginFieldFetching with a specific return type that let you know the object fetched. We deprecated beginFieldFetch in v22.1 as a result (which was an unusual change for a version that was only week old but we didn't want to make a breaking change - hence the deprecation and not removal.

@dondonz
Copy link
Member

dondonz commented Jun 4, 2024

Following the discussion and @bbakerman's javadoc PR I'll close out this issue, but feel free to open a new one if you have new questions.

@dondonz dondonz closed this as completed Jun 4, 2024
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

No branches or pull requests

3 participants