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

Perf: Better response times from api/public/v1/courses #4616

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

inulty-dfe
Copy link
Contributor

@inulty-dfe inulty-dfe commented Oct 24, 2024

Context

Counting the number of courses in a recruitment cycle is very expensive. We do this on every request to the courses endpoint.

This is an "agonising" endpoint in Skylight

image

Loadtesting the change

The tests were run with Siege.

Siege documentation

All tests were run against review apps. The review apps all had 1 replica with 512MiB memory.

Before changes

inulty-dfe ~/Code/publish-teacher-training ( perf-api-providers) $ siege -c 10 -t 1m -if app/controllers/api/loadtest-urls-other-branch.txt

{       "transactions":                           91,
        "availability":                       100.00,
        "elapsed_time":                        59.50,
        "data_transferred":                    19.95,
        "response_time":                        5.97,
        "transaction_rate":                     1.53,
        "throughput":                           0.34,
        "concurrency":                          9.13,
        "successful_transactions":                31,
        "failed_transactions":                     0,
        "longest_transaction":                 19.52,
        "shortest_transaction":                 0.97
}71
}

With just loaded? changes

inulty-dfe ~/Code/publish-teacher-training ( perf-api-providers) $ siege -c 10 -t 1m -if app/controllers/api/loadtest-urls-no-cache.txt

{       "transactions":                           75,
        "availability":                       100.00,
        "elapsed_time":                        59.49,
        "data_transferred":                    26.39,
        "response_time":                        7.16,
        "transaction_rate":                     1.26,
        "throughput":                           0.44,
        "concurrency":                          9.03,
        "successful_transactions":                45,
        "failed_transactions":                     0,
        "longest_transaction":                 19.76,
        "shortest_transaction":                 1.11
}

With count query cached and loaded? changes

inulty-dfe ~/Code/publish-teacher-training ( perf-api-providers) $ siege -c 10 -t 1m -if app/controllers/api/loadtest-urls.txt

{       "transactions":                           94,
        "availability":                       100.00,
        "elapsed_time":                        59.86,
        "data_transferred":                    51.11,
        "response_time":                        6.07,
        "transaction_rate":                     1.57,
        "throughput":                           0.85,
        "concurrency":                          9.53,
        "successful_transactions":                84,
        "failed_transactions":                     0,
        "longest_transaction":                 13.18,
        "shortest_transaction":                 1.90
}

Result

Operation Original No cache changes Cache changes
throughput 0.34 0.44 0.85
successful_transactions 31 45 84
data_transferred 19.95 26.39 51.11

The URLs

The URLs used were variations on this list.
I added no_cache for the "no cache" test run.
I changed the PR number for the "original" test run.

image

Changes proposed in this pull request

  • Use loaded? to avoid making requests to the database when we already have records in memory.
  • Cache the course record count per recruitment cycle for a period of time.
  • The filter field is not used in the cache key because it is ignored in the query. The API does not filter courses.

Guidance to review

  • How long should the course count be cached? I would suggest 1 day.

Things to check

  • This code does not rely on migrations in the same Pull Request
  • If this code includes a migration adding or changing columns, it also backfills existing records for consistency
  • If this code adds a column to the DB, decide whether it needs to be in analytics yml file or analytics blocklist
  • API release notes have been updated if necessary
  • Required environment variables have been updated added to the Azure KeyVault
  • Inform data insights team due to database changes
  • Make sure all information from the Trello card is in here
  • Rebased main
  • Cleaned commit history
  • Tested by running locally
  • Attach PR to Trello card

@inulty-dfe inulty-dfe added the deploy_v2 DO NOT USE label Oct 24, 2024
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 24, 2024 16:09 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 24, 2024 16:19 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 25, 2024 13:24 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 25, 2024 15:57 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 25, 2024 16:10 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 25, 2024 16:32 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 25, 2024 17:05 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 28, 2024 09:15 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 28, 2024 11:58 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 29, 2024 09:51 Destroyed
@inulty-dfe inulty-dfe changed the title Add custom instrumentation to api pagination Perf: Better response times from api/public/v1/courses Oct 30, 2024
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 30, 2024 12:34 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 30, 2024 13:21 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 30, 2024 14:02 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 30, 2024 16:45 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 30, 2024 17:11 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 30, 2024 17:23 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 31, 2024 09:01 Destroyed
@inulty-dfe inulty-dfe marked this pull request as ready for review October 31, 2024 09:13
@inulty-dfe inulty-dfe requested a review from a team as a code owner October 31, 2024 09:13
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 31, 2024 09:21 Destroyed
@github-actions github-actions bot temporarily deployed to review_aks-4616 October 31, 2024 09:27 Destroyed
Copy link
Contributor

@gms-gs gms-gs left a comment

Choose a reason for hiding this comment

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

Niceee!! <3

@github-actions github-actions bot temporarily deployed to review_aks-4616 October 31, 2024 10:08 Destroyed
Comment on lines +588 to 591
return enrichments.select(&:last_published_timestamp_utc).max_by(&:last_published_timestamp_utc)&.last_published_timestamp_utc if enrichments.loaded?

enrichments.maximum(:last_published_timestamp_utc)
end
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like it can be generalised for all associations.

Have you considered looking into ActiveRecord/Associations/CollectionProxy? A #maximum method there might be able to address this issue across the entire app with a single change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that looks tempting. but I would be careful about doing that. It's something we should discuss though, it seems like a useful thing to do.

def cached_course_count
year = permitted_params[:recruitment_cycle_year] || RecruitmentCycle.current.year

if permitted_params[:no_cache]
Copy link
Contributor

Choose a reason for hiding this comment

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

Below might be something for another PR.

It might be more conventional to read the Cache-Control request header and return cached vs non-cached results.

I haven't quite dug into if that's indeed the right header, or what the header value should be (I think no-cache?), and also if my assumption is considered standard practice, but it might be worth looking into.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm happy to remove this, I don't think it should be merged. Undocumented api params probably aren't a good idea.

As for the cache headers, I believe those (response headers) are for instructing how the client should cache, not how the server should respond.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that this section is indicating that there are request header standards too, but yeah, I haven't seen it in practice yet. I wonder how the browser implements the "Disable cache" checkbox.

But yeah, I agree. Let's remove the no_cache for now.

def cached_course_count
year = permitted_params[:recruitment_cycle_year] || RecruitmentCycle.current.year

Rails.cache.fetch("api_course_count_#{year}", expires_in: 5.minutes) do
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's have a discussion about this expiration time?

Copy link
Contributor

@tomas-stefano tomas-stefano left a comment

Choose a reason for hiding this comment

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

Everything looks good. Great initiative. ✌️

Let's have a separate discussion about the cache & expiration.

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

Successfully merging this pull request may close these issues.

4 participants