Articles validated while a feed has no active route are permanently unroutable #157

Closed
opened 2026-08-16 17:28:56 +02:00 by myrmidex · 0 comments
Owner

Summary

An article validated while its feed has no active route creates no route_articles rows, but is still stamped validated_at. Because validation runs only once per article, adding a route later cannot recover it — the article is permanently unroutable and invisible to the pipeline.

The gap

CreateRouteArticlesAction::execute() loops over the feed's active routes:

$activeRoutes = Route::where('feed_id', $article->feed_id)
    ->where('is_active', true)
    ->get();

foreach ($activeRoutes as $route) {
    RouteArticle::firstOrCreate(...);
}

When $activeRoutes is empty the loop body never runs and nothing is created. Meanwhile ValidateArticleAction stamps articles.validated_at regardless, and ValidateArticleListener early-returns on a non-null validated_at. There is no second chance — the article is never reconsidered.

Note this is independent of keyword filtering. Keywords only decide pending vs rejected; firstOrCreate runs unconditionally per active route. With zero active routes, no row is created at any status.

Observed on PROD

v1.4.1, inspected 2026-08-16, feed 2 (Belga):

  • 128 articles, all 128 validated, validated_at spread daily across 2026-08-02 → 08-16
  • 124 have no route article at all
  • The feed's single route (feed_id=2, platform_channel_id=4, is_active=1) was created 2026-08-15 09:55:00
  • All 4 existing route articles postdate the route; earliest is 2026-08-15 13:05

Everything else in the chain is healthy: the Belga API returns 50 URLs, articles save daily, and the one approved route article published successfully. The articles are simply stranded between validation and routing.

Why this matters

The failure is silent — no warning, no notification, nothing in the UI. The system reports success while producing nothing, the same family as #115 and #116.

It is not Belga-specific: any feed that exists before its route does will strand its entire backlog. Creating a feed and adding its route later is a natural user sequence, and discovery starts on the feed immediately.

Fix directions (not chosen — for discussion)

  • Don't stamp validated_at when $activeRoutes is empty, so validation retries once a route appears. Cheapest, but leaves articles repeatedly re-validated while a feed has no route.
  • Backfill route articles for unrouted articles when a route is created. Recovers history, but re-fetch cost against a large backlog needs bounding, and it is a question whether weeks-old articles should resurface at all.

These differ in cost and in intent; worth deciding deliberately rather than defaulting.

Acceptance criteria

  • An article validated while its feed has no active route becomes routable once a route is added
  • No re-fetch storm when a route is created against a large existing backlog
  • Feeds that already have routes are unaffected
  • The inactive-route case behaves the same as the no-route case
  • Tests cover: validation with zero active routes, a route added afterwards, and a route toggled inactive then active
  • #128 "Removing route should not remove articles" — adjacent but distinct. That one is about articles lost when a route is removed; this is about articles unroutable because a route did not yet exist. Both stem from route lifecycle and article lifecycle being decoupled, and a fix for either may inform the other.
  • #115 / #116 — same silent-failure family (reports success, produces nothing)

Note on existing data

This fix will not retroactively rescue the 124 already-stranded articles on PROD unless the backfill direction is chosen. Recovering those is a separate data operation (clearing validated_at on articles with no route article), deliberately out of scope here.

## Summary An article validated while its feed has no active route creates no `route_articles` rows, but is still stamped `validated_at`. Because validation runs only once per article, adding a route later cannot recover it — the article is permanently unroutable and invisible to the pipeline. ## The gap `CreateRouteArticlesAction::execute()` loops over the feed's active routes: ```php $activeRoutes = Route::where('feed_id', $article->feed_id) ->where('is_active', true) ->get(); foreach ($activeRoutes as $route) { RouteArticle::firstOrCreate(...); } ``` When `$activeRoutes` is empty the loop body never runs and nothing is created. Meanwhile `ValidateArticleAction` stamps `articles.validated_at` regardless, and `ValidateArticleListener` early-returns on a non-null `validated_at`. There is no second chance — the article is never reconsidered. Note this is independent of keyword filtering. Keywords only decide *pending vs rejected*; `firstOrCreate` runs unconditionally per active route. With zero active routes, no row is created at any status. ## Observed on PROD v1.4.1, inspected 2026-08-16, feed 2 (Belga): - 128 articles, **all 128 validated**, `validated_at` spread daily across 2026-08-02 → 08-16 - **124 have no route article at all** - The feed's single route (`feed_id=2`, `platform_channel_id=4`, `is_active=1`) was created **2026-08-15 09:55:00** - All 4 existing route articles postdate the route; earliest is 2026-08-15 13:05 Everything else in the chain is healthy: the Belga API returns 50 URLs, articles save daily, and the one approved route article published successfully. The articles are simply stranded between validation and routing. ## Why this matters The failure is **silent** — no warning, no notification, nothing in the UI. The system reports success while producing nothing, the same family as #115 and #116. It is not Belga-specific: any feed that exists before its route does will strand its entire backlog. Creating a feed and adding its route later is a natural user sequence, and discovery starts on the feed immediately. ## Fix directions (not chosen — for discussion) - **Don't stamp `validated_at` when `$activeRoutes` is empty**, so validation retries once a route appears. Cheapest, but leaves articles repeatedly re-validated while a feed has no route. - **Backfill route articles for unrouted articles when a route is created.** Recovers history, but re-fetch cost against a large backlog needs bounding, and it is a question whether weeks-old articles *should* resurface at all. These differ in cost and in intent; worth deciding deliberately rather than defaulting. ## Acceptance criteria - [ ] An article validated while its feed has no active route becomes routable once a route is added - [ ] No re-fetch storm when a route is created against a large existing backlog - [ ] Feeds that already have routes are unaffected - [ ] The inactive-route case behaves the same as the no-route case - [ ] Tests cover: validation with zero active routes, a route added afterwards, and a route toggled inactive then active ## Related - #128 "Removing route should not remove articles" — adjacent but distinct. That one is about articles lost when a route is **removed**; this is about articles unroutable because a route did not **yet exist**. Both stem from route lifecycle and article lifecycle being decoupled, and a fix for either may inform the other. - #115 / #116 — same silent-failure family (reports success, produces nothing) ## Note on existing data This fix will not retroactively rescue the 124 already-stranded articles on PROD unless the backfill direction is chosen. Recovering those is a separate data operation (clearing `validated_at` on articles with no route article), deliberately out of scope here.
myrmidex added this to the v1.4.2 milestone 2026-08-16 17:28:56 +02:00
myrmidex added the
bug
label 2026-08-16 17:28:56 +02:00
myrmidex self-assigned this 2026-08-16 17:28:56 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/fedi-feed-router#157
No description provided.