Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache expiration fix #1683

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ public class CacheItem
public string ETag;
/// <summary> Can be 'null' as not all APIs populated this value. Last-Modified value of API response in GMT: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified </summary>
public DateTime? LastModified;

public DateTime ExpirationDate;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace Mapbox.Platform.Cache
using Unity.UNetWeaver;
using UnityEngine;

namespace Mapbox.Platform.Cache
{
using System;
using Mapbox.Platform;
Expand All @@ -11,8 +14,6 @@

public class CachingWebFileSource : IFileSource, IDisposable
{


#if MAPBOX_DEBUG_CACHE
private string _className;
#endif
Expand All @@ -22,6 +23,9 @@ public class CachingWebFileSource : IFileSource, IDisposable
private Func<string> _getMapsSkuToken;
private bool _autoRefreshCache;

private const string EtagHeaderName = "ETag";
private const string LastModifiedHeaderName = "Last-Modified";
private const string CacheControlHeaderName = "Cache-Control";

public CachingWebFileSource(string accessToken, Func<string> getMapsSkuToken, bool autoRefreshCache)
{
Expand All @@ -34,8 +38,7 @@ public CachingWebFileSource(string accessToken, Func<string> getMapsSkuToken, bo
}


#region idisposable

#region idisposable

~CachingWebFileSource()
{
Expand Down Expand Up @@ -64,12 +67,12 @@ protected virtual void Dispose(bool disposeManagedResources)
}
}
}

_disposed = true;
}
}


#endregion
#endregion


/// <summary>
Expand Down Expand Up @@ -102,7 +105,8 @@ public void Clear()
}


public void ReInit() {
public void ReInit()
{
foreach (var cache in _caches)
{
cache.ReInit();
Expand All @@ -118,7 +122,6 @@ string uri
, string tilesetId = null
)
{

if (string.IsNullOrEmpty(tilesetId))
{
throw new Exception("Cannot cache without a tileset id");
Expand Down Expand Up @@ -150,6 +153,7 @@ string uri
uriBuilder.Query = accessTokenQuery + "&" + mapsSkuToken;
}
}

string finalUrl = uriBuilder.ToString();

#if MAPBOX_DEBUG_CACHE
Expand All @@ -166,58 +170,52 @@ string uri
callback(Response.FromCache(cachedItem.Data));

// check for updated tiles online if this is enabled in the settings
if (_autoRefreshCache)
if (cachedItem.ExpirationDate < DateTime.Now)
{
// check if tile on the web is newer than the one we already have locally
IAsyncRequestFactory.CreateRequest(
finalUrl,
(Response headerOnly) =>
timeout,
"If-None-Match", cachedItem.ETag,
(Response response) =>
{
// on error getting information from API just return. tile we have locally has already been returned above
if (headerOnly.HasError)
if (response.HasError || response.StatusCode == null)
{
return;
}

// TODO: remove Debug.Log before PR
//UnityEngine.Debug.LogFormat(
// "{1}{0}cached:{2}{0}header:{3}"
// , Environment.NewLine
// , finalUrl
// , cachedItem.ETag
// , headerOnly.Headers["ETag"]
//);

// data from cache is the same as on the web:
// * tile has already been returned above
// * make sure all all other caches have it too, but don't force insert via cache.add(false)
// additional ETag empty check: for backwards compability with old caches
if (!string.IsNullOrEmpty(cachedItem.ETag) && cachedItem.ETag.Equals(headerOnly.Headers["ETag"]))
if (response.StatusCode == 304) // 304 NOT MODIFIED
{
foreach (var cache in _caches)
{
cache.Add(tilesetId, tileId, cachedItem, false);
}
cachedItem.ExpirationDate = GetExpirationDate(response);
}
else if (response.StatusCode == 200) // 200 OK, it means etag&data has changed so need to update cache
{
string eTag = ETag(response);

// not all APIs populate 'Last-Modified' header
// don't log error if it's missing
DateTime? lastModified = GetLastModified(response);

DateTime expirationDate = GetExpirationDate(response);

cachedItem.Data = response.Data;
cachedItem.ETag = eTag;
cachedItem.LastModified = lastModified;
cachedItem.ExpirationDate = expirationDate;
}
else

foreach (var cache in _caches)
{
// TODO: remove Debug.Log before PR
UnityEngine.Debug.LogWarningFormat(
"updating cached tile {1} tilesetId:{2}{0}cached etag:{3}{0}remote etag:{4}{0}{5}"
, Environment.NewLine
, tileId
, tilesetId
, cachedItem.ETag
, headerOnly.Headers["ETag"]
, finalUrl
);

// request updated tile and pass callback to return new data to subscribers
requestTileAndCache(finalUrl, tilesetId, tileId, timeout, callback);
cache.Add(tilesetId, tileId, cachedItem, true);
}

callback(Response.FromCache(cachedItem.Data));
}
, timeout
, HttpRequestType.Head
);
}

Expand All @@ -233,34 +231,22 @@ string uri
}
}


private IAsyncRequest requestTileAndCache(string url, string tilesetId, CanonicalTileId tileId, int timeout, Action<Response> callback)
{
return IAsyncRequestFactory.CreateRequest(
url,
(Response r) =>
(Response response) =>
{
// if the request was successful add tile to all caches
if (!r.HasError && null != r.Data)
if (!response.HasError && null != response.Data)
{
string eTag = string.Empty;
DateTime? lastModified = null;

if (!r.Headers.ContainsKey("ETag"))
{
UnityEngine.Debug.LogWarningFormat("no 'ETag' header present in response for {0}", url);
}
else
{
eTag = r.Headers["ETag"];
}
string eTag = ETag(response);

// not all APIs populate 'Last-Modified' header
// don't log error if it's missing
if (r.Headers.ContainsKey("Last-Modified"))
{
lastModified = DateTime.ParseExact(r.Headers["Last-Modified"], "r", null);
}
DateTime? lastModified = GetLastModified(response);

DateTime expirationDate = GetExpirationDate(response);

// propagate to all caches forcing update
foreach (var cache in _caches)
Expand All @@ -270,27 +256,76 @@ private IAsyncRequest requestTileAndCache(string url, string tilesetId, Canonica
, tileId
, new CacheItem()
{
Data = r.Data,
Data = response.Data,
ETag = eTag,
LastModified = lastModified
LastModified = lastModified,
ExpirationDate = expirationDate
}
, true // force insert/update
);
}
}

if (null != callback)
{
r.IsUpdate = true;
callback(r);
response.IsUpdate = true;
callback(response);
}
}, timeout);
}

private string ETag(Response response)
{
string eTag = String.Empty;
if (!response.Headers.ContainsKey(EtagHeaderName))
{
Debug.LogWarning("no 'ETag' header present in response");
}
else
{
eTag = response.Headers[EtagHeaderName];
}

return eTag;
}

private DateTime? GetLastModified(Response response)
{
DateTime? lastModified = null;
if (response.Headers.ContainsKey(LastModifiedHeaderName))
{
lastModified = DateTime.ParseExact(response.Headers[LastModifiedHeaderName], "r", null);
}

class MemoryCacheAsyncRequest : IAsyncRequest
return lastModified;
}

private DateTime GetExpirationDate(Response response)
{
DateTime expirationDate = DateTime.Now;
if (response.Headers.ContainsKey(CacheControlHeaderName))
{
var cacheEntries = response.Headers[CacheControlHeaderName].Split(',');
if (cacheEntries.Length > 0)
{
foreach (var entry in cacheEntries)
{
var value = entry.Split('=');
if (value[0] == "max-age")
{
expirationDate = expirationDate + TimeSpan.FromSeconds(int.Parse(value[1]));
return expirationDate;
}
}
}
}

return expirationDate;
}


class MemoryCacheAsyncRequest : IAsyncRequest
{
public string RequestUrl { get; private set; }


Expand All @@ -302,14 +337,14 @@ public MemoryCacheAsyncRequest(string requestUrl)

public bool IsCompleted
{
get
{
return true;
}
get { return true; }
}


public HttpRequestType RequestType { get { return HttpRequestType.Get; } }
public HttpRequestType RequestType
{
get { return HttpRequestType.Get; }
}


public void Cancel()
Expand All @@ -318,4 +353,4 @@ public void Cancel()
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ tile_set INTEGER REFERENCES tilesets (id) ON DELETE CASCADE ON UPDATE CASCAD
tile_row BIGINT NOT NULL,
tile_data BLOB NOT NULL,
timestamp INTEGER NOT NULL,
expirationdate INTEGER NOT NULL,
etag TEXT,
lastmodified INTEGER,
PRIMARY KEY(
Expand Down Expand Up @@ -248,6 +249,7 @@ public void Add(string tilesetName, CanonicalTileId tileId, CacheItem item, bool
tile_row = tileId.Y,
tile_data = item.Data,
timestamp = (int)UnixTimestampUtils.To(DateTime.Now),
expirationdate = (int)UnixTimestampUtils.To(item.ExpirationDate),
etag = item.ETag
});
if (1 != rowsAffected)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,8 @@ public class tiles

/// <summary>Last-Modified header value of API response. Not all APIs populate it, will be -1 in that case. </summary>
public int? lastmodified { get; set; }

/// <summary>Last-Modified header value of API response. Not all APIs populate it, will be -1 in that case. </summary>
public int expirationdate { get; set; }
}
}
Loading