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
17 changes: 17 additions & 0 deletions Core/Resgrid.Model/PermanentWeatherAlertException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace Resgrid.Model
{
/// <summary>
/// Thrown when a weather alert API request fails due to a permanent, non-retriable condition
/// (e.g. invalid NWS zone code). Unlike <see cref="TransientWeatherAlertException"/>, callers
/// should mark the source as permanently failed and stop retrying until the user corrects the configuration.
/// </summary>
public class PermanentWeatherAlertException : Exception
{
public PermanentWeatherAlertException(string message) : base(message) { }

public PermanentWeatherAlertException(string message, Exception innerException)
: base(message, innerException) { }
}
}
2 changes: 2 additions & 0 deletions Core/Resgrid.Model/WeatherAlertSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class WeatherAlertSource : IEntity

public bool IsFailure { get; set; }

public bool IsPermanentFailure { get; set; }

[MaxLength(2000)]
public string ErrorMessage { get; set; }

Expand Down
18 changes: 18 additions & 0 deletions Core/Resgrid.Services/WeatherAlertService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,22 @@ public async Task ProcessWeatherAlertSourceAsync(Guid sourceId, CancellationToke
source.LastPollUtc = DateTime.UtcNow;
source.LastSuccessUtc = DateTime.UtcNow;
source.IsFailure = false;
source.IsPermanentFailure = false;
source.ErrorMessage = null;
await _weatherAlertSourceRepository.UpdateAsync(source, ct, true);
}
catch (PermanentWeatherAlertException ex)
{
// Permanent failures (invalid zone codes, etc.) — record the error
// and mark the source as failed. Don't rethrow so the poller continues
// without noisy Fatal logs. The source will be retried only after the
// user edits and saves it.
source.LastPollUtc = DateTime.UtcNow;
source.IsFailure = true;
source.IsPermanentFailure = true;
source.ErrorMessage = ex.Message;
await _weatherAlertSourceRepository.UpdateAsync(source, ct, true);
}
catch (TransientWeatherAlertException ex)
{
// Transient failures (rate-limit, server errors) — record the error
Expand Down Expand Up @@ -268,6 +281,11 @@ public async Task ProcessAllActiveSourcesAsync(CancellationToken ct = default)

foreach (var source in sources)
{
// Skip sources with permanent failures (e.g. invalid zone codes).
// They will be retried only after the user edits and saves the source.
if (source.IsFailure && source.IsPermanentFailure)
continue;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Check if it's time to poll based on interval
if (source.LastPollUtc.HasValue)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using FluentMigrator;

namespace Resgrid.Providers.Migrations.Migrations
{
[Migration(68)]
public class M0068_AddIsPermanentFailureToWeatherAlertSources : Migration
{
public override void Up()
{
Alter.Table("WeatherAlertSources")
.AddColumn("IsPermanentFailure").AsBoolean().NotNullable().WithDefaultValue(false);
}

public override void Down()
{
Delete.Column("IsPermanentFailure").FromTable("WeatherAlertSources");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using FluentMigrator;

namespace Resgrid.Providers.MigrationsPg.Migrations
{
[Migration(68)]
public class M0068_AddIsPermanentFailureToWeatherAlertSourcesPg : Migration
{
public override void Up()
{
Alter.Table("weatheralertsources")
.AddColumn("ispermanentfailure").AsBoolean().NotNullable().WithDefaultValue(false);
}

public override void Down()
{
Delete.Column("ispermanentfailure").FromTable("weatheralertsources");
}
}
}
12 changes: 6 additions & 6 deletions Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ public async Task<List<WeatherAlert>> FetchAlertsAsync(WeatherAlertSource source
var stateCodes = zones.Where(z => z.Length == 2).ToArray();
var zoneCodes = zones.Where(z => z.Length > 2).ToArray();

// Validate zone codes before calling the NWS API to produce a clear error
// instead of a cryptic 400 Bad Request from the upstream API.
var validationError = GetZoneValidationError(source.AreaFilter);
if (validationError != null)
throw new HttpRequestException(
$"Invalid NWS zone code in area filter for department {source.DepartmentId}: {validationError}");
// Validate zone codes before calling the NWS API to produce a clear error
// instead of a cryptic 400 Bad Request from the upstream API.
var validationError = GetZoneValidationError(source.AreaFilter);
if (validationError != null)
throw new PermanentWeatherAlertException(
$"Invalid NWS zone code in area filter for department {source.DepartmentId}: {validationError}");

var queryParams = new List<string>();
if (stateCodes.Length > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ public async Task<ActionResult<GetWeatherAlertSourcesResult>> SaveSource([FromBo
}
}

// Clear any previous permanent failure so the source will be retried on next poll.
source.IsFailure = false;
source.IsPermanentFailure = false;
source.ErrorMessage = null;

await _weatherAlertService.SaveSourceAsync(source);

var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId, false);
Expand Down Expand Up @@ -482,6 +487,7 @@ private static WeatherAlertSourceResultData MapSourceToResultData(WeatherAlertSo
LastPollUtc = source.LastPollUtc?.TimeConverterToString(department),
LastSuccessUtc = source.LastSuccessUtc?.TimeConverterToString(department),
IsFailure = source.IsFailure,
IsPermanentFailure = source.IsPermanentFailure,
ErrorMessage = source.ErrorMessage
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class WeatherAlertSourceResultData
public string LastPollUtc { get; set; }
public string LastSuccessUtc { get; set; }
public bool IsFailure { get; set; }
public bool IsPermanentFailure { get; set; }
public string ErrorMessage { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
areaFilter = source.AreaFilter ?? "",
pollInterval = source.PollIntervalMinutes,
active = source.Active,
isPermanentFailure = source.IsPermanentFailure,
errorMessage = source.ErrorMessage ?? ""
});
}
Expand Down
Loading