简体   繁体   中英

Handling task counts while maintaining performance

I know this is a somewhat application specific question, but I wanted to see if someone has a clever solution I haven't thought about. This is a tough one, so major points (and props) to anyone that can elegantly solve this.

I have a ruby on rails app with a global navigation that contains task counts (see image below)

具有任务计数的全球导航

The problem is that generating these counts requires some intense queries. And because its global, it's on EVERY page. They also need to update as people take care of tasks. This makes it especially challenging.

That said, right now I'm caching the nav HTML for 10 minutes via memcached. If someone takes care of a task, oh well. The nav gets updated every 10 minutes. Terrible solution, I know, but it's my stop gap measure until I come up with a proper solution.

I want to avoid adding a bunch of hooks all over application (ex: a delivery is processed, a hook is triggered that decreases the count for each relevant user). Keep in mind, these counts deal with a number of entities in the system, and the logic is somewhat complex. It deals with access (each user has different counts depending on a rights system), and determining these counts can be complex in some situations. Hooks would get messy real fast, and the logic behind generating these counts would be duplicated.

Any help would be greatly appreciated.

If adding additional code for auto incrementing/decrementing the counters is ruled out, one of the solutions is to use cache and use it effectively. Pseudocode below.

class User

  def counters
    needs_update(user_id)
    memcache.read(counter_user_id)
  end

  def needs_update(user_id)
      some_job_queue_for_counters.insert(Job.new(User.update_counters(user_id)))
  end

  #executed by the job runner
  def update_counters user_id
     counter_hash = {:counter1 => complex_logic1, :counter2 => complex_logic2 ...}
     memcache.write(counter_user_id, counter_hash.to_json.gzipped)
  end

end

A couple of points to note.

  1. Make an AJAX call from the browser, say once every 1 minute or more depending on the time to execute counter logic.
  2. The counters method is as fast as it can get to, because the memcache.read() return a compressed json string. Zero processing to generate the response.
  3. You can warm up the memcache for all users once at the start of the application. Then after that counter update happens only for users that call the couters method, ie, for the users that kept the browser/mobile app open.
  4. needs_update method can do job coalescing for user_id to avoid duplicates.

Update:

The simple solution would be to delete all cached navigation html for all affected users of a change. You could also load the navHTML via an ajax call so that you don't have to wait for that to load for the page to load (and keep a cached version of the navHTML around in localStorage on the client and whenever the page loads you can then trigger a reset of the navHTML if needed).

A more complex solution would be:

Cache this in 2 layers (navHTML and then individual items/users/status items). You'll have to find a way to scope updates so that they know what counts they should trigger a reset of. So if you change the status of a delivery in a state machine, you should reset all counters for that delivery, and delete the nav cache for affected users.

I'd put all the keys in for each delivery item in cache in some namespaced manner, like "delivery:234324-user:123-status", so whenever Delivery with id #234324 changes, you iterate and delete all cache items that belong to that delivery (probably best to keep it as dumb as possible). You can calculate and store the counts to cache when somebody requests a page and just do the cache deletes when the actual object changes. You can then fully cache the nav html, but whenever something changes that has your user and id in it (like user-123) you delete the nav cache as well forcing navHTML to reload. Add to that some kind of cache time for the nav as well (like max age 1 min or something) to account for errors. The nav item should build via the cache of individual items that it doesn't have to go through each item.

You'll probably want to add Cache Sweeper connected to your associated models http://guides.rubyonrails.org/caching_with_rails.html


Here's counter cache for reference (I think building the database model to allow for use of counter cache is the best way to deal with this).

http://railscasts.com/episodes/23-counter-cache-column

It's possible to either use it automagically with a has_many relation or to manually update counts. I assume that if your model is complex and you have a many-to-many relation that you might put the counter caches on a relational object.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM