AssetCache.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. /*
  2. * Copyright (c) Contributors, http://opensimulator.org/
  3. * See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the OpenSim Project nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. using System;
  28. using System.Collections.Generic;
  29. using System.Reflection;
  30. using System.Threading;
  31. using OpenMetaverse;
  32. using OpenMetaverse.Packets;
  33. using log4net;
  34. using OpenSim.Framework.Statistics;
  35. using GlynnTucker.Cache;
  36. namespace OpenSim.Framework.Communications.Cache
  37. {
  38. public delegate void AssetRequestCallback(UUID assetID, AssetBase asset);
  39. /// <summary>
  40. /// Manages local cache of assets and their sending to viewers.
  41. ///
  42. /// This class actually encapsulates two largely separate mechanisms. One mechanism fetches assets either
  43. /// synchronously or async and passes the data back to the requester. The second mechanism fetches assets and
  44. /// sends packetised data directly back to the client. The only point where they meet is AssetReceived() and
  45. /// AssetNotFound(), which means they do share the same asset and texture caches.
  46. ///
  47. /// TODO: Assets in this cache are effectively immortal (they are never disposed of through old age).
  48. /// This is not a huge problem at the moment since other memory use usually dwarfs that used by assets
  49. /// but it's something to bear in mind.
  50. /// </summary>
  51. public class AssetCache : IAssetReceiver
  52. {
  53. protected ICache m_memcache = new SimpleMemoryCache();
  54. private static readonly ILog m_log
  55. = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
  56. /// <summary>
  57. /// The cache of assets. This does not include textures.
  58. /// </summary>
  59. //private Dictionary<UUID, AssetInfo> Assets;
  60. /// <summary>
  61. /// The cache of textures.
  62. /// </summary>
  63. //private Dictionary<UUID, TextureImage> Textures;
  64. /// <summary>
  65. /// Assets requests which are waiting for asset server data. This includes texture requests
  66. /// </summary>
  67. private Dictionary<UUID, AssetRequest> RequestedAssets;
  68. /// <summary>
  69. /// Asset requests with data which are ready to be sent back to requesters. This includes textures.
  70. /// </summary>
  71. private List<AssetRequest> AssetRequests;
  72. /// <summary>
  73. /// Until the asset request is fulfilled, each asset request is associated with a list of requesters
  74. /// </summary>
  75. private Dictionary<UUID, AssetRequestsList> RequestLists;
  76. /// <summary>
  77. /// The 'server' from which assets can be requested and to which assets are persisted.
  78. /// </summary>
  79. private readonly IAssetServer m_assetServer;
  80. public IAssetServer AssetServer
  81. {
  82. get { return m_assetServer; }
  83. }
  84. /// <summary>
  85. /// Report statistical data.
  86. /// </summary>
  87. public void ShowState()
  88. {
  89. m_log.InfoFormat("Memcache:{0} RequestLists:{1}",
  90. m_memcache.Count,
  91. // AssetRequests.Count,
  92. // RequestedAssets.Count,
  93. RequestLists.Count);
  94. }
  95. /// <summary>
  96. /// Clear the asset cache.
  97. /// </summary>
  98. public void Clear()
  99. {
  100. m_log.Info("[ASSET CACHE]: Clearing Asset cache");
  101. if (StatsManager.SimExtraStats != null)
  102. StatsManager.SimExtraStats.ClearAssetCacheStatistics();
  103. Initialize();
  104. }
  105. /// <summary>
  106. /// Initialize the cache.
  107. /// </summary>
  108. private void Initialize()
  109. {
  110. AssetRequests = new List<AssetRequest>();
  111. RequestedAssets = new Dictionary<UUID, AssetRequest>();
  112. RequestLists = new Dictionary<UUID, AssetRequestsList>();
  113. }
  114. /// <summary>
  115. /// Constructor. Initialize will need to be called separately.
  116. /// </summary>
  117. /// <param name="assetServer"></param>
  118. public AssetCache(IAssetServer assetServer)
  119. {
  120. m_log.Info("[ASSET CACHE]: Creating Asset cache");
  121. Initialize();
  122. m_assetServer = assetServer;
  123. m_assetServer.SetReceiver(this);
  124. Thread assetCacheThread = new Thread(RunAssetManager);
  125. assetCacheThread.Name = "AssetCacheThread";
  126. assetCacheThread.IsBackground = true;
  127. assetCacheThread.Start();
  128. ThreadTracker.Add(assetCacheThread);
  129. }
  130. /// <summary>
  131. /// Process the asset queue which holds data which is packeted up and sent
  132. /// directly back to the client.
  133. /// </summary>
  134. public void RunAssetManager()
  135. {
  136. while (true)
  137. {
  138. try
  139. {
  140. ProcessAssetQueue();
  141. Thread.Sleep(500);
  142. }
  143. catch (Exception e)
  144. {
  145. m_log.Error("[ASSET CACHE]: " + e);
  146. }
  147. }
  148. }
  149. /// <summary>
  150. /// Only get an asset if we already have it in the cache.
  151. /// </summary>
  152. /// <param name="assetId"></param>
  153. /// <param name="asset"></param>
  154. /// <returns>true if the asset was in the cache, false if it was not</returns>
  155. public bool TryGetCachedAsset(UUID assetId, out AssetBase asset)
  156. {
  157. Object tmp;
  158. if (m_memcache.TryGet(assetId, out tmp))
  159. {
  160. asset = (AssetBase)tmp;
  161. //m_log.Info("Retrieved from cache " + assetId);
  162. return true;
  163. }
  164. asset = null;
  165. return false;
  166. }
  167. /// <summary>
  168. /// Asynchronously retrieve an asset.
  169. /// </summary>
  170. /// <param name="assetId"></param>
  171. /// <param name="callback">
  172. /// <param name="isTexture"></param>
  173. /// A callback invoked when the asset has either been found or not found.
  174. /// If the asset was found this is called with the asset UUID and the asset data
  175. /// If the asset was not found this is still called with the asset UUID but with a null asset data reference</param>
  176. public void GetAsset(UUID assetId, AssetRequestCallback callback, bool isTexture)
  177. {
  178. //m_log.DebugFormat("[ASSET CACHE]: Requesting {0} {1}", isTexture ? "texture" : "asset", assetId);
  179. // Xantor 20080526:
  180. // if a request is made for an asset which is not in the cache yet, but has already been requested by
  181. // something else, queue up the callbacks on that requestor instead of swamping the assetserver
  182. // with multiple requests for the same asset.
  183. AssetBase asset;
  184. if (TryGetCachedAsset(assetId, out asset))
  185. {
  186. callback(assetId, asset);
  187. }
  188. else
  189. {
  190. // m_log.DebugFormat("[ASSET CACHE]: Adding request for {0} {1}", isTexture ? "texture" : "asset", assetId);
  191. NewAssetRequest req = new NewAssetRequest(callback);
  192. AssetRequestsList requestList;
  193. lock (RequestLists)
  194. {
  195. if (RequestLists.TryGetValue(assetId, out requestList)) // do we already have a request pending?
  196. {
  197. // m_log.DebugFormat("[ASSET CACHE]: Intercepted Duplicate request for {0} {1}", isTexture ? "texture" : "asset", assetId);
  198. // add to callbacks for this assetId
  199. RequestLists[assetId].Requests.Add(req);
  200. }
  201. else
  202. {
  203. // m_log.DebugFormat("[ASSET CACHE]: Adding request for {0} {1}", isTexture ? "texture" : "asset", assetId);
  204. requestList = new AssetRequestsList();
  205. requestList.TimeRequested = DateTime.Now;
  206. requestList.Requests.Add(req);
  207. RequestLists.Add(assetId, requestList);
  208. m_assetServer.RequestAsset(assetId, isTexture);
  209. }
  210. }
  211. }
  212. }
  213. /// <summary>
  214. /// Synchronously retreive an asset. If the asset isn't in the cache, a request will be made to the persistent store to
  215. /// load it into the cache.
  216. /// </summary>
  217. ///
  218. /// XXX We'll keep polling the cache until we get the asset or we exceed
  219. /// the allowed number of polls. This isn't a very good way of doing things since a single thread
  220. /// is processing inbound packets, so if the asset server is slow, we could block this for up to
  221. /// the timeout period. Whereever possible we want to use the asynchronous callback GetAsset()
  222. ///
  223. /// <param name="assetID"></param>
  224. /// <param name="isTexture"></param>
  225. /// <returns>null if the asset could not be retrieved</returns>
  226. public AssetBase GetAsset(UUID assetID, bool isTexture)
  227. {
  228. // I'm not going over 3 seconds since this will be blocking processing of all the other inbound
  229. // packets from the client.
  230. const int pollPeriod = 200;
  231. int maxPolls = 15;
  232. AssetBase asset;
  233. if (TryGetCachedAsset(assetID, out asset))
  234. {
  235. return asset;
  236. }
  237. m_assetServer.RequestAsset(assetID, isTexture);
  238. do
  239. {
  240. Thread.Sleep(pollPeriod);
  241. if (TryGetCachedAsset(assetID, out asset))
  242. {
  243. return asset;
  244. }
  245. }
  246. while (--maxPolls > 0);
  247. m_log.WarnFormat("[ASSET CACHE]: {0} {1} was not received before the retrieval timeout was reached",
  248. isTexture ? "texture" : "asset", assetID.ToString());
  249. return null;
  250. }
  251. /// <summary>
  252. /// Add an asset to both the persistent store and the cache.
  253. /// </summary>
  254. /// <param name="asset"></param>
  255. public void AddAsset(AssetBase asset)
  256. {
  257. if (!m_memcache.Contains(asset.FullID))
  258. {
  259. m_log.Info("[CACHE] Caching " + asset.FullID + " for 24 hours from last access");
  260. // Use 24 hour rolling asset cache.
  261. m_memcache.AddOrUpdate(asset.FullID, asset, TimeSpan.FromHours(24));
  262. // According to http://wiki.secondlife.com/wiki/AssetUploadRequest, Local signifies that the
  263. // information is stored locally. It could disappear, in which case we could send the
  264. // ImageNotInDatabase packet to tell the client this.
  265. //
  266. // However, this doesn't quite appear to work with local textures that are part of an avatar's
  267. // appearance texture set. Whilst sending an ImageNotInDatabase does trigger an automatic rebake
  268. // and reupload by the client, if those assets aren't pushed to the asset server anyway, then
  269. // on crossing onto another region server, other avatars can no longer get the required textures.
  270. // There doesn't appear to be any signal from the sim to the newly region border crossed client
  271. // asking it to reupload its local texture assets to that region server.
  272. //
  273. // One can think of other cunning ways around this. For instance, on a region crossing or teleport,
  274. // the original sim could squirt local assets to the new sim. Or the new sim could have pointers
  275. // to the original sim to fetch the 'local' assets (this is getting more complicated).
  276. //
  277. // But for now, we're going to take the easy way out and store local assets globally.
  278. //
  279. // TODO: Also, Temporary is now deprecated. We should start ignoring it and not passing it out from LLClientView.
  280. if (!asset.Temporary || asset.Local)
  281. {
  282. m_assetServer.StoreAsset(asset);
  283. }
  284. }
  285. }
  286. /// <summary>
  287. /// Allows you to clear a specific asset by uuid out
  288. /// of the asset cache. This is needed because the osdynamic
  289. /// texture code grows the asset cache without bounds. The
  290. /// real solution here is a much better cache archicture, but
  291. /// this is a stop gap measure until we have such a thing.
  292. /// </summary>
  293. public void ExpireAsset(UUID uuid)
  294. {
  295. // uuid is unique, so no need to worry about it showing up
  296. // in the 2 caches differently. Also, locks are probably
  297. // needed in all of this, or move to synchronized non
  298. // generic forms for Dictionaries.
  299. if (m_memcache.Contains(uuid))
  300. {
  301. m_memcache.Remove(uuid);
  302. }
  303. }
  304. // See IAssetReceiver
  305. public void AssetReceived(AssetBase asset, bool IsTexture)
  306. {
  307. AssetInfo assetInf = new AssetInfo(asset);
  308. if (!m_memcache.Contains(assetInf.FullID))
  309. {
  310. m_memcache.AddOrUpdate(assetInf.FullID, assetInf, TimeSpan.FromHours(24));
  311. if (StatsManager.SimExtraStats != null)
  312. {
  313. StatsManager.SimExtraStats.AddAsset(assetInf);
  314. }
  315. if (RequestedAssets.ContainsKey(assetInf.FullID))
  316. {
  317. AssetRequest req = RequestedAssets[assetInf.FullID];
  318. req.AssetInf = assetInf;
  319. req.NumPackets = CalculateNumPackets(assetInf.Data);
  320. RequestedAssets.Remove(assetInf.FullID);
  321. // If it's a direct request for a script, drop it
  322. // because it's a hacked client
  323. if (req.AssetRequestSource != 2 || assetInf.Type != 10)
  324. AssetRequests.Add(req);
  325. }
  326. }
  327. // Notify requesters for this asset
  328. AssetRequestsList reqList;
  329. lock (RequestLists)
  330. {
  331. if (RequestLists.TryGetValue(asset.FullID, out reqList))
  332. RequestLists.Remove(asset.FullID);
  333. }
  334. if (reqList != null)
  335. {
  336. if (StatsManager.SimExtraStats != null)
  337. StatsManager.SimExtraStats.AddAssetRequestTimeAfterCacheMiss(DateTime.Now - reqList.TimeRequested);
  338. foreach (NewAssetRequest req in reqList.Requests)
  339. {
  340. // Xantor 20080526 are we really calling all the callbacks if multiple queued for 1 request? -- Yes, checked
  341. // m_log.DebugFormat("[ASSET CACHE]: Callback for asset {0}", asset.FullID);
  342. req.Callback(asset.FullID, asset);
  343. }
  344. }
  345. }
  346. // See IAssetReceiver
  347. public void AssetNotFound(UUID assetID, bool IsTexture)
  348. {
  349. // m_log.WarnFormat("[ASSET CACHE]: AssetNotFound for {0}", assetID);
  350. // Remember the fact that this asset could not be found to prevent delays from repeated requests
  351. m_memcache.Add(assetID, null, TimeSpan.FromHours(24));
  352. // Notify requesters for this asset
  353. AssetRequestsList reqList;
  354. lock (RequestLists)
  355. {
  356. if (RequestLists.TryGetValue(assetID, out reqList))
  357. RequestLists.Remove(assetID);
  358. }
  359. if (reqList != null)
  360. {
  361. if (StatsManager.SimExtraStats != null)
  362. StatsManager.SimExtraStats.AddAssetRequestTimeAfterCacheMiss(DateTime.Now - reqList.TimeRequested);
  363. foreach (NewAssetRequest req in reqList.Requests)
  364. {
  365. req.Callback(assetID, null);
  366. }
  367. }
  368. }
  369. /// <summary>
  370. /// Calculate the number of packets required to send the asset to the client.
  371. /// </summary>
  372. /// <param name="data"></param>
  373. /// <returns></returns>
  374. private static int CalculateNumPackets(byte[] data)
  375. {
  376. const uint m_maxPacketSize = 600;
  377. int numPackets = 1;
  378. if (data.LongLength > m_maxPacketSize)
  379. {
  380. // over max number of bytes so split up file
  381. long restData = data.LongLength - m_maxPacketSize;
  382. int restPackets = (int)((restData + m_maxPacketSize - 1) / m_maxPacketSize);
  383. numPackets += restPackets;
  384. }
  385. return numPackets;
  386. }
  387. /// <summary>
  388. /// Handle an asset request from the client. The result will be sent back asynchronously.
  389. /// </summary>
  390. /// <param name="userInfo"></param>
  391. /// <param name="transferRequest"></param>
  392. public void AddAssetRequest(IClientAPI userInfo, TransferRequestPacket transferRequest)
  393. {
  394. UUID requestID = UUID.Zero;
  395. byte source = 2;
  396. if (transferRequest.TransferInfo.SourceType == 2)
  397. {
  398. //direct asset request
  399. requestID = new UUID(transferRequest.TransferInfo.Params, 0);
  400. }
  401. else if (transferRequest.TransferInfo.SourceType == 3)
  402. {
  403. //inventory asset request
  404. requestID = new UUID(transferRequest.TransferInfo.Params, 80);
  405. source = 3;
  406. //Console.WriteLine("asset request " + requestID);
  407. }
  408. //check to see if asset is in local cache, if not we need to request it from asset server.
  409. //Console.WriteLine("asset request " + requestID);
  410. if (!m_memcache.Contains(requestID))
  411. {
  412. //not found asset
  413. // so request from asset server
  414. if (!RequestedAssets.ContainsKey(requestID))
  415. {
  416. AssetRequest request = new AssetRequest();
  417. request.RequestUser = userInfo;
  418. request.RequestAssetID = requestID;
  419. request.TransferRequestID = transferRequest.TransferInfo.TransferID;
  420. request.AssetRequestSource = source;
  421. request.Params = transferRequest.TransferInfo.Params;
  422. RequestedAssets.Add(requestID, request);
  423. m_assetServer.RequestAsset(requestID, false);
  424. }
  425. return;
  426. }
  427. // It has an entry in our cache
  428. AssetBase asset = (AssetBase)m_memcache[requestID];
  429. // FIXME: We never tell the client about assets which do not exist when requested by this transfer mechanism, which can't be right.
  430. if (null == asset)
  431. {
  432. //m_log.DebugFormat("[ASSET CACHE]: Asset transfer request for asset which is {0} already known to be missing. Dropping", requestID);
  433. return;
  434. }
  435. // Scripts cannot be retrieved by direct request
  436. if (transferRequest.TransferInfo.SourceType == 2 && asset.Type == 10)
  437. return;
  438. // The asset is knosn to exist and is in our cache, so add it to the AssetRequests list
  439. AssetRequest req = new AssetRequest();
  440. req.RequestUser = userInfo;
  441. req.RequestAssetID = requestID;
  442. req.TransferRequestID = transferRequest.TransferInfo.TransferID;
  443. req.AssetRequestSource = source;
  444. req.Params = transferRequest.TransferInfo.Params;
  445. req.AssetInf = new AssetInfo(asset);
  446. req.NumPackets = CalculateNumPackets(asset.Data);
  447. AssetRequests.Add(req);
  448. }
  449. /// <summary>
  450. /// Process the asset queue which sends packets directly back to the client.
  451. /// </summary>
  452. private void ProcessAssetQueue()
  453. {
  454. //should move the asset downloading to a module, like has been done with texture downloading
  455. if (AssetRequests.Count == 0)
  456. {
  457. //no requests waiting
  458. return;
  459. }
  460. // if less than 5, do all of them
  461. int num = Math.Min(5, AssetRequests.Count);
  462. AssetRequest req;
  463. AssetRequestToClient req2 = null;
  464. for (int i = 0; i < num; i++)
  465. {
  466. req = AssetRequests[i];
  467. if (req2 == null)
  468. {
  469. req2 = new AssetRequestToClient();
  470. }
  471. // Trying to limit memory usage by only creating AssetRequestToClient if needed
  472. //req2 = new AssetRequestToClient();
  473. req2.AssetInf = req.AssetInf;
  474. req2.AssetRequestSource = req.AssetRequestSource;
  475. req2.DataPointer = req.DataPointer;
  476. req2.DiscardLevel = req.DiscardLevel;
  477. req2.ImageInfo = req.ImageInfo;
  478. req2.IsTextureRequest = req.IsTextureRequest;
  479. req2.NumPackets = req.NumPackets;
  480. req2.PacketCounter = req.PacketCounter;
  481. req2.Params = req.Params;
  482. req2.RequestAssetID = req.RequestAssetID;
  483. req2.TransferRequestID = req.TransferRequestID;
  484. req.RequestUser.SendAsset(req2);
  485. }
  486. //remove requests that have been completed
  487. for (int i = 0; i < num; i++)
  488. {
  489. AssetRequests.RemoveAt(0);
  490. }
  491. }
  492. public class AssetRequest
  493. {
  494. public IClientAPI RequestUser;
  495. public UUID RequestAssetID;
  496. public AssetInfo AssetInf;
  497. public TextureImage ImageInfo;
  498. public UUID TransferRequestID;
  499. public long DataPointer = 0;
  500. public int NumPackets = 0;
  501. public int PacketCounter = 0;
  502. public bool IsTextureRequest;
  503. public byte AssetRequestSource = 2;
  504. public byte[] Params = null;
  505. //public bool AssetInCache;
  506. //public int TimeRequested;
  507. public int DiscardLevel = -1;
  508. }
  509. public class AssetInfo : AssetBase
  510. {
  511. public AssetInfo(AssetBase aBase)
  512. {
  513. Data = aBase.Data;
  514. FullID = aBase.FullID;
  515. Type = aBase.Type;
  516. Name = aBase.Name;
  517. Description = aBase.Description;
  518. }
  519. }
  520. public class TextureImage : AssetBase
  521. {
  522. public TextureImage(AssetBase aBase)
  523. {
  524. Data = aBase.Data;
  525. FullID = aBase.FullID;
  526. Type = aBase.Type;
  527. Name = aBase.Name;
  528. Description = aBase.Description;
  529. }
  530. }
  531. /// <summary>
  532. /// A list of requests for a particular asset.
  533. /// </summary>
  534. public class AssetRequestsList
  535. {
  536. /// <summary>
  537. /// A list of requests for assets
  538. /// </summary>
  539. public List<NewAssetRequest> Requests = new List<NewAssetRequest>();
  540. /// <summary>
  541. /// Record the time that this request was first made.
  542. /// </summary>
  543. public DateTime TimeRequested;
  544. }
  545. /// <summary>
  546. /// Represent a request for an asset that has yet to be fulfilled.
  547. /// </summary>
  548. public class NewAssetRequest
  549. {
  550. public AssetRequestCallback Callback;
  551. public NewAssetRequest(AssetRequestCallback callback)
  552. {
  553. Callback = callback;
  554. }
  555. }
  556. }
  557. }