-
Notifications
You must be signed in to change notification settings - Fork 11
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
5120 – Create Tags in the background #2005
Conversation
@caiosba I tried to summarize what I've been thinking and some of the issues I went through. On where I think this work could goThis is a work in progress. I thought it would be nice if we could maybe have a wrapper or something, so the usage of this worker would be similar to TestsI wrote two tests to try to guide this work, I think they make sense?
On Tag creation:Contexts
How it works
Annotations
|
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.
Thanks @vasconsaurus , the direction is correct, I left a suggestion for better encapsulation and more clear code. Please let me know if it makes sense to you.
@@ -263,6 +263,6 @@ def create_claim_description_and_fact_check | |||
end | |||
|
|||
def create_tags | |||
self.set_tags.each { |tag| Tag.create!(annotated: self, tag: tag.strip, skip_check_ability: true) } if self.set_tags.is_a?(Array) | |||
self.set_tags.each { |tag| GenericWorker.perform_in(1.second, 'Tag', 'create!', annotated_type: 'ProjectMedia' , annotated_id: self.id, tag: tag.strip, skip_check_ability: true) } if self.set_tags.is_a?(Array) |
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.
Suggestion: I think it's going to be easier to encapsulate and to unit-test if we wrap the whole execution in a single worker run. Something like:
def create_tags
if self.set_tags.is_a?(Array)
GenericWorker.perform_in(1.second, 'ProjectMedia', 'create_tags_in_background', self.id, self.set_tags.to_json)
end
end
def self.create_tags_in_background(project_media_id, tags_array_as_json)
project_media = ProjectMedia.find(project_media_id)
tags = JSON.parse(tags_array_as_json)
tags.each { |tag| Tag.create! annotated: project_media, tag: tag.strip, skip_check_ability: true) }
end
Rationale:
- For clarity, I think it's important for the signature of
GenericWorker
to be explicitly class/method/arguments (I see it's implicit now) - more on that in the next comment - Each tag in a separate background job run means background job overhead for a small task, and it can also lead to inconsistencies
app/workers/generic_worker.rb
Outdated
def perform(klass_name, *args) | ||
klass = klass_name.constantize | ||
options = args.extract_options! |
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.
Suggestion for clarity: The signature could be explicitly klass_name, klass_method, method_args
.
@jayjay-w @melsawy , as we talked, please support/follow @vasconsaurus here too. |
Sure Caio |
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.
Left comments related to User.current and tests
a1bde47
to
3536121
Compare
3536121
to
49f71c0
Compare
app/models/project_media.rb
Outdated
@@ -528,6 +528,19 @@ def add_nested_objects(ms) | |||
ms.attributes[:requests] = requests | |||
end | |||
|
|||
def self.create_tags(**params) |
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 to make the code more clear and more predictable, it's better to explicitly list all arguments that the method expects, instead of **params
(**params
is more common in meta-programming).
test/models/project_media_8_test.rb
Outdated
|
||
class ProjectMedia8Test < ActiveSupport::TestCase | ||
def setup | ||
super |
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.
Tests will run faster if you don't run everything that super
runs. I suggest you start with empty setup
and teardown
methods and then add only the steps you actually need:
def setup
end
def teardown
end
test/workers/generic_worker_test.rb
Outdated
|
||
class GenericWorkerTest < ActiveSupport::TestCase | ||
def setup | ||
super |
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.
Tests will run faster if you don't run everything that super
runs. I suggest you start with empty setup
and teardown
methods and then add only the steps you actually need:
def setup
end
def teardown
end
Great stuff @vasconsaurus ! The error you're seeing in Sidekiq usually means that code was changed but the application was not restarted after that. Just remember to do I agree we should not do a drop-in replacement. Let's have our own implementation with our own signature. It will be easier to track during the migration too. I added other comments directly to the code - nothing that changes the direction, and I also don't see anything missing. Once comments are addressed, all tests are passing and all the code is covered, I think this is ready for final review and tests. Great work! |
I tried to create a general structure of the worker and two tests: - one test just to make sure the GenericWorker works - one test to make sure tags are created in the background BUT: - The tests are wonky - The worker test passes but shouldn't - The tag tests doesn't work because create_tags is private: - I think that is the place where/when we create the tags, seem we use it in a after_create callback. But I'm not completelly sure, and if I'm right, not sure the best way to test it.
I think the tags are created by a after_create callback inside ProjectMedia. I was having a bit of trouble testing that, so I added the creating of tags to the sample_data method, ans tested that it worked. Now that I know that works, I'll try moving it to the background.
- I added a new test for GenericWorker and specified which models we are testing. I think that might be easier than a too generic test. - I think now the tests make sense (though I'm not sure the tags test should be in there or inside project media tests) - Both tests trying to create tags fail, because we need to pass a whole object to annotations, so that is an issue
I'm unsure if it's ok to change this, but looking in our GraphQL wiki we should be able to create a Tag by sending the annotated_type and id. If we update create_tags to send those two values instead of the entire object, we are able to do it in the background job.
When creating tags while creating an item, we should wrap the whole execution in a single worker run. Each tag in a separate background job run means background job overhead for a small task, and it can also lead to inconsistencies
What we are testing is that tags are being created when we create a project media. The method we use to do that is a method called by project_media, so I thought it made more sense to move the tests.
Each tag in a separate background job run means background job overhead for a small task, and it can also lead to inconsistencies
We should notify Sentry when we fail to create the tags. (CheckSentry.notify also logs the error) Added tests: - make sure create_tags_in_background creates tags associated with the project_media - make sure an error is not raised, but an error is sent to Sentry Added .with_indifferent_access to make sure we are able to get the data from the params hash.
We log the revisions for tag creation in case User.current exists otherwise no logs for this action
- call the notify inside else, before I was always sending a sentry error - it's better to send the project_media_id to Sentry, even if there isn't a project media object, an id might exist
I think the method that was called 'in_background' is actually foreground and vice-versa.
…d of **params while doing this I found a bug: GenericWorker worked for methods that took hashes as a parameter, but not for methods that took standalone parameters. (not sure if standalone parameters is the correct way to call this)
Tests will run faster if you don't run everything that super runs. request here: #2005 (comment)
this way we make sure the team was created, the background job did what it was supposed to.
dffb66d
to
b510a7b
Compare
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.
Thanks @vasconsaurus for addressing all the comments from my last review! It's looking great! Just one last ask here: please add a test for this case:
check-api/app/workers/generic_worker.rb
Line 15 in 2c9c1d0
klass.public_send(klass_method) |
app/workers/generic_worker.rb
Outdated
def perform(klass_name, klass_method, *method_args) | ||
klass = klass_name.constantize | ||
options = method_args.extract_options!.with_indifferent_access | ||
if options |
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.
@vasconsaurus the tests you added in your last commit are still not covering line 15 - I found out that the problem is here... options
is always a hash, so if options
is always true
... you should replace this conditional by something like unless options.blank?
, because when there are no parameters, options
value 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.
@caiosba I updated this, but have a question regarding tests:
The two tests I added were passing before, so they are not really working as expected. How could I fix it?
Also, is there any extra logging or error handling that I should add?
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.
Thanks, Manu. No, I don't think any additional logging or error handling is needed here. But for the test, I suggest you actually assess that the method is executed. For now your test for the no-arguments case just verifies that no exception is raised.
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.
true 🤦♀️ still, do you think I should verify, assert anything else?
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.
true 🤦♀️ still, do you think I should verify, assert anything else?
In the test I think you should make sure that the method is executed... for example, Blank.create!
is a class method that doesn't take any arguments and you can use assert_difference 'Blank.count' do
to make sure it was executed.
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 updated to use assert_difference
both here and for the test in line 28.
But one last comment: even when the test wasn't hitting the else
, because I was using if options
, the test still succeeded. So blank was still created.
So I guess what I mean is, this test makes sure Blank
is created, but it doesn't make sure of which branch we are hitting.
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.
Correct, I think that the other block was still able to handle it just fine. That said, maybe the if/else is not even needed, but it's more readable/clear to have them separate.
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.
Yup, so I was wondering about that. I think it might be nice to have the option with just the one straightforward step, but it isn't really needed. Should we keep it or remove it (the conditional)?
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.
Yeah, that's what I said, I think it's more clear to have the conditional. Ruby Meta programming can be harder to read, otherwise :)
options is always a hash, so `if options` is always `true`. replaced this conditional by `unless options.blank?`, because when there are no parameters, options value is `{}`.
@melsawy , since you requested changes before, please re-review this PR. |
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.
LGTM, great work @vasconsaurus
You can merge once apply Caio's requests and check test coverage.
Pretty straight forward. Sets the maximum retry count to 3. Note We want to add custom retry logic, but that works differently between Sidekiq 5 and 6. So we think it is better to focus later on the Sidekiq upgrade, and until there, worker instances can handle their exceptions instead of relying on the generic worker for that. References: 5120, #2005 PR: 2052
Description
Context
While working on
API/Zapier Error: The app did not respond in-time. It may or may not have completed successfully
we thought tags creation could be causing the slow response.We couldn’t pinpoint it as the cause, but it’s still a good performance improvement and should help improve the response time. So we are moving forward with it.
Implementation Notes
Sidekiq’s
delayed extensions
were deprecated in version 5 and removed in 7. So we are taking this opportunity to create aGenericJob
, and eventually substitute the instances of.delay
/.delay_for
we have.References: 5120, 4959
How has this been tested?
rails test test/models/tag_test.rb:288
rails test test/workers/generic_worker_test.rb
Things to pay attention to during code review
Please describe parts of the change that require extra attention during code review, for example:
Checklist