Skip to content

Which data source wins an event is decided three different ways, and the documented one is not implemented #2476

Description

@Flix6x

When several data sources report the same event and something asks for one value per event, a choice
gets made. Today that choice follows three different rules depending on the case, one documented
rule is not implemented at all, and the last resort is the source's name in alphabetical order.

What actually happens today

Situation What decides
Sources share a name, type and model (e.g. two rows for one scheduler) latest version, then highest source id — freshness is not consulted
Sources differ in name, type or model (e.g. a meter and a scheduler) highest version number, compared across unrelated producers, then freshest belief, then source name, alphabetically
The caller passed sources=[...] in a chosen order nothing — the order is ignored

Probes against a development database, all on one event:

# the caller's order does nothing (aaa holds the older belief, zzz the fresher one)
sources=no list      -> winner='zzz source' value=22.0
sources=[aaa, zzz]   -> winner='zzz source' value=22.0
sources=[zzz, aaa]   -> winner='zzz source' value=22.0

# with belief times tied, the name decides (ids deliberately opposed to names)
ids: zzz=1 aaa=2  ->  winner: name='aaa reporter' id=2

# within one source family, the highest id wins even when the other is fresher
ids: a=1 b=2 (a holds the fresher belief)  ->  winner: id=2

# version numbers are compared between unrelated producers
no list                 -> forecaster v9 value=22.0
[scheduler, forecaster] -> forecaster v9 value=22.0
   (scheduler v1 = 11.0, believed at the event; forecaster v9 = 22.0, believed 12h earlier)

That last one is the sharpest of the three. keep_latest_version compares versions only within a
(name, type, model) family, which is the only place a version number means anything. But the
selection that runs afterwards ranks versions across every source in the frame:

versions = [Version(source.version if source.version else "0.0.0") for source in unique_sources]
version_ranks = {version: rank for rank, version in enumerate(sorted(set(versions)))}

So a forecaster at v9 outranks a scheduler at v1 on the strength of the number alone, whatever the
belief times say and whatever the caller asked for. Two teams' independent version numbers are being
compared as though they were the same series.

How it got here

  • Make source optional in the input field of the AggregatorReporter #819 (2023-08) added sources=[...] to the AggregatorReporter, for the case of one source taking over
    from another on the same sensor: "computing the aggregated power of a group of devices including a scheduled
    device. We could get actual power commands and scheduled power commands mixed up."
    It also added the
    ValueError when a sensor has several sources and none were named.

    Asked during review whether "the first source" meant the array order or the oldest belief, the answer was
    written into test_source_transition's docstring: "the first source defined in the sources array is
    prioritized"
    . That was never implemented. The implementation took head(1) over an ascending index, which
    is the oldest belief per event, with the source name breaking exact ties. The reviewer noted at the time
    that "this test doesn't actually check that", and it still does not: every belief in the fixture shares
    belief_horizon=24h, so those events tie on belief time and the name decides.

  • [reporting] Add flag to filter the data with a the latest version source #1045 (2024-05) added version filtering (keep_latest_version).

  • Fix/reporting/latest version and excludes source types #1306 (2025-01) deliberately changed the cross-source rule, in its own words: "favour latest version
    first, freshest belief second"
    . That flipped oldest-wins to freshest-wins. It added test coverage for the
    version half, and fixed a typo in the docstring above while leaving its false claim standing.

  • Speed up post-processing of sensor data searches #2328 (2026-07) extracted _select_latest_version_and_belief_per_event as a fast path, preserving Fix/reporting/latest version and excludes source types #1306's
    semantics.

So the docstring describes the intention of 2023, the code follows the decision of 2025, and the two have never
agreed.

Why it matters

Reporters and the transition case (#819's own use case). A meter takes over from a scheduler on one sensor.
The overlapping event goes to the meter today, because the meter believed it later — which is the wanted answer,
but it arrives by way of a rule chosen for other reasons in #1306. Under the 2023 rule the scheduler won it:

today's rule  -> 'meter'     value=22.0
2023 rule     -> 'scheduler' value=11.0

The sources=[...] order, which exists for exactly this case, contributes nothing either way.

Two forecasting models side by side. Two forecasters writing to one sensor during a transition, different
names, no versions: whichever forecasted most recently wins each event, and where they tie, the alphabetically
first name wins. Renaming a forecaster can therefore change what a report says about the past. This is where
the name rule bites hardest, because forecasters legitimately run in parallel for a while.

A bumped scheduler version. Within one scheduler, handled well and tested: the newer version wins. Across
schedulers it goes wrong in both directions. A plugin's scheduler and a built-in one are different families, so
keep_latest_version will not compare their versions — but the selection afterwards does, so whichever happens
to carry the higher number wins, and a plugin that started at v10 outranks a built-in scheduler at v3 forever.

KPIs. With no source filter at all, a KPI lands on these rules for every event. #2472 stops it from summing
several sources; which one it then reports is decided by the chain above, so a rename can move a KPI.

Schedules recorded per configuration (#2464). Once a scheduler's data source records the flex config it
computed under, one sensor can hold schedules from several sources that share a name, type, model and version.
Those are one family, so the highest id wins and the freshest belief is ignored. That happens to mean "the most
recently created configuration", which is reasonable, but it is a different rule from the one applied to any
other pair of sources.

Recommendation

1. Delete the false claim now. test_source_transition's docstring promises precedence the code has never
had. Whatever else is decided, that line should go, so it stops being read as a contract.

2. Stop comparing versions across families. A version number orders the releases of one producer. It says
nothing about a different producer, so sorted(set(versions)) over the whole frame is comparing quantities that
were never on the same scale. Version should rank only within a (name, type, model) family, as
keep_latest_version already does.

3. Decide the rest of the precedence once, and put it in one place. Proposed, in two steps:

Within a family, reduce to one row: latest version, then freshest belief, then highest id.

Between families, choose one:

  1. The caller's sources order, when one was given. This is the only rule a caller controls, it is what
    Make source optional in the input field of the AggregatorReporter #819 set out to provide, and it makes the transition case explicit instead of accidental.
  2. Freshest belief. Fix/reporting/latest version and excludes source types #1306's deliberate default for when nobody expressed a preference.
  3. Highest source id. A last resort that is stable under renaming, unlike the name.

Under this, a scheduler at v1 listed before a forecaster at v9 wins, which is what the caller asked for; with no
list given, the fresher of the two wins. Neither outcome depends on version numbers from different series, or on
what the sources happen to be called.

Two behaviour changes beyond implementing the caller's order: version stops crossing family lines, and within a
family freshness starts to count before the id, where today the id wins outright. test_source_transition and
test_keep_last_version would need updating.

4. Pin the whole chain in one test. Today the behaviour is inferred from aggregator tests that were written
for filtering. A single test naming each step, with a case per step, would have caught both the unimplemented
promise and the 2025 flip.

5. Note it in the changelog when it changes. Ambiguous events silently changing hands is exactly the sort of
thing a host notices in a report long after the upgrade.

If the caller's order is more than we want to take on, the minimum worth doing is (1), (2) and (5), plus
replacing the alphabetical last resort with the id. That removes the rename hazard and the cross-family version
comparison, which are the two that can silently pick a value nobody would defend.

Related

Came out of the review of #2472, which stops a KPI summing several sources for one event but deliberately does
not touch which source wins. Relevant to #2464, which makes multi-source schedule sensors ordinary.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions