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

Skip redundant queries when include_data is disabled #47

Conversation

stokarenko
Copy link
Collaborator

There is a general problem inside AMS related to custom association finder combined with include_data setting:

  class BlogPost3Serializer < BaseTestSerializer
    belongs_to :category do
      include_data :if_sideloaded
      object.categories.last
    end
  end

The problem is that belongs_to block will be fully evaluated each time for each object, and only after that AMS is able to take into account include_data mode -

  def value(serializer, include_slice)
    # ...
    block_value = instance_exec(serializer, &block) if block
    return unless include_data?(include_slice)
    # ...
  end

That causing redundant (and so huge potentially!) SQL queries and AR objects allocation when include_data appears to be false but belongs_to block defines instant (not a kind of AR::Relation) custom association finder.

Described problem is a very specific use case for pure AMS applications. The bad news is that ams_lazy_relationships is always utilize the association block -

def define_lazy_association(type, name, options, block)
  #...
  block ||= lambda do |serializer|
    serializer.public_send("lazy_#{name}")
  end

  public_send(type, name.to_sym, real_relationship_options, &block)
  #...
end

This way we break include_data optimizations for the host application.

In order to overcome that we are forced to monkey-patch AmsLazyRelationships::Extensions::Reflection#value method and make it to be ready for Proc returned by association block. This way we will use a kind of

  block ||= lambda do |serializer|
    -> { serializer.public_send("lazy_#{name}") }
  end

as association block, then AMS will evaluate it, get the value of include_data
setting, make a decision do we need to continue with that association, if so -
will finally evaluate the proc with lazy relationship inside it.

In additional, now we allow skipping the custom lazy finder when our goal is just to define include_data setting nor some links/metas:

  lazy_has_many :comments do
    include_data :if_sideloaded
    link :self, 'a link'
    meta name: 'Dan Brown'
   end

@stokarenko
Copy link
Collaborator Author

@Bajena

lazy_dig is the last piece of puzzle for me )

Well, found one more missing piece, please take a look )
Also please check these two PRs:

I merged them without your approval but hope that is not an issue since they are about documentation and tests. They blocked include_data stuff a bit )

@stokarenko stokarenko requested a review from Bajena January 14, 2020 18:17
@stokarenko stokarenko force-pushed the skip-redundant-queries-when-include-data-is-disabled-vol2 branch 4 times, most recently from 941ca34 to 3fdda71 Compare January 14, 2020 19:08
@stokarenko
Copy link
Collaborator Author

Have no ideas how to overcome that Cognitive Complexity of 6 CC issue..
Will try again tomorrow if that is strongly required but as I feel that is exact case when consider refactoring will be worse than original )

@Bajena
Copy link
Owner

Bajena commented Jan 14, 2020

Hey again;)
Thanks for another good finding - I'll do my best to review and ship it this week.

Regarding #46 - it'd be cool to have 0.10.0rc4 tests back as my we're using it in our company :D
If that's a problem for you I can try to add it back myself.

@stokarenko
Copy link
Collaborator Author

Hey again;)
Thanks for another good finding - I'll do my best to review and ship it this week.

Regarding #46 - it'd be cool to have 0.10.0rc4 tests back as my we're using it in our company :D
If that's a problem for you I can try to add it back myself.

No problem, I'll add )
0.10.0rc4 has the issue on its dependencies and raises undefined method "underscore" on String right on pure require "active_model_serializers". Need to require active_support before it, that's it )

@stokarenko
Copy link
Collaborator Author

it'd be cool to have 0.10.0rc4 tests back as my we're using it in our company :D

Here we go ) #48

@stokarenko stokarenko force-pushed the skip-redundant-queries-when-include-data-is-disabled-vol2 branch from 3fdda71 to b0ad1df Compare January 14, 2020 21:04
@stokarenko
Copy link
Collaborator Author

Rebased to master with testing against v0.10.0.rc4, pushed )

@stokarenko stokarenko force-pushed the skip-redundant-queries-when-include-data-is-disabled-vol2 branch from b0ad1df to 4333617 Compare January 15, 2020 10:34
@stokarenko stokarenko force-pushed the skip-redundant-queries-when-include-data-is-disabled-vol2 branch from 4333617 to a87b378 Compare January 15, 2020 10:42
@stokarenko
Copy link
Collaborator Author

Method define_lazy_association has a Cognitive Complexity of 6

ugh, no chance to escape..

Copy link
Owner

@Bajena Bajena left a comment

Choose a reason for hiding this comment

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

Sorry that it took some time. I did a first round of review - let me know what you think about my comments.
Btw, good to know that include_data option exists in newer AMS versions. I'd never notice it, because we're using 0.10.0rc4 in my company 🗡

