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

Stress Testing Throughput for Club Submission Applications #606

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

shiva-menta
Copy link

Conduct customizable stress tests using python manage.py stress_test. Change parameters including number of users, applications per user, number of clubs, etc.

HTTP requests are mocked using APIRequestFactory. Tested minimally before and after #603, and saw some performance gains in SQLite, but SQLite's concurrency features should be considered (thread explaining how SQLite handles concurrent writes!). Have not tested on Docker Postgres yet.

Copy link

gitguardian bot commented Jan 24, 2024

⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id Secret Commit Filename
9335321 Generic Private Key ede0a26 nginx/certs/_wildcard.localhost.net-key.pem View secret
9335321 Generic Private Key 33aa378 nginx/certs/_wildcard.localhost.net-key.pem View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Our GitHub checks need improvements? Share your feedbacks!


self.uri = "/users/question_response/"
self.factory = APIRequestFactory()
self.view = UserViewSet.as_view({"post": "question_response"})
Copy link
Member

Choose a reason for hiding this comment

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

This will create objects for real right? So we want to tear down?

Copy link
Author

Choose a reason for hiding this comment

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

Yeah the tearDown() function handles that, but if it needs to be more explicit I can do that.

self.users = list(User.objects.filter(username__startswith=self.user_prefix))
print("Finished setting up users.")

@sync_to_async
Copy link
Member

@rohangpta rohangpta Jan 24, 2024

Choose a reason for hiding this comment

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

This decorator is misleading. All this is doing is taking your function and convering it to an awaitable. It unfortunately doesn't make your code "async for free". In fact, with thread_sensitive = True (which is the default behaviour), I believe these functions will all run in the main thread, and probably even slower then usual since it goes through the overhead of "asyncifying". Some more information here

It isn't worth changing it back since this is just a test but I thought I should let you know about this behaviour. IIf you wanted better async, you might want to use httpx or something like that (I think newer versions of Django support Async more natively).

Copy link
Author

Choose a reason for hiding this comment

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

Yeah I was starting to notice that in the end. I originally was designing it as sending HTTP requests and was going to use httpx but then the mocking seemed like a better option. Thanks for sharing!

return end_time - start_time

def tearDown(self):
Club.objects.filter(code__startswith=self.club_prefix).delete()
Copy link
Member

Choose a reason for hiding this comment

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

Might have stray ClubApplication, ApplicationSubmission, etc objects which you'll need to tear down

Copy link
Author

Choose a reason for hiding this comment

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

Checking from the models, this should all be handled with on_delete behavior? It should form separate "trees" with roots of Clubs and Users.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, this makes sense. Please do double check empirically though; we should definitely avoid excess footprint on the DB, especially if we test in prod multiple times.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah just double checked, no rows are left over. The only concern I guess could be the autoincrementing index and offsetting that by the number of test instances. Should not be an issue at all, but if we want full isolation, we could create a separate database on the same instance.

Copy link
Member

Choose a reason for hiding this comment

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

I think this should be fine! Thank you for double checking

@rohangpta
Copy link
Member

Can you post your test results here?

@rohangpta
Copy link
Member

rohangpta commented Jan 24, 2024

Great start Shiva! I think we're ready to take some steps towards productionising this test, some notes:

  • Test overall looks good to me, I like your choice of mocking HTTP request objects and overall the code is cleanly structured.
  • How long does this test take to run locally? Could you add some sleeps between submissions to make it more realistic?
  • Can you check whether your asyncifying is doing anything? (you can do this by noting the current end_time - start_time as a baseline, and comparing that against N * (individual end_time - start_time) values. I'm curious if sync_to_async is actually allowing parallel processing here, although my understanding is not.
  • Let's talk through the points here and here on Friday: some points on optimisation and DB configuration!
  • Before we productionise this, we should enable as many AWS insight metrics as possible so we can see where exactly the load is landing.

@shiva-menta
Copy link
Author

Can you post your test results here?

Yeah I'm going to conduct more testing tonight, but I wanted to address the async behaviors first and potentially consider doing HTTP requests instead, but if our goal is to isolate database behavior, mocking makes more sense. Will show benchmarks for SQLite and PostgreSQL!

@shiva-menta
Copy link
Author

shiva-menta commented Jan 25, 2024

Great start Shiva! I think we're ready to take some steps towards productionising this test, some notes:

  • Test overall looks good to me, I like your choice of mocking HTTP request objects and overall the code is cleanly structured.
  • How long does this test take to run locally? Could you add some sleeps between submissions to make it more realistic?
  • Can you check whether your asyncifying is doing anything? (you can do this by noting the current end_time - start_time as a baseline, and comparing that against N * (individual end_time - start_time) values. I'm curious if sync_to_async is actually allowing parallel processing here, although my understanding is not.
  • Let's talk through the points here and here on Friday: some points on optimisation and DB configuration!
  • Before we productionise this, we should enable as many AWS insight metrics as possible so we can see where exactly the load is landing.

Took a look, and yeah you're completely correct about sync_to_async, I made the incorrect assumption of seeing a difference in thread completion times implied concurrent connections; I believe it just indicated the overhead. When I set thread_sensitive=False, I get a bunch of database is locked errors due to the concurrent writes. I can't seem to configure SQLite's timeout setting properly so the database writes aren't blocking until the database is unlocked from the main thread.

I believe the implementation will be highly dependent on the actual database for now. I'm going to try seeing if PostgreSQL functions as is, because I'm not sure how informative SQLite will be if our main goal is to test a database with concurrency capabilities (table-locking vs. row-locking).

@rohangpta
Copy link
Member

rohangpta commented Jan 25, 2024

Yeah sounds good. I don't think thread_sensitive=False will end up working here since database adapters in Django require that they are accessed in the same thread that they were created in, but I agree that it's probably best to save the bulk of our testing for Postgres

@rohangpta
Copy link
Member

@shiva-menta did we ever have any results on this? Curious about how things match up since we didn't actually seem to have any DB issues in prod this cycle.

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

Successfully merging this pull request may close these issues.

2 participants