Skip to content

Count panos that come back, so we can tell whether imagery expiries reverse #4935

Description

@jonfroehlich

Split the nightly imagery sweep's still_there tally so that panos which came back are counted separately from panos that were never gone.

Why

pano_data.expired_at (#4932, evolution 358) is row state, not an event log: it is cleared when a re-check finds the imagery back, and cleared again by PanoDataTable.upsert whenever a user successfully views the pano. So an expiry that later reverses leaves no trace at all, and the /admin/street-status expiry chart quietly loses that week's count.

We decided on #4932 not to build an append-only expiry log yet — the chart is now labelled as the snapshot it is — on the condition that we'd revisit if reversals turned out to be common. This issue is what makes that condition checkable, because right now it isn't: the quantity we'd need to see is exactly the one nothing records.

Note that two columns (expired_at + returned_at) do not solve it either. A pano that expires, returns, and expires again overwrites both, so history still mutates — just later, and after looking stable long enough to be trusted. Only counting events survives that case, which is why this is the cheap measure and a full event log is the real fix.

This matters more than "imagery occasionally returns" suggests. Genuine returns are probably rare, since a re-drive replaces a pano rather than restoring its id. The other cause is false positives from transient provider errors (cf. #4918), and those recur on the same panos — so reversals, if they happen, concentrate in exactly the panos whose data we'd least trust.

What

PanoDataService.checkForImagery already re-checks a budget of already-expired panos alongside unexpired ones, so it knows when one comes back — it just folds that into stillThere:

ImageryCheckResult(
  stillThere = responses.count(_.contains(true)),
  gone       = responses.count(_.contains(false)),
  errors     = responses.count(_.isEmpty)
)

Split stillThere into "unexpired, still fine" and "was expired, came back", add the latter to ImageryCheckResult and to the details JSON that JobRunService.record writes into background_job_run.

The value is that background_job_run is a table, not a dashboard reading: nobody has to watch it nightly, and the answer is available retroactively whenever we wonder —

SELECT sum((details->>'returned')::int)
FROM background_job_run
WHERE job_name = 'check-image-expiry-actor';

It also counts events, so a pano that flip-flops three times contributes three.

Implementation note

checkImageryBounded uses mapAsyncUnordered and documents its results as arriving in completion order, so the expired subset cannot be recovered by slicing the response list positionally. Each pano needs to carry a "was expired" flag through the stream (or the two subsets need to be tallied by separate calls).

Decision this feeds

If the count comes back meaningfully non-zero, go straight to an append-only pano expiry/return log and have the chart read that instead. If it stays near zero, the current snapshot semantics are fine as they are and nothing further is needed.

Follow-up to #4932 / #4928.

🤖 Generated with Claude Code (claude-opus-5[1m])

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions