| /* |
| * Copyright 2013 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // Author: pulkitg@google.com (Pulkit Goyal) |
| |
| #include "pagespeed/opt/http/cache_property_store.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/logging.h" |
| #include "pagespeed/kernel/base/abstract_mutex.h" |
| #include "pagespeed/kernel/base/cache_interface.h" |
| #include "pagespeed/kernel/base/proto_util.h" |
| #include "pagespeed/kernel/base/scoped_ptr.h" |
| #include "pagespeed/kernel/base/shared_string.h" |
| #include "pagespeed/kernel/base/stl_util.h" |
| #include "pagespeed/kernel/base/thread_system.h" |
| #include "pagespeed/kernel/cache/cache_stats.h" |
| #include "pagespeed/opt/http/property_cache.pb.h" |
| #include "pagespeed/opt/logging/log_record.h" |
| |
| namespace net_instaweb { |
| |
| // Property cache key prefixes. |
| const char CachePropertyStore::kPagePropertyCacheKeyPrefix[] = "prop_page/"; |
| |
| CachePropertyStore::CachePropertyStore(const GoogleString& cache_key_prefix, |
| CacheInterface* cache, |
| Timer* timer, |
| Statistics* stats, |
| ThreadSystem* thread_system) |
| : cache_key_prefix_(cache_key_prefix), |
| default_cache_(cache), |
| timer_(timer), |
| stats_(stats), |
| thread_system_(thread_system) { |
| } |
| |
| CachePropertyStore::~CachePropertyStore() { |
| STLDeleteValues(&cohort_cache_map_); |
| } |
| |
| namespace { |
| |
| class CachePropertyStoreGetCallback : public PropertyStoreGetCallback { |
| public: |
| CachePropertyStoreGetCallback( |
| AbstractMutex* mutex, |
| PropertyPage* page, |
| bool is_cancellable, |
| BoolCallback* done, |
| Timer* timer) |
| : PropertyStoreGetCallback( |
| mutex, page, is_cancellable, done, timer) { |
| } |
| virtual ~CachePropertyStoreGetCallback() { |
| } |
| |
| void SetStateInPropertyPage( |
| const PropertyCache::Cohort* cohort, |
| CacheInterface::KeyState state, |
| bool valid) { |
| ScopedMutex lock(mutex()); |
| if (page() == NULL) { |
| return; |
| } |
| page()->log_record()->SetCacheStatusForCohortInfo( |
| page()->page_type(), |
| cohort->name(), |
| valid, |
| state); |
| page()->SetCacheState(cohort, state); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(CachePropertyStoreGetCallback); |
| }; |
| |
| // Tracks multiple cache lookups. When they are all complete, page->Done() is |
| // called. |
| // |
| // TODO(pulkitg): Use CacheInterface::MultiGet() instead of using |
| // CacheInterface::Get() for each cohort. |
| class CachePropertyStoreCallbackCollector { |
| public: |
| CachePropertyStoreCallbackCollector( |
| CachePropertyStoreGetCallback* property_store_callback, |
| int num_pending, |
| AbstractMutex* mutex) |
| : property_store_callback_(property_store_callback), |
| pending_(num_pending), |
| success_(false), |
| mutex_(mutex) { |
| } |
| |
| void Done(bool success) { |
| { |
| ScopedMutex lock(mutex_.get()); |
| success_ |= success; // Declare victory a if *any* lookups completed. |
| --pending_; |
| if (pending_ > 0) { |
| return; |
| } |
| } |
| property_store_callback_->Done(success_); |
| delete this; |
| } |
| |
| private: |
| CachePropertyStoreGetCallback* property_store_callback_; |
| int pending_; |
| bool success_; |
| scoped_ptr<AbstractMutex> mutex_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CachePropertyStoreCallbackCollector); |
| }; |
| |
| // Helper class to receive low-level cache callbacks, decode them |
| // as properties. |
| class CachePropertyStoreCacheCallback : public CacheInterface::Callback { |
| public: |
| CachePropertyStoreCacheCallback( |
| const PropertyCache::Cohort* cohort, |
| CachePropertyStoreGetCallback* property_store_callback, |
| CachePropertyStoreCallbackCollector* callback_collector) |
| : cohort_(cohort), |
| property_store_callback_(property_store_callback), |
| callback_collector_(callback_collector) { |
| } |
| virtual ~CachePropertyStoreCacheCallback() {} |
| |
| virtual void Done(CacheInterface::KeyState state) { |
| bool valid = false; |
| if (state == CacheInterface::kAvailable) { |
| StringPiece value_string = value()->Value(); |
| ArrayInputStream input(value_string.data(), value_string.size()); |
| PropertyCacheValues values; |
| if (values.ParseFromZeroCopyStream(&input)) { |
| int64 min_write_timestamp_ms = kint64max; |
| // The values in a cohort could have different write_timestamp_ms |
| // values, since it is populated in UpdateValue. But since all values |
| // in a cohort are written (and read) together we need to treat either |
| // all as valid or none as valid. Hence we look at the oldest write |
| // timestamp to make this decision. |
| for (int i = 0; i < values.value_size(); ++i) { |
| min_write_timestamp_ms = std::min( |
| min_write_timestamp_ms, values.value(i).write_timestamp_ms()); |
| } |
| // Return valid for empty cohort, and if IsCacheValid returns true for |
| // Value with oldest timestamp. |
| if (values.value_size() == 0) { |
| valid = true; |
| } else { |
| for (int i = 0; i < values.value_size(); ++i) { |
| const PropertyValueProtobuf& pcache_value = values.value(i); |
| valid = property_store_callback_-> |
| AddPropertyValueProtobufToPropertyPage( |
| cohort_, pcache_value, min_write_timestamp_ms); |
| } |
| } |
| } |
| } |
| property_store_callback_->SetStateInPropertyPage(cohort_, state, valid); |
| callback_collector_->Done(valid); |
| delete this; |
| } |
| |
| private: |
| const PropertyCache::Cohort* cohort_; |
| CachePropertyStoreGetCallback* property_store_callback_; |
| CachePropertyStoreCallbackCollector* callback_collector_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CachePropertyStoreCacheCallback); |
| }; |
| |
| } // namespace |
| |
| GoogleString CachePropertyStore::CacheKey( |
| const StringPiece& url, |
| const StringPiece& options_signature_hash, |
| const StringPiece& cache_key_suffix, |
| const PropertyCache::Cohort* cohort) const { |
| return StrCat( |
| cache_key_prefix_, |
| url, "_", |
| options_signature_hash, |
| cache_key_suffix, "@", |
| cohort->name()); |
| } |
| |
| void CachePropertyStore::Get( |
| const GoogleString& url, |
| const GoogleString& options_signature_hash, |
| const GoogleString& cache_key_suffix, |
| const PropertyCache::CohortVector& cohort_list, |
| PropertyPage* page, |
| BoolCallback* done, |
| AbstractPropertyStoreGetCallback** callback) { |
| if (cohort_list.empty()) { |
| *callback = NULL; |
| done->Run(true); |
| return; |
| } |
| CachePropertyStoreGetCallback* property_store_get_callback = |
| new CachePropertyStoreGetCallback( |
| thread_system_->NewMutex(), |
| page, |
| enable_get_cancellation(), |
| done, |
| timer_); |
| *callback = property_store_get_callback; |
| CachePropertyStoreCallbackCollector* collector = |
| new CachePropertyStoreCallbackCollector( |
| property_store_get_callback, |
| cohort_list.size(), |
| thread_system_->NewMutex()); |
| for (int j = 0, n = cohort_list.size(); j < n; ++j) { |
| const PropertyCache::Cohort* cohort = cohort_list[j]; |
| CohortCacheMap::iterator cohort_itr = |
| cohort_cache_map_.find(cohort->name()); |
| CHECK(cohort_itr != cohort_cache_map_.end()); |
| const GoogleString cache_key = CacheKey( |
| url, options_signature_hash, cache_key_suffix, cohort); |
| cohort_itr->second->Get( |
| cache_key, |
| new CachePropertyStoreCacheCallback( |
| cohort, property_store_get_callback, collector)); |
| } |
| } |
| |
| void CachePropertyStore::Put(const GoogleString& url, |
| const GoogleString& options_signature_hash, |
| const GoogleString& cache_key_suffix, |
| const PropertyCache::Cohort* cohort, |
| const PropertyCacheValues* values, |
| BoolCallback* done) { |
| GoogleString value; |
| StringOutputStream sstream(&value); |
| values->SerializeToZeroCopyStream(&sstream); |
| CohortCacheMap::iterator cohort_itr = cohort_cache_map_.find(cohort->name()); |
| CHECK(cohort_itr != cohort_cache_map_.end()); |
| const GoogleString cache_key = CacheKey( |
| url, options_signature_hash, cache_key_suffix, cohort); |
| cohort_itr->second->PutSwappingString(cache_key, &value); |
| if (done != NULL) { |
| done->Run(true); |
| } |
| } |
| |
| void CachePropertyStore::AddCohort(const GoogleString& cohort) { |
| AddCohortWithCache(cohort, default_cache_); |
| } |
| |
| void CachePropertyStore::AddCohortWithCache( |
| const GoogleString& cohort, CacheInterface* cache) { |
| std::pair<CohortCacheMap::iterator, bool> insertions = |
| cohort_cache_map_.insert( |
| make_pair(cohort, static_cast<CacheInterface*>(NULL))); |
| CHECK(insertions.second) << cohort << " is added twice."; |
| // Create a new CacheStats for every cohort so that we can track cache |
| // statistics independently for every cohort. |
| CacheInterface* cache_stats = new CacheStats( |
| PropertyCache::GetStatsPrefix(cohort), cache, timer_, stats_); |
| insertions.first->second = cache_stats; |
| } |
| |
| GoogleString CachePropertyStore::Name() const { |
| GoogleString out; |
| for (CohortCacheMap::const_iterator p = cohort_cache_map_.begin(), |
| e = cohort_cache_map_.end(); p != e; ++p) { |
| StrAppend(&out, |
| out.empty() ? "" : "\n", |
| p->first, ":", p->second->Name()); |
| } |
| return out; |
| } |
| |
| GoogleString CachePropertyStore::FormatName2(StringPiece cohort_name1, |
| StringPiece cohort_cache1, |
| StringPiece cohort_name2, |
| StringPiece cohort_cache2) { |
| return StrCat(cohort_name1, ":", cohort_cache1, |
| "\n", |
| cohort_name2, ":", cohort_cache2); |
| } |
| |
| } // namespace net_instaweb |