Cache: Introduce soft and hard TTL

- Hard TTL; the maximum time a value will be used from the cache.
- Soft TLL; the time after which the cache value should be updated
  from the server.

So as not to unnecessarily fetch the same value multiple times, when
the soft TTL has been reached, the value will be updated for the first
client, while all other clients will use the stale value until it has
become updated.

With different soft and hard TTLs there is a definite upper bound for
how old a value can be used.
This commit is contained in:
Johan Wikman
2016-12-21 14:11:31 +02:00
parent 97fcb94daa
commit c7dfd1b0bd
14 changed files with 208 additions and 75 deletions

View File

@ -13,7 +13,8 @@ existing service.
[Cache]
type=filter
module=cache
ttl=5
hard_ttl=30
soft_ttl=20
storage=...
storage_options=...
rules=...
@ -59,6 +60,34 @@ depend upon the specific module. For instance,
storage_options=storage_specific_option1=value1,storage_specific_option2=value2
```
#### `hard_ttl`
_Hard time to live_; the maximum amount of time - in seconds - the cached
result is used before it is discarded and the result is fetched from the
backend (and cached). See also `soft_ttl` below.
```
hard_ttl=60
```
The default value is `0`, which means no limit.
#### `soft_ttl`
_Soft time to live_; the amount of time - in seconds - the cached result is
used before it is refreshed from the server. When `soft_ttl` has passed, the
result will be refreshed when the _first_ client requests the value.
However, as long as `hard_ttl` has not passed, _all_ other clients requesting
the same value will use the result from the cache while it is being fetched
from the backend. That is, as long as `soft_ttl` but not `hard_ttl` has passed,
even if several clients request the same value at the same time, there will be
just one request to the backend.
```
soft_ttl=60
```
The default value is `0`, which means no limit. If the value of `soft_ttl` is
larger than `hard_ttl` it will be adjusted down to the same value.
#### `max_resultset_rows`
Specifies the maximum number of rows a resultset can have in order to be
@ -78,16 +107,6 @@ max_resultset_size=128
```
The default value is `0`, which means no limit.
#### `ttl`
_Time to live_; the amount of time - in seconds - the cached result is used
before it is refreshed from the server.
```
ttl=60
```
The default value is `0`, which means no limit.
#### `max_count`
The maximum number of items the cache may contain. If the limit has been

View File

@ -114,11 +114,20 @@ typedef struct cache_storage_config_t
cache_thread_model_t thread_model;
/**
* Time to live; number of seconds the value is valid. A value of 0 means
* Hard Time-to-live; number of seconds the value is valid. A value of 0 means
* that there is no time-to-live, but that the value is considered fresh
* as long as it is available.
*/
uint32_t ttl;
uint32_t hard_ttl;
/**
* Soft Time-to-live; number of seconds the value is valid. A value of 0 means
* that there is no time-to-live, but that the value is considered fresh
* as long as it is available. When the soft TTL has passed, but the hard TTL
* has not yet been reached, the stale cached value will be returned, provided
* the flag @c CACHE_FLAGS_INCLUDE_STALE is specified when getting the value.
*/
uint32_t soft_ttl;
/**
* The maximum number of items the storage may store, before it should

View File

@ -72,20 +72,23 @@ class CacheStorageConfig : public CACHE_STORAGE_CONFIG
{
public:
CacheStorageConfig(cache_thread_model_t thread_model,
uint32_t ttl = 0,
uint32_t hard_ttl = 0,
uint32_t soft_ttl = 0,
uint32_t max_count = 0,
uint64_t max_size = 0)
{
this->thread_model = thread_model;
this->ttl = ttl;
this->hard_ttl = hard_ttl;
this->soft_ttl = soft_ttl;
this->max_count = max_count;
this->max_size = max_count;
this->max_size = max_size;
}
CacheStorageConfig()
{
thread_model = CACHE_THREAD_MODEL_MT;
ttl = 0;
hard_ttl = 0;
soft_ttl = 0;
max_count = 0;
max_size = 0;
}
@ -93,7 +96,8 @@ public:
CacheStorageConfig(const CACHE_STORAGE_CONFIG& config)
{
thread_model = config.thread_model;
ttl = config.ttl;
hard_ttl = config.hard_ttl;
soft_ttl = config.soft_ttl;
max_count = config.max_count;
max_size = config.max_size;
}

View File

@ -31,12 +31,13 @@ static const CACHE_CONFIG DEFAULT_CONFIG =
{
CACHE_DEFAULT_MAX_RESULTSET_ROWS,
CACHE_DEFAULT_MAX_RESULTSET_SIZE,
NULL,
NULL,
NULL,
NULL,
0,
CACHE_DEFAULT_TTL,
NULL, // rules
NULL, // storage
NULL, // storage_options
NULL, // storage_argv
0, // storage_argc
CACHE_DEFAULT_HARD_TTL,
CACHE_DEFAULT_SOFT_TTL,
CACHE_DEFAULT_MAX_COUNT,
CACHE_DEFAULT_MAX_SIZE,
CACHE_DEFAULT_DEBUG,
@ -62,7 +63,8 @@ void cache_config_finish(CACHE_CONFIG& config)
config.storage_options = NULL;
config.storage_argc = 0;
config.storage_argv = NULL;
config.ttl = 0;
config.hard_ttl = 0;
config.soft_ttl = 0;
config.debug = 0;
}
@ -403,9 +405,16 @@ bool CacheFilter::process_params(char **pzOptions, FILTER_PARAMETER **ppParams,
error = true;
}
}
else if (strcmp(pParam->name, "ttl") == 0)
else if (strcmp(pParam->name, "hard_ttl") == 0)
{
if (!config_get_uint32(*pParam, &config.ttl))
if (!config_get_uint32(*pParam, &config.hard_ttl))
{
error = true;
}
}
else if (strcmp(pParam->name, "soft_ttl") == 0)
{
if (!config_get_uint32(*pParam, &config.soft_ttl))
{
error = true;
}
@ -470,6 +479,13 @@ bool CacheFilter::process_params(char **pzOptions, FILTER_PARAMETER **ppParams,
if (!error)
{
if (config.soft_ttl > config.hard_ttl)
{
MXS_WARNING("The value of 'soft_ttl' must be less than or equal to that of 'hard_ttl'. "
"Setting 'soft_ttl' to the same value as 'hard_ttl'.");
config.soft_ttl = config.hard_ttl;
}
if (config.max_size < config.max_resultset_size)
{
MXS_ERROR("The value of 'max_size' must be at least as larged as that "

View File

@ -43,7 +43,9 @@
// Bytes
#define CACHE_DEFAULT_MAX_RESULTSET_SIZE 0
// Seconds
#define CACHE_DEFAULT_TTL 0
#define CACHE_DEFAULT_HARD_TTL 0
// Seconds
#define CACHE_DEFAULT_SOFT_TTL 0
// Integer value
#define CACHE_DEFAULT_DEBUG 0
// Positive integer
@ -62,7 +64,8 @@ typedef struct cache_config
char* storage_options; /**< Raw options for storage module. */
char** storage_argv; /**< Cooked options for storage module. */
int storage_argc; /**< Number of cooked options. */
uint32_t ttl; /**< Time to live. */
uint32_t hard_ttl; /**< Hard time to live. */
uint32_t soft_ttl; /**< Soft time to live. */
uint64_t max_count; /**< Maximum number of entries in the cache.*/
uint64_t max_size; /**< Maximum size of the cache.*/
uint32_t debug; /**< Debug settings. */

View File

@ -85,7 +85,8 @@ CacheMT* CacheMT::Create(const std::string& name,
CacheMT* pCache = NULL;
CacheStorageConfig storage_config(CACHE_THREAD_MODEL_MT,
pConfig->ttl,
pConfig->hard_ttl,
pConfig->soft_ttl,
pConfig->max_count,
pConfig->max_size);

View File

@ -89,7 +89,9 @@ json_t* CacheSimple::do_get_info(uint32_t what) const
{
json_t* pStorageInfo;
if (m_pStorage->get_info(Storage::INFO_ALL, &pStorageInfo) == CACHE_RESULT_OK)
cache_result_t result = m_pStorage->get_info(Storage::INFO_ALL, &pStorageInfo);
if (CACHE_RESULT_IS_OK(result))
{
json_object_set(pInfo, "storage", pStorageInfo);
json_decref(pStorageInfo);

View File

@ -89,7 +89,8 @@ CacheST* CacheST::Create(const std::string& name,
CacheST* pCache = NULL;
CacheStorageConfig storage_config(CACHE_THREAD_MODEL_ST,
pConfig->ttl,
pConfig->hard_ttl,
pConfig->soft_ttl,
pConfig->max_count,
pConfig->max_size);

View File

@ -208,10 +208,15 @@ cache_result_t InMemoryStorage::do_get_value(const CACHE_KEY& key, uint32_t flag
uint32_t now = time(NULL);
bool is_stale = m_config.ttl == 0 ? false : (now - entry.time > m_config.ttl);
bool is_hard_stale = m_config.hard_ttl == 0 ? false : (now - entry.time > m_config.hard_ttl);
bool is_soft_stale = m_config.soft_ttl == 0 ? false : (now - entry.time > m_config.soft_ttl);
bool include_stale = ((flags & CACHE_FLAGS_INCLUDE_STALE) != 0);
if (!is_stale || include_stale)
if (is_hard_stale)
{
m_entries.erase(i);
}
else if (!is_soft_stale || include_stale)
{
size_t length = entry.value.size();
@ -223,7 +228,7 @@ cache_result_t InMemoryStorage::do_get_value(const CACHE_KEY& key, uint32_t flag
result = CACHE_RESULT_OK;
if (is_stale)
if (is_soft_stale)
{
result |= CACHE_RESULT_STALE;
}
@ -235,7 +240,7 @@ cache_result_t InMemoryStorage::do_get_value(const CACHE_KEY& key, uint32_t flag
}
else
{
ss_dassert(is_stale);
ss_dassert(is_soft_stale);
result |= CACHE_RESULT_STALE;
}
}

View File

@ -15,21 +15,14 @@
#include "rocksdbinternals.hh"
#include <rocksdb/env.h>
#include <util/coding.h>
#include <maxscale/debug.h>
/**
* Check whether a value is stale or not.
*
* @param value A value with the timestamp at the end.
* @param ttl The time-to-live in seconds.
* @param pEnv The used RocksDB environment instance.
*
* @return True of the value is stale.
*
* Basically a copy from RocksDB/utilities/ttl/db_ttl_impl.cc:160
* but note that the here we claim the data is stale if we fail to
* get the time while the original code claims it is fresh.
* The following is basically a copy from RocksDB/utilities/ttl/db_ttl_impl.cc:160
* but note that the here we claim the data is stale if we fail to get the time
* while the original code claims it is fresh.
*/
bool RocksDBInternals::IsStale(const rocksdb::Slice& value, int32_t ttl, rocksdb::Env* pEnv)
bool RocksDBInternals::is_stale(const rocksdb::Slice& value, int32_t ttl, rocksdb::Env* pEnv)
{
if (ttl <= 0)
{ // Data is fresh if TTL is non-positive
@ -45,3 +38,9 @@ bool RocksDBInternals::IsStale(const rocksdb::Slice& value, int32_t ttl, rocksdb
int32_t timestamp = rocksdb::DecodeFixed32(value.data() + value.size() - TS_LENGTH);
return (timestamp + ttl) < curtime;
}
int32_t RocksDBInternals::extract_timestamp(const rocksdb::Slice& value)
{
ss_dassert(value.size() >= TS_LENGTH);
return rocksdb::DecodeFixed32(value.data() + value.size() - TS_LENGTH);
}

View File

@ -32,6 +32,24 @@ namespace RocksDBInternals
*/
static const uint32_t TS_LENGTH = sizeof(int32_t);
bool IsStale(const rocksdb::Slice& slice, int32_t ttl, rocksdb::Env* pEnv);
/**
* Check whether a value is stale or not.
*
* @param value A value with the timestamp at the end.
* @param ttl The time-to-live in seconds.
* @param pEnv The used RocksDB environment instance.
*
* @return True of the value is stale.
*/
bool is_stale(const rocksdb::Slice& slice, int32_t ttl, rocksdb::Env* pEnv);
/**
* Extract the timestamp from a slice coming from a rocksdb::DBWithTTL.
*
* @param value The slice whose timestamp should be extracted.
*
* @return The timestamp
*/
int32_t extract_timestamp(const rocksdb::Slice& value);
}

View File

@ -320,7 +320,7 @@ RocksDBStorage* RocksDBStorage::Create(const char* zName,
rocksdb::Status status;
rocksdb::Slice key(STORAGE_ROCKSDB_VERSION_KEY);
status = rocksdb::DBWithTTL::Open(options, path, &pDb, config.ttl);
status = rocksdb::DBWithTTL::Open(options, path, &pDb, config.hard_ttl);
if (status.ok())
{
@ -464,10 +464,32 @@ cache_result_t RocksDBStorage::get_value(const CACHE_KEY& key, uint32_t flags, G
case rocksdb::Status::kOk:
if (value.length() >= RocksDBInternals::TS_LENGTH)
{
bool is_stale = RocksDBInternals::IsStale(value, m_config.ttl, rocksdb::Env::Default());
rocksdb::Env* pEnv = rocksdb::Env::Default();
int64_t now;
if (!pEnv->GetCurrentTime(&now).ok())
{
ss_dassert(!true);
now = INT64_MAX;
}
int32_t timestamp = RocksDBInternals::extract_timestamp(value);
bool is_hard_stale = m_config.hard_ttl == 0 ? false : (now - timestamp > m_config.hard_ttl);
bool is_soft_stale = m_config.soft_ttl == 0 ? false : (now - timestamp > m_config.soft_ttl);
bool include_stale = ((flags & CACHE_FLAGS_INCLUDE_STALE) != 0);
if (!is_stale || include_stale)
if (is_hard_stale)
{
status = m_sDb->Delete(Write_options(), rocksdb_key);
if (!status.ok())
{
MXS_WARNING("Failed when deleting stale item from RocksDB.");
}
result = CACHE_RESULT_NOT_FOUND;
}
else if (!is_soft_stale || include_stale)
{
size_t length = value.length() - RocksDBInternals::TS_LENGTH;
@ -479,15 +501,19 @@ cache_result_t RocksDBStorage::get_value(const CACHE_KEY& key, uint32_t flags, G
result = CACHE_RESULT_OK;
if (is_stale)
if (is_soft_stale)
{
result |= CACHE_RESULT_STALE;
}
}
else
{
result = CACHE_RESULT_OUT_OF_RESOURCES;
}
}
else
{
ss_dassert(is_stale);
ss_dassert(is_soft_stale);
result = (CACHE_RESULT_NOT_FOUND | CACHE_RESULT_STALE);
}
}

View File

@ -167,7 +167,7 @@ Storage* StorageFactory::createStorage(const char* zName,
used_config.max_size = 0;
}
Storage* pStorage = createRawStorage(zName, config, argc, argv);
Storage* pStorage = createRawStorage(zName, used_config, argc, argv);
if (pStorage)
{

View File

@ -274,7 +274,8 @@ int TesterStorage::test_ttl(const CacheItems& cache_items)
out() << "ST" << endl;
config.thread_model = CACHE_THREAD_MODEL_ST;
config.ttl = 5;
config.hard_ttl = 6;
config.soft_ttl = 3;
Storage* pStorage;
@ -290,7 +291,8 @@ int TesterStorage::test_ttl(const CacheItems& cache_items)
out() << "MT" << endl;
config.thread_model = CACHE_THREAD_MODEL_MT;
config.ttl = 5;
config.hard_ttl = 6;
config.soft_ttl = 3;
int rv2 = EXIT_FAILURE;
pStorage = get_storage(config);
@ -313,12 +315,19 @@ int TesterStorage::test_ttl(const CacheItems& cache_items, Storage& storage)
CacheStorageConfig config;
storage.get_config(&config);
uint32_t ttl = config.ttl;
uint32_t hard_ttl = config.hard_ttl;
uint32_t soft_ttl = config.soft_ttl;
uint32_t diff = hard_ttl - soft_ttl;
if (ttl != 0)
if (diff != 0)
{
ss_dassert(cache_items.size() > 0);
out() << "Hard TTL: " << hard_ttl << endl;
out() << "Soft TTL: " << soft_ttl << endl;
uint32_t slept = 0;
const CacheItems::value_type& cache_item = cache_items[0];
cache_result_t result = storage.put_value(cache_item.first, cache_item.second);
@ -330,34 +339,59 @@ int TesterStorage::test_ttl(const CacheItems& cache_items, Storage& storage)
}
else
{
sleep(ttl - 1);
// Let's stay just below the soft_ttl value.
sleep(soft_ttl - 1);
slept += soft_ttl - 1;
GWBUF* pValue;
pValue = NULL;
result = storage.get_value(cache_item.first, 0, &pValue);
if (!CACHE_RESULT_IS_OK(result))
// We should get the item normally as we are below the soft ttl, i.e. no stale bit.
if (result != CACHE_RESULT_OK)
{
out() << "Did not get value withing ttl." << endl;
out() << "Excpected to be found, and without stale bit." << endl;
rv = EXIT_FAILURE;
}
gwbuf_free(pValue);
sleep(2); // Should get us past the ttl
sleep(2); // Expected to get us passed the soft ttl.
slept += 2;
pValue = NULL;
result = storage.get_value(cache_item.first, 0, &pValue);
// We should not get the item and the stale bit should be on.
if (!(CACHE_RESULT_IS_NOT_FOUND(result) && CACHE_RESULT_IS_STALE(result)))
{
out() << "Expected not to be found, and with stale bit." << endl;
rv = EXIT_FAILURE;
}
gwbuf_free(pValue);
pValue = NULL;
result = storage.get_value(cache_item.first, CACHE_FLAGS_INCLUDE_STALE, &pValue);
if (CACHE_RESULT_IS_OK(result) && !CACHE_RESULT_IS_STALE(result))
if (!(CACHE_RESULT_IS_OK(result) && CACHE_RESULT_IS_STALE(result)))
{
out() << "Got value normally when accepting stale, although ttl has passed." << endl;
out() << "Expected to be found, and with stale bit." << endl;
rv = EXIT_FAILURE;
}
else if (!CACHE_RESULT_IS_STALE(result))
gwbuf_free(pValue);
sleep(hard_ttl - slept + 1); // Expected to get us passed the hard ttl.
slept += hard_ttl - slept + 1;
pValue = NULL;
result = storage.get_value(cache_item.first, CACHE_FLAGS_INCLUDE_STALE, &pValue);
if (result != CACHE_RESULT_NOT_FOUND)
{
out() << "Did not get expected stale value after ttl." << endl;
out() << "Expected not to be found, and without stale bit." << endl;
rv = EXIT_FAILURE;
}
@ -366,23 +400,19 @@ int TesterStorage::test_ttl(const CacheItems& cache_items, Storage& storage)
pValue = NULL;
result = storage.get_value(cache_item.first, 0, &pValue);
if (CACHE_RESULT_IS_OK(result))
if (result != CACHE_RESULT_NOT_FOUND)
{
out() << "Got value normally, although ttl has passed." << endl;
rv = EXIT_FAILURE;
}
else if (!CACHE_RESULT_IS_NOT_FOUND(result))
{
out() << "Unexpected failure." << endl;
out() << "Expected not to be found, and without stale bit." << endl;
rv = EXIT_FAILURE;
}
gwbuf_free(pValue);
}
}
else
{
out() << "No ttl, not testing." << endl;
out() << "No difference between soft and hard ttl, not testing." << endl;
}
return rv;