c0bbaba99f
ci / build-test (push) Successful in 1m8s
MqttMessageRouter matched purely on topic with no endpoint predicate, and RouteAsync was not even passed an endpoint id. Topic filters routinely overlap between brokers — every Tasmota install publishes tele/+/SENSOR — so with two brokers a message on A was ingested by a source bound to B. HA enforced the binding on both workers; MQTT enforced it only at subscribe time. Pass the endpoint id through: MQTTnet's event args carry the topic but not the delivering connection, so CreateClient captures the id in the handler closure. ResolveTopicsAsync drops its `|| EndpointId == null` clause to match, since an unbound source is no longer routed and subscribing its topic everywhere would only invite traffic nothing consumes. That last part would silently kill unbound sources that work today, so a data migration binds them to the single broker when exactly one exists — the case where old and new behaviour coincide. Two or more brokers is left alone: the old behaviour was already ambiguous and a guess could route a meter's data to the wrong broker. HA sources are excluded; they have always required an endpoint, so binding them would activate ingestion never previously running. Claude-Session: https://claude.ai/code/session_01V6joyergfvVLFEizH1hJLd
48 lines
2.4 KiB
C#
48 lines
2.4 KiB
C#
using Microsoft.EntityFrameworkCore.Migrations;
|
|
|
|
#nullable disable
|
|
|
|
namespace MeterVault.Infrastructure.Persistence.Migrations
|
|
{
|
|
/// <inheritdoc />
|
|
public partial class BindUnboundMqttSourcesToSoleBroker : Migration
|
|
{
|
|
/// <inheritdoc />
|
|
protected override void Up(MigrationBuilder migrationBuilder)
|
|
{
|
|
// MQTT routing now honours meter_source.endpoint_id (SDD §6.1): a source is served only
|
|
// by the broker it is bound to. Previously an unbound source was subscribed on every
|
|
// broker and matched on topic alone, so unbound sources that work today would silently
|
|
// go quiet after this deploy.
|
|
//
|
|
// Backfill them onto the single broker only when exactly one exists — then the old
|
|
// "any broker" behaviour and the new "its broker" behaviour are the same thing, so the
|
|
// rewrite is provably lossless. With zero brokers there is nothing to bind to; with two
|
|
// or more the old behaviour was already ambiguous and a guess could route a meter's
|
|
// data to the wrong broker, so those are left for the operator to resolve in the UI.
|
|
//
|
|
// Enums persist as their C# names (HasConversion<string>), hence 'Mqtt'/'MqttBroker'.
|
|
// HomeAssistant sources are deliberately excluded: the HA workers have always required
|
|
// endpoint_id, so an unbound HA source is already inert and binding it here would
|
|
// activate ingestion the operator never had running.
|
|
migrationBuilder.Sql("""
|
|
UPDATE meter_source AS s
|
|
SET endpoint_id = sole.id
|
|
FROM (SELECT id FROM ingestion_endpoint WHERE type = 'MqttBroker') AS sole
|
|
WHERE s.endpoint_id IS NULL
|
|
AND s.source_type IN ('Mqtt', 'Tasmota')
|
|
AND (SELECT count(*) FROM ingestion_endpoint WHERE type = 'MqttBroker') = 1;
|
|
""");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Down(MigrationBuilder migrationBuilder)
|
|
{
|
|
// Intentionally empty. The rows this bound are indistinguishable from ones the operator
|
|
// bound by hand, so clearing endpoint_id on the way down would discard real
|
|
// configuration. Leaving the binding in place is harmless under the old routing, which
|
|
// ignored endpoint_id entirely.
|
|
}
|
|
}
|
|
}
|