Skip to content

O'Doyle Rules Cookbook.#18

Draft
thomascothran wants to merge 4 commits intooakes:masterfrom
thomascothran:cookbook
Draft

O'Doyle Rules Cookbook.#18
thomascothran wants to merge 4 commits intooakes:masterfrom
thomascothran:cookbook

Conversation

@thomascothran
Copy link

@thomascothran thomascothran commented Jun 25, 2022

#17

  • Initializing values. How to initialize attributes to nil to avoid problems with accumulators not matching.

  • Loading data. Build a graph query system to pull data from local database or services.

* Initializing values

* Loading data
@thomascothran
Copy link
Author

I have a few more patterns to add. Happy to adjust for feedback on format (and even happier to be alerted to undesirable patterns I shouldn't be recommending!).

{#_...
::initialize-thing-owner
[:what [_ :thing/id id {:then not=}]
:then (o/insert! id :thing/owner nil)]})
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using {:then not=} here you could use o/contains? to check if the fact already exists:

[:what
 [_ :thing/id id]
 :then
 (when-not (o/contains? o/*session* id :thing/owner)
   (o/insert! id :thing/owner nil))]

The problem with using {:then not=} here is that you may actually need to update :thing/id for some reason, and if you do, it'll set the :thing/owner to nil even if it already had a value.

Copy link
Author

@thomascothran thomascothran Jun 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! I've run into that limitation with {:then not=} and worked around it sometimes when I've needed to. Using o/contains? is perfect for this.

Updated to follow your suggestion

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oakes - one mistake I started making here pretty quickly was to do this:

[:what
 [_ :thing/id id]
 :when (not (o/contains? o/*session* id :thing/owner))
 :then
 (o/insert! id :thing/owner nil)]

This case can happen, which is strange:

[:what
 [id :thing/id id]
 [id :thing/owner owner]
 :when (not (o/contains? o/*session* id :thing/owner))
 :then
 (o/insert! id :thing/owner nil)]

In this case, there is a match, which is counter-intuitive to me.

Copy link
Owner

@oakes oakes Jul 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe *session* isn't bound to anything in :when blocks. I need to fix that. I'm about to board a plane but I'll look into that soon.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can, try the latest commit. It should fix this problem.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you get a chance to try the latest commit and see if it fixes this?

{::fetch-thing
[:what [_ :thing/id id {:then not=}]
:then ;; fetch-thing returns a namespaced map
(o/insert! id (fetch-thing id))] ;; `{:thing/name "name", :thing/owner 123, #_...}`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this example, is the fetch-thing fn getting the data from the session via o/query-all, or from somewhere else?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this example, fetch-thing is returning a map from the database, not from the session. (The map here is flattened and namespaced.)

The benefit of this in my use cases is that I can put a single id for an object, and the rules will fetch all the related objects. This saves me from having to gather everything up before hand, which can often span quite a few related entities and domains.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense. Is there any benefit to doing the db query from inside a rule, vs querying and inserting from the outside? I tend to avoid interacting with external things (databases, rendering, etc) from inside rules, though this is probably OK. One big gotcha that I mention on the readme is using query-all from within a rule that queries another rule. That could cause problems because it eliminates reactivity. But if you're just querying some external db then it won't be reactive anyway.

Copy link
Author

@thomascothran thomascothran Jul 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting the queries in the rules handles the joins for you automatically, somewhat like what Pathom does. If I insert one id for an entity, the rules engine will make all the queries, find all the relations, load all the related entities, and execute the rules against them all.

I've found this to be extremely useful.

To give a concrete example: for my main use case, we have a library of rules used by different services in a microservice environment. If I'm in the service that owns an entity, I want to pass in a db connection so I can use transactions. But an entity in a service is almost always related to entities in another service. Those entities I want to fetch over a network call to another service (or from Redis, depending on what it is).

Again, I might be using the library from the front end, in which case I want to make a network call for entities, but need to make those calls differently (authorization rules).

I can pass a triple to the rules engine indicating which service the rules engine is being used from. Then the rules govern how an entity is loaded based on the fact indicating where it is being called from. These are written once for all services, but handle all the different use cases.

So the rules engine turns into a pretty powerful query system + rules engine. For my use case, the query part is as much part of the value proposition as the "normal usage" of the rules.

(One potential drawback is that all these network calls need to be blocking. We typically block on network calls anyway, so this hasn't been an issue.)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of ::fetch-thing there is no join. When you use this technique are you normally querying with more than just an id, so a join is needed? Just want to understand the use case.

@thomascothran
Copy link
Author

thomascothran commented Jun 29, 2022

There are a couple problems I don't yet have a good solution for, but that could be interesting.

A big one is debuggability. It would be nice to know when the rules are updating.

My current workaround is to wrap o/insert and log or capture values. There may be a better way though.

@thomascothran
Copy link
Author

The most recent commit makes the doc more clear on how the joins work in using O'Doyle as a query engine

@oakes
Copy link
Owner

oakes commented Jul 5, 2022

Thanks i'll look them over when i have time. Just returned from my work trip so still catching up on things.

@thomascothran
Copy link
Author

One thing it may be good to add a pattern for is visibility into rules firing. Clara has tracing listener functionality.

With O'Doyle, I've done one of two things:

  1. Every then block must call a function, and those functions are instrumented (with something like debux), or
  2. Wrap the insert function to add logging around when it's called, and what it's called with.

Perhaps there are better ways to do this. But in my experience so far introducing people to O'Doyle, the most common difficulty (other than the paradigm shift) is understanding when and why rules are firing.

@oakes
Copy link
Owner

oakes commented Oct 12, 2022

Good points. I made a ticket for improving debugging here #19

@oakes
Copy link
Owner

oakes commented Oct 12, 2022

Check out the wrap-rule fn i put in that ticket. I think it's simpler than having to rewrite your rules so they call a single function and instrumenting those functions with some extra library. If you think it's useful, maybe i could add it to odoyle. That will at least tell you when rules are firing, but not necessarily why. I'll add some ideas to that ticket once i think about it more...

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants