Search code examples
javadroolskie

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.


Solution

  • 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