clara

http://www.clara-rules.org/
dominicm 2018-05-21T05:29:42.000054Z

That equality is crazy! Thanks!

dominicm 2018-05-21T05:39:30.000153Z

https://dev.clojure.org/jira/browse/CLJ-867

dominicm 2018-05-21T05:59:14.000077Z

http://clojuredocs.org/clojure.core/defrecord > and will defined Java .hashCode and .equals consistent with the contract for java.util.Map. Looks like this is intentional

dominicm 2018-05-21T07:56:29.000216Z

(definterface Foo
  (getB []))

(deftype C [a]
  Foo
  (getB [this] a))

(deftype B [a]
  Foo
  (getB [this] a))
Onto java beans I go!

2018-05-21T10:52:06.000115Z

Clara just uses clojure =. Not sure the issue above? Clojure = takes type into consideration with records.

dominicm 2018-05-21T10:54:25.000073Z

@mikerod The code that @thegeez referred to uses a java.util.HashMap, which uses java's notion of equality.

dominicm 2018-05-21T10:58:54.000022Z

Fortunately for me, I am in an intersection of 2 points: 1) My data is all the same keys 2) I use macros for generating my types So I've been able to switch to beans with deftype rather trivially, and that's resolved everything.

2018-05-21T11:52:22.000361Z

@dominicm that’s interesting. I don’t know how you were encountering the use of platform/group-by-seq though. It does sound to me like there could be potential edge cases around platform/group-by-seq though on the jvm-impl since it does make use of Java collections to (a) preserve consistent order (b) do the grouping and order preservation efficiently.

2018-05-21T12:02:25.000194Z

@mikerod if the promise is that clojure = is used, than @dominicm’s example shows a bug

2018-05-21T12:05:10.000351Z

(defrecord ClauseOne [b])
(defrecord ClauseTwo [b])

(defrule rule-one
  [ClauseOne (= ?b b)]
  [ClauseTwo (= ?b b)]
 =>
  (println "Match found:" ?b )
  )

(defrecord WrapAlice [num])
(defrecord WrapBob [num])

(comment
  (clear-ns-productions!)
  (let [alice (->WrapAlice 1)
        bob (->WrapBob 1)]
    (println "(= alice bob)" (= alice bob))
    (println "(.equals alice bob)" (.equals alice bob))
    (-> (mk-session)
        (insert
         (->ClauseOne alice)
         ;; Line below is important, without it the wrong behavior does not surface
         (->ClauseTwo alice)
         (->ClauseTwo bob)
         )
        (fire-rules)
        ))
  ;;(= alice bob) false
  ;;(.equals alice bob) true
  ;; Match found: #clara_bug.core.WrapAlice{:num 1}
  ;; Match found: #clara_bug.core.WrapBob{:num 1} ;; <- should not happen
  )

2018-05-21T12:47:28.000055Z

@thegeez I think this actually is a bug

2018-05-21T12:47:46.000058Z

I had suspicion of it being one as soon as I saw this discussion above regarding platform/group-by-seq

2018-05-21T12:49:45.000233Z

platform/group-by-seq was added on the JVM-Clara to make the operation of grouping more efficient, as well as to have deterministic ordering of the seq it returns on each run to avoid variance in rule performance. The use of Java collections for this can cause subtle problems when the bindings of fact fields are record types that are Object.equals() from Java, but not clojure.core/=. This is the case with Clj record since their Java-interop form uses Java map equality instead of incorporating the type.

2018-05-21T12:55:04.000132Z

@mikerod this might be of use: https://github.com/ztellman/bizarro-collections to get a speedy group-by-seq with the semantics of clojure =

2018-05-21T12:55:57.000029Z

I believe the issue is fairly isolated to the platform/group-by-seq. I think the solution would be to stop using a Java-based equality hashmap impl. A clj based one would be needed instead. It may be slightly tricky to do to keep similar performance characteristics. (it’s a hot spot in the rule engine).

2018-05-21T12:56:25.000340Z

