Skip to content
Merged
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
64 changes: 39 additions & 25 deletions Areas/Admin/Controllers/SettingsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,18 @@ public async Task<IActionResult> Index()
}

// Fallbacks for unset values
if (settings.MaxCacheTileSizeInMB == 0)
{
settings.MaxCacheTileSizeInMB = ApplicationSettings.DefaultMaxCacheTileSizeInMB;
}

if (settings.UploadSizeLimitMB == 0)
{
if (settings.MaxCacheTileSizeInMB == 0)
{
settings.MaxCacheTileSizeInMB = ApplicationSettings.DefaultMaxCacheTileSizeInMB;
}

if (settings.TileMetadataHotCacheSizeMB == 0)
{
settings.TileMetadataHotCacheSizeMB = ApplicationSettings.DefaultTileMetadataHotCacheSizeMB;
}

if (settings.UploadSizeLimitMB == 0)
{
settings.UploadSizeLimitMB = ApplicationSettings.DefaultUploadSizeLimitMB;
}

Expand Down Expand Up @@ -143,14 +148,21 @@ public async Task<IActionResult> Update(ApplicationSettings updatedSettings)
}

// Authenticated users should always have at least the same rate limit as anonymous users.
if (updatedSettings.TileRateLimitAuthenticatedPerMinute < updatedSettings.TileRateLimitPerMinute)
{
ModelState.AddModelError(nameof(updatedSettings.TileRateLimitAuthenticatedPerMinute),
"Authenticated rate limit must be equal to or greater than the anonymous rate limit.");
}

if (currentSettings != null)
{
if (updatedSettings.TileRateLimitAuthenticatedPerMinute < updatedSettings.TileRateLimitPerMinute)
{
ModelState.AddModelError(nameof(updatedSettings.TileRateLimitAuthenticatedPerMinute),
"Authenticated rate limit must be equal to or greater than the anonymous rate limit.");
}

if (updatedSettings.TileMetadataHotCacheSizeMB != -1 &&
(updatedSettings.TileMetadataHotCacheSizeMB < 16 || updatedSettings.TileMetadataHotCacheSizeMB > 512))
{
ModelState.AddModelError(nameof(updatedSettings.TileMetadataHotCacheSizeMB),
"Tile metadata hot cache size must be -1 (disable) or between 16 and 512 MB.");
}

if (currentSettings != null)
{
// Validate tile provider settings before model validation.
NormalizeTileProviderSettings(currentSettings, updatedSettings);
}
Expand Down Expand Up @@ -190,11 +202,12 @@ void Track<T>(string name, T oldVal, T newVal)
changes.Add("TileProviderApiKey: [updated]");
}
Track("TileRateLimitEnabled", currentSettings.TileRateLimitEnabled, updatedSettings.TileRateLimitEnabled);
Track("TileRateLimitPerMinute", currentSettings.TileRateLimitPerMinute, updatedSettings.TileRateLimitPerMinute);
Track("TileRateLimitAuthenticatedPerMinute", currentSettings.TileRateLimitAuthenticatedPerMinute, updatedSettings.TileRateLimitAuthenticatedPerMinute);
Track("TileOutboundBudgetPerIpPerMinute", currentSettings.TileOutboundBudgetPerIpPerMinute, updatedSettings.TileOutboundBudgetPerIpPerMinute);
Track("ProxyImageRateLimitEnabled", currentSettings.ProxyImageRateLimitEnabled, updatedSettings.ProxyImageRateLimitEnabled);
Track("ProxyImageRateLimitPerMinute", currentSettings.ProxyImageRateLimitPerMinute, updatedSettings.ProxyImageRateLimitPerMinute);
Track("TileRateLimitPerMinute", currentSettings.TileRateLimitPerMinute, updatedSettings.TileRateLimitPerMinute);
Track("TileRateLimitAuthenticatedPerMinute", currentSettings.TileRateLimitAuthenticatedPerMinute, updatedSettings.TileRateLimitAuthenticatedPerMinute);
Track("TileOutboundBudgetPerIpPerMinute", currentSettings.TileOutboundBudgetPerIpPerMinute, updatedSettings.TileOutboundBudgetPerIpPerMinute);
Track("TileMetadataHotCacheSizeMB", currentSettings.TileMetadataHotCacheSizeMB, updatedSettings.TileMetadataHotCacheSizeMB);
Track("ProxyImageRateLimitEnabled", currentSettings.ProxyImageRateLimitEnabled, updatedSettings.ProxyImageRateLimitEnabled);
Track("ProxyImageRateLimitPerMinute", currentSettings.ProxyImageRateLimitPerMinute, updatedSettings.ProxyImageRateLimitPerMinute);

