Search code examples
thymeleafspring-webfluxspring-data-r2dbc

Run a long running job using the fire and forget strategy with Thymeleaf in Reactor and r2dbc


I am trying to achieve a fire and forget type of effect with webflux, thymeleaf and r2dbc. I have two endpoints, one to add an employee and another to list all employees. I want to simulate a slow database access so I have a thread sleep of several seconds before I call the DB.

Now, the effect I expect to see when I call /add is that my controller returns immediately and the page add is rendered at once. However, I'm not sure how to achieve this. With the current code nap() happens before I can return a Mono. In other words, I'm trying to run a long running job in the background without blocking the controller.

I have the following model:

@Data
public class Employee {
    @Id
    private Long id;
    private String name;
}

The annotated controller has following methods:

@GetMapping(value = "/")
public String home(Model model) {
    model.addAttribute("employees", repo.findAll());
    return "home";
}

@GetMapping(value = "/add")
public Mono<String> add() {
    return Mono
        .defer(this::getEmployee)
        .doOnNext(e -> repo.save(e).subscribe())
        .thenReturn("add");
}

private Mono<Employee> getEmployee() {
    final var e = new Employee();
    e.setName("John");
    nap(); // calls thread sleep for a few sec
    return Mono.just(e);
}

My question is how can I wrap the long running job but at the same time preserve a Controller based notation (instead of functional) and also render the add page immediately? I am aware of some similar questions like this and this, but I don't seem to be able to achieve the behaviour I need.

Edit: lkatiforis' suggestion and this SO question were a push in the right direction. I had to adjust their example a bit because the employee didn't persist. The change is in add():

public String add() {
    Mono.just(employee)
        .delayElement(Duration.ofSeconds(5))
        .doOnNext(e -> repo.save(e).subscribe())
        .subscribe();
    return "add";
}

employee is just an instance of Employee with a populated name. The delayElement operator pauses for 5 seconds without blocking. Finally, I had to call subscribe() on repo.save() and at the end in order for it to work. I assume that if subscribe() is only called on doOnNext() then the main chain that starts with Mono.just() is never executed.


Solution

  • I guess nap() method executes Thread.sleep or something similar, right? Thread.sleep is blocking the main thread making the application unresponsive. You can use delayElements operator to simulate a long-running operation:

    private Mono<Employee> getEmployee() {
        final var e = new Employee();
        e.setName("John");
        return Mono.just(e).delayElement(Duration.ofSeconds(5));
    }