@@ -64,7 +64,7 @@ class UserSerializer < BaseSerializer
# The previous one is a shorthand for the following lines:
lazy_relationship :blog_posts, loader: AmsLazyRelationships::Loaders::Association.new("User", :blog_posts)
has_many :blog_posts, serializer: BlogPostSerializer do |serializer|
serializer.lazy_blog_posts
-> { serializer.lazy_blog_posts }
Copy link
Owner

Choose a reason for hiding this comment

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

It can still be called without using a proc, right?
Maybe we could keep the old version for simplicity and just mention the proc form in the include_data example you added in https://github.com/Bajena/ams_lazy_relationships/pull/47/files#diff-04c6e90faac2675aa89e2176d2eec7d8R134 ?

Suggested change
-> { serializer.lazy_blog_posts }
serializer.lazy_blog_posts

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It can still be called without using a proc, right?

sure, Reflection monkey-patch will forward non-proc value as it is.
But I believe that in readme is better to propose the best possible sample by default.
Just imagine:

  1. engineer_1 adds non-proc custom finders all over the services & serializers;
  2. engineer_2 turns ActiveModel::Serializer.config.include_data_default = false globally
  3. local naive tests will assert successfully that json response is free from data, but
  4. production servers will continue to fail due to OOM and that will be quite tricky to discover why

I think we need to leave -> { serializer.lazy_blog_posts } as a very default sample, and add a small hint that serializer.lazy_blog_posts will work as well but can produce redundant queries.

Copy link
Owner

Choose a reason for hiding this comment

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

I think we need to leave -> { serializer.lazy_blog_posts } as a very default sample, and add a small hint that serializer.lazy_blog_posts will work as well but can produce redundant queries.

Ok, I must admit that your explanation makes sense to me. Let's leave proc, but also let's mention the non-proc version as you suggested 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

roger that! )

README.md Outdated
end
```

Keep attention that custom finder should be returned in a form of lambda,
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
Keep attention that custom finder should be returned in a form of lambda,
If you're using `include_data` AMS setting make sure that your association blocks return a lambda in order to avoid redundant SQL queries when `include_data` AMS setting evaluates to `false`:

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Again, typically include_data will be turned off a way further from the project beginning, on the very last phase of make it work, make it right, make it fast ;) I think it's good to make an accent how to do the things better right from beginning )

Copy link
Owner

Choose a reason for hiding this comment

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

Ok

lib/ams_lazy_relationships/extensions/reflection.rb Outdated Show resolved Hide resolved
lib/ams_lazy_relationships/extensions/reflection.rb Outdated Show resolved Hide resolved
# Described problem is a very specific use case for pure AMS applications.
# The bad news is that `ams_lazy_relationships` is always utilize the
# association block -
# https://github.com/Bajena/ams_lazy_relationships/blob/v0.2.0/lib/ams_lazy_relationships/core/relationship_wrapper_methods.rb#L32-L36
Copy link
Owner

Choose a reason for hiding this comment

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

I'd avoid adding links to specific versions in the code. Let's get rid of lines 32-44 - they may quickly get out of date.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That is exactly why I pinned v0.2.0 tag here ))

end
end

::ActiveModel::Serializer::Reflection.prepend AmsLazyRelationships::Extensions::Reflection
Copy link
Owner

Choose a reason for hiding this comment

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

I'd consider applying this patch only to AMS versions >= 0.10.3.
Nobody using lower versions of AMS is probably going to return lambdas from their association blocks anyways as they won't benefit from that anyhow.

Copy link
Collaborator Author

@stokarenko stokarenko Jan 16, 2020

Choose a reason for hiding this comment

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

I thought a lot about that from the beginning, but this way we need to apply the same condition in default define_lazy_association block.

Even worse, readme description will be more complex as well since for lower AMS versions the proc-like custom finder will instantly fail. I.e. we propose the snippet of code which will crush on AMS version still accepted by gemspec file..

As an option, we can move all include_data stuff in separate lib file. require 'ams_lazy_relationships/efficient_finders' will engage monkey-patch and proc-like default finder, without said require nothing will be applied.

How you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As an option, we can move all include_data stuff in separate lib file.

Notice that this way ams_lazy_relationships will still silently produce a huge problems to consumers due to cancelled effect from include_data optimizations in sense of DB hits and memory usage.

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, you're right, doing it the way I suggested generates more problems than it solves. Let's keep it then. But in such case let's make sure that all AMS versions are tested for proc evaluation (specs are skipped for lower AMS versions in https://github.com/Bajena/ams_lazy_relationships/pull/47/files#diff-6e72b60a2fb8b930cc9741785114a948R871).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

They are tested already due to the new default define_lazy_association block, but I'll add an accented test with proc-like custom finder )

```ruby
class BlogPostSerializer < BaseSerializer
lazy_has_many :comments do
include_data :if_sideloaded
Copy link
Owner

