Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Messenger] Pass envelope to message handler as second __invoke parameter #42005

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
wants to merge 3 commits into from

Conversation

adioe3
Copy link

@adioe3 adioe3 commented Jul 6, 2021

Q A
Branch? 5.4
Bug fix? no
New feature? yes
Deprecations? no
License MIT

When calling message handlers __invoke() method, pass in the envelope as a second parameter. This ensures we're backwards compatible (doesn't break existing handlers) but any which might want to use the envelope can do so.

My use case for this is a trace ID we want to use for tracking requests across events with AMQP:

  • HTTP request contains X-Trace-Id, controller receives this and dispatches a message with an x-trace-id AMQP header
  • worker collects message, hands off to handler
  • handler does some work and then dispatches another message but it should preserve the x-trace-id header

@carsonbot
Copy link

Hey!

I see that this is your first PR. That is great! Welcome!

Symfony has a contribution guide which I suggest you to read.

In short:

  • Always add tests
  • Keep backward compatibility (see https://symfony.com/bc).
  • Bug fixes must be submitted against the lowest maintained branch where they apply (see https://symfony.com/releases)
  • Features and deprecations must be submitted against the 5.4 branch.

Review the GitHub status checks of your pull request and try to solve the reported issues. If some tests are failing, try to see if they are failing because of this change.

When two Symfony core team members approve this change, it will be merged and you will become an official Symfony contributor!
If this PR is merged in a lower version branch, it will be merged up to all maintained branches within a few days.

I am going to sit back now and wait for the reviews.

Cheers!

Carsonbot

@ro0NL
Copy link
Contributor

ro0NL commented Jul 6, 2021

it should preserve the x-trace-id header

shouldnt you solve this more generically at the middleware layer? or make it part of your message payload?

@chalasr chalasr added this to the 5.4 milestone Jul 6, 2021
@adioe3
Copy link
Author

adioe3 commented Jul 6, 2021

it should preserve the x-trace-id header

shouldnt you solve this more generically at the middleware layer? or make it part of your message payload?

It is impossible: the handler will dispatch another message and there is currently no way to get the envelope stamps in the handler. It is also impossible to reference a previous message from another dispatched message without some very ugly solutions like keeping a static map of spl_object_id()s or something in before/after middlewares which is gnarly.

And adding it to the message body is also bad because the trace ID is for debugging/monitoring purposes and has no value for the core functionality of the respective handlers.

That said, I am all ears for alternative solutions. Explained in code, this is what a handler does:

class SomeHandler implements HandlerInterface {
  private MessageBusInterface $bus;

  public function __construct(MessageBusInterface $bus) {
    $this->bus = $bus;
  }

  public function __invoke(SomeMessage $message) {
    // do something with $message
    $this->bus->dispatch(new AnotherMessage(...));
  }
}

I've implemented also a TraceIdStamp and I have a TraceIdMiddleware which will stamp outgoing messages (add the AMQP header) so the above invoke() would actually do

$this->bus->dispatch(
  Envelope::wrap(new AnotherMessage(...))
    ->with(new TraceIdStamp($traceId))
);

@Nyholm
Copy link
Member

Nyholm commented Jul 11, 2021

I think you can solve your problem by just type hint for an envelope in your handler.

Ie:

class FooHandler {
  public function __invoke(Envelope $envelope) {
    $message->getMessage();
     // Do work
     // Have access to all stamps and your X-Trace-Id
  }
}

Of course, with a handler like this you cannot rely on autoconfigure, you need to write DI config yourself to map a message to a handler.

@adioe3
Copy link
Author

adioe3 commented Aug 9, 2021

This doesn't work since HandleMessageMiddleware will try to pass-in the message instead of the envelope (i.e. we're not autoconfiguring/wiring anything here):

    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        $handler = null;
        $message = $envelope->getMessage();
//      ...
            try {
                $handler = $handlerDescriptor->getHandler();
                $handledStamp = HandledStamp::fromDescriptor($handlerDescriptor, $handler($message));

So even if I manually wire a handler to consume my message class it's still not getting the envelope since the handlers are not handled as services during __invoke() calls.

@adioe3
Copy link
Author

adioe3 commented Aug 9, 2021

I mean I can write my own handling middleware and jam it before the default one but that seems like extreme overkill.

@ro0NL
Copy link
Contributor

ro0NL commented Aug 9, 2021

It is also impossible to reference a previous message from another dispatched message without some very ugly solutions like keeping a static map of spl_object_id()s or something in before/after middlewares which is gnarly.

IIUC a middleware could track state yes, something like

handle($envelope, $stack) {
  if ($this->currentTrace) {
      $envelope->with(new Badge($this->currentTrace))

      return $stack()->next()->handle($envelope);
  }

  $this->currentTrace = $envelope->last(Badge::class);
  try {
    return $stack()->next()->handle($envelope);
  } finally {
    $this->currentTrace = null;
 }
} 

note im not particularly against providing envelope as second arg, other than maybe-semantics.

@adioe3
Copy link
Author

adioe3 commented Aug 10, 2021

@ro0NL I've tried that as well but it's not what I need -- that would tag anything coming down a specific middleware and I need messages from a specific request tied together. Practically speaking I need this:

  • Message A with x-trace-id: 1 is handled in handler which dispatches Message B and slaps on x-trace-id: 1
  • unrelated Message C is handled as usual

In the above stateful-middleware approach what happens is:

  • Message A's x-trace-id: 1 is stored in middleware
  • Message B dispatched from A's handler gets the x-trace-id slapped on
  • unrelated message C gets the x-trace-id slapped on which is wrong since it's not related to A or B

So far the only two solutions (aside from adding the second invoke param) I see are either:

  1. writing my own HandleMessageMiddleware that does what I want (99% copy/paste of the current) and slaps on a HandledStamp
  2. writing some kind of stateful middleware which keeps track of incoming and outgoing messages and tacks IDs on (very shaky, limits me to a single consumer process without some sort of shared cache, overall horrible solution as well)

Adding the second invoke param still feels like the best solution and semantically I don't think it's that big of an issue. If we're copying the concept or IRL mail with envelopes, you will usually have the envelope together with the mail, right?

@ro0NL
Copy link
Contributor

ro0NL commented Aug 11, 2021

im not sure i understand. Who's dispatching message C then?

As i currently see it:
controller invokes => dispatch message A with trace => dispatch message B (receives trace from most outer dispatch)
?? => dispatch message C (handled as usual)

@adioe3
Copy link
Author

adioe3 commented Aug 11, 2021

There is no controller (and the cake is a lie) -- this is all happening inside a bin/console messenger:consume loop.

The worker's job is simple: in a loop it will:

  1. fetch a message from bus/transport,
  2. run it through all the middlewares which will stamp various stamps on it,
  3. either consider it handled or send it if there's no ReceivedStamp

Now, say some external system dispatches message A with x-trace-id: 1.

The worker will:

Loop 1

  1. fetch message A from RabbitMQ
  2. it hits TraceMiddleware which saves x-trace-id: 1 internally
  3. it hits SomeHandler which will inside its __invoke() method dispatch message B
  4. mark message A as handled

Loop 2

  1. fetch newly dispatched message B
  2. it hits TraceMiddleware which has x-trace-id: 1 so it will stamp message B with x-trace-id: 1
  3. continues as normal and marks message B as handled

Loop 3

  1. fetch message C which can be any other message and it has no x-trace-id header set
  2. it hits TraceMiddleware which has x-trace-id: 1 so it will erroneously stamp message C with x-trace-id: 1
  3. continues as normal and marks message C as handled

Basically, after one message with a trace ID any future messages without a trace ID will get stamped which is wrong. In the three loops above, messages A and B are related and should have the same trace ID but message C is not.

In other words, a stateful middleware relies on the idea that messages dispatched in a handler will be processed immediately after (in a series) and that is not a good guarantee. I imagine running multiple workers would be problematic, synchronizing the x-trace-id across the workers' TraceMiddleware also.

None of these problems exist when the envelope is available in the handler.

@ro0NL
Copy link
Contributor

ro0NL commented Aug 11, 2021

im still trying to follow (sorry, i have no practical experience, but this is on my list to play with for logging purposes :)). However i dont like the idea of dealling with trace IDs at the handler level personally :/

with the approach from #42005 (comment)

In loop 1, after message A leaves we clear the state (ie. the finally block)
In loop 2, message B is handled with its own trace ID set, as previous obtained during dispatch from message A
In loop 3, message C is handled as usual given the state was already cleared internally

@ro0NL
Copy link
Contributor

ro0NL commented Aug 11, 2021

in real life, if you are a stamp collector, you need the envelop. Hence im not really against this. But in real life also we dont usualy link the envelop with the message once opened, which is a sementical concern.

@adioe3
Copy link
Author

adioe3 commented Aug 11, 2021

I think it would be easiest to try and write a solution so you can see what I mean. Just take any AMQP-compatible queue (I'm using RabbitMQ) and try to pass a custom header from one message to another that you dispatch.

I also need this for logging purposes so that we can identify when in a chain of events something went wrong but we have some handlers which dispatch other messages and, well, here we are.

Regarding the real life example, a couple alternative ones would be:

  • if I get a death threat in the mail, I'm going to contact the police with the sender's address from the envelope. I can't know it's a death threat until I've read the letter and therefore will not dispose of the envelope until I've "handled" the message
  • another would be incoming donations: for each donation I take the senders address and write back a custom "thank you" note using info from the envelope

Semantically, I think we're not losing much...

@adioe3
Copy link
Author

adioe3 commented Sep 2, 2021

@sroze when could we hope for a review/decision?

@okwinza
Copy link
Contributor

okwinza commented Sep 13, 2021

Looks like #42873 also fixes this. So this could be closed when that one is merged.

@adioe3 adioe3 force-pushed the envelope-in-handler branch from d358688 to 73ebb9e Compare September 14, 2021 19:53
@adioe3 adioe3 force-pushed the envelope-in-handler branch from 73ebb9e to 9c7cdfc Compare September 16, 2021 18:40
@adioe3
Copy link
Author

adioe3 commented Sep 21, 2021

Turns out the approach from @ro0NL works: any dispatches inside a handler are executed immediately and middleware instances are bus-specific so doing a stateful middleware which wipes the IDs post-handling works. I'm closing this as my problem is resolved and the use case is gone. Cheers!

@adioe3 adioe3 closed this Sep 21, 2021
@adioe3 adioe3 deleted the envelope-in-handler branch October 6, 2021 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants