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

Course cache #15965

Merged
merged 5 commits into from Jun 26, 2017
Merged

Course cache #15965

merged 5 commits into from Jun 26, 2017

Conversation

Bjvanminnen
Copy link
Contributor

@Bjvanminnen Bjvanminnen commented Jun 20, 2017

This PR adds a course cache, styled after the cache currently used by scripts. It also update a script method (course_link) to actually take advantage of the cache instead of hitting the db.

My biggest concern with this PR is that it ends up duplicating a lot of code. I'm not sure if it makes sense to spend a lot of time cleaning that up or not.

I think the right way to do this (and I'm open to feedback) would be to have a single file that handles our model caches. This would extract various cacheing that exists in the Script model and the Course model, and instead have most of that logic live in our model_cache.rb. This would likely have a single should_cache? method used, and there might be other opportunities to share helpers. My main concern is that I'm still only semi-competent when it comes to Ruby/Rails, and I'm not sure I'm the best person to try to build this. Would be interested in feedback from others as to how much they dislike the current repetitiveness.

@Bjvanminnen Bjvanminnen changed the title WIP: Course cache Course cache Jun 20, 2017
Copy link
Contributor

@ashercodeorg ashercodeorg left a comment

Choose a reason for hiding this comment

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

This seems like a reasonable approach to establish the cache and associated tests, with a follow-up PR to remove the code duplication. Happy to do that PR if you want, though I don't think I'm any more "qualified" than you are.

course_cache_from_cache || course_cache_from_db
end

def self.get_without_cache(id)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Though this is simply duplication of the existing paradigm, the method API and the method implementation would be better documented if id were replaced with id_or_name or course_id_or_course_name. Also below.

# TODO(bjvanminnen): share this somehow with script.rb?
def self.should_cache?
return false if Rails.application.config.levelbuilder_mode
return false if ENV['UNIT_TEST'] || ENV['CI']
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: The last two lines could just be ENV['UNIT_TEST'] || ENV['CI'].

@joshlory
Copy link
Contributor

I think the right way to do this (and I'm open to feedback) would be to have a single file that handles our model caches. This would extract various cacheing that exists in the Script model and the Course model, and instead have most of that logic live in our model_cache.rb. This would likely have a single should_cache? method used, and there might be other opportunities to share helpers.

+💯 for this option!

I'm not sure I'm the best person to try to build this.

I'm not sure anyone on the team has full knowledge of how we cache models (maybe @wjordan?). Cleaning up the various disperse caching methods into one understandable system would help!

@Bjvanminnen
Copy link
Contributor Author

This is now ready for review, having got tests passing.

One other significant change I made was to make it so that we always add scripts/courses to the cache the same way. Previously, if we added them when we first got data from the db, we did it using Model.includes so that we ended up with all the info desired. However, if we then later had a cache miss, we would add content to the cache without the includes. In practice, I suspect we don't really have cache misses, but this made reasoning about some of this in tests a lot easier (and seems like general goodness).

My plan for the time being is to punt on trying to move this all into a single file. I think this current step puts us in a better place (we get caching for courses) and though there's some duplication of pretty similar code, I don't think it really makes things any harder to understand. Feel free to push back.


# generates our course_cache from what is in the Rails cache
def self.course_cache_from_cache
Course.connection
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this do? The returned connection isn't used.

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 do not know. I copied it because it was there in the script version. My assumption was that it perhaps somehow locked the db connection for this course of this method?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it maybe ensures that a connection is established for this thread, but that should happen anyway in the below query. It doesn't appear to do anything special for the method. See http://api.rubyonrails.org/classes/ActiveRecord/Base.html

Course.all.pluck(:id).each do |course_id|
course = get_without_cache(course_id)
cache[course.name] = course
cache[course.id.to_s] = course
Copy link
Contributor

Choose a reason for hiding this comment

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

Why retrieve all the ids, then loop through and retrieve each Course via get_without_cache, instead of one query?

If this is for the included associations, how about defining them in a scope and using that in both locations?

Copy link
Contributor

@aoby aoby Jun 21, 2017

Choose a reason for hiding this comment

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

Something like:

scope :with_associated_models, -> {includes([:plc_course, :course_scripts])}

def self.course_cache_from_db
  {}.tap do |cache|
    Course.with_associated_models.find_each do |course|
      cache[course.name] = course
      cache[course.id.to_s] = course
    end
  end
end

def self.get_without_cache(id_or_name)
  # ...
  find_by = (id_or_name.to_i.to_s == id_or_name.to_s) ? :id : :name
  Course.with_associated_models.find_by(find_by => id_or_name)
end

def self.get_without_cache(id_or_name)
# a bit of trickery so we support both ids which are numbers and
# names which are strings that may contain numbers (eg. 2-3)
find_by = (id_or_name.to_i.to_s == id_or_name.to_s) ? :id : :name
Copy link
Contributor

Choose a reason for hiding this comment

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

might be cleaner to use regex instead? id_or_name =~ /^\d+$/ ? :id : :name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was copied from what's used in Script, which seems to have been working for us. I'd be semi-worried about introducing a regression trying to derive an equivalent regex.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm ok leaving this alone now, and revisiting when (if) we clean this up and consolidate the caching logic. I can help with that effort.

FWIW, we use this same regex for a similar purpose in other places.

@Bjvanminnen
Copy link
Contributor Author

The test that keeps failing for me (level_group.feature) passes pretty consistently on my local host. In addition, it's one that used to be marked @no_circle due to flakiness on circle. Josh just remarked it and a couple others as no_circle. I'm going to give this circle another attempt, but unless something new pops up would like to merge. Anyone willing to give me an approve? :)

Copy link
Contributor

@ashercodeorg ashercodeorg left a comment

Choose a reason for hiding this comment

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

SGTM.

@Bjvanminnen
Copy link
Contributor Author

Only failure was in weblab_submittable. This was disabled until recently on circle because of known flakiness, and passes for me locally, so I'm going to merge this PR.

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.

None yet

4 participants