Skip to content

WIP : Channel Adapters for Redis Streams #3227

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 2 commits into from

Conversation

akuma8
Copy link
Contributor

@akuma8 akuma8 commented Mar 24, 2020

#3226
The PR is in WIP because I need information for improvements.

Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

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

I hope you find my superficial review sufficient and we are good to continue a work here.

Thank you for your effort!

@artembilan
Copy link
Member

@akuma8 ,

please, don't squash commits when you push them to the PR: it is going to be much easier for us to review when change history is present.
Also it would be great to comment the changes in commit: kinda what has been done and the reason.
It also helps us to review faster.

See more info about good commits in Chris Beams' article: https://chris.beams.io/posts/git-commit/

Thanks for understanding.

P.S. for now it is OK because PR is small yet, but it is going to grow I believe.

@akuma8
Copy link
Contributor Author

akuma8 commented Mar 27, 2020

@artembilan Thank you for the remark, I will be careful in the future. It's a personal habit.
I let you a comment because I am stuck, please let me know when you will read it.

@artembilan
Copy link
Member

please let me know when you will read it.

I'm in review process.
Stay tuned. BTW not reason to be in hurry: we are OK to release it whenever it is ready.

Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

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

You know I have just discovered for myself a ReactiveStreamOperations in Spring Data Redis.
I my experience it is better to develop Reactive component and let it to be used in the blocking scenarios.

Can we revise your solution in favor of ReactiveStreamOperations?
See ReactiveMongoDbStoringMessageHandler for example.

The RedisStreamInboundChannelAdapter could also be based on the ReactiveStreamOperations.read(). The returned Flux could be damped into a ReactiveStreamsSubscribableChannel if configured that way or as regular send() otherwise.

Let me know WDYT?

Copy link
Contributor Author

@akuma8 akuma8 left a comment

Choose a reason for hiding this comment

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

Can we revise your solution in favor of ReactiveStreamOperations?

Sure, I saw ReactiveStreamOperations in the doc but I thought it only be relevant to use it in a full reactive context which is not our case but I will revise my first solution if this one is better.

@akuma8
Copy link
Contributor Author

akuma8 commented Apr 2, 2020

@artembilan I tried the reactive approach without success. I followed this https://docs.spring.io/spring-data/redis/docs/2.3.0.M4/reference/html/#redis.streams.receive.containers with StreamReceiver but when I publish a message to the stream it is not received in the inbound side. At application startup messages are well read but not at runtime. I didn't have that issue with the blocking approach.
When talking about ReactiveStreamOperations I was'nt sure if you also want the outbound side to be reactive so I didn't change it. I just modified the inbound side. Please let me know if I have to change it too.
I need other hints to solve the inbound issue, I don't know why I don't receive messages.
Thanks a lot

@artembilan
Copy link
Member

Hi, @akuma8 !

Thank you for update! I'll review your solution and try to figure problems the next week.
Meanwhile the answer to your question: yes, I want an outbound part as reactive as well. You need to implement a ReactiveMessageHandler over there. See a ReactiveMongoDbStoringMessageHandler for example.

Right now I'm working on some reactive streams functionality for the MessageProducerSupport. I somehow believe your inbound channel adapter would benefit from that.
Let's see the next week how it is going!

@akuma8
Copy link
Contributor Author

akuma8 commented Apr 3, 2020

Hi @artembilan,
Thanks for this feedback. I modified the outbound to take account your remark (see the 2nd commit) and I renamed it to ReactiveRedisStreamMessageHandler. As usual the outbound side works as expected but it's not the case for the inbound. Please let me know when your reactive streams functionality will be available.
Thanks

@artembilan
Copy link
Member

I have just raised a PR #3240 about reactive Message Producer.
I have over there an implementation for MongoDb change stream.
I think that one should be a sample how this Redis Streams has to be implemented on the inbound side.
Let's hope it will be merged next week, so you would have some foundation to rely on!

Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

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

@akuma8 ,

