/ libi2pd / Profiling.cpp
Profiling.cpp
  1  /*
  2  * Copyright (c) 2013-2025, The PurpleI2P Project
  3  *
  4  * This file is part of Purple i2pd project and licensed under BSD3
  5  *
  6  * See full license text in LICENSE file at top of project tree
  7  */
  8  
  9  #include <sys/stat.h>
 10  #include <unordered_map>
 11  #include <list>
 12  #include <thread>
 13  #include <iomanip>
 14  #include <boost/property_tree/ptree.hpp>
 15  #include <boost/property_tree/ini_parser.hpp>
 16  #include "Base.h"
 17  #include "FS.h"
 18  #include "Log.h"
 19  #include "Timestamp.h"
 20  #include "NetDb.hpp"
 21  #include "Profiling.h"
 22  
 23  namespace i2p
 24  {
 25  namespace data
 26  {
 27  	static i2p::fs::HashedStorage g_ProfilesStorage("peerProfiles", "p", "profile-", "txt");
 28  	static std::unordered_map<i2p::data::IdentHash, std::shared_ptr<RouterProfile> > g_Profiles;
 29  	static std::mutex g_ProfilesMutex;
 30  	static std::list<std::pair<i2p::data::IdentHash, std::function<void (std::shared_ptr<RouterProfile>)> > > g_PostponedUpdates;
 31  	static std::mutex g_PostponedUpdatesMutex;
 32  	
 33  	RouterProfile::RouterProfile ():
 34  		m_IsUpdated (false), m_LastDeclineTime (0), m_LastUnreachableTime (0),
 35  		m_LastUpdateTime (i2p::util::GetSecondsSinceEpoch ()), m_LastAccessTime (0),
 36  		m_LastPersistTime (0), m_NumTunnelsAgreed (0), m_NumTunnelsDeclined (0),
 37  		m_NumTunnelsNonReplied (0),m_NumTimesTaken (0), m_NumTimesRejected (0),
 38  		m_HasConnected (false), m_IsDuplicated (false)
 39  	{
 40  	}
 41  
 42  	void RouterProfile::UpdateTime ()
 43  	{
 44  		m_LastUpdateTime = i2p::util::GetSecondsSinceEpoch ();
 45  		m_IsUpdated = true;
 46  	}
 47  
 48  	void RouterProfile::Save (const IdentHash& identHash)
 49  	{
 50  		// fill sections
 51  		boost::property_tree::ptree participation;
 52  		participation.put (PEER_PROFILE_PARTICIPATION_AGREED, m_NumTunnelsAgreed);
 53  		participation.put (PEER_PROFILE_PARTICIPATION_DECLINED, m_NumTunnelsDeclined);
 54  		participation.put (PEER_PROFILE_PARTICIPATION_NON_REPLIED, m_NumTunnelsNonReplied);
 55  		boost::property_tree::ptree usage;
 56  		usage.put (PEER_PROFILE_USAGE_TAKEN, m_NumTimesTaken);
 57  		usage.put (PEER_PROFILE_USAGE_REJECTED, m_NumTimesRejected);
 58  		usage.put (PEER_PROFILE_USAGE_CONNECTED, m_HasConnected);
 59  		if (m_IsDuplicated)
 60  			usage.put (PEER_PROFILE_USAGE_DUPLICATED, true);
 61  		// fill property tree
 62  		boost::property_tree::ptree pt;
 63  		pt.put (PEER_PROFILE_LAST_UPDATE_TIMESTAMP, m_LastUpdateTime);
 64  		if (m_LastUnreachableTime)
 65  			pt.put (PEER_PROFILE_LAST_UNREACHABLE_TIME, m_LastUnreachableTime);
 66  		pt.put_child (PEER_PROFILE_SECTION_PARTICIPATION, participation);
 67  		pt.put_child (PEER_PROFILE_SECTION_USAGE, usage);
 68  
 69  		// save to file
 70  		std::string ident = identHash.ToBase64 ();
 71  		std::string path = g_ProfilesStorage.Path(ident);
 72  
 73  		try {
 74  			boost::property_tree::write_ini (path, pt);
 75  		} catch (std::exception& ex) {
 76  			/* boost exception verbose enough */
 77  			LogPrint (eLogError, "Profiling: ", ex.what ());
 78  		}
 79  	}
 80  
 81  	void RouterProfile::Load (const IdentHash& identHash)
 82  	{
 83  		m_IsUpdated = false;
 84  		std::string ident = identHash.ToBase64 ();
 85  		std::string path = g_ProfilesStorage.Path(ident);
 86  		boost::property_tree::ptree pt;
 87  
 88  		if (!i2p::fs::Exists(path))
 89  		{
 90  			LogPrint(eLogWarning, "Profiling: No profile yet for ", ident);
 91  			return;
 92  		}
 93  
 94  		try
 95  		{
 96  			boost::property_tree::read_ini (path, pt);
 97  		} catch (std::exception& ex)
 98  		{
 99  			/* boost exception verbose enough */
100  			LogPrint (eLogError, "Profiling: ", ex.what ());
101  			return;
102  		}
103  
104  		try
105  		{
106  			auto ts = pt.get (PEER_PROFILE_LAST_UPDATE_TIMESTAMP, 0);
107  			if (ts)
108  				m_LastUpdateTime = ts;
109  			else	
110  			{	
111  				// try old lastupdatetime 
112  				auto ut = pt.get (PEER_PROFILE_LAST_UPDATE_TIME, "");
113  				if (ut.length () > 0)
114  				{	
115  					std::istringstream ss (ut); std::tm t;
116  					ss >> std::get_time(&t, "%Y-%b-%d %H:%M:%S");
117  					if (!ss.fail())
118  						m_LastUpdateTime = mktime (&t); // t is local time
119  				}	
120  			}	
121  			if (i2p::util::GetSecondsSinceEpoch () - m_LastUpdateTime < PEER_PROFILE_EXPIRATION_TIMEOUT)
122  			{
123  				m_LastUnreachableTime = pt.get (PEER_PROFILE_LAST_UNREACHABLE_TIME, 0);
124  				try
125  				{
126  					// read participations
127  					auto participations = pt.get_child (PEER_PROFILE_SECTION_PARTICIPATION);
128  					m_NumTunnelsAgreed = participations.get (PEER_PROFILE_PARTICIPATION_AGREED, 0);
129  					m_NumTunnelsDeclined = participations.get (PEER_PROFILE_PARTICIPATION_DECLINED, 0);
130  					m_NumTunnelsNonReplied = participations.get (PEER_PROFILE_PARTICIPATION_NON_REPLIED, 0);
131  				}
132  				catch (boost::property_tree::ptree_bad_path& ex)
133  				{
134  					LogPrint (eLogWarning, "Profiling: Missing section ", PEER_PROFILE_SECTION_PARTICIPATION, " in profile for ", ident);
135  				}
136  				try
137  				{
138  					// read usage
139  					auto usage = pt.get_child (PEER_PROFILE_SECTION_USAGE);
140  					m_NumTimesTaken = usage.get (PEER_PROFILE_USAGE_TAKEN, 0);
141  					m_NumTimesRejected = usage.get (PEER_PROFILE_USAGE_REJECTED, 0);
142  					m_HasConnected = usage.get (PEER_PROFILE_USAGE_CONNECTED, false);
143  					m_IsDuplicated = usage.get (PEER_PROFILE_USAGE_DUPLICATED, false);
144  				}
145  				catch (boost::property_tree::ptree_bad_path& ex)
146  				{
147  					LogPrint (eLogWarning, "Profiling: Missing section ", PEER_PROFILE_SECTION_USAGE, " in profile for ", ident);
148  				}
149  			}
150  			else
151  				*this = RouterProfile ();
152  		}
153  		catch (std::exception& ex)
154  		{
155  			LogPrint (eLogError, "Profiling: Can't read profile ", ident, " :", ex.what ());
156  		}
157  	}
158  
159  	void RouterProfile::TunnelBuildResponse (uint8_t ret)
160  	{
161  		UpdateTime ();
162  		if (ret > 0)
163  		{
164  			m_NumTunnelsDeclined++;
165  			m_LastDeclineTime = i2p::util::GetSecondsSinceEpoch ();
166  		}
167  		else
168  		{
169  		    m_NumTunnelsAgreed++;
170  			m_LastDeclineTime = 0;
171  		}
172  	}
173  
174  	void RouterProfile::TunnelNonReplied ()
175  	{
176  	    m_NumTunnelsNonReplied++;
177  		UpdateTime ();
178  		if (m_NumTunnelsNonReplied > 2*m_NumTunnelsAgreed && m_NumTunnelsNonReplied > 3)
179  		{
180  			m_LastDeclineTime = i2p::util::GetSecondsSinceEpoch ();
181  		}
182  	}
183  
184  	void RouterProfile::Unreachable (bool unreachable)
185  	{
186  		m_LastUnreachableTime = unreachable ? i2p::util::GetSecondsSinceEpoch () : 0;
187  		UpdateTime ();
188  	}
189  		
190  	void RouterProfile::Connected ()
191  	{
192  		m_HasConnected = true;
193  		UpdateTime ();
194  	}
195  
196  	void RouterProfile::Duplicated ()
197  	{
198  		m_IsDuplicated = true;
199  	}	
200  		
201  	bool RouterProfile::IsLowPartcipationRate () const
202  	{
203  		return 4*m_NumTunnelsAgreed < m_NumTunnelsDeclined; // < 20% rate
204  	}
205  
206  	bool RouterProfile::IsLowReplyRate () const
207  	{
208  		auto total = m_NumTunnelsAgreed + m_NumTunnelsDeclined;
209  		return m_NumTunnelsNonReplied > 10*(total + 1);
210  	}
211  
212  	bool RouterProfile::IsDeclinedRecently (uint64_t ts)
213  	{
214  		if (!m_LastDeclineTime) return false;
215  		if (ts > m_LastDeclineTime + PEER_PROFILE_DECLINED_RECENTLY_INTERVAL ||
216  		    ts + PEER_PROFILE_DECLINED_RECENTLY_INTERVAL < m_LastDeclineTime)
217  			m_LastDeclineTime = 0;
218  		return (bool)m_LastDeclineTime;
219  	}
220  
221  	bool RouterProfile::IsBad ()
222  	{
223  		if (IsUnreachable () || m_IsDuplicated) return true;
224  		auto ts = i2p::util::GetSecondsSinceEpoch ();
225  		if (ts > PEER_PROFILE_MAX_DECLINED_INTERVAL + m_LastDeclineTime) return false;
226  		if (IsDeclinedRecently (ts)) return true; 
227  		auto isBad = IsAlwaysDeclining () || IsLowPartcipationRate () /*|| IsLowReplyRate ()*/;
228  		if (isBad && m_NumTimesRejected > 10*(m_NumTimesTaken + 1))
229  		{
230  			// reset profile
231  			m_NumTunnelsAgreed = 0;
232  			m_NumTunnelsDeclined = 0;
233  			m_NumTunnelsNonReplied = 0;
234  			isBad = false;
235  		}
236  		if (isBad) m_NumTimesRejected++; else m_NumTimesTaken++;
237  		return isBad;
238  	}
239  
240  	bool RouterProfile::IsUnreachable ()
241  	{
242  		if (!m_LastUnreachableTime) return false;
243  		auto ts = i2p::util::GetSecondsSinceEpoch ();
244  		if (ts > m_LastUnreachableTime + PEER_PROFILE_UNREACHABLE_INTERVAL ||
245  		    ts + PEER_PROFILE_UNREACHABLE_INTERVAL < m_LastUnreachableTime)
246  			m_LastUnreachableTime = 0;
247  		return (bool)m_LastUnreachableTime;
248  	}
249  
250  	bool RouterProfile::IsUseful() const 
251  	{
252  	    return IsReal () || m_NumTunnelsNonReplied >= PEER_PROFILE_USEFUL_THRESHOLD;
253  	}
254  
255  	std::shared_ptr<RouterProfile> GetRouterProfile (const IdentHash& identHash)
256  	{
257  		{
258  			std::unique_lock<std::mutex> l(g_ProfilesMutex);
259  			auto it = g_Profiles.find (identHash);
260  			if (it != g_Profiles.end ())
261  			{
262  				it->second->SetLastAccessTime (i2p::util::GetSecondsSinceEpoch ());
263  				return it->second;
264  			}	
265  		}
266  		auto profile = netdb.NewRouterProfile ();
267  		profile->Load (identHash); // if possible
268  		std::lock_guard<std::mutex> l(g_ProfilesMutex);
269  		g_Profiles.emplace (identHash, profile);
270  		return profile;
271  	}
272  
273  	bool IsRouterBanned (const IdentHash& identHash)
274  	{
275  		std::lock_guard<std::mutex> l(g_ProfilesMutex);
276  		auto it = g_Profiles.find (identHash);
277  		if (it != g_Profiles.end ())
278  			return it->second->IsUnreachable ();
279  		return false;
280  	}	
281  
282  	bool IsRouterDuplicated (const IdentHash& identHash)
283  	{
284  		std::lock_guard<std::mutex> l(g_ProfilesMutex);
285  		auto it = g_Profiles.find (identHash);
286  		if (it != g_Profiles.end ())
287  			return it->second->IsDuplicated ();
288  		return false;
289  	}	
290  		
291  	void InitProfilesStorage ()
292  	{
293  		g_ProfilesStorage.SetPlace(i2p::fs::GetDataDir());
294  		g_ProfilesStorage.Init(i2p::data::GetBase64SubstitutionTable(), 64);
295  	}
296  		
297  	static void SaveProfilesToDisk (std::list<std::pair<i2p::data::IdentHash, std::shared_ptr<RouterProfile> > >&& profiles)
298  	{
299  		for (auto& it: profiles)
300  			if (it.second) it.second->Save (it.first);
301  	}	
302  		
303  	std::future<void> PersistProfiles ()
304  	{
305  		auto ts = i2p::util::GetSecondsSinceEpoch ();
306  		std::list<std::pair<i2p::data::IdentHash, std::shared_ptr<RouterProfile> > > tmp;
307  		{
308  			std::lock_guard<std::mutex> l(g_ProfilesMutex);
309  			for (auto it = g_Profiles.begin (); it != g_Profiles.end ();)
310  			{
311  				if (it->second->IsUpdated () && ts > it->second->GetLastPersistTime () + PEER_PROFILE_PERSIST_INTERVAL)
312  				{
313  					tmp.push_back (*it);
314  					it->second->SetLastPersistTime (ts);
315  					it->second->SetUpdated (false);
316  				}	
317  				if (!it->second->IsUpdated () && ts > std::max (it->second->GetLastUpdateTime (), it->second->GetLastAccessTime ()) + PEER_PROFILE_PERSIST_INTERVAL)
318  					it = g_Profiles.erase (it);
319  				else
320  					it++;
321  			}
322  		}
323  		if (!tmp.empty ())
324  			return std::async (std::launch::async, SaveProfilesToDisk, std::move (tmp));
325  		return std::future<void>();
326  	}
327  
328  	void SaveProfiles ()
329  	{
330  		std::unordered_map<i2p::data::IdentHash, std::shared_ptr<RouterProfile> > tmp;
331  		{
332  			std::lock_guard<std::mutex> l(g_ProfilesMutex);
333  			std::swap (tmp, g_Profiles);
334  		}
335  		auto ts = i2p::util::GetSecondsSinceEpoch ();
336  		for (auto& it: tmp)
337  			if (it.second->IsUseful() && (it.second->IsUpdated () || ts - it.second->GetLastUpdateTime () < PEER_PROFILE_EXPIRATION_TIMEOUT))
338  				it.second->Save (it.first);
339  	}
340  
341  	static void DeleteFilesFromDisk ()
342  	{
343  		std::vector<std::string> files;
344  		g_ProfilesStorage.Traverse(files);
345  		
346  		struct stat st;
347  		std::time_t now = std::time(nullptr);
348  		for (const auto& path: files) 
349  		{
350  			if (stat(path.c_str(), &st) != 0) 
351  			{	
352  				LogPrint(eLogWarning, "Profiling: Can't stat(): ", path);
353  				continue;
354  			}
355  			if (now - st.st_mtime >= PEER_PROFILE_EXPIRATION_TIMEOUT) 
356  			{
357  				LogPrint(eLogDebug, "Profiling: Removing expired peer profile: ", path);
358  				i2p::fs::Remove(path);
359  			}
360  		}
361  	}	
362  		
363  	std::future<void> DeleteObsoleteProfiles ()
364  	{
365  		{
366  			auto ts = i2p::util::GetSecondsSinceEpoch ();
367  			std::lock_guard<std::mutex> l(g_ProfilesMutex);
368  			for (auto it = g_Profiles.begin (); it != g_Profiles.end ();)
369  			{
370  				if (ts - it->second->GetLastUpdateTime () >= PEER_PROFILE_EXPIRATION_TIMEOUT)
371  					it = g_Profiles.erase (it);
372  				else
373  					it++;
374  			}
375  		}
376  
377  		return std::async (std::launch::async, DeleteFilesFromDisk);
378  	}
379  
380  	bool UpdateRouterProfile (const IdentHash& identHash, std::function<void (std::shared_ptr<RouterProfile>)> update)
381  	{
382  		if (!update) return true;
383  		std::shared_ptr<RouterProfile> profile;
384  		{
385  			std::lock_guard<std::mutex> l(g_ProfilesMutex);
386  			auto it = g_Profiles.find (identHash);
387  			if (it != g_Profiles.end ())
388  				profile = it->second;
389  		}
390  		if (profile)
391  		{
392  			update (profile);
393  			return true;
394  		}	
395  		// postpone
396  		std::lock_guard<std::mutex> l(g_PostponedUpdatesMutex);
397  		g_PostponedUpdates.emplace_back (identHash, update);
398  		return false;
399  	}	
400  
401  	static void ApplyPostponedUpdates (std::list<std::pair<i2p::data::IdentHash, std::function<void (std::shared_ptr<RouterProfile>)> > >&& updates)
402  	{
403  		for (const auto& [ident, update] : updates)
404  		{
405  			auto profile = GetRouterProfile (ident);
406  			update (profile);
407  		}	
408  	}	
409  		
410  	std::future<void> FlushPostponedRouterProfileUpdates ()
411  	{
412  		if (g_PostponedUpdates.empty ()) return std::future<void>();
413  
414  		std::list<std::pair<i2p::data::IdentHash, std::function<void (std::shared_ptr<RouterProfile>)> > > updates;
415  		{
416  			std::lock_guard<std::mutex> l(g_PostponedUpdatesMutex);
417  			g_PostponedUpdates.swap (updates);
418  		}		
419  		return std::async (std::launch::async, ApplyPostponedUpdates, std::move (updates));	
420  	}	
421  }
422  }