Skip to content

Increase robustness of outbox flushing with respect to outbox entry deserialization issues #851

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

Merged
merged 7 commits into from
May 16, 2025

Conversation

skastenholz
Copy link
Contributor

Background:

  • We are utilizing this framework in all our microservices for certain requests to external and internal downstream services.
  • In doing so JacksonInvocationSerializer is used to serialize/deserialize invocations (in a most flexible way).

Problem:

Analysis:

Solution:

  • The approach suggested here is to handle the IOException at a later point in time at the DefaultPersistor.
  • During deserialization of all relevant outbox entries the ones on which the problems applied are ignored (instead of failing the whole operation).
  • In doing so deserializeable entries are processed and the processing of remaining outbox entries is not blocked.

Open points:

  • Please review the solution and provide your feedback.
  • With respect to tests I would plan to extend AbstractPersistorTest, but am struggling since the InvocationSerializer is not exposed yet. Any hints would be welcome.

@skastenholz skastenholz changed the title expose single IOException in order to handle it at DefaultPersistor. Increase robustness of outbox flushing with respect to outbox entry deserialization issues Apr 30, 2025
@badgerwithagun
Copy link
Member

badgerwithagun commented May 8, 2025

This looks like a sensible first step. I think, however, that we should follow normal error behaviour for anything that fails to deserialise:

  • push back the nextAttemptTime
  • increment the attemptCount (if the record has no group, i.e. is unordered)
  • Fire the appropriate TransactionOutboxListener events if appropriate (with a syntheticTransactionOutboxEntry constructed from the normal fields on the record but with a dummy invocation)

This will avoid the same record getting re-read continously, allow blocking to progress as normal, and ensure that anyone who is using TransactionOutboxListener to monitor things to detect that there is a problem.

@badgerwithagun
Copy link
Member

We're also going to need testts for these behaviours!

@skastenholz
Copy link
Contributor Author

Alright, good point. I tried different approaches and came up with the one comitted. In doing so I needed to modify core classes and, in particular, you may have suggestions regarding the Invocation class. Also I added/modified some tests.

Please have a look and provide your feedback. Thanks.

this.parameterTypes = new Class<?>[] {};
this.args = new Object[] {};
this.mdc = null;
this.exceptionDuringDeserialization = Optional.of(exception);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than adding a new property here (which will impact on ser/deser of all Invocations, not just this one), why not create a FailedDeserializingInvocation, something like this:

class FailedDeserializingInvocation extends Invocation {

  private final IOException e;

  FailedDeserializingInvocation(IOException e) {
    this.className = "";
    this.methodName = "";
    this.parameterTypes = new Class<?>[] {};
    this.args = new Object[] {};
    this.mdc = null;
    this.e= e;
  }

  @Override
  void invoke(Object instance, TransactionOutboxListener listener)
      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
     throw new WrappedCheckedException(e);
  }

}

Should make the code elsewhere cleaner too.

You will need to replace @Value on Invocation to make it nonfinal so this can be done:

@ToString
@EqualsAndHashCode
@AllArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@Getter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, makes sense. Done.

package com.gruelbox.transactionoutbox;

/** Thrown when de-serialization of an invocation fails. */
class DeserializationFailedException extends RuntimeException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a standard exception somewhere in the core lib for generically wrapping checked exceptions, which is all that's going on here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the hint. Done.

Copy link
Member

@badgerwithagun badgerwithagun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really nice approach. I like how neatly it works when you push the exception down into the invocation. Great stuff.

Just a few tweaks on the implementation, but the approach and testing are sound.

@badgerwithagun badgerwithagun merged commit 1cf92cc into gruelbox:master May 16, 2025
23 checks passed
@badgerwithagun
Copy link
Member

Merged! Thanks :)

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.

2 participants