MXS-1401 Support multiple cache rules

The possibility to have multiple cache rules in a cache
configuration file is now handled throughout the cache
filter.

The major difference is that while you earlier directly
queried the Cache whether data should be stored to the
cache and whether data in the cache should be used, you
now query the Cache whether data should be stored to the
cache and, if so, get a CacheRules object from which you
subsequently query whether data from the cache should
be used.
This commit is contained in:
Johan Wikman 2018-04-24 18:23:49 +03:00
parent 605f771518
commit a3a8b5523e
15 changed files with 249 additions and 166 deletions

View File

@ -481,13 +481,14 @@ SET @maxscale.cache.populate=false;
# Rules
The caching rules are expressed as a JSON object.
The caching rules are expressed as a JSON object or as an array
of JSON objects.
There are two decisions to be made regarding the caching; in what circumstances
should data be stored to the cache and in what circumstances should the data in
the cache be used.
In the JSON object this is visible as follows:
Expressed in JSON this looks as follows
```
{
@ -495,12 +496,27 @@ In the JSON object this is visible as follows:
use: [ ... ]
}
```
or, in case an array is used, as
```
[
{
store: [ ... ],
use: [ ... ]
},
{ ... }
]
```
The `store` field specifies in what circumstances data should be stored to
the cache and the `use` field specifies in what circumstances the data in
the cache should be used. In both cases, the value is a JSON array containg
objects.
If an array of rule objects is specified, then, when looking for a rule that
matches, the `store` field of each object are evaluated in sequential order
until a match is found. Then, the `use` field of that object is used when
deciding whether data in the cache should be used.
## When to Store
By default, if no rules file have been provided or if the `store` field is

View File

