1

I'm working on a Spring Boot application where I created some REST APIs. There is a Readings entity, Alert entity. I'm sending readings via POST method and in the postReadings method of the ReadingsService class I'm checking whether the readings match certain criteria using KIE and wrote the rules in a rules.drl file. I'm creating an Alert object and setting it as a global using session.setGlobal method. Then I'm checking if the Alert object is null and saving it to the Alert Repository. In the drools file I added a print statement to check whether all the alerts are correctly created. All the alerts are correctly printed via the print statement, however, only some of them are being saved to the alert repository. Can anyone please help?

This is the ReadingsService

@Service
public class ReadingsServiceImpl implements ReadingsService{

@Autowired
ReadingsRepository repository;

@Autowired
VehicleRepository vehicleRepo;

@Autowired
private KieSession session;

@Autowired
AlertRepository alertRepo;


@Override
public List<Readings> findAll() {
    return repository.findAll();
}

@Override
public Readings postReadings(Readings readings) {
    Alert alert = new Alert();
    session.setGlobal("alert", alert);
    session.insert(readings);
    session.fireAllRules();
    if(!Optional.ofNullable(alert).map(o -> o.getVin()).orElse("").isBlank()) {
        alertRepo.save(alert);
    }
    return repository.save(readings);
}

This is the drools file

import com.shiva.truckerapi.entity.Readings;
global com.shiva.truckerapi.entity.Alert alert;
rule "EngineCoolantOrCheckEngineLight"
when
    readingsObject: Readings(engineCoolantLow || checkEngineLightOn);
then
    alert.setVin(readingsObject.getVin());
    alert.setPriotity("LOW");
    alert.setDescription("Engine Coolant LOW Or Check Engine Light ON");
    alert.setTimestamp(readingsObject.getTimestamp());
    alert.setLatitude(readingsObject.getLatitude());
    alert.setLongitude(readingsObject.getLongitude());
    System.out.println(alert.toString());
end;
Chetan Joshi
  • 5,582
  • 4
  • 30
  • 43

2 Answers2

1

There's nothing wrong with your rule. However your save is guarded by an "if" statement that checks for the presence of a non-blank/non-null VIN, so that's likely causing the issue of the missing saves:

if(!Optional.ofNullable(alert).map(o -> o.getVin()).orElse("").isBlank()) {
  alertRepo.save(alert);
}

You can easily write unit tests to verify this behavior -- that if the alert exists (which is always does since you instantiate it earlier in the same method) and there is a VIN, then it saves; and that if the alert exists and there is no VIN that it does not save.


This is a very old-fashioned way of saving the output for rules. These days we generally use actions with side effects in our rules on the right hand side, or use objects to pass back information.

Your current set up even has a logic flaw: you will always have an Alert because you instantiate it before you add it to your session.

If you want to use a global like this, you'll need to add some sort of indicator to the Alert class itself to indicate that the alert has been added. So, for example, if you simply add a boolean added = false method with appropriate getters, you can update your rule like this:

rule "Engine Coolant Or Check Engine Light"
when
    readingsObject: Readings(engineCoolantLow || checkEngineLightOn);
then
    alert.setAdded(true); // Mark the alert as now being added
    alert.setVin(readingsObject.getVin());
    alert.setPriotity("LOW");
    alert.setDescription("Engine Coolant LOW Or Check Engine Light ON");
    alert.setTimestamp(readingsObject.getTimestamp());
    alert.setLatitude(readingsObject.getLatitude());
    alert.setLongitude(readingsObject.getLongitude());
    System.out.println(alert.toString());
end

And then you can fix your calling method to not key off of the nullability of a non-null object, and instead key off of whether you have added an alert:

public Readings postReadings(Readings readings) {
    Alert alert = new Alert();
    session.setGlobal("alert", alert);
    session.insert(readings);
    session.fireAllRules();
    if(alert.isAdded()) {
        alertRepo.save(alert);
    }
    return repository.save(readings);
}

(I'm assuming, of course, that you've got the appropriate connection and transaction handling in your 'repository' instances.)

If you still need a non-null VIN, you should be checking for that on the rule's left hand side; something like this:

readingsObject: Readings( vin != null,
                          engineCoolantLow || checkEngineLightOn )

Related question: How to return the value from Cosequence of drl file to java

Roddy of the Frozen Peas
  • 14,380
  • 9
  • 49
  • 99
  • Thanks for the response. I tried your approach and the problem still persists. As per my understanding, after the alert object is inserted into the session, the next statement which is 'saving to the repo' is not waiting for the execution of statements in the drools file because this is asynchronous execution. I included a synchronized block inside the postReadings method and everything worked fine. Can you please tell me a better way to pass back the Alert object to the postReadings method instead of using a global variable like I did? – shiva prasad reddy Oct 05 '20 at 15:05
  • Sounds like the problem is with your transaction management, not with your rules – Roddy of the Frozen Peas Oct 05 '20 at 16:36
  • For the transaction part, I just extended the JpaRepository and I didn't override any methods. I just went with the default implementations. However, everything worked fine when I added a synchronized block. Just wanted to know whether there is any other better way to implement it. If you have time I can share all the code so that you can have a look. – shiva prasad reddy Oct 05 '20 at 17:21
  • there's no threading involved here. if it's fixed by a synchronization block, it's something elsewhere in your code. – Roddy of the Frozen Peas Oct 05 '20 at 17:43
  • Okay, I'll try to debug my code and rewrite it again. Thank you – shiva prasad reddy Oct 05 '20 at 17:57
1

Drools evaluates rule in the same (single) thread unless you make additional configuration. Thus postReadings() and drools then block will be invoked by the same thread. But postReadings() can be executed in concurrent context and it uses shared global object reference Alert alert (because of the shared session instance). Drools has nothing to do with the ordinal problem of making your ReadingsServiceImpl thread safe. And there are many ways to achieve this and the very first antipattern is 'global variables'... Do not use shared object in drools file but use shared thread safe service reference. You can use different synchronization approaches, thread local variables to make your service deal with your inner stuff in a thread safe way but do you really need shared object here at all?

import com.shiva.truckerapi.entity.Readings;
import com.shiva.truckerapi.entity.Alert;

global com.shiva.truckerapi.service.ReadingsService readingsService;

rule "EngineCoolantOrCheckEngineLight"
when
    readingsObject: Readings(engineCoolantLow || checkEngineLightOn);
then
    Alert alert = new Alert();
    alert.setVin(readingsObject.getVin());
    alert.setPriotity("LOW");
    alert.setDescription("Engine Coolant LOW Or Check Engine Light ON");
    alert.setTimestamp(readingsObject.getTimestamp());
    alert.setLatitude(readingsObject.getLatitude());
    alert.setLongitude(readingsObject.getLongitude());
    System.out.println(alert.toString());

