DEV Community

Cover image for Pub-sub Redis in Micronaut
João Esperancinha
João Esperancinha

Posted on

Pub-sub Redis in Micronaut

A few years ago I struggled a lot to get a simple Redis Pub-sub working locally. I'm working on a private project looking to illustrate the Kong framework in action. For that I need some sort of Queue system and decided to venture on one that is relatively new to me.

I started out by thinking out a way to first test this. In this way I wanted to make this in a more thought-out way. The tests would help, because I'm not interested at this point to have something fully running. I just want to start with testing as it should be. So going through the Micronaut website, I found out that I would need a few dependencies, namely this one:

<dependency>
    <groupId>io.micronaut.redis</groupId>
    <artifactId>micronaut-redis-lettuce</artifactId>
    <version>5.2.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

This is a lettuce dependency, which is mostly a Redis client, which allows to access a lot of functionalities in Redis. Then I created this configuration in the application.yml file:

redis:
    uri: redis://localhost
    timeout: 30s
Enter fullscreen mode Exit fullscreen mode

I added the timeout in order to guarantee connection. This may be removed in the future.

Because I love all sorts of tests and in this case I was interested in seeing how Redis behaves I resourced to the use of Testcontainers. If you do not know, Testcontainers is a framework that allows you to start and stop containers, programmatically, via code, using your Docker environment locally or in a pipeline. Anyway I created this abstraction to allow this to happen for now:

abstract class AbstractBuyOddYuccaConcertContainerTest {
    companion object {
        @Container
        @JvmField
        val postgreSQLContainer: TestPostgresSQLContainer = TestPostgresSQLContainer("postgres:14")
            .withUsername("kong")
            .withPassword("kong")
            .withDatabaseName("yucca")
            .withExposedPorts(POSTGRESQL_PORT)
            .withCreateContainerCmdModifier { cmd ->
                cmd.withHostConfig(
                    HostConfig().withPortBindings(
                        PortBinding(
                            bindPort(POSTGRESQL_PORT),
                            ExposedPort(POSTGRESQL_PORT)
                        )
                    )
                )
            }


        @Container
        @JvmField
        val redis: GenericContainer<*> = GenericContainer(parse("redis:5.0.3-alpine"))
            .withExposedPorts(REDIS_PORT)
            .withCreateContainerCmdModifier { cmd ->
                cmd.withHostConfig(
                    HostConfig().withPortBindings(
                        PortBinding(
                            bindPort(REDIS_PORT),
                            ExposedPort(REDIS_PORT)
                        )
                    )
                )
            }

        private val config = ClassicConfiguration()

        init {
            postgreSQLContainer.start()
            redis.start()
            config.setDataSource(
                postgreSQLContainer.jdbcUrl,
                postgreSQLContainer.username,
                postgreSQLContainer.password
            )
            config.schemas = arrayOf("ticket")
            Flyway(config).migrate()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case I'm not only using Redis, but I'm also using Postgres. This is for other functionalities in my project, but the point here is to explain how I got to test the first Redis functionality in my project.

This is the test:

@MicronautTest
internal class ReservationsServiceTest @Inject constructor(
    private val reservationsService: ReservationsService
) : AbstractBuyOddYuccaConcertContainerTest() {

    @Test
    fun `should get all reservations`(): Unit = runBlocking {
        reservationsService.getAll().toList().shouldNotBeNull()
    }

    companion object {
        @JvmStatic
        @BeforeAll
        fun setup() {
            redis.start()
            postgreSQLContainer.start()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The reason why I'm, at this moment, forcing the start of the containers in the @BeforeAll test, is because I noticed that @MicronautTest doesn't seem to work very well with the TestContainers framework with my current setup and the Redis client seems start being configured very much before the container starts.

Of course for this part, I didn't really adhere strictly to the idea of making the integration test first and then perform the implementation. What I did is to firstly implement the service in this way:

@Singleton
class ReservationsService(
    private val receiptRepository: ReceiptRepository,
    private val redisClient: RedisClient
) {
    init {
        val statefulRedisPubSubConnection = redisClient.connectPubSub()
        statefulRedisPubSubConnection.addListener(Listener())
        val redisPubSubAsyncCommands = statefulRedisPubSubConnection.async()
        redisPubSubAsyncCommands.subscribe("channel1")
        redisClient.connectPubSub().async().publish("channel1", "test")
    }

    @OptIn(DelicateCoroutinesApi::class)
    suspend fun createTicket(ticketDto: @Valid TicketDto) = GlobalScope.launch {
        receiptRepository.save(Receipt()).toDto
    }

    fun getAll(): Flow<Receipt> = receiptRepository.findAll()
}

class Listener : RedisPubSubAdapter<String, String>() {
    override fun message(p0: String?, p1: String?) {
    }
}
Enter fullscreen mode Exit fullscreen mode

Of course this is meant in the future to be where the production code starts. However, this class represents the first successful result in trying to access Redis. Among the interfaces readily available in the Lettuce framework, the only one that suits me at the moment in order to implement a Pub-Sub mechanism is the RedisClient. The reason why this is the only code that works for me and for now is because I realized that in order for the Pub Sub to work, I need to create an async commands for the publisher and an async commands for the subscriber. Once the RedisClient is injected, the way that the experimental code works goes something like this

  • Create a Pub Sub connection

val statefulRedisPubSubConnection = redisClient.connectPubSub()

  • Add a listener (It has to implement an interface of type RedisPubSubListener. RedisPubSubAdapter implements all methods and they are all empty by default)

statefulRedisPubSubConnection.addListener(Listener())

  • Get the commands instance to be able to issue commands

val redisPubSubAsyncCommands = statefulRedisPubSubConnection.async()

  • Create a channel by subscribing to it

redisPubSubAsyncCommands.subscribe("channel1")

  • Send the first message in another connection

redisClient.connectPubSub().async().publish("channel1", "test")

Now I'm very happy to see this working locally. I can spin off the Redis container, connect to it and see the first Pub Sub working! If you are interested in follow the changes in my project, please have a look at it here: https://github.com/jesperancinha/buy-odd-yucca-concert/tree/master/buy-oyc-api-service/src/main. My goal of this quick post was to show you how I came about to understand the basics of a Pub Sub system implemented with Redis. You can find more documentation about this project here: https://github.com/jesperancinha/buy-odd-yucca-concert/tree/master/buy-oyc-api-service.


Originally posted on Buy me a coffee - Pub-sub Redis in Micronaut


References

Top comments (0)