AssetCache.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  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:{1} RequestLists:{2}",
  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. /// A callback invoked when the asset has either been found or not found.
  173. /// If the asset was found this is called with the asset UUID and the asset data
  174. /// If the asset was not found this is still called with the asset UUID but with a null asset data reference</param>
  175. public void GetAsset(UUID assetId, AssetRequestCallback callback, bool isTexture)
  176. {
  177. //m_log.DebugFormat("[ASSET CACHE]: Requesting {0} {1}", isTexture ? "texture" : "asset", assetId);
  178. // Xantor 20080526:
  179. // if a request is made for an asset which is not in the cache yet, but has already been requested by
  180. // something else, queue up the callbacks on that requestor instead of swamping the assetserver
  181. // with multiple requests for the same asset.
  182. AssetBase asset;
  183. if (TryGetCachedAsset(assetId, out asset))
  184. {
  185. callback(assetId, asset);
  186. }
  187. else
  188. {
  189. // m_log.DebugFormat("[ASSET CACHE]: Adding request for {0} {1}", isTexture ? "texture" : "asset", assetId);
  190. NewAssetRequest req = new NewAssetRequest(callback);
  191. AssetRequestsList requestList;
  192. lock (RequestLists)
  193. {
  194. if (RequestLists.TryGetValue(assetId, out requestList)) // do we already have a request pending?
  195. {
  196. // m_log.DebugFormat("[ASSET CACHE]: Intercepted Duplicate request for {0} {1}", isTexture ? "texture" : "asset", assetId);
  197. // add to callbacks for this assetId
  198. RequestLists[assetId].Requests.Add(req);
  199. }
  200. else
  201. {
  202. // m_log.DebugFormat("[ASSET CACHE]: Adding request for {0} {1}", isTexture ? "texture" : "asset", assetId);
  203. requestList = new AssetRequestsList();
  204. requestList.TimeRequested = DateTime.Now;
  205. requestList.Requests.Add(req);
  206. RequestLists.Add(assetId, requestList);
  207. m_assetServer.RequestAsset(assetId, isTexture);
  208. }
  209. }
  210. }
  211. }
  212. /// <summary>
  213. /// Synchronously retreive an asset. If the asset isn't in the cache, a request will be made to the persistent store to
  214. /// load it into the cache.
  215. ///
  216. /// XXX We'll keep polling the cache until we get the asset or we exceed
  217. /// the allowed number of polls. This isn't a very good way of doing things since a single thread
  218. /// is processing inbound packets, so if the asset server is slow, we could block this for up to
  219. /// the timeout period. What we might want to do is register asynchronous callbacks on asset
  220. /// receipt in the same manner as the TextureDownloadModule. Of course,
  221. /// a timeout before asset receipt usually isn't fatal, the operation will work on the retry when the
  222. /// asset is much more likely to have made it into the cache.
  223. /// </summary>
  224. /// <param name="assetID"></param>
  225. /// <param name="isTexture"></param>
  226. /// <returns>null if the asset could not be retrieved</returns>
  227. public AssetBase GetAsset(UUID assetID, bool isTexture)
  228. {
  229. // I'm not going over 3 seconds since this will be blocking processing of all the other inbound
  230. // packets from the client.
  231. int pollPeriod = 200;
  232. int maxPolls = 15;
  233. AssetBase asset;
  234. if (TryGetCachedAsset(assetID, out asset))
  235. {
  236. return asset;
  237. }
  238. m_assetServer.RequestAsset(assetID, isTexture);
  239. do
  240. {
  241. Thread.Sleep(pollPeriod);
  242. if (TryGetCachedAsset(assetID, out asset))
  243. {
  244. return asset;
  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. // Notify requesters for this asset
  351. AssetRequestsList reqList;
  352. lock (RequestLists)
  353. {
  354. if (RequestLists.TryGetValue(assetID, out reqList))
  355. RequestLists.Remove(assetID);
  356. }
  357. if (reqList != null)
  358. {
  359. if (StatsManager.SimExtraStats != null)
  360. StatsManager.SimExtraStats.AddAssetRequestTimeAfterCacheMiss(DateTime.Now - reqList.TimeRequested);
  361. foreach (NewAssetRequest req in reqList.Requests)
  362. {
  363. req.Callback(assetID, null);
  364. }
  365. }
  366. }
  367. /// <summary>
  368. /// Calculate the number of packets required to send the asset to the client.
  369. /// </summary>
  370. /// <param name="data"></param>
  371. /// <returns></returns>
  372. private static int CalculateNumPackets(byte[] data)
  373. {
  374. const uint m_maxPacketSize = 600;
  375. int numPackets = 1;
  376. if (data.LongLength > m_maxPacketSize)
  377. {
  378. // over max number of bytes so split up file
  379. long restData = data.LongLength - m_maxPacketSize;
  380. int restPackets = (int)((restData + m_maxPacketSize - 1) / m_maxPacketSize);
  381. numPackets += restPackets;
  382. }
  383. return numPackets;
  384. }
  385. /// <summary>
  386. /// Handle an asset request from the client. The result will be sent back asynchronously.
  387. /// </summary>
  388. /// <param name="userInfo"></param>
  389. /// <param name="transferRequest"></param>
  390. public void AddAssetRequest(IClientAPI userInfo, TransferRequestPacket transferRequest)
  391. {
  392. UUID requestID = UUID.Zero;
  393. byte source = 2;
  394. if (transferRequest.TransferInfo.SourceType == 2)
  395. {
  396. //direct asset request
  397. requestID = new UUID(transferRequest.TransferInfo.Params, 0);
  398. }
  399. else if (transferRequest.TransferInfo.SourceType == 3)
  400. {
  401. //inventory asset request
  402. requestID = new UUID(transferRequest.TransferInfo.Params, 80);
  403. source = 3;
  404. //Console.WriteLine("asset request " + requestID);
  405. }
  406. //check to see if asset is in local cache, if not we need to request it from asset server.
  407. //Console.WriteLine("asset request " + requestID);
  408. if (!m_memcache.Contains(requestID))
  409. {
  410. //not found asset
  411. // so request from asset server
  412. if (!RequestedAssets.ContainsKey(requestID))
  413. {
  414. AssetRequest request = new AssetRequest();
  415. request.RequestUser = userInfo;
  416. request.RequestAssetID = requestID;
  417. request.TransferRequestID = transferRequest.TransferInfo.TransferID;
  418. request.AssetRequestSource = source;
  419. request.Params = transferRequest.TransferInfo.Params;
  420. RequestedAssets.Add(requestID, request);
  421. m_assetServer.RequestAsset(requestID, false);
  422. }
  423. return;
  424. }
  425. // It has an entry in our cache
  426. AssetBase asset = (AssetBase)m_memcache[requestID];
  427. // FIXME: We never tell the client about assets which do not exist when requested by this transfer mechanism, which can't be right.
  428. if (null == asset)
  429. {
  430. //m_log.DebugFormat("[ASSET CACHE]: Asset transfer request for asset which is {0} already known to be missing. Dropping", requestID);
  431. return;
  432. }
  433. // Scripts cannot be retrieved by direct request
  434. if (transferRequest.TransferInfo.SourceType == 2 && asset.Type == 10)
  435. return;
  436. // The asset is knosn to exist and is in our cache, so add it to the AssetRequests list
  437. AssetRequest req = new AssetRequest();
  438. req.RequestUser = userInfo;
  439. req.RequestAssetID = requestID;
  440. req.TransferRequestID = transferRequest.TransferInfo.TransferID;
  441. req.AssetRequestSource = source;
  442. req.Params = transferRequest.TransferInfo.Params;
  443. req.AssetInf = new AssetInfo(asset);
  444. req.NumPackets = CalculateNumPackets(asset.Data);
  445. AssetRequests.Add(req);
  446. }
  447. /// <summary>
  448. /// Process the asset queue which sends packets directly back to the client.
  449. /// </summary>
  450. private void ProcessAssetQueue()
  451. {
  452. //should move the asset downloading to a module, like has been done with texture downloading
  453. if (AssetRequests.Count == 0)
  454. {
  455. //no requests waiting
  456. return;
  457. }
  458. // if less than 5, do all of them
  459. int num = Math.Min(5, AssetRequests.Count);
  460. AssetRequest req;
  461. AssetRequestToClient req2 = null;
  462. for (int i = 0; i < num; i++)
  463. {
  464. req = (AssetRequest)AssetRequests[i];
  465. if (req2 == null)
  466. {
  467. req2 = new AssetRequestToClient();
  468. }
  469. // Trying to limit memory usage by only creating AssetRequestToClient if needed
  470. //req2 = new AssetRequestToClient();
  471. req2.AssetInf = (AssetBase)req.AssetInf;
  472. req2.AssetRequestSource = req.AssetRequestSource;
  473. req2.DataPointer = req.DataPointer;
  474. req2.DiscardLevel = req.DiscardLevel;
  475. req2.ImageInfo = (AssetBase)req.ImageInfo;
  476. req2.IsTextureRequest = req.IsTextureRequest;
  477. req2.NumPackets = req.NumPackets;
  478. req2.PacketCounter = req.PacketCounter;
  479. req2.Params = req.Params;
  480. req2.RequestAssetID = req.RequestAssetID;
  481. req2.TransferRequestID = req.TransferRequestID;
  482. req.RequestUser.SendAsset(req2);
  483. }
  484. //remove requests that have been completed
  485. for (int i = 0; i < num; i++)
  486. {
  487. AssetRequests.RemoveAt(0);
  488. }
  489. }
  490. public class AssetRequest
  491. {
  492. public IClientAPI RequestUser;
  493. public UUID RequestAssetID;
  494. public AssetInfo AssetInf;
  495. public TextureImage ImageInfo;
  496. public UUID TransferRequestID;
  497. public long DataPointer = 0;
  498. public int NumPackets = 0;
  499. public int PacketCounter = 0;
  500. public bool IsTextureRequest;
  501. public byte AssetRequestSource = 2;
  502. public byte[] Params = null;
  503. //public bool AssetInCache;
  504. //public int TimeRequested;
  505. public int DiscardLevel = -1;
  506. }
  507. public class AssetInfo : AssetBase
  508. {
  509. public AssetInfo(AssetBase aBase)
  510. {
  511. Data = aBase.Data;
  512. FullID = aBase.FullID;
  513. Type = aBase.Type;
  514. Name = aBase.Name;
  515. Description = aBase.Description;
  516. }
  517. }
  518. public class TextureImage : AssetBase
  519. {
  520. public TextureImage(AssetBase aBase)
  521. {
  522. Data = aBase.Data;
  523. FullID = aBase.FullID;
  524. Type = aBase.Type;
  525. Name = aBase.Name;
  526. Description = aBase.Description;
  527. }
  528. }
  529. /// <summary>
  530. /// A list of requests for a particular asset.
  531. /// </summary>
  532. public class AssetRequestsList
  533. {
  534. /// <summary>
  535. /// A list of requests for assets
  536. /// </summary>
  537. public List<NewAssetRequest> Requests = new List<NewAssetRequest>();
  538. /// <summary>
  539. /// Record the time that this request was first made.
  540. /// </summary>
  541. public DateTime TimeRequested;
  542. }
  543. /// <summary>
  544. /// Represent a request for an asset that has yet to be fulfilled.
  545. /// </summary>
  546. public class NewAssetRequest
  547. {
  548. public AssetRequestCallback Callback;
  549. public NewAssetRequest(AssetRequestCallback callback)
  550. {
  551. Callback = callback;
  552. }
  553. }
  554. }
  555. }