    readingsService.onAlert(alert);
end;

Service changes

@PostConstruct
private void postConstruct() {
    session.setGlobal("readingsService", this);
}

@Override
public void postReadings(Readings readings) {
    session.insert(readings);
    session.fireAllRules();
    repository.save(readings);
}

@Override
public void onAlert(Alert alert) {
    // this looks ugly. There should not be produced 'invalid' alert, if vin is empty alert should not be generated by the rule itself
    if(!Optional.ofNullable(alert).map(o -> o.getVin()).orElse("").isBlank()) {
        alertRepo.save(alert);
    }
}
Mike
  • 20,010
  • 25
  • 97
  • 140
  • Thanks for the response. I didn't know how to implement it and used a shared object.I need to write three more rules in the drools file and each rule should create an alert object if a certain condition is met, and all these alert objects should be saved to the `AlertRepository`. Can you please tell me how to approach this problem? – shiva prasad reddy Oct 05 '20 at 21:58
  • For the first question, I cannot upvote as of now because I do not have enough reputation to do so. I tried upvoting and got this message 'Thanks for the feedback! Votes cast by those with less than 15 reputation are recorded, but do not change the publicly displayed post score.' For the second question, I read your answer line by line so many times. You clearly explained the drools part but I'm getting errors while executing. I think I made some mistakes in the service part and I'm trying to figure that out. – shiva prasad reddy Oct 07 '20 at 00:13
  • See updated answer. If you get errors please share stack trace – Mike Oct 07 '20 at 07:53