Reactive programming offers a powerful paradigm for handling asynchronous and event-driven code. When working with reactive frameworks, it's important to understand the distinction between two key timelines: the Construction Timeline and the Subscription Timeline. In this article, we'll explore these timelines, their significance, and the potential bugs that can occur if they are confused.
Developing a Mental Model
When writing reactive code for the first time it's important to realize you are dealing with two timelines in your code. Consider this simple code snippet. Unless you're familiar with reactor, it's not clear that there are two timelines at play.
fun isBirthday(userId: Int): Mono<Boolean> {
val today = Date.now()
return getUser(userId).map {
today == it.birthdate
}
}
fun getUser(userId: Int): Mono<User>
Construction
The first is what I call the Construction timeline. This is the code that
constructs your chain of Monos. It defines the initial Mono and uses
reactive operations to chain business logic together. Think top-level usages
of map
, flatMap
, filter
, etc.
Subscription
The second is what I call the Subscription timeline. This is the code that
operates on the values within the reactive chain. This logic is found within
the blocks associated with the reactive operations. That is, the predicates
passed to map
, flatMap
, filter
, etc.
Example Revisited
Let's take a look at how the timelines manifest in our previous snippet.
fun isBirthday(userId: Int): Mono<Boolean> {
// Construction Timeline
val today = Date.now()
// Construction Timeline
return getUser(userId).map {
// Subscription Timeline
today == it.birthdate
}
}
- The
isBirthday
function is called in the construction timeline. - The variable
today
is bound toDate.now()
in the construction timeline. - The
getUser
method is called in the construction timeline. - The comparison of the birthday to
today
happens in the subscription timeline
The key point is that the construction of the Mono happens first, and subscription happens only once values are emitted.
Fixing a bug
There's a bug in the above code-snippet. Can you find it?
The bug is the following. Since the variable today
is evaluated in the
construction timeline, but used in the subscription timeline, the value of
today
is determine much earlier than it is used. It is possible that today
becomes out of date. That is, we might set up a server on one day, but
subscribe a day later. The value of today
won't change in that
case, since it was bound earlier. (This is only possible thanks to
closures, a
non-reactive feature found in many programming languages)
Here's a fix:
fun isBirthday(userId: Int): Mono<Boolean> {
// Construction Timeline
return getUser(userId).map {
// Subscription Timeline
val today = Date.now()
today == it.birthdate
}
}
In this fix we move the binding of the today
variable from the
Construction timeline into the Subscription timeline. In this way the value of
today
is more likely to correspond to the actual day.
This is a pattern of bug that can appear in many ways throughout a reactive codebase. It's important to keep timelines in mind when pre-computing values or defining variables.
Print Debugging Reactive Code
A common issue that comes up for first time reactive programmers is: "How do I print out this value?"
In the below snippet the programmer has added a println around the result of
the getUser
call. When she go to run the code she finds that the User
object is not printed. Instead, the program prints something
opaque like MonoLiftFuseable
or MonoJust
.
fun isBirthday(userId: Int): Mono<Boolean> {
val user = getUser(userId)
println(user)
return user.map {
val today = Date.now()
today == it.birthdate
}
}
The problem comes about from the mixing up timelines. The programmer wants to print a value in the construction timeline, but that value is only found in the subscription timeline. By moving the print statement into the subscription timeline the programmer can now access the value to print it.
fun isBirthday(userId: Int): Mono<Boolean> {
// Construction Timeline
val user = getUser(userId)
return user.map {
// Subscription Timeline
println(it)
val today = Date.now()
today == it.birthdate
}
}
A common question when running into this error is: "How can I extract values from a Mono?" This is often leads to wrong results. You seldom want to extract results from a mono. Extracting means subscribing or blocking, both of those operations defeat the purpose of using a reactive library.
Handling Nested Monos
Another common task is to call two services sequentially. Imagine a scenario
where after we call the user service we to schedule it on the calendar service.
An initial implementation might look like this, where we call the second
service in a map
operator following the first service. This is almost
right, but it produces an unexpected type.
fun createBirthdayEvent(userId: Int): Mono<Event> {
return getUser(userId).map { user ->
createEvent(user)
}
}
fun createEvent(birthDate: Date): Mono<Event>
The expected return type is Mono<Event>
but the actual return type becomes
Mono<Mono<Event>>
. Now the programmer asks "How can I flatten a Mono of a
Mono?"
To do this we can use the flatMap
operator.
As a general rule, use
flatMap
when the operation's return type is another Mono or Flux. Usemap
when the operation in other cases.
At the heart of it, this too is an error caused by mixing up timelines. To
understand this scenario we have to update our understanding of timelines.
There isn't a single construction and subscription timeline, rather each of
these timelines exists for each Mono in your system. So in our example the
construction timeline of the createEvent
timeline happens in the subscription
timeline of the getUser
Mono. In other words, it's nested. The flatMap
operator lets us flatten the createEvent
subscription timeline to be at
the same level as getUser
's subscription timeline.
fun createBirthdayEvent(userId: Int): Mono<Event> {
return getUser(userId).flatMap { user ->
createEvent(user)
}
}
Conclusion
This article covers some early issues you'll discover on your journey with reactive programming. Hopefully it gives you tools you need to address future issues that come up in your work.
If you found this article helpful, or wish I had covered something else, email me, or toot me on Mastodon. https://fosstodon.org/@hpincket