From 48d024360321fbebe6c9ce55bb0efe973825a4dc Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Mon, 9 Oct 2023 15:07:36 +0200 Subject: [PATCH] Fix a spuriously failing recursorcache_cc test: reset globals (indirectly) used For SyncRes tests we have a general mechanism setting the globals before the test are run, but the non-syncres tests do not have that, while they still use some globals. In this particular case, the test would fail if the last SyncRes test run before was setting SyncRes::s_locked_ttlperc. While there, pass the time to the prune functions. This avoids potential timing issues for some tests. --- pdns/cachecleaner.hh | 3 +- pdns/recursordist/negcache.cc | 4 +-- pdns/recursordist/negcache.hh | 2 +- pdns/recursordist/rec-main.cc | 12 ++++---- pdns/recursordist/recpacketcache.cc | 4 +-- pdns/recursordist/recpacketcache.hh | 2 +- pdns/recursordist/recursor_cache.cc | 12 ++++++-- pdns/recursordist/recursor_cache.hh | 4 ++- pdns/recursordist/test-negcache_cc.cc | 8 +++--- pdns/recursordist/test-recpacketcache_cc.cc | 27 ++++++++--------- pdns/recursordist/test-recursorcache_cc.cc | 32 +++++++++++++-------- 11 files changed, 64 insertions(+), 46 deletions(-) diff --git a/pdns/cachecleaner.hh b/pdns/cachecleaner.hh index 8373ae6f01..fa45ca5736 100644 --- a/pdns/cachecleaner.hh +++ b/pdns/cachecleaner.hh @@ -131,9 +131,8 @@ uint64_t pruneLockedCollectionsVector(std::vector& maps) } template -uint64_t pruneMutexCollectionsVector(C& container, std::vector& maps, uint64_t maxCached, uint64_t cacheSize) +uint64_t pruneMutexCollectionsVector(time_t now, C& container, std::vector& maps, uint64_t maxCached, uint64_t cacheSize) { - const time_t now = time(nullptr); uint64_t totErased = 0; uint64_t toTrim = 0; uint64_t lookAt = 0; diff --git a/pdns/recursordist/negcache.cc b/pdns/recursordist/negcache.cc index be67da138b..af6cd1fa6a 100644 --- a/pdns/recursordist/negcache.cc +++ b/pdns/recursordist/negcache.cc @@ -284,10 +284,10 @@ void NegCache::clear() * * \param maxEntries The maximum number of entries that may exist in the cache. */ -void NegCache::prune(size_t maxEntries) +void NegCache::prune(time_t now, size_t maxEntries) { size_t cacheSize = size(); - pruneMutexCollectionsVector(*this, d_maps, maxEntries, cacheSize); + pruneMutexCollectionsVector(now, *this, d_maps, maxEntries, cacheSize); } /*! diff --git a/pdns/recursordist/negcache.hh b/pdns/recursordist/negcache.hh index 1ac2855e31..86c3b562f4 100644 --- a/pdns/recursordist/negcache.hh +++ b/pdns/recursordist/negcache.hh @@ -98,7 +98,7 @@ public: bool getRootNXTrust(const DNSName& qname, const struct timeval& now, NegCacheEntry& ne, bool serveStale, bool refresh); size_t count(const DNSName& qname); size_t count(const DNSName& qname, QType qtype); - void prune(size_t maxEntries); + void prune(time_t now, size_t maxEntries); void clear(); size_t doDump(int fd, size_t maxCacheEntries, time_t now = time(nullptr)); size_t wipe(const DNSName& name, bool subtree = false); diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index 25e6d7d74c..0c9ca1cf8f 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -2404,18 +2404,18 @@ static void houseKeepingWork(Logr::log_t log) else if (info.isHandler()) { if (g_packetCache) { static PeriodicTask packetCacheTask{"packetCacheTask", 5}; - packetCacheTask.runIfDue(now, []() { - g_packetCache->doPruneTo(g_maxPacketCacheEntries); + packetCacheTask.runIfDue(now, [now]() { + g_packetCache->doPruneTo(now.tv_sec, g_maxPacketCacheEntries); }); } static PeriodicTask recordCachePruneTask{"RecordCachePruneTask", 5}; - recordCachePruneTask.runIfDue(now, []() { - g_recCache->doPrune(g_maxCacheEntries); + recordCachePruneTask.runIfDue(now, [now]() { + g_recCache->doPrune(now.tv_sec, g_maxCacheEntries); }); static PeriodicTask negCachePruneTask{"NegCachePrunteTask", 5}; - negCachePruneTask.runIfDue(now, []() { - g_negCache->prune(g_maxCacheEntries / 8); + negCachePruneTask.runIfDue(now, [now]() { + g_negCache->prune(now.tv_sec, g_maxCacheEntries / 8); }); static PeriodicTask aggrNSECPruneTask{"AggrNSECPruneTask", 5}; diff --git a/pdns/recursordist/recpacketcache.cc b/pdns/recursordist/recpacketcache.cc index 4ae3cb5170..f19176a141 100644 --- a/pdns/recursordist/recpacketcache.cc +++ b/pdns/recursordist/recpacketcache.cc @@ -251,10 +251,10 @@ void RecursorPacketCache::insertResponsePacket(unsigned int tag, uint32_t qhash, assert(map.getEntriesCount() == shard->d_map.size()); // NOLINT(cppcoreguidelines-pro-bounds-array-to-pointer-decay): clib implementation } -void RecursorPacketCache::doPruneTo(size_t maxSize) +void RecursorPacketCache::doPruneTo(time_t now, size_t maxSize) { size_t cacheSize = size(); - pruneMutexCollectionsVector(*this, d_maps, maxSize, cacheSize); + pruneMutexCollectionsVector(now, *this, d_maps, maxSize, cacheSize); } uint64_t RecursorPacketCache::doDump(int file) diff --git a/pdns/recursordist/recpacketcache.hh b/pdns/recursordist/recpacketcache.hh index 22207068a9..0e51cea628 100644 --- a/pdns/recursordist/recpacketcache.hh +++ b/pdns/recursordist/recpacketcache.hh @@ -83,7 +83,7 @@ public: bool getResponsePacket(unsigned int tag, const std::string& queryPacket, DNSName& qname, uint16_t* qtype, uint16_t* qclass, time_t now, std::string* responsePacket, uint32_t* age, vState* valState, uint32_t* qhash, OptPBData* pbdata, bool tcp); void insertResponsePacket(unsigned int tag, uint32_t qhash, std::string&& query, const DNSName& qname, uint16_t qtype, uint16_t qclass, std::string&& responsePacket, time_t now, uint32_t ttl, const vState& valState, OptPBData&& pbdata, bool tcp); - void doPruneTo(size_t maxSize); + void doPruneTo(time_t now, size_t maxSize); uint64_t doDump(int file); uint64_t doWipePacketCache(const DNSName& name, uint16_t qtype = 0xffff, bool subtree = false); diff --git a/pdns/recursordist/recursor_cache.cc b/pdns/recursordist/recursor_cache.cc index d3e434017e..12067195c5 100644 --- a/pdns/recursordist/recursor_cache.cc +++ b/pdns/recursordist/recursor_cache.cc @@ -54,6 +54,14 @@ uint16_t MemRecursorCache::s_maxServedStaleExtensions; +void MemRecursorCache::resetStaticsForTests() +{ + s_maxServedStaleExtensions = 0; + SyncRes::s_refresh_ttlperc = 0; + SyncRes::s_locked_ttlperc = 0; + SyncRes::s_minimumTTL = 0; +} + MemRecursorCache::MemRecursorCache(size_t mapsCount) : d_maps(mapsCount == 0 ? 1 : mapsCount) { @@ -821,10 +829,10 @@ uint64_t MemRecursorCache::doDump(int fd, size_t maxCacheEntries) return count; } -void MemRecursorCache::doPrune(size_t keep) +void MemRecursorCache::doPrune(time_t now, size_t keep) { size_t cacheSize = size(); - pruneMutexCollectionsVector(*this, d_maps, keep, cacheSize); + pruneMutexCollectionsVector(now, *this, d_maps, keep, cacheSize); } namespace boost diff --git a/pdns/recursordist/recursor_cache.hh b/pdns/recursordist/recursor_cache.hh index 7cc2f0316c..ccbcad2b50 100644 --- a/pdns/recursordist/recursor_cache.hh +++ b/pdns/recursordist/recursor_cache.hh @@ -71,7 +71,7 @@ public: void replace(time_t, const DNSName& qname, const QType qt, const vector& content, const vector>& signatures, const std::vector>& authorityRecs, bool auth, const DNSName& authZone, boost::optional ednsmask = boost::none, const OptTag& routingTag = boost::none, vState state = vState::Indeterminate, boost::optional from = boost::none, bool refresh = false, time_t ttl_time = time(nullptr)); - void doPrune(size_t keep); + void doPrune(time_t now, size_t keep); uint64_t doDump(int fd, size_t maxCacheEntries); size_t doWipeCache(const DNSName& name, bool sub, QType qtype = 0xffff); @@ -80,6 +80,8 @@ public: pdns::stat_t cacheHits{0}, cacheMisses{0}; + static void resetStaticsForTests(); + private: struct CacheEntry { diff --git a/pdns/recursordist/test-negcache_cc.cc b/pdns/recursordist/test-negcache_cc.cc index 96172d458f..be31d1e7af 100644 --- a/pdns/recursordist/test-negcache_cc.cc +++ b/pdns/recursordist/test-negcache_cc.cc @@ -291,7 +291,7 @@ BOOST_AUTO_TEST_CASE(test_prune) BOOST_CHECK_EQUAL(cache.size(), 400U); - cache.prune(100); + cache.prune(now.tv_sec, 100); BOOST_CHECK_EQUAL(cache.size(), 100U); } @@ -313,7 +313,7 @@ BOOST_AUTO_TEST_CASE(test_prune_many_shards) BOOST_CHECK_EQUAL(cache.size(), 400U); - cache.prune(100); + cache.prune(now.tv_sec, 100); BOOST_CHECK_EQUAL(cache.size(), 100U); } @@ -340,7 +340,7 @@ BOOST_AUTO_TEST_CASE(test_prune_valid_entries) /* power2 has been inserted more recently, so it should be removed last */ - cache.prune(1); + cache.prune(now.tv_sec, 1); BOOST_CHECK_EQUAL(cache.size(), 1U); NegCache::NegCacheEntry got; @@ -362,7 +362,7 @@ BOOST_AUTO_TEST_CASE(test_prune_valid_entries) /* power2 has been updated more recently, so it should be removed last */ - cache.prune(1); + cache.prune(now.tv_sec, 1); BOOST_CHECK_EQUAL(cache.size(), 1U); got = NegCache::NegCacheEntry(); diff --git a/pdns/recursordist/test-recpacketcache_cc.cc b/pdns/recursordist/test-recpacketcache_cc.cc index a3d783b091..353407ea27 100644 --- a/pdns/recursordist/test-recpacketcache_cc.cc +++ b/pdns/recursordist/test-recpacketcache_cc.cc @@ -35,31 +35,32 @@ BOOST_AUTO_TEST_CASE(test_recPacketCacheSimple) string qpacket((const char*)&packet[0], packet.size()); pw.startRecord(qname, QType::A, ttd); - BOOST_CHECK_EQUAL(rpc.getResponsePacket(tag, qpacket, time(nullptr), &fpacket, &age, &qhash), false); - BOOST_CHECK_EQUAL(rpc.getResponsePacket(tag, qpacket, qname, QType::A, QClass::IN, time(nullptr), &fpacket, &age, &qhash), false); + time_t now = time(nullptr); + BOOST_CHECK_EQUAL(rpc.getResponsePacket(tag, qpacket, now, &fpacket, &age, &qhash), false); + BOOST_CHECK_EQUAL(rpc.getResponsePacket(tag, qpacket, qname, QType::A, QClass::IN, now, &fpacket, &age, &qhash), false); ARecordContent ar("127.0.0.1"); ar.toPacket(pw); pw.commit(); string rpacket((const char*)&packet[0], packet.size()); - rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), time(0), ttd, vState::Indeterminate, boost::none, false); + rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), now, ttd, vState::Indeterminate, boost::none, false); BOOST_CHECK_EQUAL(rpc.size(), 1U); - rpc.doPruneTo(0); + rpc.doPruneTo(now, 0); BOOST_CHECK_EQUAL(rpc.size(), 0U); - rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), time(0), ttd, vState::Indeterminate, boost::none, false); + rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), now, ttd, vState::Indeterminate, boost::none, false); BOOST_CHECK_EQUAL(rpc.size(), 1U); rpc.doWipePacketCache(qname); BOOST_CHECK_EQUAL(rpc.size(), 0U); - rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), time(0), ttd, vState::Indeterminate, boost::none, false); + rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), now, ttd, vState::Indeterminate, boost::none, false); BOOST_CHECK_EQUAL(rpc.size(), 1U); uint32_t qhash2 = 0; - bool found = rpc.getResponsePacket(tag, qpacket, time(nullptr), &fpacket, &age, &qhash2); + bool found = rpc.getResponsePacket(tag, qpacket, now, &fpacket, &age, &qhash2); BOOST_CHECK_EQUAL(found, true); BOOST_CHECK_EQUAL(qhash, qhash2); BOOST_CHECK_EQUAL(fpacket, rpacket); - found = rpc.getResponsePacket(tag, qpacket, qname, QType::A, QClass::IN, time(nullptr), &fpacket, &age, &qhash2); + found = rpc.getResponsePacket(tag, qpacket, qname, QType::A, QClass::IN, now, &fpacket, &age, &qhash2); BOOST_CHECK_EQUAL(found, true); BOOST_CHECK_EQUAL(qhash, qhash2); BOOST_CHECK_EQUAL(fpacket, rpacket); @@ -73,9 +74,9 @@ BOOST_AUTO_TEST_CASE(test_recPacketCacheSimple) pw2.getHeader()->id = dns_random_uint16(); qpacket.assign((const char*)&packet[0], packet.size()); - found = rpc.getResponsePacket(tag, qpacket, time(nullptr), &fpacket, &age, &qhash); + found = rpc.getResponsePacket(tag, qpacket, now, &fpacket, &age, &qhash); BOOST_CHECK_EQUAL(found, false); - found = rpc.getResponsePacket(tag, qpacket, qname, QType::A, QClass::IN, time(nullptr), &fpacket, &age, &qhash); + found = rpc.getResponsePacket(tag, qpacket, qname, QType::A, QClass::IN, now, &fpacket, &age, &qhash); BOOST_CHECK_EQUAL(found, false); rpc.doWipePacketCache(DNSName("com"), 0xffff, true); @@ -175,7 +176,7 @@ BOOST_AUTO_TEST_CASE(test_recPacketCacheSimplePost2038) rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), future, ttd, vState::Indeterminate, boost::none, false); BOOST_CHECK_EQUAL(rpc.size(), 1U); - rpc.doPruneTo(0); + rpc.doPruneTo(time(nullptr), 0); BOOST_CHECK_EQUAL(rpc.size(), 0U); rpc.insertResponsePacket(tag, qhash, string(qpacket), qname, QType::A, QClass::IN, string(rpacket), future, ttd, vState::Indeterminate, boost::none, false); BOOST_CHECK_EQUAL(rpc.size(), 1U); @@ -280,7 +281,7 @@ BOOST_AUTO_TEST_CASE(test_recPacketCache_Tags) BOOST_CHECK_EQUAL(rpc.size(), 2U); /* remove all responses from the cache */ - rpc.doPruneTo(0); + rpc.doPruneTo(time(nullptr), 0); BOOST_CHECK_EQUAL(rpc.size(), 0U); /* reinsert both */ @@ -393,7 +394,7 @@ BOOST_AUTO_TEST_CASE(test_recPacketCache_TCP) BOOST_CHECK_EQUAL(rpc.size(), 2U); /* remove all responses from the cache */ - rpc.doPruneTo(0); + rpc.doPruneTo(time(nullptr), 0); BOOST_CHECK_EQUAL(rpc.size(), 0U); /* reinsert both */ diff --git a/pdns/recursordist/test-recursorcache_cc.cc b/pdns/recursordist/test-recursorcache_cc.cc index e456d3f116..71a05440ee 100644 --- a/pdns/recursordist/test-recursorcache_cc.cc +++ b/pdns/recursordist/test-recursorcache_cc.cc @@ -13,6 +13,7 @@ BOOST_AUTO_TEST_SUITE(recursorcache_cc) static void simple(time_t now) { + MemRecursorCache::resetStaticsForTests(); MemRecursorCache MRC; std::vector records; @@ -387,6 +388,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCacheSimpleDistantFuture) BOOST_AUTO_TEST_CASE(test_RecursorCacheGhost) { + MemRecursorCache::resetStaticsForTests(); MemRecursorCache MRC; std::vector records; @@ -430,6 +432,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCacheReplaceAuthByNonAuthMargin) { // Test #12140: as QM does a best NS lookup and then uses it, incoming infra records should update // cache, otherwise they might expire in-between. + MemRecursorCache::resetStaticsForTests(); MemRecursorCache MRC; std::vector records; @@ -475,6 +478,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCacheReplaceAuthByNonAuthMargin) BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingExpiredEntries) { + MemRecursorCache::resetStaticsForTests(); MemRecursorCache MRC(1); std::vector records; @@ -522,7 +526,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingExpiredEntries) /* we ask that 10 entries remain in the cache, this is larger than the cache size (2), so 1 entry will be looked at as the code rounds up the 10% of entries per shard to look at */ - MRC.doPrune(10); + MRC.doPrune(now, 10); BOOST_CHECK_EQUAL(MRC.size(), 1U); /* the remaining entry should be power2, but to get it @@ -555,7 +559,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingExpiredEntries) /* we ask that 10 entries remain in the cache, this is larger than the cache size (2), so 1 entry will be looked at as the code rounds up the 10% of entries per shard to look at */ - MRC.doPrune(10); + MRC.doPrune(now, 10); BOOST_CHECK_EQUAL(MRC.size(), 1U); /* the remaining entry should be power1, but to get it @@ -569,6 +573,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingExpiredEntries) BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingValidEntries) { + MemRecursorCache::resetStaticsForTests(); MemRecursorCache MRC(1); std::vector records; @@ -615,7 +620,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingValidEntries) /* the one for power2 having been inserted more recently should be removed last */ /* we ask that only entry remains in the cache */ - MRC.doPrune(1); + MRC.doPrune(now, 1); BOOST_CHECK_EQUAL(MRC.size(), 1U); /* the remaining entry should be power2 */ @@ -649,7 +654,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingValidEntries) to the back of the expunge queue, so power2 should be at the front and should this time be removed first */ /* we ask that only entry remains in the cache */ - MRC.doPrune(1); + MRC.doPrune(now, 1); BOOST_CHECK_EQUAL(MRC.size(), 1U); /* the remaining entry should be power1 */ @@ -681,7 +686,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingValidEntries) /* the entry for power1 should have been moved to the back of the expunge queue due to the hit, so power2 should be at the front and should this time be removed first */ /* we ask that only entry remains in the cache */ - MRC.doPrune(1); + MRC.doPrune(now, 1); BOOST_CHECK_EQUAL(MRC.size(), 1U); /* the remaining entry should be power1 */ @@ -691,7 +696,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingValidEntries) /* check that power2 is gone */ BOOST_CHECK_EQUAL(MRC.get(now, power2, QType(dr2.d_type), MemRecursorCache::None, &retrieved, who, boost::none, nullptr), -1); - MRC.doPrune(0); + MRC.doPrune(now, 0); BOOST_CHECK_EQUAL(MRC.size(), 0U); /* add a lot of netmask-specific entries */ @@ -716,7 +721,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingValidEntries) /* remove a bit less than half of them */ size_t keep = 129; - MRC.doPrune(keep); + MRC.doPrune(now, keep); BOOST_CHECK_EQUAL(MRC.size(), keep); BOOST_CHECK_EQUAL(MRC.ecsIndexSize(), 1U); @@ -740,13 +745,14 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_ExpungingValidEntries) BOOST_CHECK_EQUAL(found, keep); /* remove the rest */ - MRC.doPrune(0); + MRC.doPrune(now, 0); BOOST_CHECK_EQUAL(MRC.size(), 0U); BOOST_CHECK_EQUAL(MRC.ecsIndexSize(), 0U); } BOOST_AUTO_TEST_CASE(test_RecursorCacheECSIndex) { + MemRecursorCache::resetStaticsForTests(); MemRecursorCache MRC(1); const DNSName power("powerdns.com."); @@ -797,7 +803,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCacheECSIndex) BOOST_CHECK_EQUAL(getRR(retrieved.at(0))->getCA().toString(), dr1Content.toString()); /* wipe everything */ - MRC.doPrune(0); + MRC.doPrune(now, 0); BOOST_CHECK_EQUAL(MRC.size(), 0U); BOOST_CHECK_EQUAL(MRC.ecsIndexSize(), 0U); @@ -835,7 +841,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCacheECSIndex) BOOST_CHECK_EQUAL(getRR(retrieved.at(0))->getCA().toString(), dr1Content.toString()); /* wipe everything */ - MRC.doPrune(0); + MRC.doPrune(now, 0); BOOST_CHECK_EQUAL(MRC.size(), 0U); BOOST_CHECK_EQUAL(MRC.ecsIndexSize(), 0U); @@ -870,7 +876,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCacheECSIndex) BOOST_CHECK_EQUAL(MRC.size(), 2U); /* wipe everything */ - MRC.doPrune(0); + MRC.doPrune(now, 0); BOOST_CHECK_EQUAL(MRC.size(), 0U); BOOST_CHECK_EQUAL(MRC.ecsIndexSize(), 0U); @@ -899,13 +905,14 @@ BOOST_AUTO_TEST_CASE(test_RecursorCacheECSIndex) BOOST_CHECK_EQUAL(MRC.ecsIndexSize(), 1U); /* wipe everything */ - MRC.doPrune(0); + MRC.doPrune(now, 0); BOOST_CHECK_EQUAL(MRC.size(), 0U); BOOST_CHECK_EQUAL(MRC.ecsIndexSize(), 0U); } BOOST_AUTO_TEST_CASE(test_RecursorCache_Wipe) { + MemRecursorCache::resetStaticsForTests(); MemRecursorCache MRC; const DNSName power("powerdns.com."); @@ -996,6 +1003,7 @@ BOOST_AUTO_TEST_CASE(test_RecursorCache_Wipe) BOOST_AUTO_TEST_CASE(test_RecursorCacheTagged) { + MemRecursorCache::resetStaticsForTests(); MemRecursorCache MRC; const DNSName authZone("."); -- 2.47.2