// Trip Place Auto-Visited settings
Track("VisitedRequiredHits", currentSettings.VisitedRequiredHits, updatedSettings.VisitedRequiredHits);
Expand Down Expand Up @@ -237,11 +250,12 @@ void Track<T>(string name, T oldVal, T newVal)
currentSettings.TileProviderAttribution = updatedSettings.TileProviderAttribution;
currentSettings.TileProviderApiKey = updatedSettings.TileProviderApiKey;
currentSettings.TileRateLimitEnabled = updatedSettings.TileRateLimitEnabled;
currentSettings.TileRateLimitPerMinute = updatedSettings.TileRateLimitPerMinute;
currentSettings.TileRateLimitAuthenticatedPerMinute = updatedSettings.TileRateLimitAuthenticatedPerMinute;
currentSettings.TileOutboundBudgetPerIpPerMinute = updatedSettings.TileOutboundBudgetPerIpPerMinute;
currentSettings.ProxyImageRateLimitEnabled = updatedSettings.ProxyImageRateLimitEnabled;
currentSettings.ProxyImageRateLimitPerMinute = updatedSettings.ProxyImageRateLimitPerMinute;
currentSettings.TileRateLimitPerMinute = updatedSettings.TileRateLimitPerMinute;
currentSettings.TileRateLimitAuthenticatedPerMinute = updatedSettings.TileRateLimitAuthenticatedPerMinute;
currentSettings.TileOutboundBudgetPerIpPerMinute = updatedSettings.TileOutboundBudgetPerIpPerMinute;
currentSettings.TileMetadataHotCacheSizeMB = updatedSettings.TileMetadataHotCacheSizeMB;
currentSettings.ProxyImageRateLimitEnabled = updatedSettings.ProxyImageRateLimitEnabled;
currentSettings.ProxyImageRateLimitPerMinute = updatedSettings.ProxyImageRateLimitPerMinute;

// Trip Place Auto-Visited settings
currentSettings.VisitedRequiredHits = updatedSettings.VisitedRequiredHits;
Expand Down
41 changes: 41 additions & 0 deletions Areas/Admin/Views/Settings/Index.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
@model ApplicationSettings
@{
var tileMetadataHotCacheEntries = Wayfarer.Services.TileMetadataHotCache.GetApproximateEntryLimit(Model.TileMetadataHotCacheSizeMB);
}
@using Wayfarer.Util
@{
var isRegistrationOpen = (ViewData["IsRegistrationOpen"] as bool?) ?? false;
Expand Down Expand Up @@ -720,6 +723,44 @@
</div>
</div>

<div class="col-md-8 mt-3">
<label asp-for="TileMetadataHotCacheSizeMB" class="form-label">
Tile Metadata Hot Cache Budget:
</label>

<div class="input-group">
<input asp-for="TileMetadataHotCacheSizeMB"
class="form-control"
type="number"
min="-1"
max="512"
data-entry-bytes="@Wayfarer.Services.TileMetadataHotCache.EstimatedBytesPerHotMetadataEntry" />
<span class="input-group-text">MB</span>
</div>

<span asp-validation-for="TileMetadataHotCacheSizeMB" class="text-danger"></span>

<div class="form-text">
• Approximate RAM budget for the in-process tile metadata hot cache used only for zoom levels
≥ 9.<br/>
• Stores tile metadata only. It does not store tile image bytes.<br/>
• Set to <code>-1</code> to disable the feature.<br/>
• Allowed values are <code>-1</code> or <code>16-512 MB</code>.<br/>
• The derived entry count is approximate and does not represent exact process memory usage.<br/>
</div>

<div class="alert alert-secondary mt-2 mb-0 py-2" id="tileMetadataHotCacheHint">
@if (Model.TileMetadataHotCacheSizeMB == -1)
{
<span>Hot metadata cache disabled.</span>
}
else
{
<span>@Model.TileMetadataHotCacheSizeMB MB ≈ @tileMetadataHotCacheEntries metadata entries</span>
}
</div>
</div>


<div class="col-md-4">
<div class="row mt-3">
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# CHANGELOG

## [1.2.28] - 2026-04-13

### Added
- `TileMetadataHotCacheSizeMB` application setting with Admin Settings UI support and client-side derived entry-count hint for the zoom `>= 9` tile metadata hot cache (#217)

### Changed
- Warm zoom `>= 9` tile hits now use an in-process metadata hot cache to avoid the per-hit Postgres metadata read on the common fresh-cache path (#217)
- Hot metadata cache invalidation now participates in purge, eviction, and tile-delete paths while preserving DB/file durability ordering and existing revalidation behavior (#217)

### Fixed
- Fresh hot-hit `LastAccessed` throttling is now atomic per tile and retries immediately after failed DB persists instead of suppressing writes for the full cooldown window (#217)

## [1.2.27] - 2026-03-27

### Fixed
Expand Down
Loading
Loading