tell, me, please, how that would be hard for you to divide this PR into two.
One for already working functionality on the outbound side.
And then we can come back to be inbound one when PR #3240 is merged.
This way we won't have everything blocked.

Thank for understanding.

@akuma8
Copy link
Contributor Author

akuma8 commented Apr 9, 2020

Hi @artembilan
Since Monday I am sick, it’s not the Covid-19 😁.
It’s a good idea to split this PR in 2, I’ll do that next week, I hope getting better.

@artembilan
Copy link
Member

Stay safe, @akuma8 , and get well soon!

No need to be in a hurry: looks like no pressure for the feature yet. So, we are good to bring it whenever it is ready.

When you come back from your sick time off, please, divide this into two PRs - inbound & outbound.
Please, make sure you contribute some tests for this new channel adapters.

At a glance MessageHandler part is OK - should be merged shortly after an appropriate PR.

I haven't looked into an inbound part because wanted to be sure that MessageProducerSupport.subscribeToPublisher(Publisher<Message<?>>) approach is correct.

Now it is merged to master and you can start looking into the MongoDbChangeStreamMessageProducer to make your inbound part similar way.

Fingers crossed for your health!

akuma8 added a commit to akuma8/spring-integration that referenced this pull request Apr 23, 2020
Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

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

Hi @akuma8 !

Any chances to see an update from you on the matter?

What we have so far and what we still need to do to have that Redis Stream Inbound Channel Adapter?

Thanks

@akuma8
Copy link
Contributor Author

akuma8 commented Jul 16, 2020

Hi @artembilan,
I am a bit busy but I started looking at the inbound part, I hope opening a first PR next week.
I also opened that issue about reading List from a Redis Stream
I'll let you know.
Thanks

@artembilan
Copy link
Member

Hi @akuma8 !

No problem with your load!

Just let me know that you can make it before September or should I pull this task from you and fix myself!
Great to see the fix in Spring Data Redis. I think it's time reinstate your test for list. Nothing more we need to do on the matter.
See a related PR in Spring Data: they have some tests to investigate.

@akuma8
Copy link
Contributor Author

akuma8 commented Jul 16, 2020

@artembilan,

... you can make it before September...

Yes, I think so. If we keep doing it like we did for the outbound side I can make it before September.
I am also looking for a way to test the fix from Spring Data Redis, if you can include the related version, please let me know.

@artembilan
Copy link
Member

if you can include the related version, please let me know.

Just did: ebec500

Upgraded to Spring Data 2020.0.0-SNAPSHOT

@akuma8
Copy link
Contributor Author

akuma8 commented Jul 21, 2020

Hi @artembilan,
I have some questions about implementation choices.
Redis Stream Api provides 2 ways to read data, we can read as a simple client with the XREAD command or as a client belonging to a Consumer Group: XREADGROUP. Should we provide one inbound implementation including the 2 options or devide them in 2 different implementations?
Also, should the implementations use the reactive model (as we did for the outbound side)? If yes, using MongoDbChangeStreamMessageProducer as an example is still valid?
Thanks a lot

@artembilan
Copy link
Member

Yes, if that is possible, I'd prefer to have a one inbound component with a consumerGroup property (optional).
And yes, it would be better to have it as reactive. I think the MongoDbChangeStreamMessageProducer is a good sample to borrow ideas.

Thanks

@akuma8 akuma8 force-pushed the gh-3226 branch 2 times, most recently from 8deed7a to 9c482ec Compare July 28, 2020 16:32
akuma8 added 2 commits July 28, 2020 18:36
 - Changed the imperative `StreamMessageListenerContainer` listener in favor of the reactive one `StreamReceiver`
 - Added RedisHeaders values for Redis Stream

Outbound:
 - Added a new constructor taking `Expression` as parameter
 - Changed the imperative `StreamOperations` in favor of `ReactiveStreamOperations`
 - Renaming class from `RedisStreamMessageHandler` to `ReactiveRedisStreamMessageHandler`

