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

Spring integration error when using MessageRequestReplyReceiverContext data in observation predicate #8664

Closed
brianmarting opened this issue Jun 29, 2023 · 10 comments · Fixed by #8665

Comments

@brianmarting
Copy link

brianmarting commented Jun 29, 2023

In what version(s) of Spring Integration are you seeing this issue?

id 'org.springframework.boot' version '3.1.1'

implementation("org.springframework.integration:spring-integration-ip")

Describe the bug

When using the ObservationPredicate with Spring boot micrometer, I created some rules in order to observe certain contexts. But when using the context MessageRequestReplyReceiverContext and using some of its data in order to create a predicate, the rest of the chain of the application just fails with the exception:

java.lang.ClassCastException: class io.micrometer.observation.Observation$Context cannot be cast to class org.springframework.integration.support.management.observation.MessageRequestReplyReceiverContext (io.micrometer.observation.Observation$Context and org.springframework.integration.support.management.observation.MessageRequestReplyReceiverContext are in unnamed module of loader 'app')

Our application flow is just a simple TCP nio inbound, with a broker outbound. But it seems to be going wrong before we receive it in the the TCP inbound.

        observationRegistry.observationConfig()
                .observationPredicate((name, context) -> shouldBeObserved(context));

Example of usage in observation predicate when using headers:

        ...
        
        if (context instanceof MessageRequestReplyReceiverContext messageRequestReplyReceiverContext) {
            return Optional.ofNullable(messageRequestReplyReceiverContext.getCarrier())
                    .map(Message::getHeaders)
                    .map(headers -> !headers.containsKey("SOMEKEY"))
                    .orElse(true);
        }

       ...

Example of usage in observation predicate when using payload:

        ...
        
        if (context instanceof MessageRequestReplyReceiverContext messageRequestReplyReceiverContext) {
            return Optional.ofNullable(messageRequestReplyReceiverContext.getCarrier())
                    .map(Message::getPayload)
                    .map(payload -> new String((byte[]) payload, StandardCharsets.UTF_8))
                    .map(payload -> !payload.equals(SOMEDATA))
                    .orElse(true);
        }

       ...

Exception:
image

Expected behavior

Expected to not break the application when simply casting the payload or using the headers in the generic message.

@brianmarting brianmarting added status: waiting-for-triage The issue need to be evaluated and its future decided type: bug labels Jun 29, 2023
@artembilan
Copy link
Member

I think the problem is in the Micrometer Observation.
I believe your ObservationPredicate returns false, therefore Observation.createNotStarted() returns for us a NoopObservation.
Then that observeWithContext() is called against NoopObservation.CONTEXT which is indeed not a MessageRequestReplyReceiverContext.

We probably can do some workaround in Spring Integration for this or similar API usage, but the real bug and possible fix must go to Micrometer Observation.

CC @marcingrzejszczak , @jonatan-ivanov

@artembilan
Copy link
Member

Right. My theory is correct.
Just modified our IntegrationObservabilityZipkinTests for this code:

observationRegistry.observationConfig()
					.observationPredicate((name, context) -> !(context instanceof MessageRequestReplyReceiverContext));

and got the same exception:

Caused by: java.lang.ClassCastException: class io.micrometer.observation.Observation$Context cannot be cast to class org.springframework.integration.support.management.observation.MessageRequestReplyReceiverContext (io.micrometer.observation.Observation$Context and org.springframework.integration.support.management.observation.MessageRequestReplyReceiverContext are in unnamed module of loader 'app')
	at io.micrometer.observation.Observation.observeWithContext(Observation.java:603)
	at org.springframework.integration.gateway.MessagingGatewaySupport.sendAndReceiveWithObservation(MessagingGatewaySupport.java:602)
	at org.springframework.integration.gateway.MessagingGatewaySupport.sendAndReceive(MessagingGatewaySupport.java:553)
	at org.springframework.integration.gateway.MessagingGatewaySupport.sendAndReceiveMessage(MessagingGatewaySupport.java:532)
	at org.springframework.integration.support.management.observation.IntegrationObservabilityZipkinTests$TestMessagingGatewaySupport.process(IntegrationObservabilityZipkinTests.java:179)

Not sure what is the fix in Micrometer should be, but that observeWithContext() is broken when we are in a NoopObservation mode.