@thegeez maybe, but we need a “linked hash map”

2018-05-21T12:56:37.000120Z

the deterministic “order preservation” is an important characteristic

2018-05-21T12:57:00.000378Z

I believe the bizarro only has hash map (unordered)

2018-05-21T12:58:39.000375Z

a java.util.LinkedHashMap is used to do the grouping by operations, then it is seq’ed in platform/group-by-seq. The order the seq comes out, we want to be the same given the same input, independently from one process/machine to another (really, the Java runtime execution)

2018-05-21T12:59:11.000369Z

If we didn’t need the order preservation part, we could just use a clj transient map and likely not have a problem with performance

2018-05-21T13:00:03.000704Z

One solution would just be to write some sort of collection in Clara to fit this goal. That maybe the smoothest path forward.

dominicm 2018-05-21T13:03:34.000145Z

https://github.com/amalloy/ordered supports transients.

2018-05-21T13:03:56.000508Z

I don't know of one of the shelve

2018-05-21T13:04:32.000486Z

But when does the case occur that the order is different between process? And when do these two different orderings meet?

2018-05-21T13:19:28.000210Z

@dominicm @thegeez I logged https://github.com/cerner/clara-rules/issues/393 - fixed link

2018-05-21T13:19:56.000449Z

it has quite a bit of detail on this

2018-05-21T13:20:15.000614Z

determinism from one JVM instance to another is pretty important in some production scenarios

2018-05-21T13:20:39.000347Z

platform/group-by-seq is hit in a hot spot in the engine - it has a fairly large effect on performance

2018-05-21T13:21:06.000019Z

the order of its results can play a large role in the activity that takes place within the engines evaluation of the network facts

2018-05-21T13:21:35.000104Z

If the order changes from run to run, then the performance characteristics change too. This can make it hard to track down a bottleneck that comes up only on occasion

2018-05-21T13:22:17.000086Z

Also, different orders of evaluation could potentially even lead to things like exceptions thrown in defects in written rules. However, may only sometimes these defects would manifest themselves - aka hard to reproduce problems

2018-05-21T13:22:54.000137Z

A while back, all known places within Clara’s engine evaluation and compiler were made deterministic run-over-run to alleviate these issues

2018-05-21T13:27:18.000694Z

This is quickly going beyond what I know of rules systems, which is very little. I'm only surprised to see the "order changes from run to run", I would expect the ordering to be arbitrary, but the same from run to run with the same input data

2018-05-21T13:44:00.000097Z

@thegeez the ordering guarantees are arbitrary from a user-level semantics

2018-05-21T13:44:10.000116Z

and the outputs are produced in a deterministic way from that level

2018-05-21T13:44:21.000019Z

I’m talking on more of a low-level evaluation within the engine

2018-05-21T13:44:34.000339Z

It is basically opaque to the user-level semantics

2018-05-21T13:45:20.000787Z

however, it has real world implications, (a) deterministic performance characteristics (b) deterministic order things actually are evaluated (mostly useful to reproduce error cases)

2018-05-21T13:46:12.000468Z

Things get more involved if you consider using the truth maintenance system within Clara, ie performing a typical clara.rules/insert! on a rule right-hand side (RHS).

2018-05-21T13:46:35.000301Z

The engine works to put the system into a consistent logical state

2018-05-21T13:46:45.000034Z

The “works” part, is where time is spent

2018-05-21T13:47:16.000488Z

There are different orders/paths to take to reach the same consistent logical state, depending on how rules are chosen for evaluation, how facts are propagated from node to node in the internal network etc

2018-05-21T13:47:32.000342Z

The determinism I’m talking about is in being deterministic in those inner choices taken

2018-05-21T13:55:26.000423Z

If "defsomething A, defsomething B, execute" should do the same as "defsomething B, defsomething A, execute" I would expect the usage of a HashMap instead of a LinkedHashMap, with its insertion-order preserving. That part is still fuzzy in my thinking

2018-05-21T14:09:37.000696Z

@thegeez I guess I’m not able to explain it correctly

2018-05-21T14:09:57.000580Z

Yes, they can do the same thing, however, the way they accomplish it may be different depending on the order

2018-05-21T14:10:08.000511Z

hash map can arbitrarily change the order

2018-05-21T14:10:13.000448Z

and it does

2018-05-21T14:10:37.000583Z

For example, we had correct rules running in a production case before. However, about 1 in 10 times, it performed about 100x worse

2018-05-21T14:10:58.000207Z

that 1 in 10 time, was when the hash map ordered 2 groups in a flipped way

2018-05-21T14:11:14.000110Z

the engine then went on to do extra work, it hit a performance bottleneck case

2018-05-21T14:11:26.000334Z

it still was functionally correct, it just went down a “bad path” performance-wise

2018-05-21T14:11:53.000683Z

fixing the determinism allowed the case to be pinpointed and recreated in a consistent way, the bottleneck was then fixed

2018-05-21T14:12:45.000397Z

There are many places within the engine evaluation where the “order things are done” can make measurable performance differences. Even if the outcome is always the same no matter the order.

2018-05-21T14:13:15.000142Z

It is hard to explain most of these, since they are detailed things about the engine/network/fact propagation

2018-05-21T14:14:18.000797Z

Also, truth maintenance can evaluate a rule, evaluated its RHS actions, like insert!, but then later, realize that that rule isn’t satisfied anymore due to some other rule that evaluated. The engine can “retract” the work done by the rule in that case.

2018-05-21T14:14:37.000074Z

There are cases (many) where it’d be better to never have evaluated that rule at all, it wasted time

2018-05-21T14:15:29.000558Z

The situation is really that the engine can at times “speculatively” evaluate a rule that is later found to be invalidated by other rules. There are ways for user-level directed rule orderings, and also there are engine-level “heuristics” or similar that attempt to avoid premature evaluation

2018-05-21T14:15:46.000049Z

So a rule could have a RHS that says (throw (Exception.))

2018-05-21T14:16:06.000316Z

This rule may never be true after fire-rules, however, during the truth maintenance cycle, it could actually be evaluated

2018-05-21T14:16:22.000337Z

So if you have non-determinism in that cycle, you may only sometimes hit the exception side-effect

2018-05-21T14:17:00.000599Z

It doesn’t (and likely wouldn’t) be something trivial like that. It could be something less obvious like (insert! (call-my-fn-that-has-a-defect-for-some-args ?x))

2018-05-21T14:17:36.000785Z

If that rule sometimes evaluated, but needs to retract later by the truth maintenance, and this only happens 1 in 10 times, you may have a 1 in 10 times sort of scenario you are trying to recreate to debug your defect in your rules

2018-05-21T14:18:30.000244Z

This will manifest itself in cases like, our tests fail arbitrarily at times when run on our build servers, but when we rerun the build/retest the error is gone

2018-05-21T14:19:07.000628Z

The determinism of ordering used within these areas of Clara help both of these scenarios, the performance situations, and the finding failures scenarios situation

👍 1
2018-05-21T16:06:26.000617Z

@mikerod Thanks for the explanation

alex-dixon 2018-05-21T16:12:02.000759Z

@mikerod ditto that was great

zylox 2018-05-21T16:12:31.000335Z

we should probably codify that mentality somwhere because we do think of it as first class here when considering changes

👍 1
zylox 2018-05-21T16:12:40.000226Z

(here being cerner)

zylox 2018-05-21T16:13:22.000718Z

not sure where though. When writing rules it shouldnt be a consideration or a concern to the user

curtis.shaffer 2018-05-21T20:00:43.000409Z

engine behavior? or guiding principles perhaps?

2018-05-21T20:03:32.000615Z

I think https://github.com/cerner/clara-rules/blob/master/CONTRIBUTING.md is intended for that purpose in the GitHub templates, perhaps something there?

👍 1
zylox 2018-05-21T20:22:26.000189Z

that makes a degree of sense to me