Choose a reason for hiding this comment

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

Wasn't it supposed to be:

Suggested change
include_data :if_sideloaded
include_data false

?

Copy link
Collaborator Author

@stokarenko stokarenko Jan 16, 2020

Choose a reason for hiding this comment

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

:if_sideloaded is the most complex example, this way data will be included for explicit ?include=association and rejected without it.

In second case we need to suppress any kind of finders but we need to keep the finder alive for the time when first case will happen.

i.e.

  lazy_has_many :comments do
    include_data false
    nil
  end

is completely ok and ams_lazy_relationship will behave like that without this PR.

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, thanks for explanation - so this will work correctly, without N+1s, both when the resource is included and when it isn't 👍

README.md Outdated
```

Feel free to skip custom lazy finder for association if your goal is just to
define `include_data` setting nor some links/metas:
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
define `include_data` setting nor some links/metas:
define `include_data` setting to render some links/metas:

?

Copy link
Collaborator Author

@stokarenko stokarenko Jan 16, 2020

Choose a reason for hiding this comment

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

include_data is not about links and metas, but mostly about skipping so-called json relationships.

By default AMS will add to response the primary keys for each association defined in serializer. I.e.

class OrganizationSerializer
  has_many :users
end

OrganizationSerializer.new(org).as_json
# {data: {relationships: { users: { [{id: 1}, {id: 2}, ..., {id: 123_567_89} ] } } } }

That happens always even without explicit ?include=users, that produces SELECT * FROM users query on the way and memory allocation for 123_567_89 AR objects.

include_data = false or :if_sideloaded is a way to survive.

I never tried but believe that include_data = :if_sideloaded can be combined with links and metas

Copy link
Owner

Choose a reason for hiding this comment

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

True, you're right, I missed the point :) But TBH here I mostly meant that this sentence is not a correct english sentence and tried to rework it a bit. Would you be able to rephrase it a bit?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

will try, thanks! )

stokarenko and others added 2 commits January 16, 2020 12:31
Co-Authored-By: Jan Bajena <bajena3@gmail.com>
Co-Authored-By: Jan Bajena <bajena3@gmail.com>
@stokarenko
Copy link
Collaborator Author

@Bajena Ready for the second round of review )

Copy link
Owner

@Bajena Bajena left a comment

Choose a reason for hiding this comment

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

Just the specs and some minor doc changes left and we should be able to :shipit: !

@@ -64,7 +64,7 @@ class UserSerializer < BaseSerializer
# The previous one is a shorthand for the following lines:
lazy_relationship :blog_posts, loader: AmsLazyRelationships::Loaders::Association.new("User", :blog_posts)
has_many :blog_posts, serializer: BlogPostSerializer do |serializer|
serializer.lazy_blog_posts
-> { serializer.lazy_blog_posts }
Copy link
Owner

Choose a reason for hiding this comment

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

I think we need to leave -> { serializer.lazy_blog_posts } as a very default sample, and add a small hint that serializer.lazy_blog_posts will work as well but can produce redundant queries.

Ok, I must admit that your explanation makes sense to me. Let's leave proc, but also let's mention the non-proc version as you suggested 👍

README.md Outdated
end
```

Keep attention that custom finder should be returned in a form of lambda,
Copy link
Owner

Choose a reason for hiding this comment

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

Ok

README.md Outdated
```

Feel free to skip custom lazy finder for association if your goal is just to
define `include_data` setting nor some links/metas:
Copy link
Owner

Choose a reason for hiding this comment

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

True, you're right, I missed the point :) But TBH here I mostly meant that this sentence is not a correct english sentence and tried to rework it a bit. Would you be able to rephrase it a bit?

```ruby
class BlogPostSerializer < BaseSerializer
lazy_has_many :comments do
include_data :if_sideloaded
Copy link
Owner

Choose a reason for hiding this comment

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

Ok, thanks for explanation - so this will work correctly, without N+1s, both when the resource is included and when it isn't 👍

end
end

::ActiveModel::Serializer::Reflection.prepend AmsLazyRelationships::Extensions::Reflection
Copy link
Owner

Choose a reason for hiding this comment

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

Ok, you're right, doing it the way I suggested generates more problems than it solves. Let's keep it then. But in such case let's make sure that all AMS versions are tested for proc evaluation (specs are skipped for lower AMS versions in https://github.com/Bajena/ams_lazy_relationships/pull/47/files#diff-6e72b60a2fb8b930cc9741785114a948R871).

@stokarenko
Copy link
Collaborator Author

@Bajena Everything is done I hope ) Please release a new gem version with this stuff )

Copy link
Owner

@Bajena Bajena left a comment

Choose a reason for hiding this comment

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

Great stuff!

@Bajena Bajena merged commit e6d09b0 into Bajena:master Jan 16, 2020
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