Skip to content
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

Fixes #36466 - Allow gathering information on deleted records #9725

Merged

Conversation

ofedoren
Copy link
Member

@ofedoren ofedoren commented Jun 6, 2023

Foreman allows subscriptions to the events that occur on ActiveRecord objects (such as an object being created). The problem is that when an object is destroyed, the dependent bonds are being destroyed also with relational links, and can't be referenced afterwards.

Mostly this change can be tested either via foreman_webhooks plugin, or in Rails console :/

The fix is simple: try to load the ActiveModel object with references to dependent objects before the actual deletion. For example, host's interface gets deleted before the host and apparently with the references, so host.interfaces on a deleted host will always return an empty array.

I've tried to do preloading automatical, so devs don't need to specify what needs to be in Model.includes, but just in case I've made this to be hybrid: most of the scopes for preloading are being created automatically, but there is possibility to override or add explicitly scopes in the model itself via self.preload_scopes; super + [:scope]; end. And since some dependent models can be created by plugins, there is a plugin DSL entry to add scopes from plugins as well (usage is: extend_preload_scopes(Model, [:scope])). Automatical scope builder takes in consideration plugins as well.

@adamruzicka, could you take a look at this as well?

@theforeman-bot
Copy link
Member

Issues: #36466

@ofedoren ofedoren force-pushed the feat-36466-preload-deleted-objs branch 2 times, most recently from da78b43 to 581fc30 Compare June 8, 2023 13:47
@adamruzicka
Copy link
Contributor

Sorry for the delay, I finally got around to reading through it properly and taking it for a spin. The only thing I'm not all the fond of is that it pulls in a lot of stuff just in case someone might need it. Yes, I know we don't know up front what people will need, but it is still a lot of stiff.

It would be nice if we could only preload associations that are exposed in the object's jail, but I'm not sure if that's feasible.

@ofedoren
Copy link
Member Author

It would be nice if we could only preload associations that are exposed in the object's jail, but I'm not sure if that's feasible.

One of the first iterations on this PR was combination of filters against all object's associations based on Jail methods. But I couldn't come up with something, so I decided to drop that. But now since I've got something actually working I'll try to add the Jail based filter against this solution. In theory this will work, I just need to find a way :)

@ofedoren ofedoren force-pushed the feat-36466-preload-deleted-objs branch from 581fc30 to 750fa9a Compare July 19, 2023 12:13
@ofedoren
Copy link
Member Author

@adamruzicka, after looking at this more, I've noticed that some methods are wrapped or renamed (such as params instead of parameters or host_parameters, which can be used in includes). I don't see a way how to automate this, so we don't filter these out. If we take as an example host params, they are dependent on a host, so if a host is deleted they are being deleted also. If a user would want to fetch params of a deleted host they would be empty since parameters would be filtered due to params being allowed in Jail instead of parameters. In this case we can use self.preload_scopes; [:host_parameters]; end as a workaround. But since plugins can extend this as well, I can't be sure that the filter won't remove anything else, which we allow in Jail unless I manually review all the stuff, which I'd like to avoid by this PR. We also can add the filter, but if there is another issue with another relation not being available, we could manually add it via self.preload_scopes; super + [:scope]; end. What do you think?

P.S. Pushed a refactor only.

@adamruzicka
Copy link
Contributor

I'm still worried about loading so many things even if it is not needed.

/me puts on the wild idea hat
How about instead of making the data available when we need it, we split the on_delete notification into two parts? First part would be run as a before_destroy _, prepend: true callback, inside it consumers of the event could render (and store) all the things they need and then once the record is really destroyed, those pre-rendered notifications would be sent out.

It would be a somewhat bigger change than what you're proposing here, but it could be reasonably cheap during runtime. It is quite possible I'm missing something so please bear with me :) How does this sound?

@ofedoren
Copy link
Member Author

