Skip to content

then not= doesn't stop an infinite loop that i think it should #33

@drewverlee

Description

@drewverlee

this code

(def rules
  (o/ruleset
    {::foo
     [:what
      [id ::x x {:then not=}]
      :then
      (o/insert! (inc id) {::x 10})]}))

(def *session
  (atom (reduce o/add-rule (o/->session) rules)))

(swap! *session
       (fn [session]
         (-> session
             (o/insert 0 ::x 1)
             o/fire-rules)))

results in this error

This may be an infinite loop.
The current recursion limit is 16 (set by the :recursion-limit option of fire-rules).

Cycle detected! :drews.national-institutes-of-health/foo is triggering itself.

Try using {:then false} to prevent triggering rules in an infinite loop.

i expected their to be no loop because while the first insert would pass because (not= 10 1) is true, on the second (not= 10 10) would be false and it would stop. For reasons i don't understand if i hardcode the id passed to insert:

(def rules
  (o/ruleset
    {::foo
     [:what
      [id ::x x {:then not=}]
      :then
      (o/insert! 2 {::x 10})]}))

It works, this implies whats being compared, by the then block, is really it per entity. is that right?

Activity

keychera

keychera commented on Aug 1, 2025

@keychera

Forgive me if you already knew the answer. I want to answer this because I want to understand more about this library and maybe this will be helpful to someone else

To answer the question "is it really per entity?" I would say yes but I wouldn't use the term entity since the word isn't used anywere in this library. What we have are id , attribute , and value. The {:then compare-fn} block is the comparison of the value of the attribute of the same id.

you can see the behaviour more clearly by using the debugger outlined in the readme like this (as babashka snippet since this library is bb-compatible).

(require '[babashka.deps :as deps])
(deps/add-deps '{net.sekao/odoyle-rules {:mvn/version "1.3.1"}})
(require '[odoyle.rules :as o])

(defn debugger-wrapper [rule]
  (o/wrap-rule rule
               {:what
                (fn [f session new-fact old-fact]
                  (println "the rule" (:name rule) "is comparing old:" old-fact  "and new:" new-fact)
                  (f session new-fact old-fact))
                :then
                (fn [f session match]
                  (println "firing" (:name rule))
                  (f session match))}))

(def session (->> (o/ruleset
                   {::foo
                    [:what
                     [id ::x x {:then not=}]
                     :then
                     (o/insert! (inc id) {::x 10})]})
                  (map debugger-wrapper)
                  (reduce o/add-rule (o/->session))))

(-> session
    (o/insert 0 {::x 10})
    (o/fire-rules {:recursion-limit 1})
    (o/query-all))

you will see that the print output will be

#'rules/debugger-wrapper
the rule :rules/foo is comparing old: nil and new: #odoyle.rules.Fact{:id 0, :attr :rules/x, :value 10}
firing :rules/foo
the rule :rules/foo is comparing old: nil and new: #odoyle.rules.Fact{:id 1, :attr :rules/x, :value 10}
firing :rules/foo
the rule :rules/foo is comparing old: nil and new: #odoyle.rules.Fact{:id 2, :attr :rules/x, :value 10}
firing :rules/foo
the rule :rules/foo is comparing old: nil and new: #odoyle.rules.Fact{:id 3, :attr :rules/x, :value 10}

clojure.lang.ExceptionInfo: Recursion limit hit.
This may be an infinite loop.
The current recursion limit is 1 (set by the :recursion-limit option of fire-rules).

Cycle detected! :rules/foo is triggering itself.

Try using {:then false} to prevent triggering rules in an infinite loop. user odoyle/rules.cljc:549:5

As you can see, what your rule does is to add a new attribute to a new id which have no attribute at all, which make not= always be true.

(though what I don't expect is that giving it {:recursion-limit 1}, the fire-rules still calls the rules 3 times. )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @drewverlee@keychera

        Issue actions