Leos Literak
Leos Literak

Reputation: 9474

Drools initialized from database is empty

I was able to use Drools to categorize transactions when I had a rules stored as a file in resources folder. But I need CRUD API, so I started to load rules from a database. I spent an afternoon, but no transaction is categorized and I think it's because of misconfiguration.

gradle:

droolsVersion = '8.44.0.Final'
implementation ("org.drools:drools-ruleunits-engine:${droolsVersion}")
implementation ("org.kie:kie-ci:${droolsVersion}")
implementation ("org.drools:drools-xml-support:${droolsVersion}")

Drools Configuration bean

@Component
public class DroolsConfig {
    private final DroolsService droolsService;
    private final KieServices kieServices;
    private final KieFileSystem kieFileSystem;    
    private KieContainer kieContainer;
    private KieBase kieBase;

    public DroolsConfig(DroolsService droolsService) {
        this.droolsService = droolsService;
        kieServices = KieServices.Factory.get();
        kieFileSystem = kieServices.newKieFileSystem();
        buildInitialKieBase();
    }

    private void buildInitialKieBase() {
        kieContainer = droolsService.loadRulesFromDatabase(kieServices, kieFileSystem);
        kieBase = kieContainer.getKieBase();
    }
}

Drools init service

@Service
public class DroolsService {
    @Autowired private final DroolsRuleRepository ruleRepository;

    public KieContainer loadRulesFromDatabase(KieServices kieServices, KieFileSystem kieFileSystem) {
        List<DroolsRule> rules = ruleRepository.findByActive(true);
        log.info("Found {} active rules", rules.size());
        for (DroolsRule rule : rules) {
            kieFileSystem.write("/rules/global/" + rule.getId() + ".drl", rule.getContent());
        }

        KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
        kieBuilder.buildAll();
        log.info("Rules compiled with following messages: {}", kieBuilder.getResults().getMessages());
        for (Message message : kieBuilder.getResults().getMessages(Message.Level.ERROR)) {
            System.out.println("Error in rule file: " + message.getText());
        }

        if (kieBuilder.getResults().hasMessages(Message.Level.ERROR)) {
            throw new IllegalStateException("Errors while building rules: " + kieBuilder.getResults());
        }

        return kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());
    }
}

Rules executor

@Service
public class RuleExecutorService {
    private final DroolsConfig droolsConfig;
    private final TransactionCategorizationService categorization;

    private StatelessKieSession kieSession;

    @PostConstruct
    private void initializeSession() {
        refreshSession();
    }

    public void refreshSession() {
        log.info("Creating a new Kie session");
        var kieContainer = droolsConfig.getKieContainer();
        kieSession = kieContainer.newStatelessKieSession();
        kieSession.setGlobal("TOOLS", categorization);
        KieBase kieBase = kieSession.getKieBase();     
        for (KiePackage kiePackage : kieBase.getKiePackages()) {
            for (Rule rule : kiePackage.getRules()) {
                System.out.println("Rule: " + rule.getName());
            }
        }
    }

    public void executeRules(Transaction transaction) {
        log.info("Executing rules for transaction {}", transaction.getBankReference());
        kieSession.execute(transaction);
        log.info("Rules executed");
    }

One rule

rule "Tesco transactions"
salience 10
activation-group "shopIdentification"
when
    $t : Transaction(transactionType == null && TOOLS.containsText($t, "Tesco"))
then
    $t.setMerchant("Tesco");
    $t.setExpenseCategory("FOOD");
    $t.setTransactionType(TransactionType.EXPENSE);
end

Maven is set up (M2_HOME). I verified that the rules are loaded from the database. There is no compilation error. I have even added a listener to KieStatelessSession, but it was never invoked. When I try to inspect KieBase, it seems empty, but KieFileSystem contains the files from database.

Logs:

INFO c.l.a.s.DroolsService [restartedMain] Loading rules from database
INFO c.l.a.s.DroolsService [restartedMain] Found 14 active rules
WARN o.d.c.k.b.i.AbstractKieProject [restartedMain] No files found for KieBase defaultKieBase
INFO c.l.a.s.DroolsService [restartedMain] Rules compiled with following messages: []
INFO o.d.c.k.b.i.KieContainerImpl [restartedMain] Start creation of KieBase: defaultKieBase
INFO o.d.c.k.b.i.KieContainerImpl [restartedMain] End creation of KieBase: defaultKieBase
INFO c.l.a.s.RuleExecutorService [restartedMain] Creating a new Kie session

I guess that the third lines indicates the trouble.

Upvotes: 0

Views: 45

Answers (1)

Leos Literak
Leos Literak

Reputation: 9474

Here is the working code. The problem was that the file path in kieFileSystem.write() did not start with "src/main/resources", which is enforced in Drools.

Service

import com.lelifin.alfa.model.entity.DroolsRule;
import com.lelifin.alfa.model.entity.Transaction;
import com.lelifin.alfa.repository.DroolsRuleRepository;
import jakarta.annotation.PostConstruct;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.drools.compiler.compiler.io.memory.MemoryFileSystem;
import org.drools.compiler.kie.builder.impl.KieBuilderImpl;
import org.drools.compiler.kie.builder.impl.KieFileSystemImpl;
import org.kie.api.KieServices;
import org.kie.api.builder.Message;
import org.kie.api.definition.KiePackage;
import org.kie.api.definition.rule.Rule;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.StatelessKieSession;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.FileOutputStream;
import java.util.List;

@Service
@Slf4j
public class RuleExecutorService {
    private final TransactionClassifier categorization;
    private final DroolsRuleRepository ruleRepository;
    private final KieServices kieServices;