@ -27,13 +27,13 @@
using namespace std;
Cache::Cache(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory)
Cache::Cache(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory)
: m_name(name)
, m_config(*pConfig)
, m_sRules(sRules)
, m_rules(rules)
, m_sFactory(sFactory)
{
}
@ -43,35 +43,42 @@ Cache::~Cache()
}
//static
bool Cache::Create(const CACHE_CONFIG& config,
CacheRules** ppRules,
StorageFactory** ppFactory)
bool Cache::Create(const CACHE_CONFIG& config,
std::vector<SCacheRules>* pRules,
StorageFactory** ppFactory)
{
CacheRules* pRules = NULL;
std::vector<SCacheRules> rules;
StorageFactory* pFactory = NULL;
bool rv = false;
if (config.rules)
{
pRules = CacheRules::load(config.rules, config.debug);
rv = CacheRules::load(config.rules, config.debug, &rules);
}
else
{
pRules = CacheRules::create(config.debug);
auto_ptr<CacheRules> sRules(CacheRules::create(config.debug));
if (sRules.get())
{
rules.push_back(SCacheRules(sRules.release()));
rv = true;
}
}
if (pRules)
if (rv)
{
pFactory = StorageFactory::Open(config.storage);
if (pFactory)
{
*ppFactory = pFactory;
*ppRules = pRules;
pRules->swap(rules);
}
else
{
MXS_ERROR("Could not open storage factory '%s'.", config.storage);
delete pRules;
}
}
else
@ -155,14 +162,25 @@ cache_result_t Cache::get_default_key(const char* zDefault_db,
return CACHE_RESULT_OK;
}
bool Cache::should_store(const char* zDefaultDb, const GWBUF* pQuery)
const CacheRules* Cache::should_store(const char* zDefaultDb, const GWBUF* pQuery)
{
return m_sRules->should_store(zDefaultDb, pQuery);
}
CacheRules* pRules = NULL;
bool Cache::should_use(const MXS_SESSION* pSession)
{
return m_sRules->should_use(pSession);
auto i = m_rules.begin();
while (!pRules && (i != m_rules.end()))
{
if ((*i)->should_store(zDefaultDb, pQuery))
{
pRules = (*i).get();
}
else
{
++i;
}
}
return pRules;
}
json_t* Cache::do_get_info(uint32_t what) const
@ -173,9 +191,18 @@ json_t* Cache::do_get_info(uint32_t what) const
{
if (what & INFO_RULES)
{
json_t* pRules = const_cast<json_t*>(m_sRules->json());
json_t* pArray = json_array();
json_object_set(pInfo, "rules", pRules); // Increases ref-count of pRules, we ignore failure.
if (pArray)
{
for (auto i = m_rules.begin(); i < m_rules.end(); ++i)
{
json_t* pRules = const_cast<json_t*>((*i)->json());
json_array_append(pArray, pRules); // Increases ref-count of pRules, we ignore failure.
}
json_object_set(pInfo, "rules", pArray);
}
}
}

View File

@ -56,18 +56,9 @@ public:
* @param zDefaultDb The current default database.
* @param pQuery Buffer containing a SELECT.
*
* @return True of the result should be cached.
* @return A rules object, if the query should be stored, NULL otherwise.
*/
bool should_store(const char* zDefaultDb, const GWBUF* pQuery);
/**
* Returns whether cached results should be used.
*
* @param pSession The session in question.
*
* @return True of cached results should be used.
*/
bool should_use(const MXS_SESSION* pSession);
const CacheRules* should_store(const char* zDefaultDb, const GWBUF* pQuery);
/**
* Specifies whether a particular SessioCache should refresh the data.
@ -134,14 +125,14 @@ public:
virtual cache_result_t del_value(const CACHE_KEY& key) = 0;
protected:
Cache(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory);
Cache(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory);
static bool Create(const CACHE_CONFIG& config,
CacheRules** ppRules,
StorageFactory** ppFactory);
static bool Create(const CACHE_CONFIG& config,
std::vector<SCacheRules>* pRules,
StorageFactory** ppFactory);
json_t* do_get_info(uint32_t what) const;
@ -150,8 +141,8 @@ private:
Cache& operator = (const Cache&);
protected:
const std::string m_name; // The name of the instance; the section name in the config.
const CACHE_CONFIG& m_config; // The configuration of the cache instance.
SCacheRules m_sRules; // The rules of the cache instance.
SStorageFactory m_sFactory; // The storage factory.
const std::string m_name; // The name of the instance; the section name in the config.
const CACHE_CONFIG& m_config; // The configuration of the cache instance.
std::vector<SCacheRules> m_rules; // The rules of the cache instance.
SStorageFactory m_sFactory; // The storage factory.
};

View File

@ -1005,13 +1005,15 @@ CacheFilterSession::routing_action_t CacheFilterSession::route_COM_QUERY(GWBUF*
if (cache_action != CACHE_IGNORE)
{
if (m_pCache->should_store(m_zDefaultDb, pPacket))
const CacheRules* pRules = m_pCache->should_store(m_zDefaultDb, pPacket);
if (pRules)
{
cache_result_t result = m_pCache->get_key(m_zDefaultDb, pPacket, &m_key);
if (CACHE_RESULT_IS_OK(result))
{
routing_action = route_SELECT(cache_action, pPacket);
routing_action = route_SELECT(cache_action, *pRules, pPacket);
}
else
{
@ -1033,6 +1035,7 @@ CacheFilterSession::routing_action_t CacheFilterSession::route_COM_QUERY(GWBUF*
* Routes a SELECT packet.
*
* @param cache_action The desired action.
* @param rules The current rules.
* @param pPacket A contiguous COM_QUERY packet containing a SELECT.
*
* @return ROUTING_ABORT if the processing of the packet should be aborted
@ -1040,11 +1043,12 @@ CacheFilterSession::routing_action_t CacheFilterSession::route_COM_QUERY(GWBUF*
* ROUTING_CONTINUE if the normal processing should continue.
*/
CacheFilterSession::routing_action_t CacheFilterSession::route_SELECT(cache_action_t cache_action,
const CacheRules& rules,
GWBUF* pPacket)
{
routing_action_t routing_action = ROUTING_CONTINUE;
if (should_use(cache_action) && m_pCache->should_use(m_pSession))
if (should_use(cache_action) && rules.should_use(m_pSession))
{
uint32_t flags = CACHE_FLAGS_INCLUDE_STALE;
GWBUF* pResponse;

View File

@ -139,7 +139,7 @@ private:
};
routing_action_t route_COM_QUERY(GWBUF* pPacket);
routing_action_t route_SELECT(cache_action_t action, GWBUF* pPacket);
routing_action_t route_SELECT(cache_action_t action, const CacheRules& rules, GWBUF* pPacket);
char* set_cache_populate(const char* zName,
const char* pValue_begin,

View File

@ -19,12 +19,12 @@
using maxscale::SpinLockGuard;
using std::tr1::shared_ptr;
CacheMT::CacheMT(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory,
Storage* pStorage)
: CacheSimple(name, pConfig, sRules, sFactory, pStorage)
CacheMT::CacheMT(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory,
Storage* pStorage)
: CacheSimple(name, pConfig, rules, sFactory, pStorage)
{
spinlock_init(&m_lock_pending);
@ -41,15 +41,14 @@ CacheMT* CacheMT::Create(const std::string& name, const CACHE_CONFIG* pConfig)
CacheMT* pCache = NULL;
CacheRules* pRules = NULL;
std::vector<SCacheRules> rules;
StorageFactory* pFactory = NULL;
if (CacheSimple::Create(*pConfig, &pRules, &pFactory))
if (CacheSimple::Create(*pConfig, &rules, &pFactory))
{
shared_ptr<CacheRules> sRules(pRules);
shared_ptr<StorageFactory> sFactory(pFactory);
pCache = Create(name, pConfig, sRules, sFactory);
pCache = Create(name, pConfig, rules, sFactory);
}
return pCache;
@ -77,10 +76,10 @@ void CacheMT::refreshed(const CACHE_KEY& key, const CacheFilterSession* pSessio
}
// static
CacheMT* CacheMT::Create(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory)
CacheMT* CacheMT::Create(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory)
{
CacheMT* pCache = NULL;
@ -99,7 +98,7 @@ CacheMT* CacheMT::Create(const std::string& name,
{
MXS_EXCEPTION_GUARD(pCache = new CacheMT(name,
pConfig,
sRules,
rules,
sFactory,
pStorage));

View File

@ -30,16 +30,16 @@ public:
void refreshed(const CACHE_KEY& key, const CacheFilterSession* pSession);
private:
CacheMT(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory,
Storage* pStorage);
CacheMT(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory,
Storage* pStorage);
static CacheMT* Create(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory);
static CacheMT* Create(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory);
private:
CacheMT(const CacheMT&);

View File

@ -48,12 +48,12 @@ inline int thread_index()
}
CachePT::CachePT(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory,
const Caches& caches)
: Cache(name, pConfig, sRules, sFactory)
CachePT::CachePT(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory,
const Caches& caches)
: Cache(name, pConfig, rules, sFactory)
, m_caches(caches)
{
MXS_NOTICE("Created cache per thread.");
@ -70,15 +70,14 @@ CachePT* CachePT::Create(const std::string& name, const CACHE_CONFIG* pConfig)
CachePT* pCache = NULL;
CacheRules* pRules = NULL;
std::vector<SCacheRules> rules;
StorageFactory* pFactory = NULL;
if (Cache::Create(*pConfig, &pRules, &pFactory))
if (Cache::Create(*pConfig, &rules, &pFactory))
{
shared_ptr<CacheRules> sRules(pRules);
shared_ptr<StorageFactory> sFactory(pFactory);
pCache = Create(name, pConfig, sRules, sFactory);
pCache = Create(name, pConfig, rules, sFactory);
}
return pCache;
@ -148,10 +147,10 @@ cache_result_t CachePT::del_value(const CACHE_KEY& key)
}
// static
CachePT* CachePT::Create(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory)
CachePT* CachePT::Create(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory)
{
CachePT* pCache = NULL;
@ -173,7 +172,7 @@ CachePT* CachePT::Create(const std::string& name,
CacheST* pCacheST = 0;
MXS_EXCEPTION_GUARD(pCacheST = CacheST::Create(namest, sRules, sFactory, pConfig));
MXS_EXCEPTION_GUARD(pCacheST = CacheST::Create(namest, rules, sFactory, pConfig));
if (pCacheST)
{
@ -191,7 +190,7 @@ CachePT* CachePT::Create(const std::string& name,
if (!error)
{
pCache = new CachePT(name, pConfig, sRules, sFactory, caches);
pCache = new CachePT(name, pConfig, rules, sFactory, caches);
}
}
catch (const std::exception&)

View File

@ -44,16 +44,16 @@ private:
typedef std::tr1::shared_ptr<Cache> SCache;
typedef std::vector<SCache> Caches;
CachePT(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory,
const Caches& caches);
CachePT(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory,
const Caches& caches);
static CachePT* Create(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory);
static CachePT* Create(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory);
Cache& thread_cache();

View File

@ -16,12 +16,12 @@
#include "storage.hh"
#include "storagefactory.hh"
CacheSimple::CacheSimple(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory,
Storage* pStorage)
: Cache(name, pConfig, sRules, sFactory)
CacheSimple::CacheSimple(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory,
Storage* pStorage)
: Cache(name, pConfig, rules, sFactory)
, m_pStorage(pStorage)
{
}
@ -32,22 +32,24 @@ CacheSimple::~CacheSimple()
}
// static
bool CacheSimple::Create(const CACHE_CONFIG& config,
CacheRules** ppRules,
StorageFactory** ppFactory)
bool CacheSimple::Create(const CACHE_CONFIG& config,
std::vector<SCacheRules>* pRules,
StorageFactory** ppFactory)
{
int rv = false;
CacheRules* pRules = NULL;
std::vector<SCacheRules> rules;
StorageFactory* pFactory = NULL;
if (Cache::Create(config, &pRules, &pFactory))
rv = Cache::Create(config, &rules, &pFactory);
if (rv)
{
*ppRules = pRules;
pRules->swap(rules);
*ppFactory = pFactory;
}
return pRules != NULL;
return rv;
}
cache_result_t CacheSimple::get_value(const CACHE_KEY& key,

View File

@ -34,15 +34,15 @@ public:
cache_result_t del_value(const CACHE_KEY& key);
protected:
CacheSimple(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory,
Storage* pStorage);
CacheSimple(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& Rules,
SStorageFactory sFactory,
Storage* pStorage);
static bool Create(const CACHE_CONFIG& config,
CacheRules** ppRules,
StorageFactory** ppFactory);
static bool Create(const CACHE_CONFIG& config,
std::vector<SCacheRules>* pRules,
StorageFactory** ppFactory);
json_t* do_get_info(uint32_t what) const;

View File

@ -18,12 +18,12 @@
using std::tr1::shared_ptr;
CacheST::CacheST(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory,
Storage* pStorage)
: CacheSimple(name, pConfig, sRules, sFactory, pStorage)
CacheST::CacheST(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory,
Storage* pStorage)
: CacheSimple(name, pConfig, rules, sFactory, pStorage)
{
MXS_NOTICE("Created single threaded cache.");
}
@ -38,31 +38,29 @@ CacheST* CacheST::Create(const std::string& name, const CACHE_CONFIG* pConfig)
CacheST* pCache = NULL;
CacheRules* pRules = NULL;
std::vector<SCacheRules> rules;
StorageFactory* pFactory = NULL;
if (CacheSimple::Create(*pConfig, &pRules, &pFactory))
if (CacheSimple::Create(*pConfig, &rules, &pFactory))
{
shared_ptr<CacheRules> sRules(pRules);
shared_ptr<StorageFactory> sFactory(pFactory);
pCache = Create(name, pConfig, sRules, sFactory);
pCache = Create(name, pConfig, rules, sFactory);
}
return pCache;
}
// static
CacheST* CacheST::Create(const std::string& name,
SCacheRules sRules,
SStorageFactory sFactory,
const CACHE_CONFIG* pConfig)
CacheST* CacheST::Create(const std::string& name,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory,
const CACHE_CONFIG* pConfig)
{
ss_dassert(sRules.get());
ss_dassert(sFactory.get());
ss_dassert(pConfig);
return Create(name, pConfig, sRules, sFactory);
return Create(name, pConfig, rules, sFactory);
}
json_t* CacheST::get_info(uint32_t flags) const
@ -81,10 +79,10 @@ void CacheST::refreshed(const CACHE_KEY& key, const CacheFilterSession* pSessio
}
// static
CacheST* CacheST::Create(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory)
CacheST* CacheST::Create(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory)
{
CacheST* pCache = NULL;
@ -103,7 +101,7 @@ CacheST* CacheST::Create(const std::string& name,
{
MXS_EXCEPTION_GUARD(pCache = new CacheST(name,
pConfig,
sRules,
rules,
sFactory,
pStorage));

View File

@ -22,7 +22,7 @@ public:
static CacheST* Create(const std::string& name, const CACHE_CONFIG* pConfig);
static CacheST* Create(const std::string& name,
SCacheRules sRules,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory,
const CACHE_CONFIG* pConfig);
@ -33,16 +33,16 @@ public:
void refreshed(const CACHE_KEY& key, const CacheFilterSession* pSession);
private:
CacheST(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory,
Storage* pStorage);
CacheST(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory,
Storage* pStorage);
static CacheST* Create(const std::string& name,
const CACHE_CONFIG* pConfig,
SCacheRules sRules,
SStorageFactory sFactory);
static CacheST* Create(const std::string& name,
const CACHE_CONFIG* pConfig,
const std::vector<SCacheRules>& rules,
SStorageFactory sFactory);
private:
CacheST(const CacheST&);
CacheST& operator = (const CacheST&);

View File

@ -315,6 +315,16 @@ void cache_rules_free(CACHE_RULES *rules)
}
}
void cache_rules_free_array(CACHE_RULES** ppRules, int32_t nRules)
{
for (auto i = 0; i < nRules; ++i)
{
cache_rules_free(ppRules[i]);
}
MXS_FREE(ppRules);
}
void cache_rules_print(const CACHE_RULES *self, DCB *dcb, size_t indent)
{
if (self->root)
@ -405,39 +415,64 @@ CacheRules::~CacheRules()
}
// static
CacheRules* CacheRules::create(uint32_t debug)
std::auto_ptr<CacheRules> CacheRules::create(uint32_t debug)
{
CacheRules* pThis = NULL;
std::auto_ptr<CacheRules> sThis;
CACHE_RULES* pRules = cache_rules_create(debug);
if (pRules)
{
pThis = new (std::nothrow) CacheRules(pRules);
sThis = std::auto_ptr<CacheRules>(new (std::nothrow) CacheRules(pRules));
}
return pThis;
return sThis;
}
// static
CacheRules* CacheRules::load(const char *zPath, uint32_t debug)
bool CacheRules::load(const char *zPath, uint32_t debug, std::vector<SCacheRules>* pRules)
{
CacheRules* pThis = NULL;
bool rv = false;
pRules->clear();
CACHE_RULES** ppRules;
int32_t nRules;
if (cache_rules_load(zPath, debug, &ppRules, &nRules))
{
// TODO: Handle more that one CACHE_RULES object at this level.
ss_dassert(nRules == 1);
int j = 0;
pThis = new (std::nothrow) CacheRules(ppRules[0]);
try
{
std::vector<SCacheRules> rules;
rules.reserve(nRules);
for (int i = 0; i < nRules; ++i)
{
j = i;
CacheRules* pRules = new CacheRules(ppRules[i]);
j = i + 1;
rules.push_back(SCacheRules(pRules));
}
pRules->swap(rules);
rv = true;
}
catch (const std::exception&)
{
// Free all CACHE_RULES objects that were not pushed into 'rules' above.
for (; j < nRules; ++j)
{
cache_rules_free(ppRules[j]);
}
}
MXS_FREE(ppRules);
}
return pThis;
return rv;
}
const json_t* CacheRules::json() const

View File

@ -17,6 +17,7 @@
#include <maxscale/cdefs.h>
#include <stdbool.h>
#include <jansson.h>
#include <tr1/memory>
#include <maxscale/buffer.h>
#include <maxscale/session.h>
#include <maxscale/pcre2.h>
@ -105,6 +106,14 @@ CACHE_RULES *cache_rules_create(uint32_t debug);
*/
void cache_rules_free(CACHE_RULES *rules);
/**
* Frees all rules in an array of rules *and* the array itself.
*
* @param ppRules Pointer to array of pointers to rules.
* @param nRules The number of items in the array.
*/
void cache_rules_free_array(CACHE_RULES** ppRules, int32_t nRules);
/**
* Loads the caching rules from a file and returns corresponding object.
*
@ -175,6 +184,8 @@ MXS_END_DECLS
class CacheRules
{
public:
typedef std::tr1::shared_ptr<CacheRules> SCacheRules;
~CacheRules();
/**
@ -184,17 +195,18 @@ public:
*
* @return An empty rules object, or NULL in case of error.
*/
static CacheRules* create(uint32_t debug);
static std::auto_ptr<CacheRules> create(uint32_t debug);
/**
* Loads the caching rules from a file and returns corresponding object.
*
* @param path The path of the file containing the rules.
* @param debug The debug level.
* @param path The path of the file containing the rules.
* @param debug The debug level.
* @param pRules [out] The loaded rules.
*
* @return The corresponding rules object, or NULL in case of error.
* @return True, if the rules could be loaded, false otherwise.
*/
static CacheRules* load(const char *zPath, uint32_t debug);
static bool load(const char *zPath, uint32_t debug, std::vector<SCacheRules>* pRules);
/**
* Returns the json rules object.