@artembilan artembilan added this to the 6.2.0-M1 milestone Jun 29, 2023
@artembilan artembilan added in: core for: backport-to-6.1.x and removed status: waiting-for-triage The issue need to be evaluated and its future decided labels Jun 29, 2023
@artembilan artembilan self-assigned this Jun 29, 2023
artembilan added a commit to artembilan/spring-integration that referenced this issue Jun 29, 2023
Fixes spring-projects#8664

When an `Observation` is turned to `NoopObservation`,
the `observeWithContext()` fails with `ClassCastException`
since `NoopObservation` serves just plain `Context` not the one we supplied

* Fix `MessagingGatewaySupport.sendAndReceiveWithObservation()` same way
as it is in version `6.0.x`
* Modify `IntegrationObservabilityZipkinTests` to reject some `Observation`
via `observationPredicate()` configuration

**Cherry-pick to `6.1.x`**
@brianmarting
Copy link
Author

brianmarting commented Jun 30, 2023

Thanks for checking, we will then just wait for the fix 😄

@artembilan
Copy link
Member

Yeah... Only as a workaround for you right now I see as a downgrade to Spring Integration 6.0.6 where we don't use that observeWithContext().

@marcingrzejszczak
Copy link
Contributor

I guess the behaviour should be that if there's a no op observation the function is not called at all, right?

@artembilan
Copy link
Member

That's not correct, @marcingrzejszczak .
Our logic is like this:

	.<MessageRequestReplyReceiverContext, Message<?>>observeWithContext((ctx) -> {
					Message<?> replyMessage = doSendAndReceive(requestChannel, object, requestMessage);
					if (replyMessage != null) {
						ctx.setResponse(replyMessage);
					}
					return replyMessage;
				})

So, that doSendAndReceive() must be called anyway.

If there is no way to propagate a proper Context to the NoopObservation for this observeWithContext(), then it has to be deprecated and the fix I've provided in the PR for this issue is what we have to deal with from now on.

I remember that exactly me asked for this kind of fluent API to avoid a context variable extraction in our code, so my apologies for inconvenience 😄

@garyrussell
Copy link
Contributor

I wonder if we could have another overloaded observation method? Something like

    default <T extends Observation.Context> Observation observation(@Nullable ObservationConvention<T> customConvention,
            ObservationConvention<T> defaultConvention, Supplier<T> contextSupplier, ObservationRegistry registry
            T noopContext) {

Where the noopContext is used by the NoopOpservation (but I see it will need adding a generic parameter to NoopObservation).

??

@garyrussell
Copy link
Contributor

but I see it will need adding a generic parameter to NoopObservation

Actually, no, the cast is avoided even if stored in the Context field.

@artembilan
Copy link
Member

Yeah... The point is that NoopObservation is a constant and current architecture is not aimed to modify it with our requested context.

Perhaps observeWithContext() may provide a null for context when NoopObservation, but that would expose a required knowledge about observation API internals with such a changed state.
This way I think more and more that better to deprecate this observeWithContext().

Not sure, though, how everything else works since it just delegates to this method anyway:

    default void observe(Runnable runnable) {
        observeWithContext((context) -> {
            runnable.run();
            return null; // returned value is ignored
        });
    }

@artembilan
Copy link
Member

Looks like there is no the fix in Micrometer Observation yet, so we have to go for proposed workaround for our upcoming releases.

Thanks

garyrussell pushed a commit that referenced this issue Jul 12, 2023
Fixes #8664

When an `Observation` is turned to `NoopObservation`,
the `observeWithContext()` fails with `ClassCastException`
since `NoopObservation` serves just plain `Context` not the one we supplied

* Fix `MessagingGatewaySupport.sendAndReceiveWithObservation()` same way
as it is in version `6.0.x`
* Modify `IntegrationObservabilityZipkinTests` to reject some `Observation`
via `observationPredicate()` configuration

**Cherry-pick to `6.1.x`**
garyrussell pushed a commit that referenced this issue Jul 12, 2023
Fixes #8664

When an `Observation` is turned to `NoopObservation`,
the `observeWithContext()` fails with `ClassCastException`
since `NoopObservation` serves just plain `Context` not the one we supplied

* Fix `MessagingGatewaySupport.sendAndReceiveWithObservation()` same way
as it is in version `6.0.x`
* Modify `IntegrationObservabilityZipkinTests` to reject some `Observation`
via `observationPredicate()` configuration

**Cherry-pick to `6.1.x`**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants