Skip to content

Commit 8e88206

Browse files
committed
Merge branch 'with_cache_rework'
2 parents 3a223f1 + a71b548 commit 8e88206

33 files changed

+1142
-87
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ OpenConferenceWare stable releases and changes included, with latest at top:
55

66
* Next
77
- FIXED Event, added uniqueness validations to slug and title fields.
8+
- FIXED `rake clear` to expire the compiled javascripts.
9+
- FIXED cache consistency, rollbacks now cause expirations so that cache doesn't store stale data.
10+
- Improved performance of shared fragments, they're now only regenerated as needed and use fast preloaded data.
11+
- Improved performance of user favorites, they now only load the proposal ids for more efficient transfers.
812
- Improved models, added cascade deletes to avoid leaving orphaned records after parents are deleted.
913
- Improved "README", described the various custom environmental variables that affect the application's behavior.
1014
- Improved performance of event loading code by checking identifiers rather than loading entire objects.

app/controllers/user_favorites_controller.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ def index
1414
@user_favorites = Defer {
1515
view_cache_key = "favorites,user_#{@user.id}.#{request.format},join_#{params[:join]}"
1616
Rails.cache.fetch_object(view_cache_key) {
17-
# The :join argument, used by the AJAX, causes action to return UserFavorite records, rather than Proposal records.
17+
# The :join argument is sent by the AJAX UI to fetch a terse list of
18+
# proposal_ids so it can display stars next to favorited proposals.
1819
if params[:join] == "1"
19-
UserFavorite.find_all_by_user_id(@user.id)
20+
UserFavorite.proposal_ids_for(@user)
2021
else
2122
@user.favorites.populated
2223
end

app/helpers/shared_fragment_helper.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
# You can render the fragments from the command-line by running:
1010
# ./script/runner "SharedFragmentHelper.render_shared_fragments"
1111
module SharedFragmentHelper
12-
# Provides the TestRequest and TestRequest objects for ::render_theme_header_to_string.
12+
# Provides the TestRequest and TestRequest objects for ::render_theme_header_to_file.
1313
require 'action_controller/test_process'
1414

15+
# Should the shared fragments be rendered? Defaults to true.
16+
mattr_accessor :enabled
17+
@@enabled = true
18+
1519
# Render all shared fragments to files.
1620
def self.render_shared_fragments
1721
Rails.logger.info("SharedFragmentHelper: rendering all shared fragments to files at #{self.shared_fragments_dir}")
@@ -22,7 +26,7 @@ def self.render_shared_fragments
2226
# Render the theme headers for all events in the database to files.
2327
def self.render_theme_headers_to_files
2428
self.render_theme_header_to_file # current
25-
for event in Event.all
29+
for event in Event.lookup
2630
self.render_theme_header_to_file(event)
2731
end
2832
return true
@@ -31,6 +35,11 @@ def self.render_theme_headers_to_files
3135
# Render the theme's header to a file for the given +event+. If no event was
3236
# specified, will render the current event.
3337
def self.render_theme_header_to_file(event=nil)
38+
unless self.enabled
39+
Rails.logger.info("SharedFragmentHelper: not rendering because disabled")
40+
return false
41+
end
42+
3443
File.open(self.shared_fragment_path_for_header(event), 'w+') do |handle|
3544
handle.write(self.render_theme_header_to_string(event))
3645
end
@@ -81,7 +90,7 @@ def self.render_theme_header_to_string(event=nil)
8190
# Accept given object
8291
when String, Symbol, Fixnum
8392
# Lookup by slug
84-
event = Event.find_by_slug(event.to_s)
93+
event = Event.lookup(event.to_s)
8594
else
8695
# Use current event
8796
event = Event.current

app/mixins/cache_lookups_mixin.rb

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ def self.included(mixee)
3535
end
3636

3737
module ClassMethods
38+
# Should lookups be cached? If the "perform_caching" Rails environment
39+
# configuration setting is enabled, default to using cache.
40+
#
41+
# You can force caching on or off using the CACHELOOKUPS environmental
42+
# variable, e.g. activate with:
43+
#
44+
# CACHELOOKUPS=1 ./script/server
45+
def cache_lookups?
46+
if Rails.configuration.action_controller.perform_caching
47+
return ENV['CACHELOOKUPS'] != '0'
48+
else
49+
return ENV['CACHELOOKUPS'] == '1'
50+
end
51+
end
52+
3853
# Setup the lookup caching system.
3954
#
4055
# Arguments:
@@ -50,32 +65,40 @@ def lookup_silo_name
5065
return "#{self.name.gsub('::', '__')}_dict"
5166
end
5267

68+
def query_one(key)
69+
return self.find(:first, :conditions => {self.lookup_key => key})
70+
end
71+
72+
def query_all
73+
return self.all(self.lookup_opts)
74+
end
75+
5376
# Return instance from cache matching +key+. If +key+ is undefined, returns
5477
# array of all instances.
5578
def lookup(key=nil)
56-
### Bypass lookup caching if the following global is true:
57-
if $cache_lookup_mixin_disable
58-
if key
59-
return self.find(:first, :conditions => {self.lookup_key => key})
60-
else
61-
return self.find(:all, self.lookup_opts)
62-
end
79+
unless self.cache_lookups?
80+
return key ?
81+
self.query_one(key) :
82+
self.query_all
6383
end
6484

6585
silo = self.lookup_silo_name
66-
dict = nil
6786
ActiveRecord::Base.benchmark("Lookup: #{silo}#{key.ergo{'#'+to_s}}") do
6887
dict = self.fetch_object(silo){
6988
# FIXME Exceptions within this block are silently swallowed by something. This is bad.
70-
self.find(:all, self.lookup_opts).inject(Dictionary.new){|s,v| s[v.send(self.lookup_key)] = v; s}
89+
dict = Dictionary.new
90+
for record in self.query_all
91+
dict[record.send(self.lookup_key)] = record
92+
end
93+
dict
7194
}
95+
return key ? dict[key.kind_of?(Symbol) ? key.to_s : key] : dict.values
7296
end
73-
return key ? dict[key.with{kind_of?(Symbol) ? to_s : self}] : (dict.values || [])
7497
end
7598

7699
def expire_cache
77100
Rails.logger.info("Lookup, expiring: #{self.name}")
78-
Observist.expire(/#{lookup_silo_name}_.+/)
101+
CacheWatcher.expire(/#{lookup_silo_name}_.+/)
79102
end
80103

81104
def fetch_object(silo, &block)

app/models/event.rb

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,14 @@ def current?
7878
return self == Event.current
7979
end
8080

81-
EVENT_CURRENT_CACHE_KEY = "event_current"
82-
8381
# Return the current Event. Determines which event to return by checking to
8482
# see if a snippet says which is current, else tries to return the event
8583
# with the latest deadline, else returns a nil.
8684
def self.current
87-
return self.fetch_object(EVENT_CURRENT_CACHE_KEY) do
88-
if record = (self.current_by_settings || self.current_by_deadline)
89-
self.lookup(record.slug)
90-
else
91-
nil
92-
end
93-
end
85+
query = lambda { self.current_by_settings || self.current_by_deadline }
86+
return self.cache_lookups? ?
87+
self.fetch_object('event_current', &query) :
88+
query.call
9489
end
9590

9691
# Return current event by finding it by deadline.
@@ -112,24 +107,6 @@ def self.current_by_settings
112107
end
113108
end
114109

115-
# Delete the current cached event if it's present
116-
def self.expire_current
117-
RAILS_CACHE.delete(EVENT_CURRENT_CACHE_KEY)
118-
end
119-
120-
# Override CacheLookupsMixin to expire more
121-
def self.expire_cache
122-
self.expire_current
123-
super
124-
end
125-
126-
# Returns cached array of proposals for this event.
127-
def lookup_proposals
128-
self.fetch_object("proposals_for_event_#{self.id}") do
129-
self.proposals
130-
end
131-
end
132-
133110
# Return an array of this Event's Proposals with their Tracks for use by proposals#stats.
134111
def proposals_for_stats
135112
return self.proposals.find(

app/models/proposal.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def add_user(user)
288288
if self.users.include?(user)
289289
return nil
290290
else
291-
Observist.expire
291+
CacheWatcher.expire
292292
self.users << user
293293
return user
294294
end
@@ -299,7 +299,7 @@ def remove_user(user)
299299
user = User.get(user)
300300

301301
if self.users.include?(user)
302-
Observist.expire
302+
CacheWatcher.expire
303303
self.users.delete(user)
304304
return user
305305
else

app/models/user_favorite.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,8 @@ def self.remove(user_id, proposal_id)
3333
end
3434
end
3535

36+
# Return the ids of this +user+'s favorite proposals.
37+
def self.proposal_ids_for(user)
38+
return self.find_all_by_user_id(user.id, :select => 'proposal_id').map(&:proposal_id)
39+
end
3640
end
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
class Observist < ActiveRecord::Observer
1+
# = CacheWatcher
2+
#
3+
# Watches for changes in models and expires the cache.
4+
class CacheWatcher < ActiveRecord::Observer
5+
# Watch for changes in these classes:
26
observe \
37
Comment,
48
Event,
@@ -12,25 +16,25 @@ class Observist < ActiveRecord::Observer
1216
User,
1317
UserFavorite
1418

19+
# Expire the cache
1520
def self.expire(*args)
16-
pattern = args.first || //
17-
Rails.logger.info("Observist: expiring cache")
21+
Rails.logger.info("CacheWatcher: expiring cache")
1822
case Rails.cache
1923
when ActiveSupport::Cache::MemCacheStore
2024
Rails.cache.instance_variable_get(:@data).flush_all
2125
when ActiveSupport::Cache::FileStore
22-
Rails.cache.delete_matched(pattern) rescue nil
26+
Rails.cache.delete_matched(//) rescue nil
2327
else
2428
raise NotImplementedError, "Don't know how to expire cache: #{Rails.cache.class.name}"
2529
end
26-
27-
SharedFragmentHelper.render_shared_fragments
2830
end
2931

3032
def expire(*args)
31-
self.class.expire
33+
self.class.expire(*args)
3234
end
3335

34-
alias_method :after_save, :expire
35-
alias_method :after_destroy, :expire
36+
# Expire the cache when these triggers are called on a record
37+
alias_method :after_save, :expire
38+
alias_method :after_destroy, :expire
39+
alias_method :after_rollback, :expire
3640
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# = SharedFragmentWatcher
2+
#
3+
# Watches for changes that will affect the shared fragments and renders
4+
# them as needed.
5+
class SharedFragmentWatcher < ActiveRecord::Observer
6+
# Watch for changes in these classes:
7+
observe :event
8+
9+
# Create or update the shared fragments
10+
def self.render(*args)
11+
Rails.logger.info("SharedFragmentWatcher: rendering shared fragments")
12+
SharedFragmentHelper.render_shared_fragments
13+
end
14+
15+
def render(*args)
16+
self.class.render(*args)
17+
end
18+
19+
# Render shared fragments when these triggers are called on a record
20+
alias_method :after_save, :render
21+
alias_method :after_destroy, :render
22+
alias_method :after_rollback, :render
23+
end

config/environment.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def load_with_announcer(*args)
7575

7676
# Activate observers that should always be running
7777
# config.active_record.observers = :cacher, :garbage_collector
78-
config.active_record.observers = :observist unless defined?(Rake::Task) && ARGV.find{|t| /^db:(create|drop)/}
78+
config.active_record.observers = [:cache_watcher, :shared_fragment_watcher] unless defined?(Rake::Task) && ARGV.find{|t| /^db:(create|drop)/}
7979
# TODO Remove the above "unless" after migrating to Rails 2.3 or above. It's needed by older versions to avoid a dependency loop where creating the database requires loading the observers which load the models which have plugins that try to talk to the database.
8080

8181
# Make Active Record use UTC-base instead of local time

0 commit comments

Comments
 (0)