    private KieContainer kieContainer;

    private StatelessKieSession kieSession;

    public RuleExecutorService(DroolsRuleRepository ruleRepository, TransactionClassifier categorization) {
        this.ruleRepository = ruleRepository;
        this.categorization = categorization;
        kieServices = KieServices.Factory.get();
        buildInitialKieBase();
    }

    public void buildInitialKieBase() {
        log.info("Loading rules from database");
        var rules = ruleRepository.findByActive(true);
        log.info("Found {} active rules", rules.size());

        var startTime = System.currentTimeMillis();
        var kieFileSystem = kieServices.newKieFileSystem();
        for (DroolsRule rule : rules) {
            kieFileSystem.write("src/main/resources/rules/global/" + rule.getId() + ".drl", rule.getContent());
        }
        var endTime = System.currentTimeMillis();
        log.info("Rules persisted in KieFileSystem in {} ms", endTime - startTime);

        var kieBuilder = kieServices.newKieBuilder(kieFileSystem);
        startTime = System.currentTimeMillis();
        kieBuilder.buildAll();
        endTime = System.currentTimeMillis();

        log.info("Rules compiled in {} ms with the following messages: {}", endTime - startTime,
                kieBuilder.getResults().getMessages());

        if (kieBuilder.getResults().hasMessages(Message.Level.ERROR)) {
            throw new IllegalStateException("Building rules failed!");
        }

        // Dump the internal MFS
        var internalKieBuilder = (KieBuilderImpl) kieBuilder;
        dumpKieBuilderMFS(((KieFileSystemImpl) kieFileSystem).getMfs(), "src-mfs");
        dumpKieBuilderMFS(internalKieBuilder.getTrgMfs(), "target-mfs");

        var kr = kieServices.getRepository();
        startTime = System.currentTimeMillis();
        kieContainer = kieServices.newKieContainer(kr.getDefaultReleaseId());
        endTime = System.currentTimeMillis();
        log.info("Time to load container: {} ms", endTime - startTime);

        var kieBase = kieContainer.getKieBase();
        for (KiePackage kiePackage : kieBase.getKiePackages()) {
            for (Rule rule : kiePackage.getRules()) {
                log.info("Rule: {}", rule.getName());
            }
        }
    }

    @SneakyThrows
    public static void dumpKieBuilderMFS(MemoryFileSystem mfs, String outputDir) {
        // Create the output directory if it doesn't exist
        File outputDirectory = new File(outputDir);
        if (!outputDirectory.exists()) {
            outputDirectory.mkdirs();
        }

        // Iterate through all files in the MFS
        for (var filePaths : mfs.getFilePaths()) {
            byte[] fileContent = mfs.getBytes(filePaths);

            // Write each file to the output directory
            var fileName = filePaths.getFileName();
            File outputFile = new File(outputDirectory, fileName.replace("/", "_")); // Flatten path for simpler debugging
            try (FileOutputStream fos = new FileOutputStream(outputFile)) {
                fos.write(fileContent);
            }
        }
    }

    @PostConstruct
    private void initializeSession() {
        refreshSession();
    }

    public void refreshSession() {
        log.info("Creating a new Kie session");
        kieSession = kieContainer.newStatelessKieSession();
        kieSession.setGlobal("TOOLS", categorization);
    }

    public void executeRules(Transaction transaction) {
        log.info("Executing rules for transaction {}", transaction.getBankReference());
        kieSession.execute(transaction);
        log.info("Rules executed");
    }

    public void executeRules(List<Transaction> transactions) {
        log.info("Executing rules for {} transactions", transactions.size());
        kieSession.execute(transactions);
        log.info("Rules executed");
    }
}

Rule:

package rules.global;
global com.lelifin.alfa.service.TransactionClassifier TOOLS;

rule "Lidl transactions"
salience 10
activation-group "shopIdentification"
when
    $t : Transaction(transactionType == null && TOOLS.containsText($t, "Lidl"))
then
    $t.setMerchant("Lidl");
    $t.setExpenseCategory("FOOD");
    $t.setTransactionType(TransactionType.EXPENSE);
end

gradle

droolsVersion = '8.44.0.Final'
implementation ("org.drools:drools-ruleunits-engine:${droolsVersion}")
implementation ("org.kie:kie-ci:${droolsVersion}")
implementation ("org.drools:drools-xml-support:${droolsVersion}")

Target FS dump:

enter image description here

Upvotes: 0

Related Questions