Took into account other improvement remarks: copyright, setters position, methods names
Copy link
Member

@artembilan artembilan 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 good progress, but definitely there is still some work to do: tests, docs, JavaDocs etc.

Thanks

@@ -0,0 +1,171 @@
/*
* Copyright 2020-2021 the original author or authors.
Copy link
Member

Choose a reason for hiding this comment

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

I hope we can merge this still in 2020 😄

import org.springframework.util.StringUtils;

/**
* Read Message from a Redis Stream and publish it to the indicated output channel.
Copy link
Member

Choose a reason for hiding this comment

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

@since 5.4 and your name to @author

private Message<?> convertMessage( Record<Object, Object> record ) {
Map<String, Object> headers = new HashMap<>();
headers.put( RedisHeaders.STREAM_KEY, record.getStream() );
headers.put( RedisHeaders.STREAM_MESSAGE_ID, record.getId() );
Copy link
Member

Choose a reason for hiding this comment

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

We probably need also populate a consumerGroup header.

Map<String, Object> headers = new HashMap<>();
headers.put( RedisHeaders.STREAM_KEY, record.getStream() );
headers.put( RedisHeaders.STREAM_MESSAGE_ID, record.getId() );
return this.messageConverter.toMessage( record.getValue(), new MessageHeaders( headers ) );
Copy link
Member

Choose a reason for hiding this comment

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

Hm. Doesn't look like we do something fancy with the converter. What is wrong with just using a plain getMessageBuilderFactory()?

this.createGroupIfNotExist = createGroupIfNotExist;
}

public void setReceiver( StreamReceiver receiver ) {
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this option?
Looks like you create it in the registerListener() anyway.

this.readOffset = readOffset;
}

void setTemplate( RedisTemplate template ) {
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to set this one?
Why can't we create it based on the provided connection factory?

this.consumerName = consumerName;
}

public void setCreateGroupIfNotExist( boolean createGroupIfNotExist ) {
Copy link
Member

Choose a reason for hiding this comment

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

I would just call it setCreateGroup() and explain a behavior in JavaDocs.

}
catch ( Exception e ) {
// An exception is thrown when the group already exists
e.printStackTrace();
Copy link
Member

Choose a reason for hiding this comment

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

Probably better to just log INFO that group already exist.
Anyway it's probably better to look into a ReactiveRedisTemplate and perform the creation operation together with subsequent receiver as a reactive stream composition.

@@ -45,4 +45,8 @@ private RedisHeaders() {

public static final String MESSAGE_SOURCE = PREFIX + "messageSource";

public static final String STREAM_KEY = PREFIX + "streamKey";

public static final String STREAM_MESSAGE_ID = PREFIX + "streamMessageID";
Copy link
Member

Choose a reason for hiding this comment

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

Maybe streamMessageId? No ID, please.

Assert.hasText( consumerName, "'consumerName' must be set" );
Consumer consumer = Consumer.from( this.consumerGroupName, this.consumerName );
this.receiver
.receiveAutoAck( consumer, offset )
Copy link
Member

Choose a reason for hiding this comment

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

Looks like we also need to think about manual acknowledgement:

 * Every record must be acknowledged using
	 * {@link org.springframework.data.redis.connection.ReactiveStreamCommands#xAck(ByteBuffer, String, String...)}

See receive() API for the StreamReceiver.
This has to be populated as a IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK.
And also see SimpleAcknowledgment abstraction to wrap the call to that xAck. for the particular record returned.

Saying that I'd expect an autoAck(true|false) option on this channel adapter.

@akuma8
Copy link
Contributor Author

akuma8 commented Jul 28, 2020

Hi @artembilan,
Sorry but I forgot to tell you to not review this, I just rebased master branch.
I planned to delete this PR and opening a new one. Like I did for the outbound.

@artembilan
Copy link
Member

OK. So, I'm closing this one to avoid confusion.

Thanks for confirmation!

@artembilan artembilan closed this Jul 28, 2020
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