The Entry model does provide one other feature which I think also warrants 
keeping it around and which is even potentially more valuable than the 
race_condition_ttl. It allows the cache to store nil values.

If you have a statement like this:

  record = Rails.cache.fetch("my_cache_key"){ 
Model.expensive_query_that_returns_nil }

With the current implementation of ActiveSupport::Cache the query results 
can be cached (because the nil result is wrapped in an Entry object). This 
was not possible with the previous (Rails 2.x) version that did have the 
Entry class. It is also not possible in the current Dalli cache store 
implementation (which does away with the Entry model). With dalli, the 
above code will execute the expensive query every time.

That being said, there is certainly room for improving the efficiency of 
the Entry model and since it is something called frequently and in 
performance critical section of code, I think it is certainly warranted.

I've code up an optimized version of it 
here: 
https://github.com/bdurand/rails/commit/e78c30100f54ae4366fdb9352987bae9b8c1c1e7.

I've run some tests on performance of this improved code over the previous 
implementation as well as just using raw strings.

  value = [4952 byte string]

Test commands:


   1. Marshaling overhead in bytes: 
   Marshal.dump(ActiveSupport::Cache::Entry.new(value)).bytesize - 
   value.bytesize
   2. Milliseconds to create and serialize entry: t = Time.now; 
   100000.times{Marshal.dump(ActiveSupport::Cache::Entry.new(value))}; 
   ((Time.now - t) / 100).round(3)
   3. Milliseconds to deserialize entry and retrieve its value: t = 
   Time.now; 100000.times{e = Marshal.load(serialized_value); e.value unless 
   e.expired?}; ((Time.now - t) / 100).round(3)

Raw String (i.e. run the test code without wrapping value in the Entry 
class):


   1. 12 bytes
   2. 0.006 ms
   3. 0.005 ms

Old Implementation:


   1. 121 bytes
   2. 0.020 ms
   3. 0.016 ms
   
New Implementation:


   1. 58 bytes
   2. 0.009 ms
   3. 0.008 ms
   
The optimized implementation still adds a slight "tax" on all calls to the 
cache, but it's a 2x improvement over the previous implementation. The tax 
with the improved code is 46 bytes, and 0.003 ms increase in time to read 
and write entries so even a request reading 300 entries from the cache has 
a tax of less than 1ms.

String is pretty well optimized in Ruby, so I also ran the same series of 
tests using a more complex custom Ruby object with 11 internal instance 
variables (similar to an ActiveRecord model). The tax remained the same, 
but was far less significant compared to the overall resources needed to 
serialize and deserialize the more complex object (0.026 ms without Entry, 
0.030 ms with Entry).


On Thursday, September 20, 2012 8:40:20 AM UTC-7, Xavier Noria wrote:
>
> As you probably know, the builtin memcached store wraps values in 
> instances of ActiveSupport::Cache::Entry. I am studying whether we could 
> get rid of it and your input would be valuable in helping us take a 
> decision.
>
> In addition to the cached value, these entries store the creation 
> timestamp, TTL, and compression flag.
>
> Rails 4 uses Dalli for the builtin memcached cache store, and Dalli 
> supports compression. It uses a flag to mark the stored value. We do not 
> need to handle compression by hand in AS with Dalli.
>
> The creation and TTL values are there because albeit memcached handles 
> expiration, Active Support is doing a first expiration pass on the values: 
> on writing (in 3-2-stable, but should be also on master) Active Support 
> adds 5 minutes to the TTL set by the user. Then when an entry is read, 
> there is code common to all stores that checks whether the entry expired, 
> and if so deletes manually the entry.
>
> If you read the entry in that 5 minute window, expiration is handled by 
> AS, otherwise, it is handled by memcached (5 minutes later than the user 
> specified).
>
> That manual management of expiration makes sense if the store does not 
> support expiration (like the file store). What's the point in the memcached 
> store? We need to support :race_condition_ttl.
>
> :race_condition_ttl is an optional TTL you may pass in a fetch call that 
> goes like this:
>
> 1) If we read the entry and according to the user it has expired, then 
> reset the TTL to race_condition_ttl to have everyone else fetch the "stale" 
> entry while this process computes the new fresh value.
>
> 2) When the value is computed, store it with the normal TTL (+ 5 minutes), 
> and return it.
>
> This feature was introduced as a way to avoid the dog pile effect in heavy 
> loaded websites that result from too many processes trying to get the fresh 
> value when said value is expensive to compute. This can indeed be a problem.
>
> Memcached does not give you the TTL of a key, therefore we need this 
> timestamp to support this feature.
>
> Question is, how many people need this? I wonder if the trade-off 
> justifies taxing *all* users. There are suboptimal techniques that may help 
> sites up to certain sizes that involve a second key that acts as an 
> advisory lock or something like that. And this complete solution could be a 
> cache store implemented by someone else in a separate plugin, if you 
> totally need this :race_condition_ttl.
>
> So, the question is, do you guys use this option? Thoughts?
>
>

-- 
You received this message because you are subscribed to the Google Groups "Ruby 
on Rails: Core" group.
To view this discussion on the web visit 
https://groups.google.com/d/msg/rubyonrails-core/-/KLcC8mOSkvgJ.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/rubyonrails-core?hl=en.

Reply via email to