-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue 275: Alternative approach using a listener #447
Issue 275: Alternative approach using a listener #447
Conversation
@@ -23,6 +23,7 @@ | |||
(remove-activations! [listener node activations]) | |||
(fire-activation! [listener activation resulting-operations]) | |||
(fire-rules! [listener node]) | |||
(activation-group-transition! [listener original-group new-group]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any new functions added here are actually breaking changes to user-provided listeners since they will not be implementing these new functions and they will be called. That'll likely result on the JVM as a AbstractMethodError. This isn't an issue to be solved in this particular work. I'm just raising the general concern since it is applicable.
Aside: I have always been bothered with how this is what happens in Clojure when you add protocol functions. Even if you have Object type defaults for all functions, they'll be ignored and not inherited by individual protocol implementors on things like records.
It could be argued that you aren't supposed to define huge protocols in clj and that's where we are going wrong. However, even if this was split across N many protocols, we still have hardcoded assumptions in the engine that we can call every function them on the underlying implementations provided. However, more granularity could let implementors impl a smaller protocol and all the rest would be a default with an default eg. Object impl, so that sort of still works out.
Eg. we could have multiple protocols like:
IAlphaEventListener
IAccumEventListener
IActivationEventListener
IFireEventListener
<... etc ...>
I think an option that seems promising to alleviate this in the future is to wrap calls to user-provided listeners with a listener that Clara controls. This listener can catch the errors for unimplemented functions by the underlying listener. Constantly handling exceptions would not be ideal from a performance perspective, so this failure-case would likely need to be cached and remembered for subsequent calls. We could warn upon session creation - by checking all functions immediately - to assist in people upgrading. Or we could warn on first time we encounter failed call, however, that is less deterministic.
I'd have to double check, but we may be using the DelegatingListener
already over user-supplied listeners, so it may already be in the position to do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with the premise here, we've had breaking changes to listeners in the past and while not a big deal it still impacts consumers.
This isn't an issue to be solved in this particular work.
It might be worth it to log an issue to look into ways that we can change listeners in the future to be less impactful. For now i think i am ok with adding a new method to this protocol, and announcing it as a breaking change, like we did in the past.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've logged #457 to look into a better approach to this with a thought on how to do it. For now I think we can do as Ethan suggested above.
(:require [clara.rules.listener :as l] | ||
[clara.rules.engine :as eng])) | ||
|
||
(deftype LoopDetectorListener [cycles-count max-cycles fn-on-limit] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It relates to my comment @ https://github.com/cerner/clara-rules/pull/447/files#diff-a498c1d1444fc426bcaddeaa009e8dbfR26
It is clear it is tedious to setup a listener that implements all the functions that could be needed by any listener at a given Clara version.
(:require [clara.rules.listener :as l] | ||
[clara.rules.engine :as eng])) | ||
|
||
(deftype LoopDetectorListener [cycles-count max-cycles fn-on-limit] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fn-on-limit
is named backwards to what to I expect - ie. on-limit-fn
This is certainly more regular when it comes to cljs-land and web events - but I think it has good precedent in general. It's typically shortened to just on-limit
, but it isn't bad to say "fn" to make it clear what it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to be more explicit it could also be said that on-limit-exceeded-fn
would be more clear.
(:require [clara.rules.listener :as l] | ||
[clara.rules.engine :as eng])) | ||
|
||
(deftype LoopDetectorListener [cycles-count max-cycles fn-on-limit] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it just be called MaxCycleListener
to be more explicit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MaxCycleListener
seems a bit vague from a new user perspective, CyclicalRuleListener
perhaps...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 I like "CyclicalRuleListener", will change.
(to-transient [listener] | ||
listener)) | ||
|
||
(defn with-loop-detection [session fn-on-limit max-cycles] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do the arguments come in a different order for the fn-on-limit
& max-cycles
here than what they come in for the actual record constructor?
I'd prefer consistency. It seems more (subjectively) natural to have the fn arg come last in both cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I have changed this in #458
"Given a Throwable, return in order the ExceptionInfo data maps for all items in the | ||
chain that implement IExceptionInfo and have nonempty data maps." | ||
[t] | ||
(let [throwables ((fn append-self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just make it a let-binding instead of a recursive anonymous inline fn? It's just making the impl look harder to follow.
(let [append-self (fn append-self [prior t1]] ...)]
...)
Or use letfn
for it to avoid repeating the name if that was the issue:
#?(:clj
(defn ex-data-maps
"Given a Throwable, return in order the ExceptionInfo data maps for all items in the
chain that implement IExceptionInfo and have nonempty data maps."
[t]
(letfn [(append-self [prior t1]
(if t1
(append-self (conj prior t1) (.getCause ^Throwable t1))
prior))])
(->> t
(append-self [])
(into [] (comp (map ex-data))))))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, changed in #458
@@ -203,3 +203,19 @@ | |||
e# \newline | |||
"Non matches found: " \newline | |||
res#))))))) | |||
|
|||
#?(:clj | |||
(defn ex-data-maps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"in order" is a bit vague. I'd immediately ask "what order?" and "ascending or descending?"
Given a throwable/exception/error `t`, return all non-empty `ex-data` maps from stack trace cause chain in the order they occur traversing the chain from this `t` on up the rest of the call stack.
I also tried to make it more cljs friendly since there isn't a reason this has to stay stuck in clj-only for long. It wouldn't require much change to handle both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used a slightly modified version of this docstring in #458
Note that I think this can return empty ex-data maps; I have changed the docstring you proposed above accordingly.
;; (is (= (-> loop-data first :listeners count) 1) | ||
;; "There should only be one listener.") | ||
;; (is (-> loop-data first :listeners ^PersistentTracingListener first .-trace not-empty) | ||
;; "There should be tracing data available when a traced session throws an exception on an infinite loop.")))))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still an issue with the listener approach?
I'm not sure there necessarily should/can be a trace available when arbitrary exceptions are thrown during rule evaluation.
If we want that, I think that's a broader issue than just specifically having a trace when this max cycle logic throws.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is an infinite loop I think it is useful to able to use a trace to show which rules are looping. My approach in #458 allows the trace to be added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do like the transition to listener approach for the most part - beyond my concerns with listener function additions being breaking changes - as I discuss in the review @ #447 (comment)
I had some difficulties with Git merge conflicts when trying to add changes on this PR, so I have created a new PR branched off master at #458 that addresses the comments in this PR. Closing in favour of that PR. |
I gave the previous PR #425 some thought and on reflection, I think this can be done in a simpler way that doesn't impact as much of the core code with a listener. The actual behaviour upon reaching the limit could then be easily user-defined, probably with some default options supplied.
Thoughts? There is definitely some stuff to clean up here before merging but I wanted to get thoughts on the general approach before getting too far into this. The tests are fundamentally the same as in #425 with some minor adjustments to invoke the listener; in a final PR I'd change them further to not solely rely on exceptions being thrown, which would allow them to be cross-platform.
@EthanEChristian @mrrodriguez