@adamruzicka, I'm not quite sure if it's doable?.. Surely I'm missing something, so here are my thoughts:

  1. Since the actual payload is dependent on a webhook template, which can be anything (user created), we need to load it first. So the first step would be load webhooks to load templates.
  2. Fire first part (as per your suggestion) e.g. *_destroyed.pre.event.foreman to make step 1 and render the attached templates.
  3. Since we can't send the actual payload yet (we need confirmation from after_commit), we need to somehow store the pre-rendered webhooks. How would we ensure that all the related webhooks are pre-rendered and saved before we get confirmation to send them?
  4. Get the queue of pre-rendered webhooks and send them when *_destroyed.event.foreman is fired.

As of now, the only my concern is the step 3...

@adamruzicka
Copy link
Contributor

Oh, that's probably the part I was missing. Right now we fire an active job for every event (and webhook) that renders the payload and sends it out?

My initial line of thinking was that the ActiveSupport::Notification::Event stuff just runs a bunch of callbacks (the subscribers), these callbacks are processed sequentially and synchronously so we could just render everything there, which I must admit is a somewhat poor solution. If we try to parallelize the rendering, then things would indeed get hairy real quick.

It would be fairly straightforward if we explicitly backed everything with dynflow as katello does, but that would be a big detour right now.

@ofedoren
Copy link
Member Author

@adamruzicka, AFAICT, currently it works like this:

  1. Create (https://github.com/theforeman/foreman_webhooks/blob/master/app/subscribers/foreman_webhooks/event_subscriber.rb) an event subscriber (https://github.com/theforeman/foreman/blob/develop/app/subscribers/foreman/base_subscriber.rb) and attach (https://github.com/theforeman/foreman_webhooks/blob/master/lib/foreman_webhooks/engine.rb#L79) it to the Rails (https://github.com/theforeman/foreman/blob/develop/app/registries/foreman/plugin.rb#L605)
  2. Trigger (https://github.com/theforeman/foreman/blob/develop/app/services/foreman/observable.rb#L10) an event.
  3. Rails should call (https://github.com/theforeman/foreman/blob/develop/app/subscribers/foreman/base_subscriber.rb#L3) the subscriber, so the webhooks can be delivered (https://github.com/theforeman/foreman_webhooks/blob/master/app/subscribers/foreman_webhooks/event_subscriber.rb#L6).
  4. The plugin calls (https://github.com/theforeman/foreman_webhooks/blob/master/app/models/webhook.rb#L47) DB to gather webhooks subscribed to the event.
  5. Each webhook object calls (https://github.com/theforeman/foreman_webhooks/blob/master/app/models/webhook.rb#L133) DB to gather attached template and render it based on the passed payload (https://github.com/theforeman/foreman/blob/develop/app/services/foreman/observable.rb#L8) from Rails.
  6. After a webhook renders its own payload it creates a job (https://github.com/theforeman/foreman_webhooks/blob/master/app/models/webhook.rb#L73) to deliver the rendered payload to the target.

Unfortunately, I don't know how exactly works ActiveSupport::Notifications.instrument or when it does call on the subscribers...

@ofedoren
Copy link
Member Author

@adamruzicka, WDYT, do we leave it this way or try to "backed everything with dynflow"? :)

Copy link
Contributor

@adamruzicka adamruzicka left a comment

Choose a reason for hiding this comment

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

Yeah, sometimes wild ideas should remain in the idea stage. Let's get this is as is. Could you please squash?

@ofedoren ofedoren force-pushed the feat-36466-preload-deleted-objs branch from 750fa9a to 2058f71 Compare September 1, 2023 15:31
@ofedoren
Copy link
Member Author

ofedoren commented Sep 1, 2023

@adamruzicka, done.

@adamruzicka adamruzicka merged commit b9986c9 into theforeman:develop Sep 1, 2023
8 checks passed
@adamruzicka
Copy link
Contributor

Thank you @ofedoren !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants